zrb 1.0.0b8__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 (81) hide show
  1. zrb/__main__.py +3 -0
  2. zrb/builtin/project/add/fastapp/fastapp_task.py +1 -0
  3. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/.coveragerc +11 -0
  4. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/.gitignore +4 -0
  5. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_task.py +4 -4
  6. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/config.py +5 -0
  7. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_task.py +108 -1
  8. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +67 -4
  9. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/my_module/service/my_entity/my_entity_service.py +5 -5
  10. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/schema/my_entity.py +1 -0
  11. 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
  12. 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
  13. 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
  14. 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
  15. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/gateway_subroute.py +57 -13
  16. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/input.py +8 -0
  17. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +2 -2
  18. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/gateway/subroute/my_module.py +6 -1
  19. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/module_task_definition.py +10 -6
  20. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task.py +65 -14
  21. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task_util.py +106 -0
  22. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/util.py +6 -86
  23. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_db_repository.py +27 -11
  24. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_service.py +140 -51
  25. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/error.py +15 -0
  26. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/parser.py +1 -1
  27. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +22 -4
  28. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/auth_client.py +21 -0
  29. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/3093c7336477_add_auth_tables.py +106 -61
  30. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/8ed025bcc845_create_permissions.py +69 -0
  31. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration_metadata.py +3 -4
  32. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/route.py +15 -14
  33. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/permission_service.py +4 -4
  34. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_db_repository.py +24 -5
  35. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/role_service.py +14 -12
  36. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_db_repository.py +134 -97
  37. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_repository.py +28 -11
  38. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service.py +215 -13
  39. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service_factory.py +30 -2
  40. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py +216 -41
  41. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/auth.py +57 -0
  42. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/requirements.txt +7 -1
  43. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/permission.py +2 -0
  44. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/role.py +13 -12
  45. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/user.py +64 -12
  46. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/_util/access_token.py +19 -0
  47. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_create_permission.py +59 -0
  48. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_delete_permission.py +68 -0
  49. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_read_permission.py +71 -0
  50. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_update_permission.py +66 -0
  51. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/test_user_session.py +195 -0
  52. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_health_and_readiness.py +28 -0
  53. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_homepage.py +17 -0
  54. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_not_found_error.py +16 -0
  55. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test.sh +7 -0
  56. zrb/task/base_task.py +10 -10
  57. zrb/task/cmd_task.py +2 -5
  58. zrb/util/cmd/command.py +39 -48
  59. zrb/util/codemod/modification_mode.py +3 -0
  60. zrb/util/codemod/modify_class.py +58 -0
  61. zrb/util/codemod/modify_class_parent.py +68 -0
  62. zrb/util/codemod/modify_class_property.py +128 -0
  63. zrb/util/codemod/modify_dict.py +75 -0
  64. zrb/util/codemod/modify_function.py +65 -0
  65. zrb/util/codemod/modify_function_call.py +68 -0
  66. zrb/util/codemod/modify_method.py +88 -0
  67. zrb/util/codemod/{prepend_code_to_module.py → modify_module.py} +2 -3
  68. zrb/util/file.py +3 -2
  69. {zrb-1.0.0b8.dist-info → zrb-1.0.0b10.dist-info}/METADATA +2 -1
  70. {zrb-1.0.0b8.dist-info → zrb-1.0.0b10.dist-info}/RECORD +72 -55
  71. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/migrate.py +0 -3
  72. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/session.py +0 -48
  73. zrb/util/codemod/append_code_to_class.py +0 -35
  74. zrb/util/codemod/append_code_to_function.py +0 -38
  75. zrb/util/codemod/append_code_to_method.py +0 -55
  76. zrb/util/codemod/append_key_to_dict.py +0 -51
  77. zrb/util/codemod/append_param_to_function_call.py +0 -39
  78. zrb/util/codemod/prepend_parent_to_class.py +0 -38
  79. zrb/util/codemod/prepend_property_to_class.py +0 -55
  80. {zrb-1.0.0b8.dist-info → zrb-1.0.0b10.dist-info}/WHEEL +0 -0
  81. {zrb-1.0.0b8.dist-info → zrb-1.0.0b10.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,14 @@
1
- from fastapi import FastAPI
1
+ from typing import Annotated
2
+
3
+ from fastapi import Depends, FastAPI, Response
4
+ from fastapi.security import OAuth2PasswordRequestForm
5
+ from my_app_name.common.error import ForbiddenError
2
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
+ )
3
12
  from my_app_name.schema.permission import (
4
13
  MultiplePermissionResponse,
5
14
  PermissionCreate,
@@ -13,202 +22,368 @@ from my_app_name.schema.role import (
13
22
  RoleUpdateWithPermissions,
14
23
  )
15
24
  from my_app_name.schema.user import (
25
+ AuthUserResponse,
16
26
  MultipleUserResponse,
17
27
  UserCreateWithRoles,
28
+ UserCredentials,
18
29
  UserResponse,
30
+ UserSessionResponse,
19
31
  UserUpdateWithRoles,
20
32
  )
21
33
 
22
34
 
23
35
  def serve_auth_route(app: FastAPI):
24
36
 
37
+ @app.post("/api/v1/user-sessions", response_model=UserSessionResponse)
38
+ async def create_user_session(
39
+ response: Response, form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
40
+ ) -> UserSessionResponse:
41
+ user_session = await auth_client.create_user_session(
42
+ UserCredentials(
43
+ username=form_data.username,
44
+ password=form_data.password,
45
+ )
46
+ )
47
+ set_user_session_cookie(response, user_session)
48
+ return user_session
49
+
50
+ @app.put("/api/v1/user-sessions", response_model=UserSessionResponse)
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
57
+
58
+ @app.delete("/api/v1/user-sessions", response_model=UserSessionResponse)
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
65
+
25
66
  # Permission routes
26
67
 
27
68
  @app.get("/api/v1/permissions", response_model=MultiplePermissionResponse)
28
69
  async def get_permissions(
70
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
29
71
  page: int = 1,
30
72
  page_size: int = 10,
31
73
  sort: str | None = None,
32
74
  filter: str | None = None,
33
75
  ) -> MultiplePermissionResponse:
76
+ if not current_user.has_permission("permission:read"):
77
+ raise ForbiddenError("Access denied")
34
78
  return await auth_client.get_permissions(
35
79
  page=page, page_size=page_size, sort=sort, filter=filter
36
80
  )
37
81
 
38
82
  @app.get("/api/v1/permissions/{permission_id}", response_model=PermissionResponse)
39
- 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")
40
89
  return await auth_client.get_permission_by_id(permission_id)
41
90
 
42
91
  @app.post(
43
92
  "/api/v1/permissions/bulk",
44
93
  response_model=list[PermissionResponse],
45
94
  )
46
- 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")
47
101
  return await auth_client.create_permission_bulk(
48
- [row.with_audit(created_by="system") for row in data]
102
+ [row.with_audit(created_by=current_user.id) for row in data]
49
103
  )
50
104
 
51
105
  @app.post(
52
106
  "/api/v1/permissions",
53
107
  response_model=PermissionResponse,
54
108
  )
55
- async def create_permission(data: PermissionCreate):
56
- 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
+ )
57
118
 
