From d70f35c65e93000b650610b0c4ba0b8a84e442b9 Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Mon, 20 Jul 2020 04:06:05 +0200 Subject: [PATCH] * API-Key support. This ease automation, such as #21 --- README.md | 7 +++ wg_dashboard_backend/const.py | 1 + wg_dashboard_backend/middleware.py | 23 ++++++-- wg_dashboard_backend/models.py | 13 ++++- wg_dashboard_backend/routers/v1/user.py | 53 +++++++++++++++++-- wg_dashboard_backend/routers/v1/wg.py | 2 +- wg_dashboard_backend/schemas.py | 11 ++++ .../src/app/page/page.module.ts | 5 +- .../user/edit/api-key/api-key.component.html | 43 +++++++++++++++ .../user/edit/api-key/api-key.component.scss | 0 .../edit/api-key/api-key.component.spec.ts | 25 +++++++++ .../user/edit/api-key/api-key.component.ts | 45 ++++++++++++++++ .../app/page/user/edit/edit.component.html | 7 ++- .../app/page/user/edit/edit.component.scss | 3 ++ .../src/app/page/user/edit/edit.component.ts | 7 ++- .../src/app/services/server.service.ts | 18 +++++++ 16 files changed, 249 insertions(+), 14 deletions(-) create mode 100644 wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.html create mode 100644 wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.scss create mode 100644 wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.spec.ts create mode 100644 wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.ts diff --git a/README.md b/README.md index bdc906c..50d4421 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ The features of wg-manager includes: * Create/Delete/Modify * Bandwidth usage statistics * Export by QRCode, Text +* Authentication via API-Keys for automation (Created in GUI) **General** * Modify Admin User @@ -88,6 +89,12 @@ When docker container/server has started, go to http://localhost:8888 # API Docs 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: " http://:/api/v1/users/api-key/list` + # Environment variables | Environment | Description | Recommended | |------------------|---------------------------------------------------------------------------|-------------| diff --git a/wg_dashboard_backend/const.py b/wg_dashboard_backend/const.py index 67a1b02..1ea55ab 100644 --- a/wg_dashboard_backend/const.py +++ b/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)) ALGORITHM = "HS256" +API_KEY_LENGTH = 32 ACCESS_TOKEN_EXPIRE_MINUTES = 30 CMD_WG_COMMAND = ["wg"] CMD_WG_QUICK = ["wg-quick"] diff --git a/wg_dashboard_backend/middleware.py b/wg_dashboard_backend/middleware.py index 708decb..58e7462 100644 --- a/wg_dashboard_backend/middleware.py +++ b/wg_dashboard_backend/middleware.py @@ -11,6 +11,7 @@ from starlette.requests import Request from starlette.responses import Response import const +import models import schemas from database import SessionLocal import db.user @@ -56,7 +57,13 @@ def create_access_token(*, data: dict, expires_delta: timedelta = None): 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( 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"}, ) + # Attempt to authenticate using JWT try: payload = jwt.decode(token, const.SECRET_KEY, algorithms=[const.ALGORITHM]) username: str = payload.get("sub") - if username is None: - raise credentials_exception - 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 + user = schemas.User.from_orm( schemas.UserInDB(username=username, password="").from_db(sess) ) diff --git a/wg_dashboard_backend/models.py b/wg_dashboard_backend/models.py index c6eedb0..13fc980 100644 --- a/wg_dashboard_backend/models.py +++ b/wg_dashboard_backend/models.py @@ -1,6 +1,8 @@ +import datetime + import sqlalchemy -from sqlalchemy import Integer, Column +from sqlalchemy import Integer, Column, DateTime from sqlalchemy.orm import relationship, backref from database import Base @@ -16,6 +18,15 @@ class User(Base): 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): __tablename__ = "server" diff --git a/wg_dashboard_backend/routers/v1/user.py b/wg_dashboard_backend/routers/v1/user.py index 10840f7..1cec360 100644 --- a/wg_dashboard_backend/routers/v1/user.py +++ b/wg_dashboard_backend/routers/v1/user.py @@ -1,9 +1,12 @@ +import os 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 starlette import status - +from binascii import hexlify import const import db.user import middleware @@ -28,6 +31,51 @@ def edit(form_data: schemas.UserInDB, 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) 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) @@ -77,4 +125,3 @@ def create_user( role=form_data.role, )): raise HTTPException(status_code=400, detail="Could not create user") - diff --git a/wg_dashboard_backend/routers/v1/wg.py b/wg_dashboard_backend/routers/v1/wg.py index cad3aa1..476946d 100644 --- a/wg_dashboard_backend/routers/v1/wg.py +++ b/wg_dashboard_backend/routers/v1/wg.py @@ -46,7 +46,7 @@ def dump_database( zf.close() 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={ "Content-Disposition": f'attachment; filename="wg-manager-dump-{now}.zip"' }) diff --git a/wg_dashboard_backend/schemas.py b/wg_dashboard_backend/schemas.py index f6cd8b7..7a16513 100644 --- a/wg_dashboard_backend/schemas.py +++ b/wg_dashboard_backend/schemas.py @@ -1,3 +1,5 @@ +from datetime import datetime + import pydantic from pydantic import BaseModel, typing from sqlalchemy.orm import Session, Query @@ -100,6 +102,15 @@ class User(GenericModel): excludes = {"id"} +class UserAPIKey(GenericModel): + id: int + created_date: datetime + + +class UserAPIKeyFull(UserAPIKey): + key: str + + class UserInDB(User): password: str diff --git a/wg_dashboard_frontend/src/app/page/page.module.ts b/wg_dashboard_frontend/src/app/page/page.module.ts index f48a155..37942ab 100644 --- a/wg_dashboard_frontend/src/app/page/page.module.ts +++ b/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 { EditComponent } from './user/edit/edit.component'; import { MatButtonModule } from '@angular/material/button'; +import {MatTableModule} from "@angular/material/table"; +import { ApiKeyComponent } from './user/edit/api-key/api-key.component'; @NgModule({ - declarations: [LoginComponent, EditComponent], + declarations: [LoginComponent, EditComponent, ApiKeyComponent], imports: [ CommonModule, PageRoutingModule, @@ -22,6 +24,7 @@ import { MatButtonModule } from '@angular/material/button'; MatInputModule, FlexModule, MatButtonModule, + MatTableModule, ], }) diff --git a/wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.html b/wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.html new file mode 100644 index 0000000..1186c7a --- /dev/null +++ b/wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.html @@ -0,0 +1,43 @@ + + + API Keys + + + + You can use API-Keys to perform authenticated actions. These are less secure than using OAuth2, but at the gain for increased convenience. +
Note: A newly created API Key will only show once. This means that you have to take note of the key and safe it somewhere safe. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID. {{element.id}} API-Key {{(element.key) ? element.key : "[HIDDEN]"}} Creation Date {{element.created_date | date:'medium'}} Delete
+ + +
+
diff --git a/wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.scss b/wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.spec.ts b/wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.spec.ts new file mode 100644 index 0000000..2b44fe4 --- /dev/null +++ b/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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ApiKeyComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ApiKeyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.ts b/wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.ts new file mode 100644 index 0000000..fe9bbed --- /dev/null +++ b/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) => { + 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] + + }) + + } + +} diff --git a/wg_dashboard_frontend/src/app/page/user/edit/edit.component.html b/wg_dashboard_frontend/src/app/page/user/edit/edit.component.html index dc50960..0dc3a3f 100644 --- a/wg_dashboard_frontend/src/app/page/user/edit/edit.component.html +++ b/wg_dashboard_frontend/src/app/page/user/edit/edit.component.html @@ -1,5 +1,5 @@ -
-
+
+
@@ -47,6 +47,9 @@
+
+ +
diff --git a/wg_dashboard_frontend/src/app/page/user/edit/edit.component.scss b/wg_dashboard_frontend/src/app/page/user/edit/edit.component.scss index e69de29..fd81a9f 100644 --- a/wg_dashboard_frontend/src/app/page/user/edit/edit.component.scss +++ b/wg_dashboard_frontend/src/app/page/user/edit/edit.component.scss @@ -0,0 +1,3 @@ +.user-edit-component{ + padding: 20px; +} diff --git a/wg_dashboard_frontend/src/app/page/user/edit/edit.component.ts b/wg_dashboard_frontend/src/app/page/user/edit/edit.component.ts index aef49ba..a7de58a 100644 --- a/wg_dashboard_frontend/src/app/page/user/edit/edit.component.ts +++ b/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 { AuthService } from '@services/*'; import { Router } from '@angular/router'; +import {ServerService} from "../../../services/server.service"; @Component({ selector: 'app-edit', @@ -23,8 +24,10 @@ export class EditComponent implements OnInit { public user: any; public error: string; - constructor(private authService: AuthService, - private router: Router) { + constructor( + private authService: AuthService, + private router: Router + ) { } diff --git a/wg_dashboard_frontend/src/app/services/server.service.ts b/wg_dashboard_frontend/src/app/services/server.service.ts index a4ed45b..fafe45a 100644 --- a/wg_dashboard_frontend/src/app/services/server.service.ts +++ b/wg_dashboard_frontend/src/app/services/server.service.ts @@ -16,6 +16,9 @@ export class ServerService { public serverURL = this.base + "server"; public peerURL = this.base + "peer"; 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) { @@ -91,4 +94,19 @@ export class ServerService { public serverStats(server: 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 + }); + + } }