identity_routes.py 11 KB

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