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 src.models import StandardResponse
from .service import get_all_budget_constrains 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 = 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( async def get_target_reliability(
db_session: DbSession, db_session: DbSession,
token: Token, 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 .service import run_rbd_simulation, get_simulation_results, identify_worst_eaf_contributors
from .schema import OptimizationResult, TargetReliabiltiyQuery 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() 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( async def get_target_reliability(
db_session: DbSession, db_session: DbSession,
token: Token, token: Token,

@ -22,13 +22,17 @@ from .schema import (CalculationResultsRead,
from .service import (bulk_update_equipment, get_calculation_result, from .service import (bulk_update_equipment, get_calculation_result,
get_calculation_result_by_day, get_calculation_by_assetnum, get_all_calculations) get_calculation_result_by_day, get_calculation_by_assetnum, get_all_calculations)
from src.database.core import CollectorDbSession 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() router = APIRouter()
get_calculation = APIRouter() get_calculation = APIRouter()
@router.post( @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( async def create_calculation_time_constrains(
token: Token, token: Token,
@ -66,7 +70,8 @@ async def create_calculation_time_constrains(
@router.get( @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( async def get_all_simulation_calculations(
db_session: DbSession, db_session: DbSession,
@ -95,6 +100,7 @@ async def get_all_simulation_calculations(
CalculationTimeConstrainsParametersRead, CalculationTimeConstrainsParametersRead,
] ]
], ],
dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))]
) )
async def get_calculation_parameters( async def get_calculation_parameters(
db_session: DbSession, calculation_id: Optional[str] = Query(default=None) db_session: DbSession, calculation_id: Optional[str] = Query(default=None)
@ -112,7 +118,8 @@ async def get_calculation_parameters(
@get_calculation.get( @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")): 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': if calculation_id == 'default':
@ -132,7 +139,8 @@ async def get_calculation_results(db_session: DbSession, calculation_id, token:I
) )
@router.get( @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): 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( @router.post(
"/{calculation_id}/simulation", "/{calculation_id}/simulation",
response_model=StandardResponse[CalculationResultsRead], response_model=StandardResponse[CalculationResultsRead],
dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))]
) )
async def get_simulation_result( async def get_simulation_result(
db_session: DbSession, 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( async def update_selected_equipment(
db_session: DbSession, db_session: DbSession,
calculation_id, calculation_id,

@ -13,11 +13,14 @@ from src.overhaul_scope.schema import ScopeRead
from .schema import (OverhaulCriticalParts, OverhaulRead, from .schema import (OverhaulCriticalParts, OverhaulRead,
OverhaulSystemComponents) OverhaulSystemComponents)
from src.auth.access_control import required_permission, OHPermission
from src.overhaul_scope.model import OverhaulScope
from fastapi import Depends
router = APIRouter() 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): async def get_overhaul(db_session: DbSession, token:Token, collector_db_session:CollectorDbSession):
"""Get all scope pagination.""" """Get all scope pagination."""
overview = await get_overhaul_overview(db_session=db_session) 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(): async def get_schedules():
"""Get all overhaul schedules.""" """Get all overhaul schedules."""
schedules = get_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(): async def get_critical_parts():
"""Get all overhaul critical parts.""" """Get all overhaul critical parts."""
criticalParts = get_overhaul_critical_parts() criticalParts = get_overhaul_critical_parts()
@ -57,7 +60,8 @@ async def get_critical_parts():
@router.get( @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(): async def get_system_components():
"""Get all overhaul 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.models import DefaultMixin, IdentityMixin, TimeStampMixin
from src.auth.access_control import Allow, RolePrincipal, OHPermission
class OverhaulScope(Base, DefaultMixin): class OverhaulScope(Base, DefaultMixin):
__tablename__ = "oh_ms_overhaul" __tablename__ = "oh_ms_overhaul"
start_date = Column(DateTime(timezone=True), nullable=False) # Made non-nullable to match model 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) UUID(as_uuid=True), ForeignKey("oh_ms_maintenance_type.id"), nullable=False)
wo_parent = Column(JSON, nullable=True) 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") maintenance_type = relationship("MaintenanceType", lazy="selectin", backref="overhaul_scopes")
# activity_equipments = relationship("OverhaulActivity", lazy="selectin") # activity_equipments = relationship("OverhaulActivity", lazy="selectin")

@ -11,11 +11,13 @@ from src.models import StandardResponse
from .model import OverhaulScope from .model import OverhaulScope
from .schema import ScopeCreate, ScopePagination, ScopeRead, ScopeUpdate from .schema import ScopeCreate, ScopePagination, ScopeRead, ScopeUpdate
from .service import create, delete, get, get_all, update,get_all_oh_with_history_service 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 = 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): async def get_scopes(common: CommonParameters, scope_name: Optional[str] = None):
"""Get all scope pagination.""" """Get all scope pagination."""
# return # return
@ -26,11 +28,11 @@ async def get_scopes(common: CommonParameters, scope_name: Optional[str] = None)
message="Data retrieved successfully", 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): 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") 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): async def get_scope(db_session: DbSession, overhaul_session_id: str):
scope = await get(db_session=db_session, overhaul_session_id=overhaul_session_id) scope = await get(db_session=db_session, overhaul_session_id=overhaul_session_id)
if not scope: 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") 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): async def create_scope(db_session: DbSession, scope_in: ScopeCreate):
scope = await create(db_session=db_session, scope_in=scope_in) scope = await create(db_session=db_session, scope_in=scope_in)
return StandardResponse(data=scope, message="Data created successfully") 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( async def update_scope(
db_session: DbSession, db_session: DbSession,
scope_id: str, 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): 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) scope = await get(db_session=db_session, overhaul_session_id=scope_id, for_update=True)

Loading…
Cancel
Save