feat: Implement role-based access control for OverhaulScope and integrate it into API endpoints.

oh_security
Cizz22 4 months ago
parent a728d39fce
commit c5f7384b2d

@ -0,0 +1,122 @@
from dataclasses import dataclass
from enum import Enum
from typing import Any, List, Union
from fastapi import Request, HTTPException, status
Allow: str = "allow"
Deny: str = "deny"
@dataclass(frozen=True)
class Principal:
key: str
value: str
def __repr__(self) -> str:
return f"{self.key}:{self.value}"
def __str__(self) -> str:
return self.__repr__()
@dataclass(frozen=True)
class SystemPrincipal(Principal):
def __init__(self, value: str):
super().__init__(key="system", value=value)
@dataclass(frozen=True)
class RolePrincipal(Principal):
def __init__(self, value: str):
super().__init__(key="role", value=value)
Everyone = SystemPrincipal(value="everyone")
Authenticated = SystemPrincipal(value="authenticated")
class OHPermission(Enum):
CREATE = "create"
READ = "read"
EDIT = "edit"
DELETE = "delete"
class AccessControl:
def __init__(self, permission_exception: Any = None):
self.permission_exception = permission_exception or HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Permission denied"
)
def _acl(self, resource):
acl = getattr(resource, "__acl__", [])
if callable(acl):
return acl()
return acl
def has_permission(
self, principals: List[Principal], required_permissions: List[OHPermission], resource: Any
) -> bool:
if not isinstance(resource, list):
resource = [resource]
permits = []
for res in resource:
granted = False
acl = self._acl(res)
for action, principal, permissions in acl:
# Check if any of the required permissions are in the allowed permissions for this principal
is_permitted = any(p in permissions for p in required_permissions)
if (action == Allow and is_permitted) and (
principal in principals or principal == Everyone
):
granted = True
break
elif (action == Deny and is_permitted) and (
principal in principals or principal == Everyone
):
granted = False
break
permits.append(granted)
return all(permits)
def assert_access(
self, principals: List[Principal], required_permissions: List[OHPermission], resource: Any
):
if not self.has_permission(principals, required_permissions, resource):
raise self.permission_exception
def required_permission(permissions: Union[OHPermission, List[OHPermission]], resources: Any):
"""
FastAPI dependency for role-based access control.
"""
if isinstance(permissions, OHPermission):
permissions = [permissions]
async def dependency(request: Request):
user = getattr(request.state, "user", None)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated"
)
user_role = None
if isinstance(user, dict):
user_role = user.get("role")
else:
user_role = getattr(user, "role", None)
if not user_role:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="No user role found"
)
user_principals = [Authenticated, RolePrincipal(user_role)]
ac = AccessControl()
ac.assert_access(user_principals, permissions, resources)
return True
return dependency

