Improvments to logging\auditing

This commit is contained in:
Ubuntu
2026-05-19 10:38:26 +00:00
31 changed files with 2101 additions and 131 deletions
@@ -0,0 +1,328 @@
"""Organization API Key management endpoints."""
from flask import g, request
from marshmallow import Schema, fields, validate, ValidationError
from gatehouse_app.api.v1 import api_v1_bp
from gatehouse_app.utils.response import api_response
from gatehouse_app.utils.decorators import login_required, require_admin, full_access_required
from gatehouse_app.models.organization import OrganizationApiKey
from gatehouse_app.services.organization_service import OrganizationService
from gatehouse_app.extensions import db
from gatehouse_app.utils.constants import AuditAction
from gatehouse_app.services.audit_service import AuditService
class ApiKeyCreateSchema(Schema):
"""Schema for creating an API key."""
name = fields.Str(required=True, validate=validate.Length(min=1, max=255))
description = fields.Str(allow_none=True, validate=validate.Length(max=2000))
class ApiKeyUpdateSchema(Schema):
"""Schema for updating an API key."""
name = fields.Str(validate=validate.Length(min=1, max=255))
description = fields.Str(allow_none=True, validate=validate.Length(max=2000))
@api_v1_bp.route("/organizations/<org_id>/api-keys", methods=["GET"])
@login_required
@require_admin
@full_access_required
def list_api_keys(org_id):
"""
List all API keys for an organization.
Only accessible by organization admins.
Args:
org_id: Organization ID
Returns:
200: List of API keys (without key values)
401: Not authenticated
403: Not an admin
404: Organization not found
"""
org = OrganizationService.get_organization_by_id(org_id)
# Check if user is an admin
from gatehouse_app.models.organization.organization_member import OrganizationMember
from gatehouse_app.utils.constants import OrganizationRole
membership = OrganizationMember.query.filter_by(
user_id=g.current_user.id,
organization_id=org_id,
deleted_at=None
).first()
if not membership or membership.role not in [OrganizationRole.OWNER, OrganizationRole.ADMIN]:
return api_response(
success=False,
message="You do not have permission to manage API keys",
status=403,
error_type="AUTHORIZATION_ERROR",
)
api_keys = OrganizationApiKey.query.filter_by(
organization_id=org_id,
deleted_at=None
).all()
return api_response(
data={
"api_keys": [k.to_dict() for k in api_keys],
"count": len(api_keys),
},
message="API keys retrieved successfully",
)
@api_v1_bp.route("/organizations/<org_id>/api-keys", methods=["POST"])
@login_required
@require_admin
@full_access_required
def create_api_key(org_id):
"""
Create a new API key for an organization.
Only accessible by organization admins.
The plain text key is returned only on creation and should be stored securely.
Args:
org_id: Organization ID
Request body:
name: API key name (required)
description: Optional description
Returns:
201: API key created successfully
400: Validation error
401: Not authenticated
403: Not an admin
404: Organization not found
"""
try:
org = OrganizationService.get_organization_by_id(org_id)
# Check if user is an admin
from gatehouse_app.models.organization.organization_member import OrganizationMember
from gatehouse_app.utils.constants import OrganizationRole
membership = OrganizationMember.query.filter_by(
user_id=g.current_user.id,
organization_id=org_id,
deleted_at=None
).first()
if not membership or membership.role not in [OrganizationRole.OWNER, OrganizationRole.ADMIN]:
return api_response(
success=False,
message="You do not have permission to create API keys",
status=403,
error_type="AUTHORIZATION_ERROR",
)
schema = ApiKeyCreateSchema()
data = schema.load(request.json or {})
# Create the API key
api_key, plain_key = OrganizationApiKey.create_key(
organization_id=org_id,
name=data["name"],
description=data.get("description"),
)
AuditService.log_action(
action=AuditAction.ORG_API_KEY_CREATED,
user_id=g.current_user.id,
organization_id=org_id,
resource_type="api_key",
resource_id=str(api_key.id),
description=f"API key '{api_key.name}' created",
)
# Return the key data with the plain text key (only on creation)
key_dict = api_key.to_dict()
key_dict["key"] = plain_key # Include plain text only on creation
return api_response(
data={"api_key": key_dict},
message="API key created successfully. Store the key value securely - it cannot be retrieved later.",
status=201,
)
except ValidationError as e:
return api_response(
success=False,
message="Validation failed",
status=400,
error_type="VALIDATION_ERROR",
error_details=e.messages,
)
@api_v1_bp.route("/organizations/<org_id>/api-keys/<key_id>", methods=["PATCH"])
@login_required
@require_admin
@full_access_required
def update_api_key(org_id, key_id):
"""
Update an API key.
Only accessible by organization admins.
Args:
org_id: Organization ID
key_id: API Key ID
Request body:
name: New name (optional)
description: New description (optional)
Returns:
200: API key updated successfully
400: Validation error
401: Not authenticated
403: Not an admin
404: Organization or API key not found
"""
try:
org = OrganizationService.get_organization_by_id(org_id)
# Check if user is an admin
from gatehouse_app.models.organization.organization_member import OrganizationMember
from gatehouse_app.utils.constants import OrganizationRole
membership = OrganizationMember.query.filter_by(
user_id=g.current_user.id,
organization_id=org_id,
deleted_at=None
).first()
if not membership or membership.role not in [OrganizationRole.OWNER, OrganizationRole.ADMIN]:
return api_response(
success=False,
message="You do not have permission to update API keys",
status=403,
error_type="AUTHORIZATION_ERROR",
)
api_key = OrganizationApiKey.query.filter_by(
id=key_id,
organization_id=org_id,
deleted_at=None
).first()
if not api_key:
return api_response(
success=False,
message="API key not found",
status=404,
error_type="NOT_FOUND",
)
schema = ApiKeyUpdateSchema()
data = schema.load(request.json or {})
# Update fields
if "name" in data:
api_key.name = data["name"]
if "description" in data:
api_key.description = data["description"]
api_key.save()
AuditService.log_action(
action=AuditAction.ORG_API_KEY_UPDATED,
user_id=g.current_user.id,
organization_id=org_id,
resource_type="api_key",
resource_id=str(api_key.id),
description=f"API key '{api_key.name}' updated",
)
return api_response(
data={"api_key": api_key.to_dict()},
message="API key updated successfully",
)
except ValidationError as e:
return api_response(
success=False,
message="Validation failed",
status=400,
error_type="VALIDATION_ERROR",
error_details=e.messages,
)
@api_v1_bp.route("/organizations/<org_id>/api-keys/<key_id>", methods=["DELETE"])
@login_required
@require_admin
@full_access_required
def delete_api_key(org_id, key_id):
"""
Delete/revoke an API key.
Only accessible by organization admins.
Args:
org_id: Organization ID
key_id: API Key ID
Returns:
200: API key deleted successfully
401: Not authenticated
403: Not an admin
404: Organization or API key not found
"""
org = OrganizationService.get_organization_by_id(org_id)
# Check if user is an admin
from gatehouse_app.models.organization.organization_member import OrganizationMember
from gatehouse_app.utils.constants import OrganizationRole
membership = OrganizationMember.query.filter_by(
user_id=g.current_user.id,
organization_id=org_id,
deleted_at=None
).first()
if not membership or membership.role not in [OrganizationRole.OWNER, OrganizationRole.ADMIN]:
return api_response(
success=False,
message="You do not have permission to delete API keys",
status=403,
error_type="AUTHORIZATION_ERROR",
)
api_key = OrganizationApiKey.query.filter_by(
id=key_id,
organization_id=org_id,
deleted_at=None
).first()
if not api_key:
return api_response(
success=False,
message="API key not found",
status=404,
error_type="NOT_FOUND",
)
# Soft delete the API key
api_key.delete(soft=True)
AuditService.log_action(
action=AuditAction.ORG_API_KEY_DELETED,
user_id=g.current_user.id,
organization_id=org_id,
resource_type="api_key",
resource_id=str(api_key.id),
description=f"API key '{api_key.name}' deleted",
)
return api_response(
message="API key deleted successfully",
)
+19
View File
@@ -68,6 +68,16 @@ def update_org_ca(org_id, ca_id):
ca.max_cert_validity_hours = data["max_cert_validity_hours"]
db.session.commit()
AuditService.log_action(
action=AuditAction.CA_UPDATED,
user_id=g.current_user.id,
organization_id=org_id,
resource_type="CA",
resource_id=ca_id,
description=f"CA '{ca.name}' updated",
)
return api_response(data={"ca": ca.to_dict()}, message="CA updated successfully")
except ValidationError as e:
return api_response(success=False, message="Validation failed", status=400, error_type="VALIDATION_ERROR", error_details=e.messages)
@@ -152,6 +162,15 @@ def create_org_ca(org_id):
return api_response(success=False, message="A CA with that name already exists in this organization (it may have been recently deleted — choose a different name).", status=400, error_type="DUPLICATE_NAME")
raise
AuditService.log_action(
action=AuditAction.CA_CREATED,
user_id=g.current_user.id,
organization_id=org_id,
resource_type="CA",
resource_id=str(ca.id),
description=f"CA '{ca.name}' created",
)
return api_response(data={"ca": ca.to_dict()}, message="CA created successfully", status=201)
except MaValidationError as e:
return api_response(success=False, message="Validation failed", status=400, error_type="VALIDATION_ERROR", error_details=e.messages)
@@ -5,6 +5,8 @@ from gatehouse_app.api.v1 import api_v1_bp
from gatehouse_app.utils.response import api_response
from gatehouse_app.utils.decorators import login_required, require_admin, full_access_required
from gatehouse_app.extensions import db, bcrypt
from gatehouse_app.utils.constants import AuditAction
from gatehouse_app.services.audit_service import AuditService
@api_v1_bp.route("/organizations/<org_id>/clients", methods=["GET"])
@@ -28,6 +30,7 @@ def list_org_clients(org_id):
"redirect_uris": c.redirect_uris,
"scopes": c.scopes,
"grant_types": c.grant_types,
"allowed_cors_origins": c.allowed_cors_origins,
"is_active": c.is_active,
"created_at": c.created_at.isoformat() + "Z",
}
@@ -78,6 +81,15 @@ def create_org_client(org_id):
db.session.add(client)
db.session.commit()
AuditService.log_action(
action=AuditAction.ORG_CLIENT_CREATED,
user_id=g.current_user.id,
organization_id=org_id,
resource_type="oidc_client",
resource_id=str(client.id),
description=f"OIDC client '{client.name}' created",
)
return api_response(
data={
"client": {
@@ -125,6 +137,15 @@ def update_org_client(org_id, client_id):
db.session.commit()
AuditService.log_action(
action=AuditAction.ORG_CLIENT_UPDATED,
user_id=g.current_user.id,
organization_id=org_id,
resource_type="oidc_client",
resource_id=str(client.id),
description=f"OIDC client '{client.name}' updated",
)
return api_response(
data={
"client": {
@@ -154,4 +175,14 @@ def delete_org_client(org_id, client_id):
client.is_active = False
db.session.commit()
AuditService.log_action(
action=AuditAction.ORG_CLIENT_DEACTIVATED,
user_id=g.current_user.id,
organization_id=org_id,
resource_type="oidc_client",
resource_id=str(client.id),
description=f"OIDC client '{client.name}' deactivated",
)
return api_response(data={}, message="Client deactivated successfully")
@@ -7,6 +7,8 @@ from gatehouse_app.utils.response import api_response
from gatehouse_app.utils.decorators import login_required, require_admin, full_access_required
from gatehouse_app.schemas.organization_schema import OrganizationCreateSchema, OrganizationUpdateSchema
from gatehouse_app.services.organization_service import OrganizationService
from gatehouse_app.utils.constants import AuditAction
from gatehouse_app.services.audit_service import AuditService
@api_v1_bp.route("/organizations", methods=["POST"])
@@ -32,6 +34,14 @@ def create_organization():
description=data.get("description"),
logo_url=data.get("logo_url"),
)
AuditService.log_action(
action=AuditAction.ORG_CREATE,
user_id=g.current_user.id,
organization_id=org.id,
resource_type="organization",
resource_id=str(org.id),
description=f"Organization '{org.name}' created",
)
return api_response(data={"organization": org.to_dict()}, message="Organization created successfully", status=201)
except ValidationError as e:
return api_response(success=False, message="Validation failed", status=400, error_type="VALIDATION_ERROR", error_details=e.messages)
@@ -60,6 +70,14 @@ def update_organization(org_id):
data = schema.load(request.json)
org = OrganizationService.get_organization_by_id(org_id)
org = OrganizationService.update_organization(org=org, user_id=g.current_user.id, **data)
AuditService.log_action(
action=AuditAction.ORG_UPDATE,
user_id=g.current_user.id,
organization_id=org.id,
resource_type="organization",
resource_id=str(org.id),
description=f"Organization '{org.name}' updated",
)
return api_response(data={"organization": org.to_dict()}, message="Organization updated successfully")
except ValidationError as e:
return api_response(success=False, message="Validation failed", status=400, error_type="VALIDATION_ERROR", error_details=e.messages)
@@ -92,4 +110,12 @@ def delete_organization(org_id):
)
OrganizationService.force_delete_organization(org=org, user_id=caller.id)
AuditService.log_action(
action=AuditAction.ORG_DELETE,
user_id=caller.id,
organization_id=org.id,
resource_type="organization",
resource_id=str(org.id),
description=f"Organization '{org.name}' deleted",
)
return api_response(message="Organization deleted successfully")
@@ -136,6 +136,15 @@ def cancel_org_invite(org_id, invite_id):
return api_response(success=False, message="Invite not found", status=404)
invite.delete(soft=True)
AuditService.log_action(
action=AuditAction.ORG_INVITE_CANCELLED,
user_id=g.current_user.id,
organization_id=org_id,
resource_type="org_invite",
resource_id=invite.id,
metadata={"invited_email": invite.email, "role": invite.role},
description=f"Invitation for {invite.email} cancelled",
)
return api_response(data={}, message="Invite cancelled")
@@ -243,6 +252,30 @@ def accept_invite(token):
invite.accept()
if invite.invited_by and invite.invited_by.email:
from gatehouse_app.services.email_templates import build_invite_accepted_html
from gatehouse_app.services.notification_service import NotificationService
member_display = user.full_name or user.email
inviter_display = invite.invited_by.full_name or invite.invited_by.email
org_link = f"{current_app.config.get('APP_URL', '')}/organizations/{invite.organization_id}"
html_body = build_invite_accepted_html(
inviter_name=inviter_display,
member_name=member_display,
member_email=user.email,
org_name=invite.organization.name,
role=invite.role,
org_link=org_link,
)
NotificationService._send_email_async(
to_address=invite.invited_by.email,
subject=f"{member_display} accepted your invitation to {invite.organization.name}",
body=f"{member_display} has accepted your invitation to join {invite.organization.name} on Secuird.",
html_body=html_body,
)
has_webauthn = user.has_webauthn_enabled()
has_totp = user.has_totp_enabled()
+35 -1
View File
@@ -7,7 +7,8 @@ from gatehouse_app.utils.decorators import login_required, require_admin, full_a
from gatehouse_app.schemas.organization_schema import InviteMemberSchema, UpdateMemberRoleSchema
from gatehouse_app.services.organization_service import OrganizationService
from gatehouse_app.services.user_service import UserService
from gatehouse_app.utils.constants import OrganizationRole
from gatehouse_app.utils.constants import AuditAction, OrganizationRole
from gatehouse_app.services.audit_service import AuditService
@api_v1_bp.route("/organizations/<org_id>/members", methods=["GET"])
@@ -43,6 +44,14 @@ def add_organization_member(org_id):
role = OrganizationRole(data["role"])
member = OrganizationService.add_member(org=org, user_id=user.id, role=role, inviter_id=g.current_user.id)
AuditService.log_action(
action=AuditAction.ORG_MEMBER_ADD,
user_id=g.current_user.id,
organization_id=org.id,
resource_type="user",
resource_id=str(user.id),
description=f"Added user {user.email} to organization with role {role.value}",
)
member_dict = member.to_dict()
member_dict["user"] = user.to_dict()
return api_response(data={"member": member_dict}, message="Member added successfully", status=201)
@@ -60,6 +69,14 @@ def remove_organization_member(org_id, user_id):
OrganizationService.remove_member(org=org, user_id=user_id, remover_id=g.current_user.id)
except ValueError as e:
return api_response(success=False, message=str(e), status=403, error_type="OWNER_PROTECTION")
AuditService.log_action(
action=AuditAction.ORG_MEMBER_REMOVE,
user_id=g.current_user.id,
organization_id=org.id,
resource_type="user",
resource_id=str(user_id),
description=f"Removed user {user_id} from organization",
)
return api_response(message="Member removed successfully")
@@ -74,6 +91,14 @@ def update_member_role(org_id, user_id):
org = OrganizationService.get_organization_by_id(org_id)
new_role = OrganizationRole(data["role"])
member = OrganizationService.update_member_role(org=org, user_id=user_id, new_role=new_role, updater_id=g.current_user.id)
AuditService.log_action(
action=AuditAction.ORG_MEMBER_ROLE_CHANGE,
user_id=g.current_user.id,
organization_id=org.id,
resource_type="user",
resource_id=str(user_id),
description=f"Changed role for user {user_id} to {new_role.value}",
)
member_dict = member.to_dict()
member_dict["user"] = member.user.to_dict()
return api_response(data={"member": member_dict}, message="Member role updated successfully")
@@ -180,4 +205,13 @@ def send_mfa_reminder(org_id, user_id):
html_body=html_body,
)
AuditService.log_action(
action=AuditAction.ORG_MFA_REMINDER_SENT,
user_id=g.current_user.id,
organization_id=org_id,
resource_type="user",
resource_id=str(user_id),
description=f"MFA reminder sent to {user.email}",
)
return api_response(data={}, message="Reminder sent successfully")
+22 -1
View File
@@ -3,8 +3,9 @@ from flask import g, request
from gatehouse_app.api.v1 import api_v1_bp
from gatehouse_app.utils.response import api_response
from gatehouse_app.utils.decorators import login_required, require_admin, full_access_required
from gatehouse_app.utils.constants import OrganizationRole
from gatehouse_app.utils.constants import AuditAction, OrganizationRole
from gatehouse_app.extensions import db
from gatehouse_app.services.audit_service import AuditService
@api_v1_bp.route("/organizations/<org_id>/roles", methods=["GET"])
@@ -59,6 +60,16 @@ def assign_role_to_member(org_id, role_name):
membership.role = new_role
db.session.commit()
AuditService.log_action(
action=AuditAction.ORG_MEMBER_ROLE_CHANGE,
user_id=g.current_user.id,
organization_id=org_id,
resource_type="user",
resource_id=str(target_user_id),
description=f"Role changed to {new_role.value} for user {target_user_id}",
)
return api_response(data={"user_id": target_user_id, "role": new_role.value}, message=f"Role updated to {new_role.value}")
@@ -82,4 +93,14 @@ def remove_role_from_member(org_id, role_name, user_id):
org = OrganizationService.get_organization_by_id(org_id)
OrganizationService.remove_member(org=org, user_id=user_id, remover_id=g.current_user.id)
AuditService.log_action(
action=AuditAction.ORG_MEMBER_REMOVE,
user_id=g.current_user.id,
organization_id=org_id,
resource_type="user",
resource_id=str(user_id),
description=f"Member {user_id} removed from organization via role removal",
)
return api_response(data={"user_id": user_id}, message="Member removed from organization")