58
119
  @app.put(
59
120
  "/api/v1/permissions/bulk",
60
121
  response_model=list[PermissionResponse],
61
122
  )
62
- 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")
63
130
  return await auth_client.update_permission_bulk(
64
- permission_ids, data.with_audit(updated_by="system")
131
+ permission_ids, data.with_audit(updated_by=current_user.id)
65
132
  )
66
133
 
67
134
  @app.put(
68
135
  "/api/v1/permissions/{permission_id}",
69
136
  response_model=PermissionResponse,
70
137
  )
71
- async def update_permission(permission_id: str, data: PermissionUpdate):
72
- 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
+ )
73
148
 
74
149
  @app.delete(
75
150
  "/api/v1/permissions/bulk",
76
151
  response_model=list[PermissionResponse],
77
152
  )
78
- 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")
79
159
  return await auth_client.delete_permission_bulk(
80
- permission_ids, deleted_by="system"
160
+ permission_ids, deleted_by=current_user.id
81
161
  )
82
162
 
83
163
  @app.delete(
84
164
  "/api/v1/permissions/{permission_id}",
85
165
  response_model=PermissionResponse,
86
166
  )
87
- async def delete_permission(permission_id: str):
88
- 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
+ )
89
176
 
90
177
  # Role routes
91
178
 
92
179
  @app.get("/api/v1/roles", response_model=MultipleRoleResponse)
93
180
  async def get_roles(
181
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
94
182
  page: int = 1,
95
183
  page_size: int = 10,
96
184
  sort: str | None = None,
97
185
  filter: str | None = None,
98
186
  ) -> MultipleRoleResponse:
