identity_routes.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. from datetime import datetime
  2. from typing import Annotated, TypeVar
  3. from core_domain import ServiceHealth
  4. from fastapi import APIRouter, Depends, Header, HTTPException, Request
  5. from sqlalchemy import text
  6. from sqlalchemy.orm import Session
  7. from app.application.services import AuthApplicationService
  8. from app.db.session import get_db
  9. from app.domain.repositories import (
  10. ApiKeyRepository,
  11. RoleAssignmentRepository,
  12. RolePermissionBindingRepository,
  13. RoleRepository,
  14. UserRepository,
  15. )
  16. from app.schemas.identity import (
  17. ApiKeyCreateData,
  18. ApiKeyCreateRequestDto,
  19. ApiKeyDto,
  20. ApiKeyRevokeRequest,
  21. ApiResponse,
  22. AuthMeData,
  23. BindingRemoveRequest,
  24. DeleteData,
  25. LoginData,
  26. LoginRequestDto,
  27. PageRequest,
  28. PageResult,
  29. PermissionCheckData,
  30. PermissionCheckRequestDto,
  31. RoleDto,
  32. RolePermissionBindingAddRequest,
  33. RolePermissionBindingDto,
  34. RolePermissionBindingListRequest,
  35. TokenVerifyData,
  36. TokenVerifyRequestDto,
  37. UserDto,
  38. )
  39. router = APIRouter()
  40. DbSession = Annotated[Session, Depends(get_db)]
  41. T = TypeVar("T")
  42. def get_identity_application_service(request: Request, db: DbSession) -> AuthApplicationService:
  43. settings = request.app.state.settings
  44. return AuthApplicationService(
  45. user_repository=UserRepository(db),
  46. role_repository=RoleRepository(db),
  47. assignment_repository=RoleAssignmentRepository(db),
  48. permission_binding_repository=RolePermissionBindingRepository(db),
  49. api_key_repository=ApiKeyRepository(db),
  50. token_secret=settings.credential_encryption_key)
  51. IdentityServiceDep = Annotated[AuthApplicationService, Depends(get_identity_application_service)]
  52. AuthorizationHeader = Annotated[str | None, Header(alias="Authorization")]
  53. def ok(request: Request, data: T) -> ApiResponse[T]:
  54. return ApiResponse[T](
  55. data=data,
  56. requestId=request.headers.get("x-request-id", ""),
  57. serverTime=datetime.utcnow())
  58. def get_bearer_token(authorization: str | None) -> str:
  59. if not authorization:
  60. raise HTTPException(status_code=401, detail="missing authorization header")
  61. scheme, _, token = authorization.partition(" ")
  62. if scheme.lower() != "bearer" or not token:
  63. raise HTTPException(status_code=401, detail="invalid authorization header")
  64. return token
  65. @router.get("/health", response_model=ServiceHealth)
  66. def health_check(db: DbSession) -> ServiceHealth:
  67. db.execute(text("SELECT 1"))
  68. return ServiceHealth(service="identity-service", status="ok", database="ok")
  69. @router.post("/auth/login", response_model=ApiResponse[LoginData])
  70. def login(
  71. request: Request,
  72. payload: LoginRequestDto,
  73. service: IdentityServiceDep) -> ApiResponse[LoginData]:
  74. result = service.login(username=payload.username, password=payload.password)
  75. if result is None:
  76. raise HTTPException(status_code=401, detail="invalid username or password")
  77. return ok(
  78. request,
  79. LoginData(
  80. accessToken=result.access_token,
  81. expiresTime=result.expires_time,
  82. user=UserDto.from_entity(result.user)))
  83. @router.post("/auth/logout", response_model=ApiResponse[dict[str, bool]])
  84. def logout(request: Request) -> ApiResponse[dict[str, bool]]:
  85. return ok(request, {"ok": True})
  86. @router.post("/auth/tokens/verify", response_model=ApiResponse[TokenVerifyData])
  87. def verify_token(
  88. request: Request,
  89. payload: TokenVerifyRequestDto,
  90. service: IdentityServiceDep) -> ApiResponse[TokenVerifyData]:
  91. result = service.verify_token(access_token=payload.accessToken)
  92. return ok(
  93. request,
  94. TokenVerifyData(
  95. active=result.active,
  96. userId=result.user_id,
  97. username=result.username,
  98. expiresTime=result.expires_time,
  99. reason=result.reason))
  100. @router.post("/auth/me", response_model=ApiResponse[AuthMeData])
  101. def me(
  102. request: Request,
  103. service: IdentityServiceDep,
  104. authorization: AuthorizationHeader = None) -> ApiResponse[AuthMeData]:
  105. token = get_bearer_token(authorization)
  106. verified = service.verify_token(access_token=token)
  107. if not verified.active or verified.user_id is None:
  108. raise HTTPException(status_code=401, detail=verified.reason or "invalid token")
  109. user = service.user_repository.get_by_id(user_id=verified.user_id)
  110. if user is None:
  111. raise HTTPException(status_code=401, detail="user not found")
  112. assignments = service.assignment_repository.list_by_user(user_id=user.id)
  113. roles = []
  114. permissions: set[str] = set()
  115. for assignment in assignments:
  116. role = service.role_repository.get_by_id(role_id=assignment.role_id)
  117. if role is None:
  118. continue
  119. bindings = service.permission_binding_repository.list_all_by_role(role_id=role.id)
  120. roles.append(RoleDto.from_entity(role, permission_binding_count=len(bindings)))
  121. permissions.update(binding.permission for binding in bindings)
  122. return ok(
  123. request,
  124. AuthMeData(
  125. user=UserDto.from_entity(user),
  126. roles=roles,
  127. permissions=sorted(permissions)))
  128. @router.post("/users/list", response_model=ApiResponse[PageResult[UserDto]])
  129. def list_users(
  130. request: Request,
  131. payload: PageRequest,
  132. service: IdentityServiceDep) -> ApiResponse[PageResult[UserDto]]:
  133. items, total = service.list_users_page(
  134. page=payload.page,
  135. page_size=payload.pageSize,
  136. keyword=payload.keyword)
  137. return ok(
  138. request,
  139. PageResult[UserDto].from_items(
  140. items=[UserDto.from_entity(item) for item in items],
  141. total=total,
  142. page=payload.page,
  143. page_size=payload.pageSize))
  144. @router.post("/roles/list", response_model=ApiResponse[PageResult[RoleDto]])
  145. def list_roles(
  146. request: Request,
  147. payload: PageRequest,
  148. service: IdentityServiceDep) -> ApiResponse[PageResult[RoleDto]]:
  149. items, total = service.list_roles_page(
  150. page=payload.page,
  151. page_size=payload.pageSize,
  152. keyword=payload.keyword)
  153. binding_repo = service.permission_binding_repository
  154. return ok(
  155. request,
  156. PageResult[RoleDto].from_items(
  157. items=[
  158. RoleDto.from_entity(
  159. item,
  160. permission_binding_count=len(binding_repo.list_all_by_role(role_id=item.id)))
  161. for item in items
  162. ],
  163. total=total,
  164. page=payload.page,
  165. page_size=payload.pageSize))
  166. @router.post(
  167. "/rolePermissionBindings/list",
  168. response_model=ApiResponse[PageResult[RolePermissionBindingDto]])
  169. def list_role_permission_bindings(
  170. request: Request,
  171. payload: RolePermissionBindingListRequest,
  172. service: IdentityServiceDep) -> ApiResponse[PageResult[RolePermissionBindingDto]]:
  173. items, total = service.list_role_permission_bindings(
  174. role_id=payload.roleId,
  175. page=payload.page,
  176. page_size=payload.pageSize)
  177. return ok(
  178. request,
  179. PageResult[RolePermissionBindingDto].from_items(
  180. items=[RolePermissionBindingDto.from_entity(item) for item in items],
  181. total=total,
  182. page=payload.page,
  183. page_size=payload.pageSize))
  184. @router.post("/rolePermissionBindings/add", response_model=ApiResponse[RolePermissionBindingDto])
  185. def add_role_permission_binding(
  186. request: Request,
  187. payload: RolePermissionBindingAddRequest,
  188. service: IdentityServiceDep) -> ApiResponse[RolePermissionBindingDto]:
  189. entity = service.add_role_permission_binding(
  190. role_id=payload.roleId,
  191. permission=payload.permission,
  192. scope_type=payload.scopeType,
  193. scope_id=payload.scopeId)
  194. return ok(request, RolePermissionBindingDto.from_entity(entity))
  195. @router.post("/rolePermissionBindings/remove", response_model=ApiResponse[DeleteData])
  196. def remove_role_permission_binding(
  197. request: Request,
  198. payload: BindingRemoveRequest,
  199. service: IdentityServiceDep) -> ApiResponse[DeleteData]:
  200. deleted = service.remove_role_permission_binding(binding_id=payload.bindingId)
  201. return ok(request, DeleteData(deleted=deleted, bindingId=payload.bindingId))
  202. @router.post("/permissions/check", response_model=ApiResponse[PermissionCheckData])
  203. def check_permission(
  204. request: Request,
  205. payload: PermissionCheckRequestDto,
  206. service: IdentityServiceDep) -> ApiResponse[PermissionCheckData]:
  207. result = service.check_permission(
  208. user_id=payload.userId,
  209. permission=payload.permission,
  210. scope_type=payload.scopeType,
  211. scope_id=payload.scopeId)
  212. return ok(
  213. request,
  214. PermissionCheckData(
  215. allowed=result.allowed,
  216. reason=result.reason,
  217. matchedRoleIds=result.matched_role_ids))
  218. @router.post("/apiKeys/list", response_model=ApiResponse[PageResult[ApiKeyDto]])
  219. def list_api_keys(
  220. request: Request,
  221. payload: PageRequest,
  222. service: IdentityServiceDep) -> ApiResponse[PageResult[ApiKeyDto]]:
  223. items, total = service.list_api_keys_page(
  224. page=payload.page,
  225. page_size=payload.pageSize,
  226. keyword=payload.keyword)
  227. return ok(
  228. request,
  229. PageResult[ApiKeyDto].from_items(
  230. items=[ApiKeyDto.from_entity(item) for item in items],
  231. total=total,
  232. page=payload.page,
  233. page_size=payload.pageSize))
  234. @router.post("/apiKeys/create", response_model=ApiResponse[ApiKeyCreateData])
  235. def create_api_key(
  236. request: Request,
  237. payload: ApiKeyCreateRequestDto,
  238. service: IdentityServiceDep) -> ApiResponse[ApiKeyCreateData]:
  239. entity, secret = service.create_api_key(
  240. name=payload.name,
  241. scopes=payload.scopes,
  242. expires_time=payload.expiresTime)
  243. return ok(
  244. request,
  245. ApiKeyCreateData(
  246. apiKey=ApiKeyDto.from_entity(entity),
  247. secret=secret))
  248. @router.post("/apiKeys/revoke", response_model=ApiResponse[ApiKeyDto])
  249. def revoke_api_key(
  250. request: Request,
  251. payload: ApiKeyRevokeRequest,
  252. service: IdentityServiceDep) -> ApiResponse[ApiKeyDto]:
  253. entity = service.revoke_api_key(api_key_id=payload.apiKeyId)
  254. if entity is None:
  255. raise HTTPException(status_code=404, detail=f"api key not found: {payload.apiKeyId}")
  256. return ok(request, ApiKeyDto.from_entity(entity))