Browse Source

* API-Key support. This ease automation, such as #21

pull/29/head
Per-Arne Andersen 5 years ago
parent
commit
d70f35c65e
  1. 7
      README.md
  2. 1
      wg_dashboard_backend/const.py
  3. 23
      wg_dashboard_backend/middleware.py
  4. 13
      wg_dashboard_backend/models.py
  5. 53
      wg_dashboard_backend/routers/v1/user.py
  6. 2
      wg_dashboard_backend/routers/v1/wg.py
  7. 11
      wg_dashboard_backend/schemas.py
  8. 5
      wg_dashboard_frontend/src/app/page/page.module.ts
  9. 43
      wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.html
  10. 0
      wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.scss
  11. 25
      wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.spec.ts
  12. 45
      wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.ts
  13. 7
      wg_dashboard_frontend/src/app/page/user/edit/edit.component.html
  14. 3
      wg_dashboard_frontend/src/app/page/user/edit/edit.component.scss
  15. 7
      wg_dashboard_frontend/src/app/page/user/edit/edit.component.ts
  16. 18
      wg_dashboard_frontend/src/app/services/server.service.ts

7
README.md

@ -15,6 +15,7 @@ The features of wg-manager includes:
* Create/Delete/Modify * Create/Delete/Modify
* Bandwidth usage statistics * Bandwidth usage statistics
* Export by QRCode, Text * Export by QRCode, Text
* Authentication via API-Keys for automation (Created in GUI)
**General** **General**
* Modify Admin User * Modify Admin User
@ -88,6 +89,12 @@ When docker container/server has started, go to http://localhost:8888
# API Docs # API Docs
The API docs is found [here](./docs/api.md). The API docs is found [here](./docs/api.md).
# API-Keys
1. Login to wg-manager
2. Go to edit profile
3. Create API-Key and take note of the key. Use the X-API-Key header to authenticate.
4. Example: `curl -i -H "X-API-Key: <key-goes-here>" http://<host>:<port>/api/v1/users/api-key/list`
# Environment variables # Environment variables
| Environment | Description | Recommended | | Environment | Description | Recommended |
|------------------|---------------------------------------------------------------------------|-------------| |------------------|---------------------------------------------------------------------------|-------------|

1
wg_dashboard_backend/const.py

