345 lines
12 KiB
Python
345 lines
12 KiB
Python
import uuid
|
|
import json
|
|
import logging
|
|
from typing import List, Optional
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import or_, cast, String
|
|
from fastapi import HTTPException, status
|
|
from app.models.auth.role_model import Role
|
|
from app.models.auth.role_access_model import RoleAccess
|
|
from app.models.auth.role_module_access_model import RoleModuleAccess
|
|
from app.models.auth.module_access_model import ModuleAccess
|
|
from app.models.auth.tenant_module_model import TenantModule
|
|
from app.models.auth.access_model import Access
|
|
from app.schemas.auth.role_schema import RoleCreate, RoleUpdate, RoleResponse, RolePaginatedResponse
|
|
from app.services.auth.event_service import EventService
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class RoleService:
|
|
|
|
@staticmethod
|
|
def create_role(db: Session, role_data: RoleCreate, emit_events: bool = True) -> Role:
|
|
existing = (
|
|
db.query(Role)
|
|
.filter(
|
|
Role.role_name == role_data.role_name,
|
|
Role.tenant_id == role_data.tenant_id,
|
|
)
|
|
.first()
|
|
)
|
|
|
|
if existing:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Role name already exists for this tenant",
|
|
)
|
|
|
|
role = Role(
|
|
role_name=role_data.role_name,
|
|
tenant_id=role_data.tenant_id,
|
|
is_default=role_data.is_default or False,
|
|
)
|
|
|
|
db.add(role)
|
|
db.commit()
|
|
db.refresh(role)
|
|
|
|
if role_data.access_ids:
|
|
RoleService.assign_accesses(db, role.id, role_data.access_ids)
|
|
|
|
if emit_events:
|
|
assigned_modules = (
|
|
db.query(RoleModuleAccess, ModuleAccess)
|
|
.join(ModuleAccess, RoleModuleAccess.module_access_id == ModuleAccess.id)
|
|
.filter(RoleModuleAccess.role_id == role.id)
|
|
.all()
|
|
)
|
|
|
|
if assigned_modules:
|
|
module_map = {}
|
|
|
|
for rma, ma in assigned_modules:
|
|
mid = str(ma.module_id)
|
|
if mid not in module_map:
|
|
module_map[mid] = []
|
|
module_map[mid].append(ma.access_code)
|
|
|
|
from app.models.auth.tenant_module_model import TenantModule
|
|
|
|
env_map = {}
|
|
if role.tenant_id:
|
|
tm_assignments = db.query(TenantModule).filter(
|
|
TenantModule.tenant_id == role.tenant_id,
|
|
TenantModule.module_id.in_([uuid.UUID(m) for m in module_map.keys()])
|
|
).all()
|
|
for tm in tm_assignments:
|
|
env_map[str(tm.module_id)] = tm.assigned_environment_slug or "prod"
|
|
|
|
targets = []
|
|
for mid, codes in module_map.items():
|
|
env_slug = env_map.get(mid, "prod")
|
|
targets.append({
|
|
"module_id": mid,
|
|
"environment_slug": env_slug,
|
|
"permissions": codes
|
|
})
|
|
|
|
if targets:
|
|
provisioning_id = str(uuid.uuid4())
|
|
payload = {
|
|
"role_id": str(role.id),
|
|
"role_name": role.role_name,
|
|
"tenant_id": str(role.tenant_id) if role.tenant_id else None,
|
|
"provisioning_id": provisioning_id,
|
|
"targets": targets
|
|
}
|
|
|
|
logger.info(f"ROLE_PROVISION_REQUESTED Payload: {json.dumps(payload, default=str)}")
|
|
|
|
EventService.emit_event(
|
|
db,
|
|
event_type="ROLE_PROVISION_REQUESTED",
|
|
payload=payload,
|
|
tenant_id=role.tenant_id
|
|
)
|
|
|
|
db.commit()
|
|
|
|
return role
|
|
|
|
@staticmethod
|
|
def assign_accesses(db: Session, role_id: uuid.UUID, access_ids: List[uuid.UUID]):
|
|
db.query(RoleAccess).filter(RoleAccess.role_id == role_id).delete()
|
|
db.query(RoleModuleAccess).filter(RoleModuleAccess.role_id == role_id).delete()
|
|
|
|
if not access_ids:
|
|
return
|
|
|
|
saas_accesses = db.query(Access).filter(Access.id.in_(access_ids)).all()
|
|
saas_ids = {a.id for a in saas_accesses}
|
|
|
|
for access in saas_accesses:
|
|
db.add(RoleAccess(role_id=role_id, access_id=access.id))
|
|
|
|
remaining_ids = set(access_ids) - saas_ids
|
|
|
|
if remaining_ids:
|
|
module_accesses = db.query(ModuleAccess).filter(ModuleAccess.id.in_(remaining_ids)).all()
|
|
for access in module_accesses:
|
|
db.add(RoleModuleAccess(role_id=role_id, module_access_id=access.id))
|
|
|
|
db.commit()
|
|
|
|
@staticmethod
|
|
def get_all_roles(db: Session, tenant_id: uuid.UUID = None):
|
|
query = db.query(Role)
|
|
if tenant_id:
|
|
query = query.filter(Role.tenant_id == tenant_id)
|
|
return query.all()
|
|
|
|
@staticmethod
|
|
def get_role_by_id(db: Session, role_id: uuid.UUID) -> Role:
|
|
role = db.query(Role).filter(Role.id == role_id).first()
|
|
if not role:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Role not found"
|
|
)
|
|
return role
|
|
|
|
@staticmethod
|
|
def update_role(
|
|
db: Session,
|
|
role_id: uuid.UUID,
|
|
role_data: RoleUpdate,
|
|
is_superadmin: bool = False,
|
|
) -> Role:
|
|
role = RoleService.get_role_by_id(db, role_id)
|
|
|
|
def get_module_permissions_snapshot(r_id):
|
|
snapshot_data = (
|
|
db.query(RoleModuleAccess, ModuleAccess)
|
|
.join(ModuleAccess, RoleModuleAccess.module_access_id == ModuleAccess.id)
|
|
.filter(RoleModuleAccess.role_id == r_id)
|
|
.all()
|
|
)
|
|
snapshot_map = {}
|
|
for rma, ma in snapshot_data:
|
|
mid = str(ma.module_id)
|
|
if mid not in snapshot_map:
|
|
snapshot_map[mid] = set()
|
|
snapshot_map[mid].add(ma.access_code)
|
|
return snapshot_map
|
|
|
|
before_snapshot = get_module_permissions_snapshot(role_id)
|
|
|
|
update_dict = role_data.model_dump(exclude_unset=True)
|
|
role_name_changed = "role_name" in update_dict
|
|
|
|
if role.is_default and not is_superadmin:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Default roles can only be modified by superadmins.",
|
|
)
|
|
|
|
if "access_ids" in update_dict:
|
|
access_ids = update_dict.pop("access_ids")
|
|
if access_ids is not None:
|
|
RoleService.assign_accesses(db, role_id, access_ids)
|
|
|
|
for key, value in update_dict.items():
|
|
setattr(role, key, value)
|
|
|
|
db.commit()
|
|
db.refresh(role)
|
|
|
|
after_snapshot = get_module_permissions_snapshot(role_id)
|
|
|
|
all_modules = set(before_snapshot.keys()) | set(after_snapshot.keys())
|
|
|
|
env_map = {}
|
|
if role.tenant_id:
|
|
tm_assignments = db.query(TenantModule).filter(
|
|
TenantModule.tenant_id == role.tenant_id,
|
|
TenantModule.module_id.in_([uuid.UUID(m) for m in all_modules])
|
|
).all()
|
|
for tm in tm_assignments:
|
|
env_map[str(tm.module_id)] = tm.assigned_environment_slug or "prod"
|
|
|
|
diff_targets = []
|
|
|
|
for mid in all_modules:
|
|
before_set = before_snapshot.get(mid, set())
|
|
after_set = after_snapshot.get(mid, set())
|
|
|
|
added = list(after_set - before_set)
|
|
removed = list(before_set - after_set)
|
|
|
|
is_active_module = mid in after_snapshot and len(after_snapshot[mid]) > 0
|
|
|
|
if added or removed or (role_name_changed and is_active_module):
|
|
env_slug = env_map.get(mid, "prod")
|
|
diff_targets.append({
|
|
"module_id": mid,
|
|
"environment_slug": env_slug,
|
|
"added_permissions": added,
|
|
"removed_permissions": removed
|
|
})
|
|
|
|
if diff_targets:
|
|
provisioning_id = str(uuid.uuid4())
|
|
payload = {
|
|
"role_id": str(role.id),
|
|
"role_name": role.role_name,
|
|
"tenant_id": str(role.tenant_id) if role.tenant_id else None,
|
|
"provisioning_id": provisioning_id,
|
|
"targets": diff_targets
|
|
}
|
|
|
|
logger.info(f"ROLE_UPDATED Payload: {json.dumps(payload, default=str)}")
|
|
|
|
EventService.emit_event(
|
|
db,
|
|
event_type="ROLE_UPDATED",
|
|
payload=payload,
|
|
tenant_id=role.tenant_id
|
|
)
|
|
|
|
db.commit()
|
|
|
|
return role
|
|
|
|
@staticmethod
|
|
def delete_role(db: Session, role_id: uuid.UUID, is_superadmin: bool = False):
|
|
role = RoleService.get_role_by_id(db, role_id)
|
|
|
|
if role.is_default and not is_superadmin:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Default roles can only be deleted by superadmins.",
|
|
)
|
|
|
|
active_modules = (
|
|
db.query(ModuleAccess.module_id)
|
|
.join(RoleModuleAccess, RoleModuleAccess.module_access_id == ModuleAccess.id)
|
|
.filter(RoleModuleAccess.role_id == role.id)
|
|
.distinct()
|
|
.all()
|
|
)
|
|
|
|
if active_modules:
|
|
module_ids = [m[0] for m in active_modules]
|
|
env_map = {}
|
|
if role.tenant_id:
|
|
tm_assignments = db.query(TenantModule).filter(
|
|
TenantModule.tenant_id == role.tenant_id,
|
|
TenantModule.module_id.in_(module_ids)
|
|
).all()
|
|
for tm in tm_assignments:
|
|
env_map[str(tm.module_id)] = tm.assigned_environment_slug or "prod"
|
|
|
|
targets = []
|
|
for m in active_modules:
|
|
mid = str(m[0])
|
|
env_slug = env_map.get(mid, "prod")
|
|
targets.append({"module_id": mid, "environment_slug": env_slug})
|
|
|
|
if targets:
|
|
provisioning_id = str(uuid.uuid4())
|
|
payload = {
|
|
"role_id": str(role.id),
|
|
"tenant_id": str(role.tenant_id) if role.tenant_id else None,
|
|
"provisioning_id": provisioning_id,
|
|
"targets": targets
|
|
}
|
|
|
|
logger.info(f"ROLE_DEPROVISION_REQUESTED Payload: {json.dumps(payload, default=str)}")
|
|
|
|
EventService.emit_event(
|
|
db,
|
|
event_type="ROLE_DEPROVISION_REQUESTED",
|
|
payload=payload,
|
|
tenant_id=role.tenant_id
|
|
)
|
|
|
|
db.delete(role)
|
|
db.commit()
|
|
return {"message": "Role deleted successfully"}
|
|
|
|
@staticmethod
|
|
def get_roles_paginated(
|
|
db: Session,
|
|
tenant_id: Optional[uuid.UUID] = None,
|
|
page: int = 1,
|
|
page_size: int = 10,
|
|
search: Optional[str] = None,
|
|
) -> RolePaginatedResponse:
|
|
|
|
query = db.query(Role)
|
|
|
|
if tenant_id is not None:
|
|
query = query.filter(Role.tenant_id == tenant_id)
|
|
|
|
if search and search.strip():
|
|
search_term = search.strip()
|
|
query = query.filter(
|
|
or_(
|
|
Role.role_name.ilike(f"%{search_term}%"),
|
|
cast(Role.id, String).ilike(f"%{search_term}%"),
|
|
)
|
|
)
|
|
|
|
total = query.count()
|
|
|
|
offset = (page - 1) * page_size
|
|
roles = query.offset(offset).limit(page_size).all()
|
|
|
|
total_pages = (total + page_size - 1) // page_size if total > 0 else 0
|
|
|
|
return RolePaginatedResponse(
|
|
items=[RoleResponse.model_validate(role) for role in roles],
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
total_pages=total_pages,
|
|
) |