fix: audit logs for tenants, user, roles, modules and environment and modules of tenant and modules.
2 migration scripts
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
"""create audit logs table
|
||||
|
||||
Revision ID: 73b754d5b2c5
|
||||
Revises: c37ba6143f83
|
||||
Create Date: 2026-03-30 15:43:16.849980
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '73b754d5b2c5'
|
||||
down_revision: Union[str, Sequence[str], None] = 'c37ba6143f83'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('audit_logs',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('module_name', sa.String(length=100), nullable=False),
|
||||
sa.Column('action_type', sa.String(length=20), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_audit_logs_id'), 'audit_logs', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_audit_logs_module_name'), 'audit_logs', ['module_name'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_audit_logs_module_name'), table_name='audit_logs')
|
||||
op.drop_index(op.f('ix_audit_logs_id'), table_name='audit_logs')
|
||||
op.drop_table('audit_logs')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,50 @@
|
||||
"""add_audit_log_extra_columns
|
||||
|
||||
Revision ID: f9cf173f48f9
|
||||
Revises: 73b754d5b2c5
|
||||
Create Date: 2026-03-31 13:57:11.328101
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'f9cf173f48f9'
|
||||
down_revision: Union[str, Sequence[str], None] = '73b754d5b2c5'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('audit_logs', sa.Column('entity_id', sa.String(length=255), nullable=True))
|
||||
op.add_column('audit_logs', sa.Column('entity_name', sa.String(length=255), nullable=True))
|
||||
op.add_column('audit_logs', sa.Column('performed_by_id', sa.UUID(), nullable=True))
|
||||
op.add_column('audit_logs', sa.Column('performed_by_email', sa.String(length=255), nullable=True))
|
||||
op.add_column('audit_logs', sa.Column('ip_address', sa.String(length=45), nullable=True))
|
||||
op.add_column('audit_logs', sa.Column('old_values', postgresql.JSONB(astext_type=sa.Text()), nullable=True))
|
||||
op.add_column('audit_logs', sa.Column('new_values', postgresql.JSONB(astext_type=sa.Text()), nullable=True))
|
||||
op.create_index(op.f('ix_audit_logs_action_type'), 'audit_logs', ['action_type'], unique=False)
|
||||
op.create_index(op.f('ix_audit_logs_created_at'), 'audit_logs', ['created_at'], unique=False)
|
||||
op.create_index(op.f('ix_audit_logs_performed_by_email'), 'audit_logs', ['performed_by_email'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_audit_logs_performed_by_email'), table_name='audit_logs')
|
||||
op.drop_index(op.f('ix_audit_logs_created_at'), table_name='audit_logs')
|
||||
op.drop_index(op.f('ix_audit_logs_action_type'), table_name='audit_logs')
|
||||
op.drop_column('audit_logs', 'new_values')
|
||||
op.drop_column('audit_logs', 'old_values')
|
||||
op.drop_column('audit_logs', 'ip_address')
|
||||
op.drop_column('audit_logs', 'performed_by_email')
|
||||
op.drop_column('audit_logs', 'performed_by_id')
|
||||
op.drop_column('audit_logs', 'entity_name')
|
||||
op.drop_column('audit_logs', 'entity_id')
|
||||
# ### end Alembic commands ###
|
||||
+4
-1
@@ -6,6 +6,7 @@ import logging
|
||||
from sqlalchemy import text
|
||||
from app.config.settings import settings
|
||||
from app.config.database import engine
|
||||
from app.routes.admin import audit_logs
|
||||
import app.models.auth.user_model
|
||||
import app.models.auth.role_model
|
||||
import app.models.auth.tenant_model
|
||||
@@ -16,6 +17,7 @@ import app.models.auth.tenant_module_model
|
||||
import app.models.auth.sso_grant_model
|
||||
import app.models.auth.access_model
|
||||
import app.models.system.event_log_model
|
||||
import app.models.system.audit_log
|
||||
import asyncio
|
||||
from app.services.auth.event_service import EventService
|
||||
from app.config.database import SessionLocal
|
||||
@@ -206,7 +208,8 @@ def create_app() -> FastAPI:
|
||||
from app.routes.admin.modules import router as admin_modules_router
|
||||
from app.routes.admin.module_environments import router as admin_module_env_router
|
||||
from app.routes.admin.tenant_modules import router as admin_tenant_modules_router
|
||||
|
||||
|
||||
app.include_router(audit_logs.router, prefix="/api/admin/audit-logs", tags=["Admin - Audit Logs"])
|
||||
app.include_router(admin_modules_router, prefix="/api/admin/modules", tags=["Admin - Modules"])
|
||||
app.include_router(admin_module_env_router, prefix="/api/admin/modules", tags=["Admin - Module Environments"])
|
||||
app.include_router(admin_tenant_modules_router, prefix="/api/admin/tenants", tags=["Admin - Tenant Modules"])
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from .helpers import get_client_ip
|
||||
@@ -0,0 +1,7 @@
|
||||
from fastapi import Request
|
||||
|
||||
def get_client_ip(request: Request) -> str:
|
||||
forwarded = request.headers.get("X-Forwarded-For")
|
||||
if forwarded:
|
||||
return forwarded.split(",")[0].strip()
|
||||
return request.client.host if request.client else "unknown"
|
||||
@@ -0,0 +1,38 @@
|
||||
import uuid
|
||||
from sqlalchemy import Column, String, DateTime, Text
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from sqlalchemy.sql import func
|
||||
from app.config.database import Base
|
||||
|
||||
class AuditLog(Base):
|
||||
__tablename__ = "audit_logs"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
|
||||
|
||||
module_name = Column(String(100), nullable=False, index=True)
|
||||
|
||||
action_type = Column(String(20), nullable=False, index=True)
|
||||
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
entity_id = Column(String(255), nullable=True)
|
||||
|
||||
entity_name = Column(String(255), nullable=True)
|
||||
|
||||
performed_by_id = Column(UUID(as_uuid=True), nullable=True)
|
||||
performed_by_email = Column(String(255), nullable=True, index=True)
|
||||
|
||||
ip_address = Column(String(45), nullable=True)
|
||||
|
||||
old_values = Column(JSONB, nullable=True)
|
||||
|
||||
new_values = Column(JSONB, nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), index=True)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"<AuditLog {self.action_type} on {self.module_name}"
|
||||
f" by {self.performed_by_email}"
|
||||
)
|
||||
@@ -0,0 +1,50 @@
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config.database import get_db
|
||||
from app.middleware.auth_middleware import get_current_user, User
|
||||
from app.models.system.audit_log import AuditLog
|
||||
from app.schemas.auth.audit_schema import AuditLogListResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/", response_model=AuditLogListResponse)
|
||||
def get_audit_logs(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
module_name: Optional[str] = Query(None, description="Filter by module name"),
|
||||
action_type: Optional[str] = Query(None, description="CREATE | UPDATE | DELETE"),
|
||||
performed_by_email: Optional[str] = Query(None, description="Filter by actor email"),
|
||||
limit: int = Query(50, ge=1, le=500),
|
||||
offset: int = Query(0, ge=0),
|
||||
):
|
||||
"""
|
||||
Fetch audit logs with optional filters.
|
||||
Only accessible to authenticated users (super-admin in practice).
|
||||
"""
|
||||
query = db.query(AuditLog)
|
||||
|
||||
if module_name:
|
||||
query = query.filter(AuditLog.module_name == module_name)
|
||||
if action_type:
|
||||
query = query.filter(AuditLog.action_type == action_type.upper())
|
||||
if performed_by_email:
|
||||
query = query.filter(
|
||||
AuditLog.performed_by_email.ilike(f"%{performed_by_email}%")
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
items = (
|
||||
query.order_by(AuditLog.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
return AuditLogListResponse(
|
||||
items=items,
|
||||
total=total,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
@@ -1,13 +1,35 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
import uuid as uuid_module
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config.database import get_db
|
||||
from app.middleware.auth_middleware import get_current_user, require_access, User
|
||||
from app.schemas.auth.module_environment_schema import EnvironmentCreate, EnvironmentUpdate, EnvironmentResponse
|
||||
from app.controllers.auth.module_environment_controller import ModuleEnvironmentController
|
||||
from app.models.auth.module_environment_model import ModuleEnvironment
|
||||
from app.schemas.auth.module_environment_schema import EnvironmentCreate, EnvironmentResponse, EnvironmentUpdate
|
||||
from app.services.auth.module_service import ModuleService
|
||||
from app.services.system.audit_log_service import AuditLogService
|
||||
from app.helper.helpers import get_client_ip
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
def _get_env_snapshot(db: Session, module_id: str, env_id: str) -> dict:
|
||||
"""Helper to capture state before changes for the Audit Log."""
|
||||
try:
|
||||
env = db.query(ModuleEnvironment).filter(
|
||||
ModuleEnvironment.id == uuid_module.UUID(env_id),
|
||||
ModuleEnvironment.module_id == uuid_module.UUID(module_id)
|
||||
).first()
|
||||
if not env: return {}
|
||||
return {
|
||||
"slug": env.slug,
|
||||
"backend_base_url": env.backend_base_url,
|
||||
"is_default": env.is_default,
|
||||
"is_active": env.is_active
|
||||
}
|
||||
except: return {}
|
||||
|
||||
@router.get("/{module_id}/environments", response_model=List[EnvironmentResponse])
|
||||
def list_environments(
|
||||
module_id: str,
|
||||
@@ -17,18 +39,35 @@ def list_environments(
|
||||
):
|
||||
return ModuleEnvironmentController.list_environments(db, module_id)
|
||||
|
||||
@router.post("/{module_id}/environments", response_model=EnvironmentResponse)
|
||||
@router.post("/{module_id}/environments", response_model=EnvironmentResponse, status_code=201)
|
||||
def create_environment(
|
||||
request: Request,
|
||||
module_id: str,
|
||||
env_data: EnvironmentCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
_: bool = Depends(require_access("modules.manage")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
return ModuleEnvironmentController.create_environment(db, module_id, env_data)
|
||||
module = ModuleService.get_module(db, module_id)
|
||||
result = ModuleEnvironmentController.create_environment(db, module_id, env_data)
|
||||
|
||||
AuditLogService.log(
|
||||
db=db,
|
||||
module_name="Module Environments",
|
||||
action_type="CREATE",
|
||||
entity_id=str(result.id),
|
||||
entity_name=f"{module.module_name} / {result.slug}",
|
||||
description=f"Environment '{result.slug}' created for '{module.module_name}'",
|
||||
performed_by_id=str(current_user.id),
|
||||
performed_by_email=current_user.email,
|
||||
ip_address=_get_client_ip(request),
|
||||
new_values=env_data.model_dump(exclude={"trust_credentials"})
|
||||
)
|
||||
return result
|
||||
|
||||
@router.put("/{module_id}/environments/{env_id}", response_model=EnvironmentResponse)
|
||||
def update_environment(
|
||||
request: Request,
|
||||
module_id: str,
|
||||
env_id: str,
|
||||
env_data: EnvironmentUpdate,
|
||||
@@ -36,24 +75,76 @@ def update_environment(
|
||||
_: bool = Depends(require_access("modules.manage")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
return ModuleEnvironmentController.update_environment(db, module_id, env_id, env_data)
|
||||
module = ModuleService.get_module(db, module_id)
|
||||
old_values = _get_env_snapshot(db, module_id, env_id)
|
||||
|
||||
result = ModuleEnvironmentController.update_environment(db, module_id, env_id, env_data)
|
||||
|
||||
AuditLogService.log(
|
||||
db=db,
|
||||
module_name="Module Environments",
|
||||
action_type="UPDATE",
|
||||
entity_id=env_id,
|
||||
entity_name=f"{module.module_name} / {result.slug}",
|
||||
description=f"Environment '{result.slug}' updated",
|
||||
performed_by_id=str(current_user.id),
|
||||
performed_by_email=current_user.email,
|
||||
ip_address=_get_client_ip(request),
|
||||
old_values=old_values,
|
||||
new_values=env_data.model_dump(exclude_unset=True, exclude={"trust_credentials"})
|
||||
)
|
||||
return result
|
||||
|
||||
@router.patch("/{module_id}/environments/{env_id}/default")
|
||||
def set_default_environment(
|
||||
request: Request,
|
||||
module_id: str,
|
||||
env_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
_: bool = Depends(require_access("modules.manage")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
return ModuleEnvironmentController.set_default_environment(db, module_id, env_id)
|
||||
module = ModuleService.get_module(db, module_id)
|
||||
result = ModuleEnvironmentController.set_default_environment(db, module_id, env_id)
|
||||
|
||||
AuditLogService.log(
|
||||
db=db,
|
||||
module_name="Module Environments",
|
||||
action_type="UPDATE",
|
||||
entity_id=env_id,
|
||||
entity_name=f"{module.module_name}",
|
||||
description=f"Set default environment for {module.module_name}",
|
||||
performed_by_id=str(current_user.id),
|
||||
performed_by_email=current_user.email,
|
||||
ip_address=_get_client_ip(request),
|
||||
new_values={"is_default": True}
|
||||
)
|
||||
return result
|
||||
|
||||
@router.delete("/{module_id}/environments/{env_id}")
|
||||
def delete_environment(
|
||||
request: Request,
|
||||
module_id: str,
|
||||
env_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
_: bool = Depends(require_access("modules.manage")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
return ModuleEnvironmentController.delete_environment(db, module_id, env_id)
|
||||
module = ModuleService.get_module(db, module_id)
|
||||
snapshot = _get_env_snapshot(db, module_id, env_id)
|
||||
|
||||
result = ModuleEnvironmentController.delete_environment(db, module_id, env_id)
|
||||
|
||||
AuditLogService.log(
|
||||
db=db,
|
||||
module_name="Module Environments",
|
||||
action_type="DELETE",
|
||||
entity_id=env_id,
|
||||
entity_name=f"{module.module_name} / {snapshot.get('slug')}",
|
||||
description=f"Environment deleted from {module.module_name}",
|
||||
performed_by_id=str(current_user.id),
|
||||
performed_by_email=current_user.email,
|
||||
ip_address=_get_client_ip(request),
|
||||
old_values=snapshot
|
||||
)
|
||||
return result
|
||||
+71
-28
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.models.auth.module_model import Module
|
||||
@@ -10,6 +10,8 @@ from app.services.auth.module_permission_service import ModulePermissionService
|
||||
from app.middleware.auth_middleware import get_current_user, require_access, User
|
||||
from app.schemas.auth.module_schema import ModuleCreate, ModuleUpdate, ModuleResponse
|
||||
from app.controllers.auth.module_controller import ModuleController
|
||||
from app.services.system.audit_log_service import AuditLogService
|
||||
from app.helper.helpers import get_client_ip
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -21,14 +23,30 @@ def list_modules(
|
||||
):
|
||||
return ModuleController.list_modules(db)
|
||||
|
||||
@router.post("/", response_model=ModuleResponse)
|
||||
@router.post("/", response_model=ModuleResponse, status_code=201)
|
||||
def create_module(
|
||||
request: Request,
|
||||
module_data: ModuleCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
_: bool = Depends(require_access("modules.manage")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
return ModuleController.create_module(db, module_data)
|
||||
result = ModuleController.create_module(db, module_data)
|
||||
|
||||
# Professional Audit Logging
|
||||
AuditLogService.log(
|
||||
db=db,
|
||||
module_name="Modules",
|
||||
action_type="CREATE",
|
||||
entity_id=str(result.id),
|
||||
entity_name=result.module_name,
|
||||
description=f"Module '{result.module_name}' created",
|
||||
performed_by_id=str(current_user.id),
|
||||
performed_by_email=current_user.email,
|
||||
ip_address=_get_client_ip(request),
|
||||
new_values=module_data.model_dump()
|
||||
)
|
||||
return result
|
||||
|
||||
@router.get("/{module_id}", response_model=ModuleResponse)
|
||||
def get_module(
|
||||
@@ -41,48 +59,72 @@ def get_module(
|
||||
|
||||
@router.put("/{module_id}", response_model=ModuleResponse)
|
||||
def update_module(
|
||||
request: Request,
|
||||
module_id: str,
|
||||
module_data: ModuleUpdate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
_: bool = Depends(require_access("modules.manage")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
return ModuleController.update_module(db, module_id, module_data)
|
||||
# Snapshot before update
|
||||
existing = ModuleController.get_module(db, module_id)
|
||||
old_values = {"name": existing.module_name, "status": existing.status}
|
||||
|
||||
result = ModuleController.update_module(db, module_id, module_data)
|
||||
|
||||
AuditLogService.log(
|
||||
db=db,
|
||||
module_name="Modules",
|
||||
action_type="UPDATE",
|
||||
entity_id=module_id,
|
||||
entity_name=result.module_name,
|
||||
description=f"Module '{result.module_name}' updated",
|
||||
performed_by_id=str(current_user.id),
|
||||
performed_by_email=current_user.email,
|
||||
ip_address=_get_client_ip(request),
|
||||
old_values=old_values,
|
||||
new_values=module_data.model_dump(exclude_unset=True)
|
||||
)
|
||||
return result
|
||||
|
||||
@router.delete("/{module_id}")
|
||||
def delete_module(
|
||||
request: Request,
|
||||
module_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
_: bool = Depends(require_access("modules.manage")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
return ModuleController.delete_module(db, module_id)
|
||||
# Capture name for the log before it's deleted
|
||||
existing = ModuleController.get_module(db, module_id)
|
||||
module_name = existing.module_name
|
||||
|
||||
@router.get("/{module_id}/permissions", response_model=List[dict])
|
||||
result = ModuleController.delete_module(db, module_id)
|
||||
|
||||
AuditLogService.log(
|
||||
db=db,
|
||||
module_name="Modules",
|
||||
action_type="DELETE",
|
||||
entity_id=module_id,
|
||||
entity_name=module_name,
|
||||
description=f"Module '{module_name}' deleted",
|
||||
performed_by_id=str(current_user.id),
|
||||
performed_by_email=current_user.email,
|
||||
ip_address=_get_client_ip(request)
|
||||
)
|
||||
return result
|
||||
|
||||
# ── Permission Sync Routes ──────────────────────────────────────────────────
|
||||
|
||||
@router.get("/{module_id}/permissions")
|
||||
def get_module_permissions(
|
||||
module_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
_: bool = Depends(require_access("modules.view")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
|
||||
module = db.query(Module).filter(Module.id == uuid.UUID(module_id)).first()
|
||||
if not module:
|
||||
raise HTTPException(status_code=404, detail="Module not found")
|
||||
|
||||
permissions = db.query(ModuleAccess).filter(
|
||||
ModuleAccess.module_id == module.id
|
||||
).all()
|
||||
|
||||
return [{
|
||||
"id": str(p.id),
|
||||
"access_code": p.access_code,
|
||||
"name": p.name,
|
||||
"category": p.category,
|
||||
"parent_id": str(p.parent_id) if p.parent_id else None,
|
||||
"scope": "module",
|
||||
"module_id": str(p.module_id)
|
||||
} for p in permissions]
|
||||
# Now using the Service method you just shared!
|
||||
return ModulePermissionService.get_module_permissions(db, module_id)
|
||||
|
||||
@router.post("/{module_id}/permissions/sync")
|
||||
def sync_module_permissions(
|
||||
@@ -91,11 +133,12 @@ def sync_module_permissions(
|
||||
_: bool = Depends(require_access("modules.manage")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
|
||||
try:
|
||||
result = ModulePermissionService.sync_permissions(db, module_id)
|
||||
return result
|
||||
return ModulePermissionService.sync_permissions(db, module_id)
|
||||
except Exception as e:
|
||||
if isinstance(e, HTTPException):
|
||||
raise e
|
||||
raise HTTPException(status_code=500, detail=f"Permission sync failed: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Permission sync failed: {str(e)}"
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, status, HTTPException, Request, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
|
||||
@@ -6,8 +6,10 @@ from app.config.database import get_db
|
||||
from app.middleware.auth_middleware import get_current_user, require_access, User
|
||||
from app.schemas.auth.tenant_module_schema import TenantModuleCreate, TenantModuleUpdate, TenantModuleResponse
|
||||
from app.controllers.auth.tenant_module_controller import TenantModuleController
|
||||
from app.services.system.audit_log_service import AuditLogService
|
||||
from app.helper.helpers import get_client_ip
|
||||
|
||||
router = APIRouter()
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/{tenant_id}/modules", response_model=List[TenantModuleResponse])
|
||||
def list_tenant_modules(
|
||||
@@ -20,16 +22,32 @@ def list_tenant_modules(
|
||||
|
||||
@router.post("/{tenant_id}/modules", response_model=TenantModuleResponse)
|
||||
def assign_module_to_tenant(
|
||||
request: Request,
|
||||
tenant_id: str,
|
||||
assignment_data: TenantModuleCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
_: bool = Depends(require_access("tenants.manage")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
return TenantModuleController.assign_module(db, tenant_id, assignment_data)
|
||||
result = TenantModuleController.assign_module(db, tenant_id, assignment_data)
|
||||
|
||||
AuditLogService.log(
|
||||
db=db,
|
||||
module_name="Tenant Modules",
|
||||
action_type="CREATE",
|
||||
entity_id=str(result.id),
|
||||
entity_name=f"Tenant {tenant_id} / Module {assignment_data.module_id}",
|
||||
description=f"Module assigned to tenant {tenant_id}",
|
||||
performed_by_id=str(current_user.id),
|
||||
performed_by_email=current_user.email,
|
||||
ip_address=_get_client_ip(request),
|
||||
new_values=assignment_data.model_dump(mode='json')
|
||||
)
|
||||
return result
|
||||
|
||||
@router.put("/{tenant_id}/modules/{tenant_module_id}", response_model=TenantModuleResponse)
|
||||
def update_tenant_module(
|
||||
request: Request,
|
||||
tenant_id: str,
|
||||
tenant_module_id: str,
|
||||
update_data: TenantModuleUpdate,
|
||||
@@ -37,14 +55,42 @@ def update_tenant_module(
|
||||
_: bool = Depends(require_access("tenants.manage")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
return TenantModuleController.update_assignment(db, tenant_id, tenant_module_id, update_data)
|
||||
result = TenantModuleController.update_assignment(db, tenant_id, tenant_module_id, update_data)
|
||||
|
||||
AuditLogService.log(
|
||||
db=db,
|
||||
module_name="Tenant Modules",
|
||||
action_type="UPDATE",
|
||||
entity_id=tenant_module_id,
|
||||
entity_name=f"Tenant {tenant_id} / Module assignment",
|
||||
description=f"Tenant module assignment updated",
|
||||
performed_by_id=str(current_user.id),
|
||||
performed_by_email=current_user.email,
|
||||
ip_address=_get_client_ip(request),
|
||||
new_values=update_data.model_dump(exclude_unset=True)
|
||||
)
|
||||
return result
|
||||
|
||||
@router.delete("/{tenant_id}/modules/{tenant_module_id}")
|
||||
def remove_module_from_tenant(
|
||||
request: Request,
|
||||
tenant_id: str,
|
||||
tenant_module_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
_: bool = Depends(require_access("tenants.manage")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
return TenantModuleController.remove_assignment(db, tenant_id, tenant_module_id)
|
||||
result = TenantModuleController.remove_assignment(db, tenant_id, tenant_module_id)
|
||||
|
||||
AuditLogService.log(
|
||||
db=db,
|
||||
module_name="Tenant Modules",
|
||||
action_type="DELETE",
|
||||
entity_id=tenant_module_id,
|
||||
entity_name=f"Tenant {tenant_id} / Module assignment",
|
||||
description=f"Tenant module assignment deleted",
|
||||
performed_by_id=str(current_user.id),
|
||||
performed_by_email=current_user.email,
|
||||
ip_address=_get_client_ip(request),
|
||||
)
|
||||
return result
|
||||
+65
-4
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, status, Query
|
||||
from fastapi import APIRouter, Depends, status, Query, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
import uuid
|
||||
@@ -13,20 +13,40 @@ from app.schemas.auth.role_schema import (
|
||||
)
|
||||
from app.middleware.auth_middleware import get_current_user, require_access
|
||||
from app.models.auth.user_model import User
|
||||
from app.services.system.audit_log_service import AuditLogService
|
||||
from app.helper.helpers import get_client_ip
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/create", response_model=RoleResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_role(
|
||||
request: Request,
|
||||
role_data: RoleCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
_=Depends(require_access("admin.role.create")),
|
||||
):
|
||||
return RoleController.create_role(
|
||||
result = RoleController.create_role(
|
||||
db, role_data, current_user.tenant_id if current_user.tenant_id else None
|
||||
)
|
||||
|
||||
AuditLogService.log(
|
||||
db=db,
|
||||
module_name="Roles",
|
||||
action_type="CREATE",
|
||||
entity_id=str(result.id),
|
||||
entity_name=result.role_name,
|
||||
description=f"Role '{result.role_name}' created",
|
||||
performed_by_id=str(current_user.id),
|
||||
performed_by_email=current_user.email,
|
||||
ip_address=_get_client_ip(request),
|
||||
new_values={
|
||||
"role_name": result.role_name,
|
||||
"tenant_id": str(result.tenant_id) if result.tenant_id else None,
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
@router.get("/get", response_model=List[RoleResponse])
|
||||
def get_all_roles(
|
||||
db: Session = Depends(get_db),
|
||||
@@ -48,26 +68,67 @@ def get_role(
|
||||
|
||||
@router.put("/update/{role_id}", response_model=RoleResponse)
|
||||
def update_role(
|
||||
request: Request,
|
||||
role_id: uuid.UUID,
|
||||
role_data: RoleUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
_=Depends(require_access("admin.role.update")),
|
||||
):
|
||||
|
||||
existing = RoleController.get_role_by_id(db, role_id)
|
||||
old_values = {"role_name": existing.role_name}
|
||||
|
||||
is_superadmin = current_user.tenant_id is None
|
||||
return RoleController.update_role(
|
||||
result = RoleController.update_role(
|
||||
db, role_id, role_data, is_superadmin=is_superadmin
|
||||
)
|
||||
|
||||
new_values = role_data.model_dump(mode='json', exclude_unset=True)
|
||||
|
||||
AuditLogService.log(
|
||||
db=db,
|
||||
module_name="Roles",
|
||||
action_type="UPDATE",
|
||||
entity_id=str(role_id),
|
||||
entity_name=result.role_name,
|
||||
description=f"Role '{result.role_name}' updated",
|
||||
performed_by_id=str(current_user.id),
|
||||
performed_by_email=current_user.email,
|
||||
ip_address=_get_client_ip(request),
|
||||
old_values=old_values,
|
||||
new_values=new_values
|
||||
)
|
||||
return result
|
||||
|
||||
@router.delete("/delete/{role_id}")
|
||||
def delete_role(
|
||||
request: Request,
|
||||
role_id: uuid.UUID,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
_=Depends(require_access("admin.role.delete")),
|
||||
):
|
||||
|
||||
existing = RoleController.get_role_by_id(db, role_id)
|
||||
role_name = existing.role_name
|
||||
|
||||
is_superadmin = current_user.tenant_id is None
|
||||
return RoleController.delete_role(db, role_id, is_superadmin=is_superadmin)
|
||||
result = RoleController.delete_role(db, role_id, is_superadmin=is_superadmin)
|
||||
|
||||
AuditLogService.log(
|
||||
db=db,
|
||||
module_name="Roles",
|
||||
action_type="DELETE",
|
||||
entity_id=str(role_id),
|
||||
entity_name=role_name,
|
||||
description=f"Role '{result.role_name}' deleted",
|
||||
performed_by_id=str(current_user.id),
|
||||
performed_by_email=current_user.email,
|
||||
ip_address=_get_client_ip(request),
|
||||
old_values={"role_name": role_name}
|
||||
)
|
||||
return result
|
||||
|
||||
@router.get("/list", response_model=RolePaginatedResponse)
|
||||
def list_roles(
|
||||
|
||||
+79
-21
@@ -1,24 +1,41 @@
|
||||
from fastapi import APIRouter, Depends, status, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
import uuid
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, status, HTTPException, Query, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config.database import get_db
|
||||
from app.controllers.auth.tenant_controller import TenantController
|
||||
from app.schemas.auth.tenant_schema import TenantCreate, TenantUpdate, TenantResponse, TenantPaginatedResponse
|
||||
from app.middleware.auth_middleware import get_current_user, require_access
|
||||
from app.middleware.auth_middleware import get_current_user, require_access, User
|
||||
from app.middleware.tenant_middleware import get_tenant_id_from_user
|
||||
from app.models.auth.user_model import User
|
||||
from app.services.system.audit_log_service import AuditLogService
|
||||
from app.helper.helpers import get_client_ip
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/create", response_model=TenantResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_tenant(
|
||||
request: Request,
|
||||
tenant_data: TenantCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
_ = Depends(require_access("superadmin.tenant.create"))
|
||||
):
|
||||
return TenantController.create_tenant(db, tenant_data)
|
||||
result = TenantController.create_tenant(db, tenant_data)
|
||||
|
||||
AuditLogService.log(
|
||||
db=db,
|
||||
module_name="Tenants",
|
||||
action_type="CREATE",
|
||||
entity_id=str(result.id),
|
||||
entity_name=result.tenant_name,
|
||||
description=f"Tenant '{result.tenant_name}' onboarded",
|
||||
performed_by_id=str(current_user.id),
|
||||
performed_by_email=current_user.email,
|
||||
ip_address=_get_client_ip(request),
|
||||
new_values=tenant_data.model_dump(mode='json')
|
||||
)
|
||||
return result
|
||||
|
||||
@router.get("/get", response_model=List[TenantResponse])
|
||||
def get_all_tenants(
|
||||
@@ -26,17 +43,9 @@ def get_all_tenants(
|
||||
current_user: User = Depends(get_current_user),
|
||||
_ = Depends(require_access("superadmin.tenant.read"))
|
||||
):
|
||||
# READ actions are typically not logged to avoid DB bloat
|
||||
return TenantController.get_all_tenants(db)
|
||||
|
||||
@router.get("/get/{tenant_id}", response_model=TenantResponse)
|
||||
def get_tenant(
|
||||
tenant_id: uuid.UUID,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
_ = Depends(require_access("superadmin.tenant.read"))
|
||||
):
|
||||
return TenantController.get_tenant_by_id(db, tenant_id)
|
||||
|
||||
@router.get("/me", response_model=TenantResponse)
|
||||
def get_my_tenant(
|
||||
db: Session = Depends(get_db),
|
||||
@@ -44,30 +53,79 @@ def get_my_tenant(
|
||||
):
|
||||
tenant_id = get_tenant_id_from_user(current_user)
|
||||
if not tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found"
|
||||
)
|
||||
raise HTTPException(status_code=404, detail="Tenant not found")
|
||||
return TenantController.get_tenant_by_id(db, tenant_id)
|
||||
|
||||
@router.put("/update/{tenant_id}", response_model=TenantResponse)
|
||||
def update_tenant(
|
||||
request: Request,
|
||||
tenant_id: uuid.UUID,
|
||||
tenant_data: TenantUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
_ = Depends(require_access("superadmin.tenant.update"))
|
||||
):
|
||||
return TenantController.update_tenant(db, tenant_id, tenant_data)
|
||||
# 1. Get snapshot BEFORE update for Audit Log
|
||||
existing = TenantController.get_tenant_by_id(db, tenant_id)
|
||||
old_snapshot = {
|
||||
"tenant_name": existing.tenant_name,
|
||||
"tenant_domain": existing.tenant_domain,
|
||||
"is_active": existing.is_active
|
||||
}
|
||||
|
||||
# 2. Perform update
|
||||
result = TenantController.update_tenant(db, tenant_id, tenant_data)
|
||||
|
||||
# 3. Prepare new values and indentify deltas
|
||||
new_snapshot = tenant_data.model_dump(mode='json', exclude_unset=True)
|
||||
|
||||
old_values = {k: old_snapshot[k] for k in new_snapshot if k in old_snapshot and old_snapshot[k] != new_snapshot[k]}
|
||||
new_values = {k: new_snapshot[k] for k in old_values}
|
||||
|
||||
# 4. Log the change
|
||||
AuditLogService.log(
|
||||
db=db,
|
||||
module_name="Tenants",
|
||||
action_type="UPDATE",
|
||||
entity_id=str(tenant_id),
|
||||
entity_name=result.tenant_name,
|
||||
description=f"Tenant '{result.tenant_name}' updated",
|
||||
performed_by_id=str(current_user.id),
|
||||
performed_by_email=current_user.email,
|
||||
ip_address=_get_client_ip(request),
|
||||
old_values=old_values,
|
||||
new_values=new_values
|
||||
)
|
||||
return result
|
||||
|
||||
@router.delete("/delete/{tenant_id}")
|
||||
def delete_tenant(
|
||||
request: Request,
|
||||
tenant_id: uuid.UUID,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
_ = Depends(require_access("superadmin.tenant.delete"))
|
||||
):
|
||||
return TenantController.delete_tenant(db, tenant_id)
|
||||
# 1. Get snapshot before deletion
|
||||
existing = TenantController.get_tenant_by_id(db, tenant_id)
|
||||
|
||||
# 2. Perform deletion
|
||||
result = TenantController.delete_tenant(db, tenant_id)
|
||||
|
||||
# 3. Log the deletion
|
||||
AuditLogService.log(
|
||||
db=db,
|
||||
module_name="Tenants",
|
||||
action_type="DELETE",
|
||||
entity_id=str(tenant_id),
|
||||
entity_name=existing.tenant_name,
|
||||
description=f"Tenant '{existing.tenant_name}' removed",
|
||||
performed_by_id=str(current_user.id),
|
||||
performed_by_email=current_user.email,
|
||||
ip_address=_get_client_ip(request),
|
||||
old_values={"tenant_name": existing.tenant_name, "domain": existing.tenant_domain}
|
||||
)
|
||||
return result
|
||||
|
||||
@router.get("/list", response_model=TenantPaginatedResponse)
|
||||
def list_tenants(
|
||||
|
||||
+64
-5
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, status, Query, BackgroundTasks
|
||||
from fastapi import APIRouter, Depends, status, Query, BackgroundTasks, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
import uuid
|
||||
@@ -7,18 +7,38 @@ from app.controllers.auth.user_controller import UserController
|
||||
from app.schemas.auth.user_schema import UserCreate, UserUpdate, UserResponse, UserPaginatedResponse
|
||||
from app.middleware.auth_middleware import get_current_user, require_access
|
||||
from app.models.auth.user_model import User
|
||||
from app.services.system.audit_log_service import AuditLogService
|
||||
from app.helper.helpers import get_client_ip
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/create", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_user(
|
||||
request: Request,
|
||||
user_data: UserCreate,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
_ = Depends(require_access("admin.user.create"))
|
||||
):
|
||||
return UserController.create_user(db, user_data, current_user, background_tasks)
|
||||
result = UserController.create_user(db, user_data, current_user, background_tasks)
|
||||
|
||||
AuditLogService.log(
|
||||
db=db,
|
||||
module_name="Users",
|
||||
action_type="CREATE",
|
||||
entity_id=str(result.id),
|
||||
entity_name=result.email,
|
||||
description=f"User '{result.email}' created",
|
||||
performed_by_id=str(current_user.id),
|
||||
performed_by_email=current_user.email,
|
||||
ip_address=_get_client_ip(request),
|
||||
new_values={
|
||||
"email": result.email,
|
||||
"tenant_id": str(result.tenant_id) if result.tenant_id else None,
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
@router.get("/get", response_model=List[UserResponse])
|
||||
def get_all_users(
|
||||
@@ -39,6 +59,7 @@ def get_user(
|
||||
|
||||
@router.put("/update/{user_id}", response_model=UserResponse)
|
||||
def update_user(
|
||||
request: Request,
|
||||
user_id: uuid.UUID,
|
||||
user_data: UserUpdate,
|
||||
background_tasks: BackgroundTasks,
|
||||
@@ -46,16 +67,54 @@ def update_user(
|
||||
current_user: User = Depends(get_current_user),
|
||||
_ = Depends(require_access("admin.user.update"))
|
||||
):
|
||||
return UserController.update_user(db, user_id, user_data, current_user, background_tasks)
|
||||
|
||||
existing = UserController.get_user_by_id(db, user_id, current_user)
|
||||
old_values = {"email": existing.email}
|
||||
|
||||
result = UserController.update_user(db, user_id, user_data, current_user, background_tasks)
|
||||
|
||||
AuditLogService.log(
|
||||
db=db,
|
||||
module_name="Users",
|
||||
action_type="UPDATE",
|
||||
entity_id=str(user_id),
|
||||
entity_name=result.email,
|
||||
description=f"User '{result.email}' updated",
|
||||
performed_by_id=str(current_user.id),
|
||||
performed_by_email=current_user.email,
|
||||
ip_address=_get_client_ip(request),
|
||||
old_values=old_values,
|
||||
new_values=user_data.model_dump(mode='json', exclude_unset=True)
|
||||
)
|
||||
return result
|
||||
|
||||
@router.delete("/delete/{user_id}")
|
||||
def delete_user(
|
||||
request: Request,
|
||||
user_id: uuid.UUID,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
_ = Depends(require_access("admin.user.delete"))
|
||||
):
|
||||
return UserController.delete_user(db, user_id, current_user)
|
||||
|
||||
existing = UserController.get_user_by_id(db, user_id, current_user)
|
||||
user_email = existing.email
|
||||
|
||||
result = UserController.delete_user(db, user_id, current_user)
|
||||
|
||||
AuditLogService.log(
|
||||
db=db,
|
||||
module_name="Users",
|
||||
action_type="DELETE",
|
||||
entity_id=str(user_id),
|
||||
entity_name=user_email,
|
||||
description=f"User '{user_email}' deleted",
|
||||
performed_by_id=str(current_user.id),
|
||||
performed_by_email=current_user.email,
|
||||
ip_address=_get_client_ip(request),
|
||||
old_values={"email": user_email}
|
||||
)
|
||||
return result
|
||||
|
||||
@router.get("/list", response_model=UserPaginatedResponse)
|
||||
def list_users(
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
|
||||
class AuditLogResponse(BaseModel):
|
||||
id: UUID
|
||||
module_name: str
|
||||
action_type: str
|
||||
entity_id: Optional[str] = None
|
||||
entity_name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
performed_by_email: Optional[str] = None
|
||||
ip_address: Optional[str] = None
|
||||
old_values: Optional[Dict[str, Any]] = None
|
||||
new_values: Optional[Dict[str, Any]] = None
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class AuditLogListResponse(BaseModel):
|
||||
items: List[AuditLogResponse]
|
||||
total: int
|
||||
limit: int
|
||||
offset: int
|
||||
@@ -0,0 +1,52 @@
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.system.audit_log import AuditLog
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AuditLogService:
|
||||
@staticmethod
|
||||
def log(
|
||||
db: Session,
|
||||
module_name: str,
|
||||
action_type: str,
|
||||
description: str,
|
||||
entity_id: Optional[str] = None,
|
||||
entity_name: Optional[str] = None,
|
||||
performed_by_id: Optional[str] = None,
|
||||
performed_by_email: Optional[str] = None,
|
||||
ip_address: Optional[str] = None,
|
||||
old_values: Optional[Dict[str, Any]] = None,
|
||||
new_values: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Write an audit log entry.
|
||||
|
||||
Failures are silently swallowed and logged — they must NEVER break
|
||||
the main operation that triggered them.
|
||||
"""
|
||||
try:
|
||||
entry = AuditLog(
|
||||
module_name=module_name,
|
||||
action_type=action_type,
|
||||
entity_id=entity_id,
|
||||
entity_name=entity_name,
|
||||
description=description,
|
||||
performed_by_id=(
|
||||
uuid.UUID(performed_by_id) if performed_by_id else None
|
||||
),
|
||||
performed_by_email=performed_by_email,
|
||||
ip_address=ip_address,
|
||||
old_values=old_values,
|
||||
new_values=new_values,
|
||||
)
|
||||
db.add(entry)
|
||||
db.commit()
|
||||
except Exception as exc:
|
||||
db.rollback()
|
||||
logger.error("AuditLogService.log failed: %s", exc, exc_info=True)
|
||||
@@ -64,6 +64,7 @@ PREDEFINED_ACCESSES = [
|
||||
("admin.user.update", "Administration", "Allow access to update users", None),
|
||||
("admin.user.delete", "Administration", "Allow access to delete users", None),
|
||||
("admin.access.read", "Administration", "Allow access to view accesses", None),
|
||||
("admin.logs.read", "Administration", "Allow access to view system audit logs", None),
|
||||
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user