@ -11,11 +11,14 @@ from src.database.core import CollectorDbSession, DbSession
from src.models import StandardResponse
from .service import get_all_budget_constrains
from src.auth.access_control import required_permission, OHPermission
from src.overhaul_scope.model import OverhaulScope
from fastapi import Depends
router = APIRouter()
@router.get("/{session_id}", response_model=StandardResponse[Dict])
@router.get("/{session_id}", response_model=StandardResponse[Dict], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
async def get_target_reliability(
db_session: DbSession,
token: Token,

@ -13,6 +13,9 @@ from src.models import StandardResponse
from .service import run_rbd_simulation, get_simulation_results, identify_worst_eaf_contributors
from .schema import OptimizationResult, TargetReliabiltiyQuery
from src.auth.access_control import required_permission, OHPermission
from src.overhaul_scope.model import OverhaulScope
from fastapi import Depends
router = APIRouter()
@ -33,7 +36,7 @@ router = APIRouter()
# )
@router.get("", response_model=StandardResponse[OptimizationResult])
@router.get("", response_model=StandardResponse[OptimizationResult], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
async def get_target_reliability(
db_session: DbSession,
token: Token,

@ -22,13 +22,17 @@ from .schema import (CalculationResultsRead,
from .service import (bulk_update_equipment, get_calculation_result,
get_calculation_result_by_day, get_calculation_by_assetnum, get_all_calculations)
from src.database.core import CollectorDbSession
from src.auth.access_control import required_permission, OHPermission
from src.overhaul_scope.model import OverhaulScope
from fastapi import Depends
router = APIRouter()
get_calculation = APIRouter()
@router.post(
"", response_model=StandardResponse[Union[dict, CalculationTimeConstrainsRead]]
"", response_model=StandardResponse[Union[dict, CalculationTimeConstrainsRead]],
dependencies=[Depends(required_permission(OHPermission.CREATE, OverhaulScope))]
)
async def create_calculation_time_constrains(
token: Token,
@ -66,7 +70,8 @@ async def create_calculation_time_constrains(
@router.get(
"", response_model=StandardResponse[List[CalculationTimeConstrainsReadNoResult]]
"", response_model=StandardResponse[List[CalculationTimeConstrainsReadNoResult]],
dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))]
)
async def get_all_simulation_calculations(
db_session: DbSession,
@ -95,6 +100,7 @@ async def get_all_simulation_calculations(
CalculationTimeConstrainsParametersRead,
]
],
dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))]
)
async def get_calculation_parameters(
db_session: DbSession, calculation_id: Optional[str] = Query(default=None)
@ -112,7 +118,8 @@ async def get_calculation_parameters(
@get_calculation.get(
"/{calculation_id}", response_model=StandardResponse[CalculationTimeConstrainsRead]
"/{calculation_id}", response_model=StandardResponse[CalculationTimeConstrainsRead],
dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))]
)
async def get_calculation_results(db_session: DbSession, calculation_id, token:InternalKey, include_risk_cost:int = Query(1, alias="risk_cost")):
if calculation_id == 'default':
@ -132,7 +139,8 @@ async def get_calculation_results(db_session: DbSession, calculation_id, token:I
)
@router.get(
"/{calculation_id}/{assetnum}", response_model=StandardResponse[EquipmentResult]
"/{calculation_id}/{assetnum}", response_model=StandardResponse[EquipmentResult],
dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))]
)
async def get_calculation_per_equipment(db_session: DbSession, calculation_id, assetnum):
@ -150,6 +158,7 @@ async def get_calculation_per_equipment(db_session: DbSession, calculation_id, a
@router.post(
"/{calculation_id}/simulation",
response_model=StandardResponse[CalculationResultsRead],
dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))]
)
async def get_simulation_result(
db_session: DbSession,
@ -167,7 +176,7 @@ async def get_simulation_result(
)
@router.post("/update/{calculation_id}", response_model=StandardResponse[List[str]])
@router.post("/update/{calculation_id}", response_model=StandardResponse[List[str]], dependencies=[Depends(required_permission(OHPermission.EDIT, OverhaulScope))])
async def update_selected_equipment(
db_session: DbSession,
calculation_id,

@ -13,11 +13,14 @@ from src.overhaul_scope.schema import ScopeRead
from .schema import (OverhaulCriticalParts, OverhaulRead,
OverhaulSystemComponents)
from src.auth.access_control import required_permission, OHPermission
from src.overhaul_scope.model import OverhaulScope
from fastapi import Depends
router = APIRouter()
@router.get("", response_model=StandardResponse[OverhaulRead])
@router.get("", response_model=StandardResponse[OverhaulRead], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
async def get_overhaul(db_session: DbSession, token:Token, collector_db_session:CollectorDbSession):
"""Get all scope pagination."""
overview = await get_overhaul_overview(db_session=db_session)
@ -36,7 +39,7 @@ async def get_overhaul(db_session: DbSession, token:Token, collector_db_session:
)
@router.get("/schedules", response_model=StandardResponse[List[ScopeRead]])
@router.get("/schedules", response_model=StandardResponse[List[ScopeRead]], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
async def get_schedules():
"""Get all overhaul schedules."""
schedules = get_overhaul_schedules()
@ -46,7 +49,7 @@ async def get_schedules():
)
@router.get("/critical-parts", response_model=StandardResponse[OverhaulCriticalParts])
@router.get("/critical-parts", response_model=StandardResponse[OverhaulCriticalParts], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
async def get_critical_parts():
"""Get all overhaul critical parts."""
criticalParts = get_overhaul_critical_parts()
@ -57,7 +60,8 @@ async def get_critical_parts():
@router.get(
"/system-components", response_model=StandardResponse[OverhaulSystemComponents]
"/system-components", response_model=StandardResponse[OverhaulSystemComponents],
dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))]
)
async def get_system_components():
"""Get all overhaul system components."""

@ -6,6 +6,8 @@ from src.database.core import Base
from src.models import DefaultMixin, IdentityMixin, TimeStampMixin
from src.auth.access_control import Allow, RolePrincipal, OHPermission
class OverhaulScope(Base, DefaultMixin):
__tablename__ = "oh_ms_overhaul"
start_date = Column(DateTime(timezone=True), nullable=False) # Made non-nullable to match model
@ -17,7 +19,25 @@ class OverhaulScope(Base, DefaultMixin):
UUID(as_uuid=True), ForeignKey("oh_ms_maintenance_type.id"), nullable=False)
wo_parent = Column(JSON, nullable=True)
@classmethod
def __acl__(self):
basic_permissions = [OHPermission.READ]
engineer_permissions = [
OHPermission.READ,
OHPermission.CREATE,
OHPermission.EDIT,
OHPermission.DELETE,
]
all_permissions = list(OHPermission)
return [
(Allow, RolePrincipal("Management"), basic_permissions),
(Allow, RolePrincipal("Engineer"), engineer_permissions),
(Allow, RolePrincipal("Admin"), all_permissions),
]
maintenance_type = relationship("MaintenanceType", lazy="selectin", backref="overhaul_scopes")
# activity_equipments = relationship("OverhaulActivity", lazy="selectin")

