Skip to content

Commit e20afb9

Browse files
Merge pull request #15 from PSNAppz/develop
Added staff portal applications, roles, and actions to retrieve user-specific permissions
2 parents 1186aa5 + c27b84d commit e20afb9

File tree

7 files changed

+174
-0
lines changed

7 files changed

+174
-0
lines changed

iam-staff-portal-api/src/iam_staff_portal_api/app.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,18 @@
66

77
_config = Settings.get_config()
88

9+
print("DB datasource:", _config.db_datasource)
10+
911
from iam_core.models import LoginProvider
1012
from iam_core.user_auth.app import Initializer as AuthInitializer
1113

1214
from .controllers.auth_controller import AuthController
15+
from .models import (
16+
StaffApplicationAction,
17+
StaffPortalApplication,
18+
StaffRole,
19+
StaffRoleAction,
20+
)
1321

1422

1523
class Initializer(AuthInitializer):
@@ -23,5 +31,9 @@ def migrate_database(self, args):
2331

2432
async def migrate():
2533
await LoginProvider.create_migrate()
34+
await StaffPortalApplication.create_migrate()
35+
await StaffRole.create_migrate()
36+
await StaffApplicationAction.create_migrate()
37+
await StaffRoleAction.create_migrate()
2638

2739
asyncio.run(migrate())

iam-staff-portal-api/src/iam_staff_portal_api/controllers/auth_controller.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from fastapi import Depends, Request, Response
44
from fastapi.responses import RedirectResponse
55
from datetime import datetime, timedelta, timezone
6+
from openg2p_fastapi_common.context import dbengine
67
from openg2p_fastapi_common.controller import BaseController
78
from iam_core.schemas import (
89
AuthPrincipal,
@@ -11,8 +12,16 @@
1112
)
1213
from iam_core.services import AuthService
1314
from iam_core.user_auth.dependencies import auth_principal, require_user_type
15+
from sqlalchemy import select
16+
from sqlalchemy.ext.asyncio import async_sessionmaker
1417

1518
from ..config import Settings
19+
from ..models import (
20+
StaffApplicationAction,
21+
StaffPortalApplication,
22+
StaffRole,
23+
StaffRoleAction,
24+
)
1625

1726
_config = Settings.get_config(strict=False)
1827

@@ -41,6 +50,16 @@ def __init__(self, **kwargs):
4150
methods=["POST"],
4251
)
4352
self.router.add_api_route("/callback", self.oauth_callback, methods=["GET"])
53+
self.router.add_api_route(
54+
"/get_staff_portal_applications",
55+
self.get_staff_portal_applications,
56+
methods=["GET"],
57+
)
58+
self.router.add_api_route(
59+
"/get_application_actions_for_user",
60+
self.get_application_actions_for_user,
61+
methods=["GET"],
62+
)
4463