187
+ if not current_user.has_permission("role:read"):
188
+ raise ForbiddenError("Access denied")
99
189
  return await auth_client.get_roles(
100
190
  page=page, page_size=page_size, sort=sort, filter=filter
101
191
  )
102
192
 
103
193
  @app.get("/api/v1/roles/{role_id}", response_model=RoleResponse)
104
- 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")
105
200
  return await auth_client.get_role_by_id(role_id)
106
201
 
107
202
  @app.post(
108
203
  "/api/v1/roles/bulk",
109
204
  response_model=list[RoleResponse],
110
205
  )
111
- 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")
112
212
  return await auth_client.create_role_bulk(
113
- [row.with_audit(created_by="system") for row in data]
213
+ [row.with_audit(created_by=current_user.id) for row in data]
114
214
  )
115
215
 
116
216
  @app.post(
117
217
  "/api/v1/roles",
118
218
  response_model=RoleResponse,
119
219
  )
120
- async def create_role(data: RoleCreateWithPermissions):
121
- 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
+ )
122
229
 
123
230
  @app.put(
124
231
  "/api/v1/roles/bulk",
125
232
  response_model=list[RoleResponse],
126
233
  )
127
- 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")
128
241
  return await auth_client.update_role_bulk(
129
- role_ids, data.with_audit(updated_by="system")
242
+ role_ids, data.with_audit(updated_by=current_user.id)
130
243
  )
131
244
 
132
245
  @app.put(
133
246
  "/api/v1/roles/{role_id}",
134
247
  response_model=RoleResponse,
135
248
  )
136
- async def update_role(role_id: str, data: RoleUpdateWithPermissions):
137
- 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
+ )
138
259
 
139
260
  @app.delete(
140
261
  "/api/v1/roles/bulk",
141
262
  response_model=list[RoleResponse],
142
263
  )
143
- async def delete_role_bulk(role_ids: list[str]):
144
- 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)
145
271
 
146
272
  @app.delete(
147
273
  "/api/v1/roles/{role_id}",
148
274
  response_model=RoleResponse,
149
275
  )
150
- async def delete_role(role_id: str):
151
- 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)
152
283
 
153
284
  # User routes
154
285
 
155
286
  @app.get("/api/v1/users", response_model=MultipleUserResponse)
156
287
  async def get_users(
288
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
157
289
  page: int = 1,
158
290
  page_size: int = 10,
159
291
  sort: str | None = None,
160
292
  filter: str | None = None,
161
293
  ) -> MultipleUserResponse:
294
+ if not current_user.has_permission("user:read"):
295
+ raise ForbiddenError("Access denied")
162
296
  return await auth_client.get_users(
163
297
  page=page, page_size=page_size, sort=sort, filter=filter
164
298
  )
165
299
 
166
300
  @app.get("/api/v1/users/{user_id}", response_model=UserResponse)
167
- 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")
168
307
  return await auth_client.get_user_by_id(user_id)
169
308
 
170
309
  @app.post(
171
310
  "/api/v1/users/bulk",
172
311
  response_model=list[UserResponse],
173
312
  )
174
- async def create_user_bulk(data: list[UserCreateWithRoles]):
175
- return await auth_client.create_user(
176
- [row.with_audit(created_by="system") for row in data]
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")
319
+ return await auth_client.create_user_bulk(
320
+ [row.with_audit(created_by=current_user.id) for row in data]
177
321
  )
178
322
 
179
323
  @app.post(
180
324
  "/api/v1/users",
181
325
  response_model=UserResponse,
182
326
  )
183
- async def create_user(data: UserCreateWithRoles):
184
- 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
+ )
185
336
 
186
337
  @app.put(
187
338
  "/api/v1/users/bulk",
188
339
  response_model=list[UserResponse],
189
340
  )
190
- 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")
191
348
  return await auth_client.update_user_bulk(
192
- user_ids, data.with_audit(updated_by="system")
349
+ user_ids, data.with_audit(updated_by=current_user.id)
193
350
  )
194
351
 
195
352
  @app.put(
196
353
  "/api/v1/users/{user_id}",
197
354
  response_model=UserResponse,
198
355
  )
199
- async def update_user(user_id: str, data: UserUpdateWithRoles):
200
- 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
+ )
201
366
 
202
367
  @app.delete(
203
368
  "/api/v1/users/bulk",
204
369
  response_model=list[UserResponse],
205
370
  )
206
- async def delete_user_bulk(user_ids: list[str]):
207
- 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)
208
378
 
209
379
  @app.delete(
210
380
  "/api/v1/users/{user_id}",
211
381
  response_model=UserResponse,
212
382
  )