@ -11,11 +11,13 @@ from src.models import StandardResponse
from .model import OverhaulScope
from .schema import ScopeCreate, ScopePagination, ScopeRead, ScopeUpdate
from .service import create, delete, get, get_all, update,get_all_oh_with_history_service
from src.auth.access_control import required_permission, OHPermission
from fastapi import Depends
router = APIRouter()
@router.get("", response_model=StandardResponse[ScopePagination])
@router.get("", response_model=StandardResponse[ScopePagination], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
async def get_scopes(common: CommonParameters, scope_name: Optional[str] = None):
"""Get all scope pagination."""
# return
@ -26,11 +28,11 @@ async def get_scopes(common: CommonParameters, scope_name: Optional[str] = None)
message="Data retrieved successfully",
)
@router.get("/history", response_model=StandardResponse[List[ScopeRead]])
@router.get("/history", response_model=StandardResponse[List[ScopeRead]], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
async def get_history(db_session: DbSession):
return StandardResponse(data=await get_all_oh_with_history_service(db_session=db_session), message="Data retrieved successfully")
@router.get("/{overhaul_session_id}", response_model=StandardResponse[ScopeRead])
@router.get("/{overhaul_session_id}", response_model=StandardResponse[ScopeRead], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
async def get_scope(db_session: DbSession, overhaul_session_id: str):
scope = await get(db_session=db_session, overhaul_session_id=overhaul_session_id)
if not scope:
@ -42,14 +44,14 @@ async def get_scope(db_session: DbSession, overhaul_session_id: str):
return StandardResponse(data=scope, message="Data retrieved successfully")
@router.post("", response_model=StandardResponse[ScopeRead])
@router.post("", response_model=StandardResponse[ScopeRead], dependencies=[Depends(required_permission(OHPermission.CREATE, OverhaulScope))])
async def create_scope(db_session: DbSession, scope_in: ScopeCreate):
scope = await create(db_session=db_session, scope_in=scope_in)
return StandardResponse(data=scope, message="Data created successfully")
@router.post("/update/{scope_id}", response_model=StandardResponse[ScopeRead])
@router.post("/update/{scope_id}", response_model=StandardResponse[ScopeRead], dependencies=[Depends(required_permission(OHPermission.EDIT, OverhaulScope))])
async def update_scope(
db_session: DbSession,
scope_id: str,
@ -70,7 +72,7 @@ async def update_scope(
)
@router.post("/delete/{scope_id}", response_model=StandardResponse[ScopeRead])
@router.post("/delete/{scope_id}", response_model=StandardResponse[ScopeRead], dependencies=[Depends(required_permission(OHPermission.DELETE, OverhaulScope))])
async def delete_scope(db_session: DbSession, scope_id: str):
scope = await get(db_session=db_session, overhaul_session_id=scope_id, for_update=True)

Loading…
Cancel
Save