feat: basic api-key based authentication

This commit is contained in:
DaanSelen
2026-03-03 16:01:00 +01:00
parent f99a24005a
commit c4d36c48bc
9 changed files with 205 additions and 62 deletions
+4 -3
View File
@@ -9,17 +9,17 @@ peer_keep_alive = 21
[Server] [Server]
hostname = 0.0.0.0 hostname = 0.0.0.0
port = 10086 port = 10086
debug_enabled = true debug_enabled = True
wg_conf_path = /etc/wireguard wg_conf_path = /etc/wireguard
awg_conf_path = /etc/amnezia/amneziawg awg_conf_path = /etc/amnezia/amneziawg
app_prefix = app_prefix =
auth_req = true auth_req = True
version = v5.0.0 version = v5.0.0
dashboard_refresh_interval = 60000 dashboard_refresh_interval = 60000
dashboard_peer_list_display = grid dashboard_peer_list_display = grid
dashboard_sort = status dashboard_sort = status
dashboard_theme = dark dashboard_theme = dark
wgdashboard_apikey = false wgdashboard_apikey = true
dashboard_language = en-US dashboard_language = en-US
log_level = DEBUG log_level = DEBUG
@@ -31,6 +31,7 @@ totp_verified = false
totp_key = ZSI2Z4QHSGVAK6TFVMZXORFDWKHDN4TQ totp_key = ZSI2Z4QHSGVAK6TFVMZXORFDWKHDN4TQ
[Other] [Other]
welcome_session = false
[Database] [Database]
type = sqlite type = sqlite
+10 -2
View File
@@ -6,6 +6,7 @@ from logging.config import dictConfig
import flask import flask
import json import json
import os import os
import secrets
from modules.config.reader import reader from modules.config.reader import reader
from modules.database.database import database from modules.database.database import database
@@ -35,11 +36,17 @@ if __name__ == '__main__':
ok, engine, session = database.create_session(config_database) ok, engine, session = database.create_session(config_database)
ok = database.ensure_contents(engine) ok = database.ensure_contents(engine)
prefix = config_server.get('app_prefix', '')
# Configure the Flask app # Configure the Flask app
app = flask.Flask("WGDashboard", template_folder=os.path.abspath("./static/dist/WGDashboardAdmin")) app = flask.Flask("WGDashboard", template_folder=os.path.abspath("./static/dist/WGDashboardAdmin"))
app.register_blueprint(routes) app.register_blueprint(routes, url_prefix=prefix)
app.wgd_config = config_contents app.wgd_config = config_contents
app.secret_key = secrets.token_urlsafe(64)
app.config['SESSION_TYPE'] = 'filesystem'
app.engine = engine app.engine = engine
app.db_session = session app.db_session = session
@@ -50,4 +57,5 @@ if __name__ == '__main__':
debug=debug_enabled, debug=debug_enabled,
host=hostname, host=hostname,
port=port, port=port,
use_reloader=False) use_reloader=False
)
+9
View File
@@ -49,6 +49,15 @@ class checks():
config_dict = {} config_dict = {}
for section in config.sections(): for section in config.sections():
items = dict(config.items(section)) items = dict(config.items(section))
for key, value in items.items():
if value.strip().lower() == 'true':
items[key] = True
elif value.strip().lower() == 'false':
items[key] = False
else:
items[key] = value
config_dict[section] = items config_dict[section] = items
return True, config_dict return True, config_dict
+50 -4
View File
@@ -3,14 +3,60 @@
from datetime import datetime from datetime import datetime
import sqlalchemy.orm import sqlalchemy.orm
import json
from .schema import Base from .schema import Base
from .schema import Apikeys from .schema import Apikeys
class functions(): class functions():
def retrieve_api_keys(session: sqlalchemy.orm.Session) -> dict: def retrieve_api_keys(session: sqlalchemy.orm.Session) -> tuple[list[dict], list[dict]]:
""" AI GENERATED
Retrieve all API keys from the database and separate them into valid and expired keys.
Parameters
----------
session : sqlalchemy.orm.Session
An active SQLAlchemy session connected to the database.
Returns
-------
tuple[list[dict], list[dict]]
A tuple containing two lists of dictionaries:
- The first list contains valid API keys (not expired at the time of the call).
- The second list contains expired API keys (already expired at the time of the call).
Each dictionary has the following keys:
- 'id': int, the primary key of the API key record
- 'key': str, the API key string
- 'creation': str, timestamp of when the key was created
- 'expiration': str or None, timestamp of when the key expires, or None if no expiration
Notes
-----
- Keys with no expiration date are considered valid.
- Expiration comparison is performed against the current system time.
"""
time_now = datetime.now() time_now = datetime.now()
stored_keys = session.query(Apikeys).all()
api_keys = session.query(Apikeys).all() expired_keys = []
print(dict(api_keys)) valid_keys = []
return {} for key in stored_keys:
expiration_date = None
if key.key_expiration:
expiration_date = datetime.strptime(key.key_expiration, "%Y-%m-%d %H:%M:%S")
key_dict = key.to_dict()
if expiration_date is None or expiration_date > time_now:
valid_keys.append(key_dict)
else:
expired_keys.append(key_dict)
print("VALID:", json.dumps(valid_keys,indent=4))
print("INVALID", json.dumps(expired_keys, indent=4))
return valid_keys, expired_keys
+12 -4
View File
@@ -24,11 +24,19 @@ class User(Base):
class Apikeys(Base): class Apikeys(Base):
__tablename__ = 'apikeys' __tablename__ = 'apikeys'
apikey_id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True, index=True) key_id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True, index=True)
apikey_data = sqlalchemy.Column(sqlalchemy.String) key_data = sqlalchemy.Column(sqlalchemy.String)
apikey_creation = sqlalchemy.Column(sqlalchemy.String) key_creation = sqlalchemy.Column(sqlalchemy.String)
apikey_expiration = sqlalchemy.Column(sqlalchemy.String) key_expiration = sqlalchemy.Column(sqlalchemy.String)
def to_dict(self):
return {
'id': self.key_id,
'key': self.key_data,
'creation': self.key_creation,
'expiration': self.key_expiration,
}
class Wireguard(Base): class Wireguard(Base):
__tablename__ = 'wireguard_interfaces' __tablename__ = 'wireguard_interfaces'
+9 -6
View File
@@ -1,12 +1,15 @@
#!/bin/env python3 #!/bin/env python3
import flask import flask
import json
def make_resp_obj(success: bool = True, message: str = "", data: dict = {}, http_code: int = 200) -> flask.wrappers.Response: def make_resp_obj(message: str = "", data: dict = {}, http_code: int = 200) -> flask.wrappers.Response:
response = flask.make_response({ resp_json_data = json.dumps({
"message": message, 'message': message,
"status": success, 'data': data
"data": data })
}, http_code)
response = flask.make_response(resp_json_data, http_code)
response.mimetype = 'application/json'
return response return response
+80 -36
View File
@@ -1,57 +1,101 @@
#!/bin/env python3 #!/bin/env python3
import logging as log
import flask import flask
import json
from .response import make_resp_obj from .response import make_resp_obj
from .utilities import helpers
from ..database.functions import functions from ..database.functions import functions
from ..utilities.utilities import utilities from ..utilities.utilities import utilities as util
routes = flask.Blueprint("routes", __name__) routes = flask.Blueprint("routes", __name__)
white_list = [
"/client",
"/static/",
"/fileDownload",
"authenticate",
"getDashboardConfiguration",
"getDashboardTheme",
"getDashboardVersion",
"sharePeer/get",
"isTotpEnabled",
"locale",
"validateAuthentication",
]
@routes.before_request @routes.before_request
def auth_required(): def auth_required():
if flask.request.method.lower() == "options": if flask.request.method.lower() == "options":
return make_resp_obj(True, "", flask.jsonify({"status": True}), 200) return make_resp_obj("", {"status": True}, 200)
ok, config_server = utilities.filter_config(flask.current_app.wgd_config, 'SERVER') ok, config_server = util.filter_config(flask.current_app.wgd_config, 'SERVER')
if not ok: if not ok:
return make_resp_obj(False, "Internal Error", {}, 500) return make_resp_obj("Internal error", {}, 500)
auth_required = config_server.get('auth_req', True) # Set to true for a safe default
if not auth_required:
return
whiteList = [
'/client',
'/static/',
'/fileDownload',
'validateAuthentication',
'authenticate',
'getDashboardConfiguration',
'getDashboardTheme',
'getDashboardVersion',
'sharePeer/get',
'isTotpEnabled',
'locale'
]
request_path = flask.request.path
http_headers = flask.request.headers
api_key = http_headers.get("wgdashboard-apikey")
auth_required_flag = config_server.get('auth_req', True)
api_key_enabled = config_server.get("wgdashboard_apikey", False) api_key_enabled = config_server.get("wgdashboard_apikey", False)
registered_api_keys = functions.retrieve_api_keys(flask.current_app.db_session)
if not api_key: if not auth_required_flag:
response = make_resp_obj() return
return response
@routes.route("/") path = flask.request.path
api_key = flask.request.headers.get("wgdashboard-apikey")
if api_key and api_key_enabled:
if helpers.is_valid_api_key(api_key):
return
else:
return make_resp_obj("WGDashboard API-key does not exist or is invalid/expired", {}, 401)
if flask.session.get("role") == "admin":
return
if helpers.is_path_allowed(path, white_list, flask.session):
return
return make_resp_obj("Unauthorized access", {}, 401)
@routes.route('/api/authenticate', methods=["POST"])
def api_authenticate():
ok, config_server = util.filter_config(flask.current_app.wgd_config, 'SERVER')
if not ok:
return make_resp_obj("Internal error", {}, 500)
auth_required_flag = config_server.get('auth_req', True)
if not auth_required_flag:
_, config_other = util.filter_config(flask.current_app.wgd_config, 'OTHER')
return make_resp_obj(
"Login successful, no authentication required",
{"welcome_session": config_other.get("welcome_session", False)},
200
)
data = request.get_json()
if not data:
return make_resp_obj("Invalid request body", {}, 400)
return make_resp_obj("Authentication required", {}, 401)
@routes.route('/', methods=["GET"])
def index(): def index():
return make_resp_obj(True, "/ Endpoint", flask.jsonify({"message": "Hello from routes file!"}), 200) return make_resp_obj(
"Pong from the /",
{},
200
)
@routes.route("/health") @routes.route('/health', methods=["GET"])
@routes.route("/healthz") @routes.route('/healthz', methods=["GET"])
def health(): def health():
return make_resp_obj(True, "Health Endpoint", flask.jsonify({"status": "ok"}), 200) return make_resp_obj(
"Health Endpoint",
{"status": "ok"},
200
)
+27
View File
@@ -0,0 +1,27 @@
#!/bin/env python3
import flask
from ..database.functions import functions
class helpers():
@staticmethod
def is_valid_api_key(api_key: str) -> bool:
valid_keys, _ = functions.retrieve_api_keys(flask.current_app.db_session)
valid_key_strings = []
for key in valid_keys:
valid_key_strings.append(key["key"])
return api_key in valid_key_strings
@staticmethod
def is_path_allowed(path: str, white_list: list[str], session_data: dict) -> bool:
# Allow if session has admin role
if session_data.get("role") == "admin" and "username" in session_data:
return True
# Check white list
for p in white_list:
if p in path:
return True
return False
+4 -7
View File
@@ -10,12 +10,10 @@ class utilities():
''' '''
Helper function to grab a specific part of the config Helper function to grab a specific part of the config
''' '''
for section_name, section_values in config_contents.items():
log.debug(f'searching for section: {filter_keyword}') if str(section_name).lower() == filter_keyword.lower():
for section in config_contents: if isinstance(section_values, dict):
if str(section).lower() == filter_keyword.lower(): return True, dict(section_values)
return True, dict(config_contents[section].items())
return False, {} return False, {}
@staticmethod @staticmethod
@@ -24,7 +22,6 @@ class utilities():
Make the directory if it does not exist yet, return only true if the directory was missing and created. Make the directory if it does not exist yet, return only true if the directory was missing and created.
''' '''
log.debug(f'checking if the directory at: {path} exists')
if os.path.exists(path) and os.path.isdir(path): if os.path.exists(path) and os.path.isdir(path):
return True return True