From c98a33eda505baf9c69391805694ca63d1384aee Mon Sep 17 00:00:00 2001 From: Lloyd Date: Thu, 18 Dec 2025 15:02:23 +0000 Subject: [PATCH] Refactor security configuration handling: update LoginHelper to retrieve security settings from the correct config path and adjust max_clients default value; modify APIEndpoints to ensure distinct admin and guest passwords, and enhance identity key validation logic in API responses. --- repeater/handler_helpers/login.py | 6 +- repeater/main.py | 3 +- repeater/web/api_endpoints.py | 27 ++++++++- repeater/web/openapi.yaml | 95 ++++++++++++++++++++++++------- 4 files changed, 102 insertions(+), 29 deletions(-) diff --git a/repeater/handler_helpers/login.py b/repeater/handler_helpers/login.py index e57dd55..3cb5307 100644 --- a/repeater/handler_helpers/login.py +++ b/repeater/handler_helpers/login.py @@ -55,10 +55,10 @@ class LoginHelper: "allow_read_only": settings.get("allow_read_only", True), } else: - # Repeater uses security from its config - security = config.get("security", {}) + # Repeater uses security from repeater.security in config + security = config.get("repeater", {}).get("security", {}) final_security = { - "max_clients": security.get("max_clients", 50), + "max_clients": security.get("max_clients", 10), "admin_password": security.get("admin_password", "admin123"), "guest_password": security.get("guest_password", "guest123"), "allow_read_only": security.get("allow_read_only", True), diff --git a/repeater/main.py b/repeater/main.py index f48cac4..0855d76 100644 --- a/repeater/main.py +++ b/repeater/main.py @@ -168,12 +168,11 @@ class RepeaterDaemon: ) # Register default repeater identity - repeater_config = self.config.get("repeater", {}) self.login_helper.register_identity( name="repeater", identity=self.local_identity, identity_type="repeater", - config=repeater_config # Pass repeater config (includes security settings) + config=self.config # Pass full config so repeater can access top-level security section ) # Register room server identities with their configs diff --git a/repeater/web/api_endpoints.py b/repeater/web/api_endpoints.py index b13986d..955304e 100644 --- a/repeater/web/api_endpoints.py +++ b/repeater/web/api_endpoints.py @@ -1141,6 +1141,12 @@ class APIEndpoints: if not name: return self._error("Missing required field: name") + # Validate passwords are different if both provided + admin_pw = settings.get("admin_password") + guest_pw = settings.get("guest_password") + if admin_pw and guest_pw and admin_pw == guest_pw: + return self._error("admin_password and guest_password must be different") + # Auto-generate identity key if not provided key_was_generated = False if not identity_key: @@ -1301,15 +1307,32 @@ class APIEndpoints: return self._error(f"Identity with name '{new_name}' already exists") identity["name"] = new_name - # Only update identity_key if a non-empty value is provided + # Only update identity_key if a valid full key is provided + # Silently reject truncated keys (containing "...") or invalid hex strings if "identity_key" in data and data["identity_key"]: - identity["identity_key"] = data["identity_key"] + new_key = data["identity_key"] + # Check if it's a truncated key (contains "...") or not a valid 64-char hex string + if "..." not in new_key and len(new_key) == 64: + try: + # Validate it's proper hex + bytes.fromhex(new_key) + identity["identity_key"] = new_key + logger.info(f"Updated identity_key for '{name}'") + except ValueError: + # Invalid hex, silently ignore + pass if "settings" in data: # Merge settings if "settings" not in identity: identity["settings"] = {} identity["settings"].update(data["settings"]) + + # Validate passwords are different if both are now set + admin_pw = identity["settings"].get("admin_password") + guest_pw = identity["settings"].get("guest_password") + if admin_pw and guest_pw and admin_pw == guest_pw: + return self._error("admin_password and guest_password must be different") # Save to config room_servers[identity_index] = identity diff --git a/repeater/web/openapi.yaml b/repeater/web/openapi.yaml index 5a3d3fa..f9ea39e 100644 --- a/repeater/web/openapi.yaml +++ b/repeater/web/openapi.yaml @@ -699,7 +699,7 @@ paths: get: tags: [ACL] summary: List ACL clients - description: Get list of clients in access control list + description: Get list of authenticated clients in access control list for an identity parameters: - name: identity_hash in: query @@ -730,7 +730,32 @@ paths: clients: type: array items: - $ref: '#/components/schemas/ACLEntry' + $ref: '#/components/schemas/ACLClient' + count: + type: integer + description: Number of clients returned + filter: + type: string + nullable: true + description: Filter applied (if any) + examples: + success: + value: + success: true + data: + clients: + - public_key: "03ccf3bb0bed9a51...21416fff" + public_key_full: "03ccf3bb0bed9a5109868a1e33ed020519aab6dbb30e42df3b11a21d21416fff" + address: "e1" + permissions: "admin" + last_activity: 1766065148 + last_login_success: 1766065146 + last_timestamp: 1766065146 + identity_name: "rrrrr" + identity_type: "room_server" + identity_hash: "0xC5" + count: 1 + filter: null /acl_remove_client: post: @@ -1441,32 +1466,58 @@ components: description: Maximum messages to keep (room_server only, hard limit 32) example: 32 - ACLEntry: + ACLClient: type: object - required: [action, network_address, port] + required: [public_key, public_key_full, address, permissions] properties: - action: + public_key: type: string - enum: [allow, deny] + description: Truncated public key for display (first 24 and last 8 chars) + example: "03ccf3bb0bed9a51...21416fff" + public_key_full: + type: string + pattern: '^[0-9a-fA-F]{64}$' + description: Full client public key (64 hex chars) + example: "03ccf3bb0bed9a5109868a1e33ed020519aab6dbb30e42df3b11a21d21416fff" + address: + type: string + description: Client address identifier + example: "e1" + permissions: + type: string + enum: [admin, guest, read_only] description: | - - allow: Permit packets from this address - - deny: Block packets from this address - example: allow - network_address: - type: string - description: IP address or CIDR range (e.g., 192.168.1.0/24) - example: "192.168.1.0/24" - port: + Client permission level: + - admin: Full access + - guest: Limited access + - read_only: Read-only access + example: "admin" + last_activity: type: integer - minimum: 0 - maximum: 65535 - description: Source port (0 = any port) - example: 0 - comment: + description: Unix timestamp of last activity + example: 1766065148 + last_login_success: + type: integer + description: Unix timestamp of last successful login + example: 1766065146 + last_timestamp: + type: integer + description: Unix timestamp from last client message + example: 1766065146 + identity_name: type: string - maxLength: 200 - description: Optional description - example: "Allow local network" + description: Name of the identity this client is authenticated to + example: "rrrrr" + identity_type: + type: string + enum: [repeater, room_server] + description: Type of identity + example: "room_server" + identity_hash: + type: string + pattern: '^0x[0-9a-fA-F]{2}$' + description: Hash of the identity + example: "0xC5" securitySchemes: # Future: API key authentication