@ -14,6 +14,7 @@ DEFAULT_POST_DOWN_v6 = os.getenv("POST_DOWN_V6", "ip6tables -D FORWARD -i %i -j
SECRET_KEY = ''.join(random.choices(string.ascii_uppercase + string.digits, k=64)) SECRET_KEY = ''.join(random.choices(string.ascii_uppercase + string.digits, k=64))
ALGORITHM = "HS256" ALGORITHM = "HS256"
API_KEY_LENGTH = 32
ACCESS_TOKEN_EXPIRE_MINUTES = 30 ACCESS_TOKEN_EXPIRE_MINUTES = 30
CMD_WG_COMMAND = ["wg"] CMD_WG_COMMAND = ["wg"]
CMD_WG_QUICK = ["wg-quick"] CMD_WG_QUICK = ["wg-quick"]

23
wg_dashboard_backend/middleware.py

@ -11,6 +11,7 @@ from starlette.requests import Request
from starlette.responses import Response from starlette.responses import Response
import const import const
import models
import schemas import schemas
from database import SessionLocal from database import SessionLocal
import db.user import db.user
@ -56,7 +57,13 @@ def create_access_token(*, data: dict, expires_delta: timedelta = None):
return encoded_jwt return encoded_jwt
def auth(token: str = Depends(oauth2_scheme), sess: Session = Depends(get_db)): def retrieve_api_key(request: Request):
return request.headers.get("X-API-Key", None)
def auth(token: str = Depends(oauth2_scheme), api_key: str = Depends(retrieve_api_key), sess: Session = Depends(get_db)):
username = None
credentials_exception = HTTPException( credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
@ -64,14 +71,22 @@ def auth(token: str = Depends(oauth2_scheme), sess: Session = Depends(get_db)):
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
# Attempt to authenticate using JWT
try: try:
payload = jwt.decode(token, const.SECRET_KEY, algorithms=[const.ALGORITHM]) payload = jwt.decode(token, const.SECRET_KEY, algorithms=[const.ALGORITHM])
username: str = payload.get("sub") username: str = payload.get("sub")
if username is None:
raise credentials_exception
except PyJWTError: except PyJWTError:
pass
try:
db_user_api_key = sess.query(models.UserAPIKey).filter_by(key=api_key).one()
username = db_user_api_key.user.username
except Exception:
pass
if username is None:
raise credentials_exception raise credentials_exception
user = schemas.User.from_orm( user = schemas.User.from_orm(
schemas.UserInDB(username=username, password="").from_db(sess) schemas.UserInDB(username=username, password="").from_db(sess)
) )

13
wg_dashboard_backend/models.py

@ -1,6 +1,8 @@
import datetime
import sqlalchemy import sqlalchemy
from sqlalchemy import Integer, Column from sqlalchemy import Integer, Column, DateTime
from sqlalchemy.orm import relationship, backref from sqlalchemy.orm import relationship, backref
from database import Base from database import Base
@ -16,6 +18,15 @@ class User(Base):
role = Column(sqlalchemy.String) role = Column(sqlalchemy.String)
class UserAPIKey(Base):
__tablename__ = "api_keys"
id = Column(Integer, primary_key=True, autoincrement=True)
key = Column(sqlalchemy.String, unique=True)
user_id = Column(Integer, sqlalchemy.ForeignKey('users.id', ondelete="CASCADE", onupdate="CASCADE"))
user = relationship("User", foreign_keys=[user_id])
created_date = Column(DateTime, default=datetime.datetime.utcnow)
class WGServer(Base): class WGServer(Base):
__tablename__ = "server" __tablename__ = "server"

53
wg_dashboard_backend/routers/v1/user.py

@ -1,9 +1,12 @@
import os
from datetime import timedelta from datetime import timedelta
from fastapi import APIRouter, HTTPException, Depends, Form from fastapi import APIRouter, HTTPException, Depends, Form, Body
from fastapi.responses import PlainTextResponse, JSONResponse
import typing
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from starlette import status from starlette import status
from binascii import hexlify
import const import const
import db.user import db.user
import middleware import middleware
@ -28,6 +31,51 @@ def edit(form_data: schemas.UserInDB,
return form_data return form_data
@router.get("/users/api-key/add", response_model=schemas.UserAPIKeyFull)
def add_api_key(
user: schemas.UserInDB = Depends(middleware.auth),
sess: Session = Depends(middleware.get_db)
):
key = hexlify(os.urandom(const.API_KEY_LENGTH)).decode()
api_key = models.UserAPIKey(
user_id=user.id,
key=key,
)
sess.add(api_key)
sess.commit()
return schemas.UserAPIKeyFull.from_orm(api_key)
@router.post("/users/api-key/delete")
def delete_api_keys(
key_id: int = Body(None, embed=True),
user: schemas.UserInDB = Depends(middleware.auth),
sess: Session = Depends(middleware.get_db)
):
count = sess.query(models.UserAPIKey)\
.filter_by(id=key_id)\
.delete()
sess.commit()
return JSONResponse({
"message": "Key deleted OK" if count == 1 else "There was an error while deleting the api-key"
})
@router.get("/users/api-key/list", response_model=typing.List[schemas.UserAPIKey])
def get_api_keys(
user: schemas.UserInDB = Depends(middleware.auth),
sess: Session = Depends(middleware.get_db)
):
keys = [schemas.UserAPIKey.from_orm(x) for x in sess.query(models.UserAPIKey)
.filter(models.UserAPIKey.user_id == user.id).all()]
return keys
@router.post("/login", response_model=schemas.Token) @router.post("/login", response_model=schemas.Token)
def login(*, username: str = Form(...), password: str = Form(...), sess: Session = Depends(middleware.get_db)): def login(*, username: str = Form(...), password: str = Form(...), sess: Session = Depends(middleware.get_db)):
user: schemas.UserInDB = schemas.UserInDB(username=username, password="").from_db(sess) user: schemas.UserInDB = schemas.UserInDB(username=username, password="").from_db(sess)
@ -77,4 +125,3 @@ def create_user(
role=form_data.role, role=form_data.role,
)): )):
raise HTTPException(status_code=400, detail="Could not create user") raise HTTPException(status_code=400, detail="Could not create user")