213
- async def delete_user(user_id: str):
214
- 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)
@@ -3,4 +3,10 @@ alembic~=1.14.0
3
3
  sqlmodel~=0.0.22
4
4
  ulid-py~=1.1.0
5
5
  passlib~=1.7.4
6
- Jinja2==3.1.5
6
+ Jinja2~=3.1.5
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):
@@ -42,6 +43,7 @@ class MultiplePermissionResponse(BaseModel):
42
43
 
43
44
 
44
45
  class Permission(SQLModel, table=True):
46
+ __tablename__ = "permissions"
45
47
  id: str = Field(default_factory=lambda: ulid.new().str, primary_key=True)
46
48
  created_at: datetime.datetime | None = Field(index=True)
47
49
  created_by: str | None = Field(index=True)
@@ -1,7 +1,6 @@
1
1
  import datetime
2
2
 
3
3
  import ulid
4
- from my_app_name.schema.permission import Permission
5
4
  from pydantic import BaseModel
6
5
  from sqlmodel import Field, SQLModel
7
6
 
@@ -22,7 +21,7 @@ class RoleCreateWithAudit(RoleCreate):
22
21
 
23
22
 
24
23
  class RoleCreateWithPermissions(RoleCreate):
25
- permission_ids: list[str] | None = None
24
+ permission_names: list[str] | None = None
26
25
 
27
26
  def with_audit(self, created_by: str) -> "RoleCreateWithPermissionsAndAudit":
28
27
  return RoleCreateWithPermissionsAndAudit(
@@ -37,14 +36,14 @@ class RoleCreateWithPermissionsAndAudit(RoleCreateWithPermissions):
37
36
  data = {
38
37
  key: val
39
38
  for key, val in self.model_dump().items()
40
- if key != "permission_ids"
39
+ if key != "permission_names"
41
40
  }
42
41
  return RoleCreateWithAudit(**data)
43
42
 
44
- def get_permission_ids(self) -> list[str]:
45
- if self.permission_ids is None:
43
+ def get_permission_names(self) -> list[str]:
44
+ if self.permission_names is None:
46
45
  return []
47
- return self.permission_ids
46
+ return self.permission_names
48
47
 
49
48
 
50
49
  class RoleUpdate(SQLModel):
@@ -60,7 +59,7 @@ class RoleUpdateWithAudit(RoleUpdate):
60
59
 
61
60
 
62
61
  class RoleUpdateWithPermissions(RoleUpdate):
63
- permission_ids: list[str] | None = None
62
+ permission_names: list[str] | None = None
64
63
 
65
64
  def with_audit(self, updated_by: str) -> "RoleUpdateWithPermissionsAndAudit":
66
65
  return RoleUpdateWithPermissionsAndAudit(
@@ -75,19 +74,19 @@ class RoleUpdateWithPermissionsAndAudit(RoleUpdateWithPermissions):
75
74
  data = {
76
75
  key: val
77
76
  for key, val in self.model_dump().items()
78
- if key != "permission_ids"
77
+ if key != "permission_names"
79
78
  }
80
79
  return RoleUpdateWithAudit(**data)
81
80
 
82
- def get_permission_ids(self) -> list[str]:
83
- if self.permission_ids is None:
81
+ def get_permission_names(self) -> list[str]:
82
+ if self.permission_names is None:
84
83
  return []
85
- return self.permission_ids
84
+ return self.permission_names
86
85
 
87
86
 
88
87
  class RoleResponse(RoleBase):
89
88
  id: str
90
- permissions: list[Permission]
89
+ permission_names: list[str]
91
90
 
92
91
 
93
92
  class MultipleRoleResponse(BaseModel):
@@ -96,6 +95,7 @@ class MultipleRoleResponse(BaseModel):
96
95
 
97
96
 
98
97
  class Role(SQLModel, table=True):
98
+ __tablename__ = "roles"
99
99
  id: str = Field(default_factory=lambda: ulid.new().str, primary_key=True)
100
100
  created_at: datetime.datetime | None = Field(index=True)
101
101
  created_by: str | None = Field(index=True)
@@ -106,6 +106,7 @@ class Role(SQLModel, table=True):
106
106
 
107
107
 
108
108
  class RolePermission(SQLModel, table=True):
109
+ __tablename__ = "role_permissions"
109
110
  id: str = Field(default_factory=lambda: ulid.new().str, primary_key=True)
110
111
  role_id: str = Field(index=True)
111
112
  permission_id: str = Field(index=True)