From 1c8477d8be754601de871aea5f4a499ad608ea92 Mon Sep 17 00:00:00 2001 From: kl Date: Mon, 15 Jun 2026 11:48:50 +0800 Subject: [PATCH 1/4] feat: add user token management contract --- apollo-openapi.yaml | 229 ++++++++++++++++++++++++++++++ tests/test_user_token_contract.py | 39 +++++ 2 files changed, 268 insertions(+) diff --git a/apollo-openapi.yaml b/apollo-openapi.yaml index cb494f0..28b17e5 100644 --- a/apollo-openapi.yaml +++ b/apollo-openapi.yaml @@ -4891,6 +4891,235 @@ paths: application/json: schema: $ref: '#/components/schemas/ExceptionResponse' + /openapi/v1/user-tokens: + get: + summary: 查询当前Portal用户访问Token(new added) + operationId: listUserTokens + deprecated: false + description: GET /openapi/v1/user-tokens,仅支持Portal用户登录态访问 + tags: + - Portal Management + responses: + '200': + description: 成功获取当前用户访问Token列表 + content: + application/json: + schema: + type: array + items: + type: object + '403': + description: 仅支持Portal用户登录态访问 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' + post: + summary: 创建当前Portal用户访问Token(new added) + operationId: createUserToken + deprecated: false + description: POST /openapi/v1/user-tokens,仅支持Portal用户登录态访问 + tags: + - Portal Management + requestBody: + content: + application/json: + schema: + type: object + required: true + responses: + '200': + description: 用户访问Token创建成功 + content: + application/json: + schema: + type: object + '403': + description: 仅支持Portal用户登录态访问 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' + /openapi/v1/user-tokens/{tokenId}: + delete: + summary: 删除当前Portal用户访问Token(new added) + operationId: deleteUserToken + deprecated: false + description: DELETE /openapi/v1/user-tokens/{tokenId},仅支持Portal用户登录态访问 + tags: + - Portal Management + parameters: + - name: tokenId + in: path + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: 用户访问Token删除成功 + '403': + description: 仅支持Portal用户登录态访问 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' + /openapi/v1/user-tokens/{tokenId}/revoke: + post: + summary: 撤销当前Portal用户访问Token(new added) + operationId: revokeUserToken + deprecated: false + description: POST /openapi/v1/user-tokens/{tokenId}/revoke,仅支持Portal用户登录态访问 + tags: + - Portal Management + parameters: + - name: tokenId + in: path + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: 用户访问Token撤销成功 + '403': + description: 仅支持Portal用户登录态访问 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' + /openapi/v1/user-tokens/{tokenId}/rotate: + post: + summary: 轮换当前Portal用户访问Token(new added) + operationId: rotateUserToken + deprecated: false + description: POST /openapi/v1/user-tokens/{tokenId}/rotate,仅支持Portal用户登录态访问 + tags: + - Portal Management + parameters: + - name: tokenId + in: path + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: 用户访问Token轮换成功 + content: + application/json: + schema: + type: object + '403': + description: 仅支持Portal用户登录态访问 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' + /openapi/v1/user-tokens/capabilities: + get: + summary: 获取Portal用户可创建Token能力(new added) + operationId: getUserTokenCapabilities + deprecated: false + description: GET /openapi/v1/user-tokens/capabilities,仅支持Portal用户登录态访问 + tags: + - Portal Management + responses: + '200': + description: 成功获取Portal用户可创建Token能力 + content: + application/json: + schema: + type: object + '403': + description: 仅支持Portal用户登录态访问 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' + /openapi/v1/user-tokens/admin: + get: + summary: 管理员查询用户访问Token(new added) + operationId: adminListUserTokens + deprecated: false + description: GET /openapi/v1/user-tokens/admin,仅支持具备超级管理员权限的Portal用户登录态访问 + tags: + - Portal Management + parameters: + - name: userId + in: query + required: false + schema: + type: string + - name: status + in: query + required: false + schema: + type: string + default: all + responses: + '200': + description: 成功获取用户访问Token列表 + content: + application/json: + schema: + type: array + items: + type: object + '403': + description: 权限不足 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' + /openapi/v1/user-tokens/admin/{tokenId}: + delete: + summary: 管理员删除用户访问Token(new added) + operationId: adminDeleteUserToken + deprecated: false + description: DELETE /openapi/v1/user-tokens/admin/{tokenId},仅支持具备超级管理员权限的Portal用户登录态访问 + tags: + - Portal Management + parameters: + - name: tokenId + in: path + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: 用户访问Token删除成功 + '403': + description: 权限不足 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' + /openapi/v1/user-tokens/admin/{tokenId}/revoke: + post: + summary: 管理员撤销用户访问Token(new added) + operationId: adminRevokeUserToken + deprecated: false + description: POST /openapi/v1/user-tokens/admin/{tokenId}/revoke,仅支持具备超级管理员权限的Portal用户登录态访问 + tags: + - Portal Management + parameters: + - name: tokenId + in: path + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: 用户访问Token撤销成功 + '403': + description: 权限不足 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' /openapi/v1/user-tokens/current: get: summary: 获取当前用户访问Token身份和权限范围(new added) diff --git a/tests/test_user_token_contract.py b/tests/test_user_token_contract.py index 6cdf0b9..a55091a 100644 --- a/tests/test_user_token_contract.py +++ b/tests/test_user_token_contract.py @@ -40,6 +40,45 @@ def test_user_token_management_paths_use_generated_contract(self): operation["responses"]["200"]["content"]["application/json"]["schema"]["$ref"], ) + def test_portal_user_token_management_paths_use_portal_management_contract(self): + for spec_file in SPEC_FILES: + spec = self._load_spec(spec_file) + + with self.subTest(spec=spec_file): + expected_operations = { + "/openapi/v1/user-tokens": (("get", "listUserTokens"), ("post", "createUserToken")), + "/openapi/v1/user-tokens/{tokenId}": (("delete", "deleteUserToken"),), + "/openapi/v1/user-tokens/{tokenId}/revoke": (("post", "revokeUserToken"),), + "/openapi/v1/user-tokens/{tokenId}/rotate": (("post", "rotateUserToken"),), + "/openapi/v1/user-tokens/capabilities": ( + ("get", "getUserTokenCapabilities"),), + "/openapi/v1/user-tokens/admin": (("get", "adminListUserTokens"),), + "/openapi/v1/user-tokens/admin/{tokenId}": ( + ("delete", "adminDeleteUserToken"),), + "/openapi/v1/user-tokens/admin/{tokenId}/revoke": ( + ("post", "adminRevokeUserToken"),), + } + for path, methods in expected_operations.items(): + for method, operation_id in methods: + operation = spec["paths"][path][method] + self.assertEqual(operation_id, operation["operationId"]) + self.assertEqual(["Portal Management"], operation["tags"]) + + list_tokens = spec["paths"]["/openapi/v1/user-tokens"]["get"] + self.assertEqual( + {"type": "object"}, + list_tokens["responses"]["200"]["content"]["application/json"]["schema"]["items"], + ) + create_token = spec["paths"]["/openapi/v1/user-tokens"]["post"] + self.assertEqual( + {"type": "object"}, + create_token["requestBody"]["content"]["application/json"]["schema"], + ) + self.assertEqual( + {"type": "object"}, + create_token["responses"]["200"]["content"]["application/json"]["schema"], + ) + def test_user_token_current_capability_schema_matches_portal_response(self): for spec_file in SPEC_FILES: spec = self._load_spec(spec_file) From d2ca313b9f8e4ecf4a90daa17d9645042c37858b Mon Sep 17 00:00:00 2001 From: kl Date: Mon, 15 Jun 2026 12:40:14 +0800 Subject: [PATCH 2/4] fix: address user token management contract review --- apollo-openapi.yaml | 204 +++++++++++++++++++++++++++++- tests/test_user_token_contract.py | 70 +++++++++- 2 files changed, 262 insertions(+), 12 deletions(-) diff --git a/apollo-openapi.yaml b/apollo-openapi.yaml index 28b17e5..6447790 100644 --- a/apollo-openapi.yaml +++ b/apollo-openapi.yaml @@ -4899,6 +4899,8 @@ paths: description: GET /openapi/v1/user-tokens,仅支持Portal用户登录态访问 tags: - Portal Management + security: + - PortalSessionAuth: [] responses: '200': description: 成功获取当前用户访问Token列表 @@ -4907,7 +4909,7 @@ paths: schema: type: array items: - type: object + $ref: '#/components/schemas/OpenUserTokenSummary' '403': description: 仅支持Portal用户登录态访问 content: @@ -4921,11 +4923,13 @@ paths: description: POST /openapi/v1/user-tokens,仅支持Portal用户登录态访问 tags: - Portal Management + security: + - PortalSessionAuth: [] requestBody: content: application/json: schema: - type: object + $ref: '#/components/schemas/OpenCreateUserTokenRequest' required: true responses: '200': @@ -4933,7 +4937,7 @@ paths: content: application/json: schema: - type: object + $ref: '#/components/schemas/OpenCreateUserTokenResponse' '403': description: 仅支持Portal用户登录态访问 content: @@ -4948,6 +4952,8 @@ paths: description: DELETE /openapi/v1/user-tokens/{tokenId},仅支持Portal用户登录态访问 tags: - Portal Management + security: + - PortalSessionAuth: [] parameters: - name: tokenId in: path @@ -4972,6 +4978,8 @@ paths: description: POST /openapi/v1/user-tokens/{tokenId}/revoke,仅支持Portal用户登录态访问 tags: - Portal Management + security: + - PortalSessionAuth: [] parameters: - name: tokenId in: path @@ -4996,6 +5004,8 @@ paths: description: POST /openapi/v1/user-tokens/{tokenId}/rotate,仅支持Portal用户登录态访问 tags: - Portal Management + security: + - PortalSessionAuth: [] parameters: - name: tokenId in: path @@ -5009,7 +5019,7 @@ paths: content: application/json: schema: - type: object + $ref: '#/components/schemas/OpenRotateUserTokenResponse' '403': description: 仅支持Portal用户登录态访问 content: @@ -5024,13 +5034,15 @@ paths: description: GET /openapi/v1/user-tokens/capabilities,仅支持Portal用户登录态访问 tags: - Portal Management + security: + - PortalSessionAuth: [] responses: '200': description: 成功获取Portal用户可创建Token能力 content: application/json: schema: - type: object + $ref: '#/components/schemas/OpenUserTokenCapability' '403': description: 仅支持Portal用户登录态访问 content: @@ -5045,6 +5057,8 @@ paths: description: GET /openapi/v1/user-tokens/admin,仅支持具备超级管理员权限的Portal用户登录态访问 tags: - Portal Management + security: + - PortalSessionAuth: [] parameters: - name: userId in: query @@ -5065,7 +5079,7 @@ paths: schema: type: array items: - type: object + $ref: '#/components/schemas/OpenUserTokenSummary' '403': description: 权限不足 content: @@ -5080,6 +5094,8 @@ paths: description: DELETE /openapi/v1/user-tokens/admin/{tokenId},仅支持具备超级管理员权限的Portal用户登录态访问 tags: - Portal Management + security: + - PortalSessionAuth: [] parameters: - name: tokenId in: path @@ -5104,6 +5120,8 @@ paths: description: POST /openapi/v1/user-tokens/admin/{tokenId}/revoke,仅支持具备超级管理员权限的Portal用户登录态访问 tags: - Portal Management + security: + - PortalSessionAuth: [] parameters: - name: tokenId in: path @@ -6417,6 +6435,13 @@ components: - Token具有对应应用的读写权限 - 不同Token可能有不同的环境和命名空间权限 - 建议为不同用途创建不同的Token + PortalSessionAuth: + type: apiKey + in: cookie + name: SESSION + description: | + Apollo Portal登录会话Cookie + 仅用于需要Portal用户登录态的Portal Management接口,不使用OpenAPI Consumer Token。 schemas: OpenAppDTO: type: object @@ -7157,6 +7182,173 @@ components: enabled: type: integer description: 是否启用,1 表示启用,0 表示禁用 + OpenCreateUserTokenRequest: + type: object + description: 创建Portal用户访问Token的请求 + required: + - name + properties: + name: + type: string + description: 用户访问Token名称 + operations: + type: array + uniqueItems: true + description: 授权的用户Token操作列表,空列表表示全部可用操作 + items: + type: string + appIds: + type: array + uniqueItems: true + description: 授权的应用ID列表,空列表表示全部可访问应用 + items: + type: string + envs: + type: array + uniqueItems: true + description: 授权的环境列表,空列表表示全部环境 + items: + type: string + namespaces: + type: array + description: 授权的命名空间范围列表,空列表表示全部命名空间 + items: + $ref: '#/components/schemas/OpenUserTokenNamespaceScope' + rateLimit: + type: integer + minimum: 0 + description: 每秒限流阈值,0或空表示不限流 + expires: + type: string + format: date-time + description: Token过期时间 + OpenUserTokenSummary: + type: object + description: Portal用户访问Token摘要 + required: + - id + - userId + - name + - tokenPrefix + - status + - operations + - appIds + - envs + - namespaces + - rateLimit + - expires + - dataChangeCreatedTime + properties: + id: + type: integer + format: int64 + description: 用户访问Token ID + userId: + type: string + description: Token所属Portal用户ID + name: + type: string + description: 用户访问Token名称 + tokenPrefix: + type: string + description: 用户访问Token前缀,用于识别Token但不暴露完整密钥 + status: + type: string + description: Token状态 + enum: + - active + - expired + - revoked + operations: + type: array + uniqueItems: true + description: 授权的用户Token操作列表,空列表表示全部可用操作 + items: + type: string + appIds: + type: array + uniqueItems: true + description: 授权的应用ID列表,空列表表示全部可访问应用 + items: + type: string + envs: + type: array + uniqueItems: true + description: 授权的环境列表,空列表表示全部环境 + items: + type: string + namespaces: + type: array + description: 授权的命名空间范围列表,空列表表示全部命名空间 + items: + $ref: '#/components/schemas/OpenUserTokenNamespaceScope' + rateLimit: + type: integer + description: 每秒限流阈值,0或空表示不限流 + expires: + type: string + format: date-time + description: Token过期时间 + lastUsedTime: + type: string + format: date-time + description: Token最近使用时间 + lastUsedIp: + type: string + description: Token最近使用IP + lastUsedUserAgent: + type: string + description: Token最近使用User-Agent + revokedAt: + type: string + format: date-time + description: Token撤销时间 + revokedBy: + type: string + description: Token撤销人 + dataChangeCreatedTime: + type: string + format: date-time + description: Token创建时间 + OpenCreateUserTokenResponse: + allOf: + - $ref: '#/components/schemas/OpenUserTokenSummary' + - type: object + required: + - tokenValue + properties: + tokenValue: + type: string + description: 新创建的完整用户访问Token,仅在创建或轮换响应中返回一次 + OpenRotateUserTokenResponse: + allOf: + - $ref: '#/components/schemas/OpenUserTokenSummary' + - type: object + required: + - tokenValue + properties: + tokenValue: + type: string + description: 轮换后的完整用户访问Token,仅在创建或轮换响应中返回一次 + OpenUserTokenCapability: + type: object + description: 当前Portal用户可创建Token的能力 + required: + - operations + - defaultExpireDays + - maxExpireDays + properties: + operations: + type: array + description: 当前Portal用户可授予的用户Token操作列表 + items: + type: string + defaultExpireDays: + type: integer + description: 默认过期天数 + maxExpireDays: + type: integer + description: 最大过期天数 OpenUserTokenCurrentCapability: type: object description: 当前用户访问Token身份、权限范围和可调用OpenAPI能力 diff --git a/tests/test_user_token_contract.py b/tests/test_user_token_contract.py index a55091a..b7e7234 100644 --- a/tests/test_user_token_contract.py +++ b/tests/test_user_token_contract.py @@ -45,6 +45,11 @@ def test_portal_user_token_management_paths_use_portal_management_contract(self) spec = self._load_spec(spec_file) with self.subTest(spec=spec_file): + portal_session_auth = spec["components"]["securitySchemes"]["PortalSessionAuth"] + self.assertEqual("apiKey", portal_session_auth["type"]) + self.assertEqual("cookie", portal_session_auth["in"]) + self.assertEqual("SESSION", portal_session_auth["name"]) + expected_operations = { "/openapi/v1/user-tokens": (("get", "listUserTokens"), ("post", "createUserToken")), "/openapi/v1/user-tokens/{tokenId}": (("delete", "deleteUserToken"),), @@ -63,20 +68,73 @@ def test_portal_user_token_management_paths_use_portal_management_contract(self) operation = spec["paths"][path][method] self.assertEqual(operation_id, operation["operationId"]) self.assertEqual(["Portal Management"], operation["tags"]) + self.assertEqual([{"PortalSessionAuth": []}], operation["security"]) + self.assertEqual( + "#/components/schemas/ExceptionResponse", + operation["responses"]["403"]["content"]["application/json"]["schema"]["$ref"], + ) list_tokens = spec["paths"]["/openapi/v1/user-tokens"]["get"] + list_tokens_schema = ( + list_tokens["responses"]["200"]["content"]["application/json"]["schema"]) + self.assertEqual("array", list_tokens_schema["type"]) self.assertEqual( - {"type": "object"}, - list_tokens["responses"]["200"]["content"]["application/json"]["schema"]["items"], + "#/components/schemas/OpenUserTokenSummary", + list_tokens_schema["items"]["$ref"], ) create_token = spec["paths"]["/openapi/v1/user-tokens"]["post"] self.assertEqual( - {"type": "object"}, - create_token["requestBody"]["content"]["application/json"]["schema"], + "#/components/schemas/OpenCreateUserTokenRequest", + create_token["requestBody"]["content"]["application/json"]["schema"]["$ref"], + ) + self.assertEqual( + "#/components/schemas/OpenCreateUserTokenResponse", + create_token["responses"]["200"]["content"]["application/json"]["schema"]["$ref"], + ) + rotate_token = spec["paths"]["/openapi/v1/user-tokens/{tokenId}/rotate"]["post"] + self.assertEqual( + "#/components/schemas/OpenRotateUserTokenResponse", + rotate_token["responses"]["200"]["content"]["application/json"]["schema"]["$ref"], + ) + capabilities = spec["paths"]["/openapi/v1/user-tokens/capabilities"]["get"] + self.assertEqual( + "#/components/schemas/OpenUserTokenCapability", + capabilities["responses"]["200"]["content"]["application/json"]["schema"]["$ref"], + ) + admin_list_tokens = spec["paths"]["/openapi/v1/user-tokens/admin"]["get"] + admin_list_tokens_schema = ( + admin_list_tokens["responses"]["200"]["content"]["application/json"]["schema"]) + self.assertEqual("array", admin_list_tokens_schema["type"]) + self.assertEqual( + "#/components/schemas/OpenUserTokenSummary", + admin_list_tokens_schema["items"]["$ref"], + ) + + schemas = spec["components"]["schemas"] + summary = schemas["OpenUserTokenSummary"] + self.assertIn("tokenPrefix", summary["required"]) + self.assertEqual("int64", summary["properties"]["id"]["format"]) + self.assertTrue(summary["properties"]["operations"]["uniqueItems"]) + self.assertEqual( + "#/components/schemas/OpenUserTokenNamespaceScope", + summary["properties"]["namespaces"]["items"]["$ref"], + ) + create_request = schemas["OpenCreateUserTokenRequest"] + self.assertEqual(["name"], create_request["required"]) + self.assertTrue(create_request["properties"]["appIds"]["uniqueItems"]) + create_response = schemas["OpenCreateUserTokenResponse"] + self.assertEqual( + "#/components/schemas/OpenUserTokenSummary", + create_response["allOf"][0]["$ref"], + ) + self.assertEqual( + "string", + create_response["allOf"][1]["properties"]["tokenValue"]["type"], ) + capability = schemas["OpenUserTokenCapability"] self.assertEqual( - {"type": "object"}, - create_token["responses"]["200"]["content"]["application/json"]["schema"], + ["operations", "defaultExpireDays", "maxExpireDays"], + capability["required"], ) def test_user_token_current_capability_schema_matches_portal_response(self): From d4417a6e15f00386724467f589ab235f23bf5a04 Mon Sep 17 00:00:00 2001 From: kl Date: Mon, 15 Jun 2026 13:43:10 +0800 Subject: [PATCH 3/4] fix: address user token review comments --- apollo-openapi.yaml | 60 +++++++++++++++++++++++++++++++ tests/test_user_token_contract.py | 19 +++++++--- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/apollo-openapi.yaml b/apollo-openapi.yaml index 6447790..afbd73c 100644 --- a/apollo-openapi.yaml +++ b/apollo-openapi.yaml @@ -4910,6 +4910,12 @@ paths: type: array items: $ref: '#/components/schemas/OpenUserTokenSummary' + '401': + description: 未登录或登录态已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' '403': description: 仅支持Portal用户登录态访问 content: @@ -4938,6 +4944,12 @@ paths: application/json: schema: $ref: '#/components/schemas/OpenCreateUserTokenResponse' + '401': + description: 未登录或登录态已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' '403': description: 仅支持Portal用户登录态访问 content: @@ -4964,6 +4976,12 @@ paths: responses: '200': description: 用户访问Token删除成功 + '401': + description: 未登录或登录态已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' '403': description: 仅支持Portal用户登录态访问 content: @@ -4990,6 +5008,12 @@ paths: responses: '200': description: 用户访问Token撤销成功 + '401': + description: 未登录或登录态已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' '403': description: 仅支持Portal用户登录态访问 content: @@ -5020,6 +5044,12 @@ paths: application/json: schema: $ref: '#/components/schemas/OpenRotateUserTokenResponse' + '401': + description: 未登录或登录态已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' '403': description: 仅支持Portal用户登录态访问 content: @@ -5043,6 +5073,12 @@ paths: application/json: schema: $ref: '#/components/schemas/OpenUserTokenCapability' + '401': + description: 未登录或登录态已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' '403': description: 仅支持Portal用户登录态访问 content: @@ -5070,6 +5106,11 @@ paths: required: false schema: type: string + enum: + - all + - active + - expired + - revoked default: all responses: '200': @@ -5080,6 +5121,12 @@ paths: type: array items: $ref: '#/components/schemas/OpenUserTokenSummary' + '401': + description: 未登录或登录态已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' '403': description: 权限不足 content: @@ -5106,6 +5153,12 @@ paths: responses: '200': description: 用户访问Token删除成功 + '401': + description: 未登录或登录态已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' '403': description: 权限不足 content: @@ -5132,6 +5185,12 @@ paths: responses: '200': description: 用户访问Token撤销成功 + '401': + description: 未登录或登录态已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' '403': description: 权限不足 content: @@ -7340,6 +7399,7 @@ components: properties: operations: type: array + uniqueItems: true description: 当前Portal用户可授予的用户Token操作列表 items: type: string diff --git a/tests/test_user_token_contract.py b/tests/test_user_token_contract.py index b7e7234..808e630 100644 --- a/tests/test_user_token_contract.py +++ b/tests/test_user_token_contract.py @@ -69,10 +69,12 @@ def test_portal_user_token_management_paths_use_portal_management_contract(self) self.assertEqual(operation_id, operation["operationId"]) self.assertEqual(["Portal Management"], operation["tags"]) self.assertEqual([{"PortalSessionAuth": []}], operation["security"]) - self.assertEqual( - "#/components/schemas/ExceptionResponse", - operation["responses"]["403"]["content"]["application/json"]["schema"]["$ref"], - ) + for status_code in ("401", "403"): + self.assertEqual( + "#/components/schemas/ExceptionResponse", + operation["responses"][status_code]["content"]["application/json"]["schema"][ + "$ref"], + ) list_tokens = spec["paths"]["/openapi/v1/user-tokens"]["get"] list_tokens_schema = ( @@ -102,6 +104,14 @@ def test_portal_user_token_management_paths_use_portal_management_contract(self) capabilities["responses"]["200"]["content"]["application/json"]["schema"]["$ref"], ) admin_list_tokens = spec["paths"]["/openapi/v1/user-tokens/admin"]["get"] + admin_status_parameter = next( + parameter for parameter in admin_list_tokens["parameters"] + if parameter["name"] == "status") + self.assertEqual( + ["all", "active", "expired", "revoked"], + admin_status_parameter["schema"]["enum"], + ) + self.assertEqual("all", admin_status_parameter["schema"]["default"]) admin_list_tokens_schema = ( admin_list_tokens["responses"]["200"]["content"]["application/json"]["schema"]) self.assertEqual("array", admin_list_tokens_schema["type"]) @@ -136,6 +146,7 @@ def test_portal_user_token_management_paths_use_portal_management_contract(self) ["operations", "defaultExpireDays", "maxExpireDays"], capability["required"], ) + self.assertTrue(capability["properties"]["operations"]["uniqueItems"]) def test_user_token_current_capability_schema_matches_portal_response(self): for spec_file in SPEC_FILES: From 19472c7f0fd5051852ee16b0b77a0514c15ecc18 Mon Sep 17 00:00:00 2001 From: kl Date: Mon, 15 Jun 2026 14:48:41 +0800 Subject: [PATCH 4/4] fix: document user token error responses --- apollo-openapi.yaml | 49 +++++++++++++++++++++++++++++++ tests/test_user_token_contract.py | 16 +++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/apollo-openapi.yaml b/apollo-openapi.yaml index afbd73c..bed0f6c 100644 --- a/apollo-openapi.yaml +++ b/apollo-openapi.yaml @@ -4944,6 +4944,12 @@ paths: application/json: schema: $ref: '#/components/schemas/OpenCreateUserTokenResponse' + '400': + description: 请求参数错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' '401': description: 未登录或登录态已过期 content: @@ -4988,6 +4994,12 @@ paths: application/json: schema: $ref: '#/components/schemas/ExceptionResponse' + '404': + description: 用户访问Token不存在或不属于当前Portal用户 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' /openapi/v1/user-tokens/{tokenId}/revoke: post: summary: 撤销当前Portal用户访问Token(new added) @@ -5020,6 +5032,12 @@ paths: application/json: schema: $ref: '#/components/schemas/ExceptionResponse' + '404': + description: 用户访问Token不存在或不属于当前Portal用户 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' /openapi/v1/user-tokens/{tokenId}/rotate: post: summary: 轮换当前Portal用户访问Token(new added) @@ -5044,6 +5062,12 @@ paths: application/json: schema: $ref: '#/components/schemas/OpenRotateUserTokenResponse' + '400': + description: 用户访问Token不可轮换 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' '401': description: 未登录或登录态已过期 content: @@ -5056,6 +5080,12 @@ paths: application/json: schema: $ref: '#/components/schemas/ExceptionResponse' + '404': + description: 用户访问Token不存在或不属于当前Portal用户 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' /openapi/v1/user-tokens/capabilities: get: summary: 获取Portal用户可创建Token能力(new added) @@ -5121,6 +5151,12 @@ paths: type: array items: $ref: '#/components/schemas/OpenUserTokenSummary' + '400': + description: 请求参数错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' '401': description: 未登录或登录态已过期 content: @@ -5165,6 +5201,12 @@ paths: application/json: schema: $ref: '#/components/schemas/ExceptionResponse' + '404': + description: 用户访问Token不存在 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' /openapi/v1/user-tokens/admin/{tokenId}/revoke: post: summary: 管理员撤销用户访问Token(new added) @@ -5197,6 +5239,12 @@ paths: application/json: schema: $ref: '#/components/schemas/ExceptionResponse' + '404': + description: 用户访问Token不存在 + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' /openapi/v1/user-tokens/current: get: summary: 获取当前用户访问Token身份和权限范围(new added) @@ -7343,6 +7391,7 @@ components: $ref: '#/components/schemas/OpenUserTokenNamespaceScope' rateLimit: type: integer + minimum: 0 description: 每秒限流阈值,0或空表示不限流 expires: type: string diff --git a/tests/test_user_token_contract.py b/tests/test_user_token_contract.py index 808e630..9805471 100644 --- a/tests/test_user_token_contract.py +++ b/tests/test_user_token_contract.py @@ -63,13 +63,26 @@ def test_portal_user_token_management_paths_use_portal_management_contract(self) "/openapi/v1/user-tokens/admin/{tokenId}/revoke": ( ("post", "adminRevokeUserToken"),), } + expected_error_responses = { + ("/openapi/v1/user-tokens", "get"): ("401", "403"), + ("/openapi/v1/user-tokens", "post"): ("400", "401", "403"), + ("/openapi/v1/user-tokens/{tokenId}", "delete"): ("401", "403", "404"), + ("/openapi/v1/user-tokens/{tokenId}/revoke", "post"): ("401", "403", "404"), + ("/openapi/v1/user-tokens/{tokenId}/rotate", "post"): ( + "400", "401", "403", "404"), + ("/openapi/v1/user-tokens/capabilities", "get"): ("401", "403"), + ("/openapi/v1/user-tokens/admin", "get"): ("400", "401", "403"), + ("/openapi/v1/user-tokens/admin/{tokenId}", "delete"): ("401", "403", "404"), + ("/openapi/v1/user-tokens/admin/{tokenId}/revoke", "post"): ( + "401", "403", "404"), + } for path, methods in expected_operations.items(): for method, operation_id in methods: operation = spec["paths"][path][method] self.assertEqual(operation_id, operation["operationId"]) self.assertEqual(["Portal Management"], operation["tags"]) self.assertEqual([{"PortalSessionAuth": []}], operation["security"]) - for status_code in ("401", "403"): + for status_code in expected_error_responses[(path, method)]: self.assertEqual( "#/components/schemas/ExceptionResponse", operation["responses"][status_code]["content"]["application/json"]["schema"][ @@ -125,6 +138,7 @@ def test_portal_user_token_management_paths_use_portal_management_contract(self) self.assertIn("tokenPrefix", summary["required"]) self.assertEqual("int64", summary["properties"]["id"]["format"]) self.assertTrue(summary["properties"]["operations"]["uniqueItems"]) + self.assertEqual(0, summary["properties"]["rateLimit"]["minimum"]) self.assertEqual( "#/components/schemas/OpenUserTokenNamespaceScope", summary["properties"]["namespaces"]["items"]["$ref"],