2
wg_dashboard_backend/routers/v1/wg.py

@ -46,7 +46,7 @@ def dump_database(
zf.close() zf.close()
in_memory.seek(0) in_memory.seek(0)
now = datetime.now().strftime("%m-%d-%Y-%H:%M:%S") now = datetime.now().strftime("%m-%d-%Y-%H:%M:%S")
return StreamingResponse(in_memory, media_type="application/zip", headers={ return StreamingResponse(in_memory, media_type="application/zip", headers={
"Content-Disposition": f'attachment; filename="wg-manager-dump-{now}.zip"' "Content-Disposition": f'attachment; filename="wg-manager-dump-{now}.zip"'
}) })

11
wg_dashboard_backend/schemas.py

@ -1,3 +1,5 @@
from datetime import datetime
import pydantic import pydantic
from pydantic import BaseModel, typing from pydantic import BaseModel, typing
from sqlalchemy.orm import Session, Query from sqlalchemy.orm import Session, Query
@ -100,6 +102,15 @@ class User(GenericModel):
excludes = {"id"} excludes = {"id"}
class UserAPIKey(GenericModel):
id: int
created_date: datetime
class UserAPIKeyFull(UserAPIKey):
key: str
class UserInDB(User): class UserInDB(User):
password: str password: str

5
wg_dashboard_frontend/src/app/page/page.module.ts

@ -9,9 +9,11 @@ import { MatInputModule } from '@angular/material/input';
import { FlexModule } from '@angular/flex-layout'; import { FlexModule } from '@angular/flex-layout';
import { EditComponent } from './user/edit/edit.component'; import { EditComponent } from './user/edit/edit.component';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import {MatTableModule} from "@angular/material/table";
import { ApiKeyComponent } from './user/edit/api-key/api-key.component';
@NgModule({ @NgModule({
declarations: [LoginComponent, EditComponent], declarations: [LoginComponent, EditComponent, ApiKeyComponent],
imports: [ imports: [
CommonModule, CommonModule,
PageRoutingModule, PageRoutingModule,
@ -22,6 +24,7 @@ import { MatButtonModule } from '@angular/material/button';
MatInputModule, MatInputModule,
FlexModule, FlexModule,
MatButtonModule, MatButtonModule,
MatTableModule,
], ],
}) })

43
wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.html

@ -0,0 +1,43 @@
<mat-card>
<mat-card-title>
API Keys
</mat-card-title>
<mat-card-content>
You can use API-Keys to perform authenticated actions. These are less secure than using OAuth2, but at the gain for increased convenience.
<br><b>Note:</b> A newly created API Key will only show <b>once</b>. This means that you have to take note of the key and safe it somewhere safe.
<table mat-table [dataSource]="dataSource" style="width: 100%">
<!-- Id Column -->
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef> ID. </th>
<td mat-cell *matCellDef="let element"> {{element.id}} </td>
</ng-container>
<!-- Key Column -->
<ng-container matColumnDef="key">
<th mat-header-cell *matHeaderCellDef> API-Key </th>
<td mat-cell *matCellDef="let element"> {{(element.key) ? element.key : "[HIDDEN]"}} </td>
</ng-container>
<!-- Created_At Column -->
<ng-container matColumnDef="created_at">
<th mat-header-cell *matHeaderCellDef> Creation Date </th>
<td mat-cell *matCellDef="let element"> {{element.created_date | date:'medium'}} </td>
</ng-container>
<!-- Delete Column -->
<ng-container matColumnDef="delete">
<th mat-header-cell *matHeaderCellDef> Delete </th>
<td mat-cell *matCellDef="let element"> <button mat-flat-button color="warn" (click)="deleteAPIKey(element)">Delete</button></td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<button mat-flat-button color="primary" (click)="createAPIKey()">New Key</button>
</mat-card-content>
</mat-card>

0
wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.scss

25
wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.spec.ts

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ApiKeyComponent } from './api-key.component';
describe('ApiKeyComponent', () => {
let component: ApiKeyComponent;
let fixture: ComponentFixture<ApiKeyComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ApiKeyComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ApiKeyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

45
wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.ts

@ -0,0 +1,45 @@
import {Component, OnInit} from '@angular/core';
import {ServerService} from "../../../../services/server.service";
@Component({
selector: 'app-api-key',
templateUrl: './api-key.component.html',
styleUrls: ['./api-key.component.scss']
})
export class ApiKeyComponent implements OnInit {
displayedColumns: string[] = ['id', 'key', 'created_at', 'delete'];
dataSource = [];
constructor(private serverService: ServerService
) { }
ngOnInit(): void {
this.serverService.getAPIKeys().subscribe((apiKeys: Array<any>) => {
this.dataSource = [...apiKeys]
console.log(this.dataSource)
})
}
deleteAPIKey(elem){
let idx = this.dataSource.indexOf(elem);
this.serverService.deleteAPIKey(elem.id).subscribe(x => {
this.dataSource.splice(idx, 1);
this.dataSource = [...this.dataSource]
})
}
createAPIKey(){
this.serverService.addAPIKey().subscribe(key => {
this.dataSource.push(key)
this.dataSource = [...this.dataSource]
})
}
}

7
wg_dashboard_frontend/src/app/page/user/edit/edit.component.html

@ -1,5 +1,5 @@
<div flex fxFill fxLayout="row" fxLayoutAlign="center center" > <div flex fxFill fxLayout="row" fxLayoutAlign="left top">
<div fxFlex="33"> <div fxFlex="33" class="user-edit-component">
<mat-card> <mat-card>
<mat-card-title> <mat-card-title>
@ -47,6 +47,9 @@
</mat-card> </mat-card>
</div> </div>
<div fxFlex="66" class="user-edit-component">
<app-api-key></app-api-key>
</div>

3
wg_dashboard_frontend/src/app/page/user/edit/edit.component.scss

@ -0,0 +1,3 @@
.user-edit-component{
padding: 20px;
}

7
wg_dashboard_frontend/src/app/page/user/edit/edit.component.ts

@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms'; import { FormControl, FormGroup, Validators } from '@angular/forms';
import { AuthService } from '@services/*'; import { AuthService } from '@services/*';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import {ServerService} from "../../../services/server.service";
@Component({ @Component({
selector: 'app-edit', selector: 'app-edit',
@ -23,8 +24,10 @@ export class EditComponent implements OnInit {
public user: any; public user: any;
public error: string; public error: string;
constructor(private authService: AuthService, constructor(
private router: Router) { private authService: AuthService,
private router: Router
) {
} }

18
wg_dashboard_frontend/src/app/services/server.service.ts

@ -16,6 +16,9 @@ export class ServerService {
public serverURL = this.base + "server"; public serverURL = this.base + "server";
public peerURL = this.base + "peer"; public peerURL = this.base + "peer";
public wgURL = this.base + "wg"; public wgURL = this.base + "wg";
public userURL = this.base + "users";
public apiKeyURL = this.userURL + "/api-key"
constructor(private config: ConfigService, private http: HttpClient, private notify: NotifierService) { constructor(private config: ConfigService, private http: HttpClient, private notify: NotifierService) {
@ -91,4 +94,19 @@ export class ServerService {
public serverStats(server: Server) { public serverStats(server: Server) {
return this.http.post(this.serverURL + '/stats', server); return this.http.post(this.serverURL + '/stats', server);
} }
public addAPIKey() {
return this.http.get(this.apiKeyURL + '/add');
}
public getAPIKeys() {
return this.http.get(this.apiKeyURL + '/list');
}
public deleteAPIKey(api_key_id: { id: number }) {
return this.http.post(this.apiKeyURL + '/delete', {
key_id: api_key_id
});
}
} }

Loading…
Cancel
Save