4564
async def get_user_profile(
4665
self,
@@ -107,3 +126,93 @@ async def oauth_callback(self, request: Request):
107126
secure=_config.auth_cookie_secure,
108127
)
109128
return response
129+
130+
async def get_staff_portal_applications(
131+
self,
132+
auth: Annotated[
133+
AuthPrincipal,
134+
Depends(require_user_type("staff", auth_dependency=auth_principal)),
135+
],
136+
):
137+
apps = await StaffPortalApplication.get_all()
138+
return [
139+
{
140+
"id": app.id,
141+
"application_mnemonic": app.application_mnemonic,
142+
"application_description": app.application_description,
143+
"icon_base64": app.icon_base64,
144+
}
145+
for app in apps
146+
]
147+
148+
async def get_application_actions_for_user(
149+
self,
150+
auth: Annotated[
151+
AuthPrincipal,
152+
Depends(require_user_type("staff", auth_dependency=auth_principal)),
153+
],
154+
):
155+
client_roles = auth.client_roles or {}
156+
if not client_roles:
157+
return []
158+
159+
result = []
160+
async_session = async_sessionmaker(dbengine.get())
161+
async with async_session() as session:
162+
for client_id, roles in client_roles.items():
163+
# Find the application by mnemonic (= Keycloak client_id)
164+
stmt = select(StaffPortalApplication).where(
165+
StaffPortalApplication.application_mnemonic == client_id,
166+
StaffPortalApplication.active == True, # noqa: E712
167+
)
168+
app_row = (await session.execute(stmt)).scalars().first()
169+
if not app_row:
170+
continue
171+
172+
# Find role IDs matching the token roles for this application
173+
role_stmt = select(StaffRole).where(
174+
StaffRole.application_id == app_row.id,
175+
StaffRole.role_mnemonic.in_(roles),
176+
StaffRole.active == True, # noqa: E712
177+
)
178+
role_rows = (await session.execute(role_stmt)).scalars().all()
179+
role_ids = [r.id for r in role_rows]
180+
181+
if not role_ids:
182+
continue
183+
184+
# Get action IDs mapped to those roles
185+
mapping_stmt = select(StaffRoleAction.action_id).where(
186+
StaffRoleAction.role_id.in_(role_ids),
187+
StaffRoleAction.active == True, # noqa: E712
188+
)
189+
action_ids = (
190+
(await session.execute(mapping_stmt)).scalars().all()
191+
)
192+
193+
if not action_ids:
194+
continue
195+
196+
# Get the action details
197+
action_stmt = select(StaffApplicationAction).where(
198+
StaffApplicationAction.id.in_(action_ids),
199+
StaffApplicationAction.active == True, # noqa: E712
200+
)
201+
action_rows = (
202+
(await session.execute(action_stmt)).scalars().all()
203+
)
204+
205+
actions = sorted(
206+
set(a.action_mnemonic for a in action_rows)
207+
)
208+
209+
if actions:
210+
result.append(
211+
{
212+
"application_id": app_row.id,
213+
"application_mnemonic": app_row.application_mnemonic,
214+
"actions": actions,
215+
}
216+
)
217+
218+
return result
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .staff_portal_application import StaffPortalApplication
2+
from .staff_role import StaffRole
3+
from .staff_application_action import StaffApplicationAction
4+
from .staff_role_action import StaffRoleAction
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing import Optional
2+
3+
from openg2p_fastapi_common.models import BaseORMModelWithTimes
4+
from sqlalchemy import Integer, String
5+
from sqlalchemy.orm import Mapped, mapped_column
6+
7+
8+
class StaffApplicationAction(BaseORMModelWithTimes):
9+
__tablename__ = "staff_application_actions"
10+
11+
action_mnemonic: Mapped[str] = mapped_column(String, nullable=False)
12+
action_description: Mapped[Optional[str]] = mapped_column(String, nullable=True)
13+
application_id: Mapped[int] = mapped_column(Integer, nullable=False)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing import Optional
2+
3+
from openg2p_fastapi_common.models import BaseORMModelWithTimes
4+
from sqlalchemy import String
5+
from sqlalchemy.orm import Mapped, mapped_column
6+
7+
8+
class StaffPortalApplication(BaseORMModelWithTimes):
9+
__tablename__ = "staff_portal_applications"
10+
11+
application_mnemonic: Mapped[str] = mapped_column(String, unique=True, nullable=False)
12+
application_description: Mapped[Optional[str]] = mapped_column(String, nullable=True)
13+
icon_base64: Mapped[Optional[str]] = mapped_column(String, nullable=True)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing import Optional
2+
3+
from openg2p_fastapi_common.models import BaseORMModelWithTimes
4+
from sqlalchemy import Integer, String
5+
from sqlalchemy.orm import Mapped, mapped_column
6+
7+
8+
class StaffRole(BaseORMModelWithTimes):
9+
__tablename__ = "staff_roles"
10+
11+
role_mnemonic: Mapped[str] = mapped_column(String, nullable=False)
12+
role_description: Mapped[Optional[str]] = mapped_column(String, nullable=True)
13+
application_id: Mapped[int] = mapped_column(Integer, nullable=False)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from openg2p_fastapi_common.models import BaseORMModelWithTimes
2+
from sqlalchemy import Integer
3+
from sqlalchemy.orm import Mapped, mapped_column
4+
5+
6+
class StaffRoleAction(BaseORMModelWithTimes):
7+
__tablename__ = "staff_role_actions"
8+
9+
role_id: Mapped[int] = mapped_column(Integer, nullable=False)
10+
action_id: Mapped[int] = mapped_column(Integer, nullable=False)

0 commit comments

Comments
 (0)