zrb 1.0.0b9__py3-none-any.whl → 1.0.0b10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/.coveragerc +11 -0
  2. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/.gitignore +4 -0
  3. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_task.py +4 -4
  4. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/config.py +5 -0
  5. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_task.py +107 -1
  6. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +67 -4
  7. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_create_my_entity.py +53 -0
  8. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_delete_my_entity.py +62 -0
  9. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_read_my_entity.py +65 -0
  10. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_update_my_entity.py +61 -0
  11. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/gateway_subroute.py +57 -13
  12. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +2 -2
  13. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/gateway/subroute/my_module.py +6 -1
  14. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/module_task_definition.py +10 -6
  15. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task.py +56 -12
  16. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task_util.py +10 -4
  17. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_service.py +136 -52
  18. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/parser.py +1 -1
  19. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +1 -0
  20. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/3093c7336477_add_auth_tables.py +46 -43
  21. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/8ed025bcc845_create_permissions.py +69 -0
  22. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_db_repository.py +5 -2
  23. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service.py +16 -21
  24. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py +193 -43
  25. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/auth.py +57 -0
  26. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/requirements.txt +6 -1
  27. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/permission.py +1 -0
  28. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/user.py +9 -0
  29. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/_util/access_token.py +19 -0
  30. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_create_permission.py +59 -0
  31. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_delete_permission.py +68 -0
  32. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_read_permission.py +71 -0
  33. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_update_permission.py +66 -0
  34. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/test_user_session.py +195 -0
  35. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_health_and_readiness.py +28 -0
  36. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_homepage.py +17 -0
  37. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_not_found_error.py +16 -0
  38. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test.sh +7 -0
  39. zrb/task/base_task.py +10 -10
  40. zrb/util/codemod/modification_mode.py +3 -0
  41. zrb/util/codemod/modify_class.py +58 -0
  42. zrb/util/codemod/modify_class_parent.py +68 -0
  43. zrb/util/codemod/modify_class_property.py +128 -0
  44. zrb/util/codemod/modify_dict.py +75 -0
  45. zrb/util/codemod/modify_function.py +65 -0
  46. zrb/util/codemod/modify_function_call.py +68 -0
  47. zrb/util/codemod/modify_method.py +88 -0
  48. zrb/util/codemod/{prepend_code_to_module.py → modify_module.py} +2 -3
  49. zrb/util/file.py +3 -2
  50. {zrb-1.0.0b9.dist-info → zrb-1.0.0b10.dist-info}/METADATA +1 -1
  51. {zrb-1.0.0b9.dist-info → zrb-1.0.0b10.dist-info}/RECORD +53 -36
  52. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/migrate.py +0 -3
  53. zrb/util/codemod/append_code_to_class.py +0 -35
  54. zrb/util/codemod/append_code_to_function.py +0 -38
  55. zrb/util/codemod/append_code_to_method.py +0 -55
  56. zrb/util/codemod/append_key_to_dict.py +0 -51
  57. zrb/util/codemod/append_param_to_function_call.py +0 -39
  58. zrb/util/codemod/prepend_parent_to_class.py +0 -38
  59. zrb/util/codemod/prepend_property_to_class.py +0 -55
  60. {zrb-1.0.0b9.dist-info → zrb-1.0.0b10.dist-info}/WHEEL +0 -0
  61. {zrb-1.0.0b9.dist-info → zrb-1.0.0b10.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,69 @@
1
+ """create_permissions
2
+
3
+ Revision ID: 8ed025bcc845
4
+ Revises: 3093c7336477
5
+ Create Date: 2025-02-08 19:09:14.536559
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ import sqlalchemy as sa
12
+ import sqlmodel # 🔥 FastApp Modification
13
+ from alembic import op
14
+ from module.auth.migration_metadata import metadata
15
+
16
+ # revision identifiers, used by Alembic.
17
+ revision: str = "8ed025bcc845"
18
+ down_revision: Union[str, None] = "3093c7336477"
19
+ branch_labels: Union[str, Sequence[str], None] = None
20
+ depends_on: Union[str, Sequence[str], None] = None
21
+
22
+
23
+ def upgrade() -> None:
24
+ op.bulk_insert(
25
+ metadata.tables["permissions"],
26
+ [
27
+ # permission
28
+ {"name": "permission:create", "description": "create permission"},
29
+ {"name": "permission:read", "description": "read permission"},
30
+ {"name": "permission:update", "description": "update permission"},
31
+ {"name": "permission:delete", "description": "delete permission"},
32
+ # role
33
+ {"name": "role:create", "description": "create role"},
34
+ {"name": "role:read", "description": "read role"},
35
+ {"name": "role:update", "description": "update role"},
36
+ {"name": "role:delete", "description": "delete role"},
37
+ # user
38
+ {"name": "user:create", "description": "create user"},
39
+ {"name": "user:read", "description": "read user"},
40
+ {"name": "user:update", "description": "update user"},
41
+ {"name": "user:delete", "description": "delete user"},
42
+ ],
43
+ )
44
+ # ### end Alembic commands ###
45
+
46
+
47
+ def downgrade() -> None:
48
+ op.execute(
49
+ sa.delete(metadata.tables["permissions"]).where(
50
+ metadata.tables["permissions"].c.name.in_(
51
+ # user
52
+ "user:create",
53
+ "user:read",
54
+ "user:update",
55
+ "user:delete",
56
+ # role
57
+ "role:create",
58
+ "role:read",
59
+ "role:update",
60
+ "role:delete",
61
+ # permission
62
+ "permission:create",
63
+ "permission:read",
64
+ "permission:update",
65
+ "permission:delete",
66
+ )
67
+ )
68
+ )
69
+ # ### end Alembic commands ###
@@ -49,7 +49,7 @@ class UserDBRepository(
49
49
 
50
50
  def _select(self) -> Select:
51
51
  return (
52
- select(User, Role, Permission, UserSession)
52
+ select(User, Role, Permission)
53
53
  .join(UserRole, UserRole.user_id == User.id, isouter=True)
54
54
  .join(Role, Role.id == UserRole.role_id, isouter=True)
55
55
  .join(RolePermission, RolePermission.role_id == Role.id, isouter=True)
@@ -62,7 +62,7 @@ class UserDBRepository(
62
62
  user_map: dict[str, dict[str, Any]] = {}
63
63
  user_role_map: dict[str, list[str]] = {}
64
64
  user_permission_map: dict[str, list[str]] = {}
65
- for user, role, permission, _ in rows:
65
+ for user, role, permission in rows:
66
66
  if user.id not in user_map:
67
67
  user_map[user.id] = {"user": user, "roles": [], "permissions": []}
68
68
  user_role_map[user.id] = []
@@ -241,6 +241,9 @@ class UserDBRepository(
241
241
  return UserSessionResponse(
242
242
  id=user_session.id,
243
243
  user_id=user_session.user_id,
244
+ access_token=user_session.access_token,
244
245
  access_token_expired_at=user_session.access_token_expired_at,
246
+ refresh_token=user_session.refresh_token,
245
247
  refresh_token_expired_at=user_session.refresh_token_expired_at,
248
+ token_type="bearer",
246
249
  )
@@ -46,8 +46,20 @@ class UserService(BaseService):
46
46
  methods=["get"],
47
47
  response_model=AuthUserResponse,
48
48
  )
49
- async def get_current_user(self, access_token: str) -> AuthUserResponse:
50
- return self._get_auth_user_by_access_token(access_token)
49
+ async def get_current_user(self, access_token: str | None) -> AuthUserResponse:
50
+ if access_token is None or access_token == "":
51
+ return self._get_guest_user()
52
+ try:
53
+ user_session = await self.user_repository.get_user_session_by_access_token(
54
+ access_token
55
+ )
56
+ user_id = user_session.user_id
57
+ if user_id == self.config.super_user:
58
+ return self._get_super_user()
59
+ user = await self.user_repository.get_by_id(user_id)
60
+ return self._to_auth_user_response(user)
61
+ except NotFoundError:
62
+ return self._get_guest_user()
51
63
 
52
64
  @BaseService.route(
53
65
  "/api/v1/user-sessions",
@@ -78,7 +90,7 @@ class UserService(BaseService):
78
90
  async def update_user_session(self, refresh_token: str) -> UserSessionResponse:
79
91
  current_user = await self._get_auth_user_by_refresh_token(refresh_token)
80
92
  current_user_session = (
81
- await self.user_respository.get_user_sesion_by_refresh_token(refresh_token)
93
+ await self.user_repository.get_user_session_by_refresh_token(refresh_token)
82
94
  )
83
95
  token_data = self._create_user_token_data(current_user.username)
84
96
  return await self.user_repository.update_user_session(
@@ -94,7 +106,7 @@ class UserService(BaseService):
94
106
  )
95
107
  async def delete_user_session(self, refresh_token: str) -> UserSessionResponse:
96
108
  current_user_session = (
97
- await self.user_respository.get_user_sesion_by_refresh_token(refresh_token)
109
+ await self.user_repository.get_user_session_by_refresh_token(refresh_token)
98
110
  )
99
111
  await self.user_repository.delete_user_sessions([current_user_session.id])
100
112
  return current_user_session
@@ -231,23 +243,6 @@ class UserService(BaseService):
231
243
  user = await self.user_repository.get_by_id(user_id)
232
244
  return self._to_auth_user_response(user)
233
245
 
234
- async def _get_auth_user_by_access_token(
235
- self, access_token: str
236
- ) -> AuthUserResponse:
237
- if access_token is None or access_token == "":
238
- return self._get_guest_user()
239
- user_session = await self.user_repository.get_user_session_by_access_token(
240
- access_token
241
- )
242
- user_id = user_session.user_id
243
- if user_id == self.config.super_user:
244
- return self._get_super_user()
245
- try:
246
- user = await self.user_repository.get_by_id(user_id)
247
- return self._to_auth_user_response(user)
248
- except NotFoundError:
249
- return self._get_guest_user()
250
-
251
246
  async def _get_user_by_credentials(
252
247
  self, credentials: UserCredentials
253
248
  ) -> AuthUserResponse:
@@ -2,7 +2,13 @@ from typing import Annotated
2
2
 
3
3
  from fastapi import Depends, FastAPI, Response
4
4
  from fastapi.security import OAuth2PasswordRequestForm
5
+ from my_app_name.common.error import ForbiddenError
5
6
  from my_app_name.module.auth.client.auth_client_factory import auth_client
7
+ from my_app_name.module.gateway.util.auth import (
8
+ get_current_user,
9
+ set_user_session_cookie,
10
+ unset_user_session_cookie,
11
+ )
6
12
  from my_app_name.schema.permission import (
7
13
  MultiplePermissionResponse,
8
14
  PermissionCreate,
@@ -16,6 +22,7 @@ from my_app_name.schema.role import (
16
22
  RoleUpdateWithPermissions,
17
23
  )
18
24
  from my_app_name.schema.user import (
25
+ AuthUserResponse,
19
26
  MultipleUserResponse,
20
27
  UserCreateWithRoles,
21
28
  UserCredentials,
@@ -37,203 +44,346 @@ def serve_auth_route(app: FastAPI):
37
44
  password=form_data.password,
38
45
  )
39
46
  )
47
+ set_user_session_cookie(response, user_session)
40
48
  return user_session
41
49
 
42
50
  @app.put("/api/v1/user-sessions", response_model=UserSessionResponse)
43
- async def update_user_session(refresh_token: str) -> UserSessionResponse:
44
- return await auth_client.update_user_session(refresh_token)
51
+ async def update_user_session(
52
+ response: Response, refresh_token: str
53
+ ) -> UserSessionResponse:
54
+ user_session = await auth_client.update_user_session(refresh_token)
55
+ set_user_session_cookie(response, user_session)
56
+ return user_session
45
57
 
46
58
  @app.delete("/api/v1/user-sessions", response_model=UserSessionResponse)
47
- async def delete_user_session(refresh_token: str) -> UserSessionResponse:
48
- return await auth_client.delete_user_session(refresh_token)
59
+ async def delete_user_session(
60
+ response: Response, refresh_token: str
61
+ ) -> UserSessionResponse:
62
+ user_session = await auth_client.delete_user_session(refresh_token)
63
+ unset_user_session_cookie(response)
64
+ return user_session
49
65
 
50
66
  # Permission routes
51
67
 
52
68
  @app.get("/api/v1/permissions", response_model=MultiplePermissionResponse)
53
69
  async def get_permissions(
70
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
54
71
  page: int = 1,
55
72
  page_size: int = 10,
56
73
  sort: str | None = None,
57
74
  filter: str | None = None,
58
75
  ) -> MultiplePermissionResponse:
76
+ if not current_user.has_permission("permission:read"):
77
+ raise ForbiddenError("Access denied")
59
78
  return await auth_client.get_permissions(
60
79
  page=page, page_size=page_size, sort=sort, filter=filter
61
80
  )
62
81
 
63
82
  @app.get("/api/v1/permissions/{permission_id}", response_model=PermissionResponse)
64
- async def get_permission_by_id(permission_id: str) -> PermissionResponse:
83
+ async def get_permission_by_id(
84
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
85
+ permission_id: str,
86
+ ) -> PermissionResponse:
87
+ if not current_user.has_permission("permission:read"):
88
+ raise ForbiddenError("Access denied")
65
89
  return await auth_client.get_permission_by_id(permission_id)
66
90
 
67
91
  @app.post(
68
92
  "/api/v1/permissions/bulk",
69
93
  response_model=list[PermissionResponse],
70
94
  )
71
- async def create_permission_bulk(data: list[PermissionCreate]):
95
+ async def create_permission_bulk(
96
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
97
+ data: list[PermissionCreate],
98
+ ) -> list[PermissionResponse]:
99
+ if not current_user.has_permission("permission:create"):
100
+ raise ForbiddenError("Access denied")
72
101
  return await auth_client.create_permission_bulk(
73
- [row.with_audit(created_by="system") for row in data]
102
+ [row.with_audit(created_by=current_user.id) for row in data]
74
103
  )
75
104
 
76
105
  @app.post(
77
106
  "/api/v1/permissions",
78
107
  response_model=PermissionResponse,
79
108
  )
80
- async def create_permission(data: PermissionCreate):
81
- return await auth_client.create_permission(data.with_audit(created_by="system"))
109
+ async def create_permission(
110
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
111
+ data: PermissionCreate,
112
+ ) -> PermissionResponse:
113
+ if not current_user.has_permission("permission:create"):
114
+ raise ForbiddenError("Access denied")
115
+ return await auth_client.create_permission(
116
+ data.with_audit(created_by=current_user.id)
117
+ )
82
118
 
83
119
  @app.put(
84
120
  "/api/v1/permissions/bulk",
85
121
  response_model=list[PermissionResponse],
86
122
  )
87
- async def update_permission_bulk(permission_ids: list[str], data: PermissionUpdate):
123
+ async def update_permission_bulk(
124
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
125
+ permission_ids: list[str],
126
+ data: PermissionUpdate,
127
+ ) -> list[PermissionResponse]:
128
+ if not current_user.has_permission("permission:update"):
129
+ raise ForbiddenError("Access denied")
88
130
  return await auth_client.update_permission_bulk(
89
- permission_ids, data.with_audit(updated_by="system")
131
+ permission_ids, data.with_audit(updated_by=current_user.id)
90
132
  )
91
133
 
92
134
  @app.put(
93
135
  "/api/v1/permissions/{permission_id}",
94
136
  response_model=PermissionResponse,
95
137
  )
96
- async def update_permission(permission_id: str, data: PermissionUpdate):
97
- return await auth_client.update_permission(data.with_audit(updated_by="system"))
138
+ async def update_permission(
139
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
140
+ permission_id: str,
141
+ data: PermissionUpdate,
142
+ ) -> PermissionResponse:
143
+ if not current_user.has_permission("permission:update"):
144
+ raise ForbiddenError("Access denied")
145
+ return await auth_client.update_permission(
146
+ permission_id, data.with_audit(updated_by=current_user.id)
147
+ )
98
148
 
99
149
  @app.delete(
100
150
  "/api/v1/permissions/bulk",
101
151
  response_model=list[PermissionResponse],
102
152
  )
103
- async def delete_permission_bulk(permission_ids: list[str]):
153
+ async def delete_permission_bulk(
154
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
155
+ permission_ids: list[str],
156
+ ) -> list[PermissionResponse]:
157
+ if not current_user.has_permission("permission:delete"):
158
+ raise ForbiddenError("Access denied")
104
159
  return await auth_client.delete_permission_bulk(
105
- permission_ids, deleted_by="system"
160
+ permission_ids, deleted_by=current_user.id
106
161
  )
107
162
 
108
163
  @app.delete(
109
164
  "/api/v1/permissions/{permission_id}",
110
165
  response_model=PermissionResponse,
111
166
  )
112
- async def delete_permission(permission_id: str):
113
- return await auth_client.delete_permission(permission_id, deleted_by="system")
167
+ async def delete_permission(
168
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
169
+ permission_id: str,
170
+ ) -> PermissionResponse:
171
+ if not current_user.has_permission("permission:delete"):
172
+ raise ForbiddenError("Access denied")
173
+ return await auth_client.delete_permission(
174
+ permission_id, deleted_by=current_user.id
175
+ )
114
176
 
115
177
  # Role routes
116
178
 
117
179
  @app.get("/api/v1/roles", response_model=MultipleRoleResponse)
118
180
  async def get_roles(
181
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
119
182
  page: int = 1,
120
183
  page_size: int = 10,
121
184
  sort: str | None = None,
122
185
  filter: str | None = None,
123
186
  ) -> MultipleRoleResponse:
187
+ if not current_user.has_permission("role:read"):
188
+ raise ForbiddenError("Access denied")
124
189
  return await auth_client.get_roles(
125
190
  page=page, page_size=page_size, sort=sort, filter=filter
126
191
  )
127
192
 
128
193
  @app.get("/api/v1/roles/{role_id}", response_model=RoleResponse)
129
- async def get_role_by_id(role_id: str) -> RoleResponse:
194
+ async def get_role_by_id(
195
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
196
+ role_id: str,
197
+ ) -> RoleResponse:
198
+ if not current_user.has_permission("role:read"):
199
+ raise ForbiddenError("Access denied")
130
200
  return await auth_client.get_role_by_id(role_id)
131
201
 
132
202
  @app.post(
133
203
  "/api/v1/roles/bulk",
134
204
  response_model=list[RoleResponse],
135
205
  )
136
- async def create_role_bulk(data: list[RoleCreateWithPermissions]):
206
+ async def create_role_bulk(
207
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
208
+ data: list[RoleCreateWithPermissions],
209
+ ) -> list[RoleResponse]:
210
+ if not current_user.has_permission("role:create"):
211
+ raise ForbiddenError("Access denied")
137
212
  return await auth_client.create_role_bulk(
138
- [row.with_audit(created_by="system") for row in data]
213
+ [row.with_audit(created_by=current_user.id) for row in data]
139
214
  )
140
215
 
141
216
  @app.post(
142
217
  "/api/v1/roles",
143
218
  response_model=RoleResponse,
144
219
  )
145
- async def create_role(data: RoleCreateWithPermissions):
146
- return await auth_client.create_role(data.with_audit(created_by="system"))
220
+ async def create_role(
221
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
222
+ data: RoleCreateWithPermissions,
223
+ ) -> RoleResponse:
224
+ if not current_user.has_permission("role:create"):
225
+ raise ForbiddenError("Access denied")
226
+ return await auth_client.create_role(
227
+ data.with_audit(created_by=current_user.id)
228
+ )
147
229
 
148
230
  @app.put(
149
231
  "/api/v1/roles/bulk",
150
232
  response_model=list[RoleResponse],
151
233
  )
152
- async def update_role_bulk(role_ids: list[str], data: RoleUpdateWithPermissions):
234
+ async def update_role_bulk(
235
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
236
+ role_ids: list[str],
237
+ data: RoleUpdateWithPermissions,
238
+ ) -> list[RoleResponse]:
239
+ if not current_user.has_permission("role:update"):
240
+ raise ForbiddenError("Access denied")
153
241
  return await auth_client.update_role_bulk(
154
- role_ids, data.with_audit(updated_by="system")
242
+ role_ids, data.with_audit(updated_by=current_user.id)
155
243
  )
156
244
 
157
245
  @app.put(
158
246
  "/api/v1/roles/{role_id}",
159
247
  response_model=RoleResponse,
160
248
  )
161
- async def update_role(role_id: str, data: RoleUpdateWithPermissions):
162
- return await auth_client.update_role(data.with_audit(updated_by="system"))
249
+ async def update_role(
250
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
251
+ role_id: str,
252
+ data: RoleUpdateWithPermissions,
253
+ ) -> RoleResponse:
254
+ if not current_user.has_permission("role:update"):
255
+ raise ForbiddenError("Access denied")
256
+ return await auth_client.update_role(
257
+ role_id, data.with_audit(updated_by=current_user.id)
258
+ )
163
259
 
164
260
  @app.delete(
165
261
  "/api/v1/roles/bulk",
166
262
  response_model=list[RoleResponse],
167
263
  )
168
- async def delete_role_bulk(role_ids: list[str]):
169
- return await auth_client.delete_role_bulk(role_ids, deleted_by="system")
264
+ async def delete_role_bulk(
265
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
266
+ role_ids: list[str],
267
+ ) -> list[RoleResponse]:
268
+ if not current_user.has_permission("role:delete"):
269
+ raise ForbiddenError("Access denied")
270
+ return await auth_client.delete_role_bulk(role_ids, deleted_by=current_user.id)
170
271
 
171
272
  @app.delete(
172
273
  "/api/v1/roles/{role_id}",
173
274
  response_model=RoleResponse,
174
275
  )
175
- async def delete_role(role_id: str):
176
- return await auth_client.delete_role(role_id, deleted_by="system")
276
+ async def delete_role(
277
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
278
+ role_id: str,
279
+ ) -> RoleResponse:
280
+ if not current_user.has_permission("role:delete"):
281
+ raise ForbiddenError("Access denied")
282
+ return await auth_client.delete_role(role_id, deleted_by=current_user.id)
177
283
 
178
284
  # User routes
179
285
 
180
286
  @app.get("/api/v1/users", response_model=MultipleUserResponse)
181
287
  async def get_users(
288
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
182
289
  page: int = 1,
183
290
  page_size: int = 10,
184
291
  sort: str | None = None,
185
292
  filter: str | None = None,
186
293
  ) -> MultipleUserResponse:
294
+ if not current_user.has_permission("user:read"):
295
+ raise ForbiddenError("Access denied")
187
296
  return await auth_client.get_users(
188
297
  page=page, page_size=page_size, sort=sort, filter=filter
189
298
  )
190
299
 
191
300
  @app.get("/api/v1/users/{user_id}", response_model=UserResponse)
192
- async def get_user_by_id(user_id: str) -> UserResponse:
301
+ async def get_user_by_id(
302
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
303
+ user_id: str,
304
+ ) -> UserResponse:
305
+ if not current_user.has_permission("user:read"):
306
+ raise ForbiddenError("Access denied")
193
307
  return await auth_client.get_user_by_id(user_id)
194
308
 
195
309
  @app.post(
196
310
  "/api/v1/users/bulk",
197
311
  response_model=list[UserResponse],
198
312
  )
199
- async def create_user_bulk(data: list[UserCreateWithRoles]):
313
+ async def create_user_bulk(
314
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
315
+ data: list[UserCreateWithRoles],
316
+ ) -> list[UserResponse]:
317
+ if not current_user.has_permission("user:create"):
318
+ raise ForbiddenError("Access denied")
200
319
  return await auth_client.create_user_bulk(
201
- [row.with_audit(created_by="system") for row in data]
320
+ [row.with_audit(created_by=current_user.id) for row in data]
202
321
  )
203
322
 
204
323
  @app.post(
205
324
  "/api/v1/users",
206
325
  response_model=UserResponse,
207
326
  )
208
- async def create_user(data: UserCreateWithRoles):
209
- return await auth_client.create_user(data.with_audit(created_by="system"))
327
+ async def create_user(
328
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
329
+ data: UserCreateWithRoles,
330
+ ) -> UserResponse:
331
+ if not current_user.has_permission("user:create"):
332
+ raise ForbiddenError("Access denied")
333
+ return await auth_client.create_user(
334
+ data.with_audit(created_by=current_user.id)
335
+ )
210
336
 
211
337
  @app.put(
212
338
  "/api/v1/users/bulk",
213
339
  response_model=list[UserResponse],
214
340
  )
215
- async def update_user_bulk(user_ids: list[str], data: UserUpdateWithRoles):
341
+ async def update_user_bulk(
342
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
343
+ user_ids: list[str],
344
+ data: UserUpdateWithRoles,
345
+ ) -> list[UserResponse]:
346
+ if not current_user.has_permission("user:update"):
347
+ raise ForbiddenError("Access denied")
216
348
  return await auth_client.update_user_bulk(
217
- user_ids, data.with_audit(updated_by="system")
349
+ user_ids, data.with_audit(updated_by=current_user.id)
218
350
  )
219
351
 
220
352
  @app.put(
221
353
  "/api/v1/users/{user_id}",
222
354
  response_model=UserResponse,
223
355
  )
224
- async def update_user(user_id: str, data: UserUpdateWithRoles):
225
- return await auth_client.update_user(data.with_audit(updated_by="system"))
356
+ async def update_user(
357
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
358
+ user_id: str,
359
+ data: UserUpdateWithRoles,
360
+ ) -> UserResponse:
361
+ if not current_user.has_permission("user:update"):
362
+ raise ForbiddenError("Access denied")
363
+ return await auth_client.update_user(
364
+ user_id, data.with_audit(updated_by=current_user.id)
365
+ )
226
366
 
227
367
  @app.delete(
228
368
  "/api/v1/users/bulk",
229
369
  response_model=list[UserResponse],
230
370
  )
231
- async def delete_user_bulk(user_ids: list[str]):
232
- return await auth_client.delete_user_bulk(user_ids, deleted_by="system")
371
+ async def delete_user_bulk(
372
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
373
+ user_ids: list[str],
374
+ ) -> list[UserResponse]:
375
+ if not current_user.has_permission("user:delete"):
376
+ raise ForbiddenError("Access denied")
377
+ return await auth_client.delete_user_bulk(user_ids, deleted_by=current_user.id)
233
378
 
234
379
  @app.delete(
235
380
  "/api/v1/users/{user_id}",
236
381
  response_model=UserResponse,
237
382
  )
238
- async def delete_user(user_id: str):
239
- return await auth_client.delete_user(user_id, deleted_by="system")
383
+ async def delete_user(
384
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
385
+ user_id: str,
386
+ ) -> UserResponse:
387
+ if not current_user.has_permission("user:delete"):
388
+ raise ForbiddenError("Access denied")
389
+ return await auth_client.delete_user(user_id, deleted_by=current_user.id)
@@ -0,0 +1,57 @@
1
+ import datetime
2
+
3
+ from fastapi import Depends, Request, Response
4
+ from fastapi.security import OAuth2PasswordBearer
5
+ from my_app_name.config import (
6
+ APP_AUTH_ACCESS_TOKEN_COOKIE_NAME,
7
+ APP_AUTH_ACCESS_TOKEN_EXPIRE_MINUTES,
8
+ APP_AUTH_REFRESH_TOKEN_COOKIE_NAME,
9
+ APP_AUTH_REFRESH_TOKEN_EXPIRE_MINUTES,
10
+ )
11
+ from my_app_name.module.auth.client.auth_client_factory import auth_client
12
+ from my_app_name.schema.user import AuthUserResponse, UserSessionResponse
13
+ from typing_extensions import Annotated
14
+
15
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/user-sessions", auto_error=False)
16
+
17
+
18
+ async def get_current_user(
19
+ request: Request,
20
+ response: Response,
21
+ bearer_access_token: Annotated[str, Depends(oauth2_scheme)],
22
+ ) -> AuthUserResponse:
23
+ # Bearer token exists
24
+ if bearer_access_token is not None and bearer_access_token != "":
25
+ return await auth_client.get_current_user(bearer_access_token)
26
+ cookie_access_token = request.cookies.get(APP_AUTH_ACCESS_TOKEN_COOKIE_NAME)
27
+ # Cookie exists
28
+ if cookie_access_token is not None and cookie_access_token != "":
29
+ cookie_user = await auth_client.get_current_user(cookie_access_token)
30
+ if cookie_user.is_guest:
31
+ # If user is guest, the cookie is not needed
32
+ unset_user_session_cookie(response)
33
+ return cookie_user
34
+ # No bearer token or cookie
35
+ return await auth_client.get_current_user("")
36
+
37
+
38
+ def set_user_session_cookie(response: Response, user_session: UserSessionResponse):
39
+ response.set_cookie(
40
+ key=APP_AUTH_ACCESS_TOKEN_COOKIE_NAME,
41
+ value=user_session.access_token,
42
+ httponly=True,
43
+ max_age=60 * APP_AUTH_ACCESS_TOKEN_EXPIRE_MINUTES,
44
+ expires=user_session.access_token_expired_at.astimezone(datetime.timezone.utc),
45
+ )
46
+ response.set_cookie(
47
+ key=APP_AUTH_REFRESH_TOKEN_COOKIE_NAME,
48
+ value=user_session.refresh_token,
49
+ httponly=True,
50
+ max_age=60 * APP_AUTH_REFRESH_TOKEN_EXPIRE_MINUTES,
51
+ expires=user_session.refresh_token_expired_at.astimezone(datetime.timezone.utc),
52
+ )
53
+
54
+
55
+ def unset_user_session_cookie(response: Response):
56
+ response.delete_cookie(APP_AUTH_ACCESS_TOKEN_COOKIE_NAME)
57
+ response.delete_cookie(APP_AUTH_REFRESH_TOKEN_COOKIE_NAME)
@@ -4,4 +4,9 @@ sqlmodel~=0.0.22
4
4
  ulid-py~=1.1.0
5
5
  passlib~=1.7.4
6
6
  Jinja2~=3.1.5
7
- python-jose~=3.3.0
7
+ python-jose~=3.3.0
8
+ passlib~=1.7.4
9
+
10
+ pytest~=8.3.4
11
+ pytest-asyncio~=0.24.0
12
+ pytest-cov~=6.0.0
@@ -34,6 +34,7 @@ class PermissionUpdateWithAudit(PermissionUpdate):
34
34
 
35
35
  class PermissionResponse(PermissionBase):
36
36
  id: str
37
+ description: str
37
38
 
38
39
 
39
40
  class MultiplePermissionResponse(BaseModel):
@@ -90,6 +90,12 @@ class AuthUserResponse(UserResponse):
90
90
  is_super_user: bool
91
91
  is_guest: bool
92
92
 
93
+ def has_permission(self, permission_name: str):
94
+ return self.is_super_user or permission_name in self.permission_names
95
+
96
+ def has_role(self, role_name: str):
97
+ return self.is_super_user or role_name in self.role_names
98
+
93
99
 
94
100
  class MultipleUserResponse(BaseModel):
95
101
  data: list[UserResponse]
@@ -111,6 +117,9 @@ class UserTokenData(SQLModel):
111
117
  class UserSessionResponse(SQLModel):
112
118
  id: str
113
119
  user_id: str
120
+ access_token: str
121
+ refresh_token: str
122
+ token_type: str
114
123
  access_token_expired_at: datetime.datetime
115
124
  refresh_token_expired_at: datetime.datetime
116
125