Skip to content

Feat/llm api key create#633

Merged
looplj merged 10 commits into
looplj:unstablefrom
x22x22:feat/llm-api-key-create
Jan 21, 2026
Merged

Feat/llm api key create#633
looplj merged 10 commits into
looplj:unstablefrom
x22x22:feat/llm-api-key-create

Conversation

@x22x22

@x22x22 x22x22 commented Jan 21, 2026

Copy link
Copy Markdown
Contributor

PR 说明:新增 LLM API Key 创建能力

背景

当前系统需要支持服务账号在不暴露主密钥的情况下,为同一项目生成用于 LLM 调用的 API Key,以便更安全地分发与使用。

没有该功能之前的问题

  • 服务账号无法生成受限用途的 API Key,只能复用自身密钥进行调用。
  • 难以对 LLM 调用进行最小权限控制与生命周期管理,存在权限过大与泄露风险。
  • 运维/集成侧缺少标准化接口来批量创建 LLM 调用用的 Key。

新增功能后如何使用

  1. 使用服务账号 API Key 发起 GraphQL mutation:createLLMAPIKey(name: String!)
  2. 返回:包含新生成的 Key 信息(id、projectID、name、key、type、status、scopes)。

GraphQL 示例:

curl -X POST "https://<your-host>/admin/graphql" \
  -H "Authorization: Bearer <SERVICE_ACCOUNT_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"query":"mutation { createLLMAPIKey(name: \"llm-key-for-app\") { id projectID name key type status scopes } }"}'

响应示例:

{
  "data": {
    "createLLMAPIKey": {
      "id": "gid://axonhub/APIKey/8",
      "projectID": "gid://axonhub/Project/2",
      "name": "llm-key-for-app",
      "key": "ah-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "type": "user",
      "status": "enabled",
      "scopes": [
        "read_channels",
        "write_requests"
      ]
    }
  }
}

说明:/admin/graphqlcreateLLMAPIKey 支持 API key,其余 GraphQL 操作仍需 JWT。

权限约束:

  • 仅服务账号 API Key 可调用
  • 需要 write:api_keys 权限

对应效果

  • 为同一项目生成仅用于 LLM 调用的 API Key,默认 scopes 为 read:channelswrite:requests
  • 调用方可安全地将新 Key 分发给下游服务,不必暴露服务账号主 Key。
  • 接口返回标准化 Key 信息,便于后续管理与审计。

单测覆盖

  • Service 层:internal/server/biz/api_key_test.go(CreateLLMAPIKey 成功/失败场景)
  • GraphQL 鉴权:internal/server/middleware/graphql_auth_test.go(仅 createLLMAPIKey 允许 API key)

Summary by CodeRabbit

  • New Features

    • Admin GraphQL mutation to create LLM-specific API keys and a dedicated admin GraphQL route with LLM API-key-aware auth.
    • API key management endpoints wired into the admin handlers.
  • Bug Fixes & Improvements

    • Stronger validation and clearer errors for missing owner, non-service-account keys, missing scopes, or empty names.
    • Project-aware permission checks for API key mutations.
  • Tests

    • Added unit and integration tests covering LLM API key creation and GraphQL auth behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings January 21, 2026 07:30
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @x22x22, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求旨在通过引入一个新功能,允许服务账号安全地为其项目生成专用于 LLM 调用的 API Key。这解决了之前服务账号无法创建受限用途密钥的问题,从而显著提升了系统的安全态势,实现了对 LLM 调用的最小权限控制和生命周期管理,并为运维和集成提供了标准化的接口。

Highlights

  • 新增 LLM API Key 创建端点: 新增了 /admin/llm/api-keys API 端点,允许通过 POST 请求创建专门用于 LLM 调用的 API Key。
  • 服务账号权限增强: 服务账号现在可以为同一项目生成具有受限权限的 LLM API Key,从而提高了安全性并实现了最小权限原则。
  • 业务逻辑与验证: 在 APIKeyService 中添加了 CreateLLMAPIKey 方法,实现了创建 LLM API Key 的核心业务逻辑,包括对所有者类型、所需权限范围(write:api_keys)和 Key 名称的严格验证。
  • 错误处理细化: 定义了新的错误类型,如 ErrAPIKeyOwnerRequiredErrServiceAccountRequiredErrAPIKeyScopeRequiredErrAPIKeyNameRequired,以更细致地处理 API Key 创建过程中的各种验证失败情况。
  • 全面测试覆盖: 为 CreateLLMAPIKey 功能添加了全面的单元测试,覆盖了成功创建、所有者为空、非服务账号所有者、缺少必要权限以及名称为空等多种场景,确保了功能的正确性和健壮性。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai

coderabbitai Bot commented Jan 21, 2026

Copy link
Copy Markdown
Contributor

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds end-to-end support for creating LLM-scoped API keys: new handler wiring, business logic to create LLM API keys, GraphQL mutation and resolver, GraphQL-specific middleware permitting service-account API keys for the createLLMAPIKey mutation, scope/privacy rule changes, and tests for service and middleware behavior.

Changes

Cohort / File(s) Change summary
API handlers & DI
internal/server/api/api_key.go, internal/server/api/fx_module.go
New APIKeyHandlersParams and APIKeyHandlers plus NewAPIKeyHandlers; registered via fx.Provide(NewAPIKeyHandlers) in API module.
Business logic & errors
internal/server/biz/api_key.go, internal/server/biz/errors.go, internal/server/biz/api_key_test.go
Added APIKeyService.CreateLLMAPIKey implementation and unit tests; introduced new error variables: ErrAPIKeyOwnerRequired, ErrServiceAccountRequired, ErrAPIKeyScopeRequired, ErrAPIKeyNameRequired.
GraphQL schema & resolvers
internal/server/gql/axonhub.graphql, internal/server/gql/axonhub.resolvers.go, internal/server/gql/generated.go
Added createLLMAPIKey(name: String!): APIKey! mutation, resolver CreateLLMAPIKey, and generated marshaling/unmarshaling plumbing.
Routes & handlers wiring
internal/server/routes.go
Added APIKey *api.APIKeyHandlers to Handlers; moved admin GraphQL handling to dedicated group /admin with GraphQL auth for LLM API keys.
Middleware & tests
internal/server/middleware/auth.go, internal/server/middleware/graphql_auth_test.go
New WithGraphQLAuthForLLMAPIKey(auth *biz.AuthService) middleware and helpers (tryJWTAuth, payload parsing, mutation detection), plus integration tests validating createLLMAPIKey allowed with API key and other mutations rejected.
Privacy / scope rules
internal/scopes/rule_apikey_scope.go, internal/scopes/rule_user_project_scope.go, internal/scopes/rule_owner.go
Added APIKeyProjectScopeWriteRule with project-aware mutation enforcement; adjusted OwnerRule and user-project rule to return privacy.Skipf when user missing in context.
Ent schema change
internal/ent/schema/api_key.go
Included scopes.APIKeyProjectScopeWriteRule(scopes.ScopeWriteAPIKeys) in APIKey mutation policy.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Router as "API Router (/admin/graphql)"
    participant Middleware as "WithGraphQLAuthForLLMAPIKey"
    participant Auth as "AuthService"
    participant APIKeySvc as "APIKeyService"
    participant DB as "Database/Ent"
    participant Resolver as "GraphQL Resolver (CreateLLMAPIKey)"

    Client->>Router: POST /admin/graphql (createLLMAPIKey {name})
    Router->>Middleware: pass request
    Middleware->>Auth: try JWT auth (optional)
    Auth-->>Middleware: JWT authenticated? (if yes -> inject user)
    alt JWT not present/valid
        Middleware->>Middleware: parse GraphQL payload
        Middleware->>APIKeySvc: validate provided API key (service account + write scope)
        APIKeySvc-->>Middleware: API key+project validated
        Middleware->>Router: inject api key & project into context
    end
    Router->>Resolver: call CreateLLMAPIKey(ctx, name)
    Resolver->>APIKeySvc: CreateLLMAPIKey(ctx, ownerKey, name)
    APIKeySvc->>DB: generate & persist new user-scoped API key (scopes: ReadChannels, WriteRequests)
    DB-->>APIKeySvc: new APIKey entity
    APIKeySvc-->>Resolver: return created APIKey
    Resolver-->>Client: HTTP 200 + APIKey payload
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

"A rabbit hops with tiny paws,
I wired keys and tuned the laws.
With hops and hops the token springs,
For LLM doors and sandboxed things.
🐇✨"

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat/llm api key create' directly reflects the main objective: adding a new GraphQL mutation to create LLM-specific API keys with service-account authentication.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

此拉取请求引入了创建 LLM 专用 API 密钥的功能,这对于细粒度访问控制和提高安全性是一个有价值的补充。该实现包括一个新的 API 端点、密钥创建的业务逻辑以及相应的单元测试。使用 "fx" 进行依赖注入和 "gin" 进行路由与现有代码库保持一致。错误处理已到位,但某些错误消息可以更具体。响应中返回原始 API 密钥存在潜在的安全隐患。

Comment thread internal/server/api/api_key.go Outdated
Comment thread internal/server/api/api_key.go Outdated
Comment thread internal/server/api/api_key.go Outdated
Comment thread internal/server/biz/api_key.go Outdated
return nil, ErrAPIKeyNameRequired
}

ctx = privacy.DecisionContext(ctx, privacy.Allow)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

使用 privacy.DecisionContext(ctx, privacy.Allow) 绕过了后续数据库操作的隐私规则。虽然这对于内部创建 API 密钥可能是故意的,但它是一种强大的机制,应充分了解其安全影响后使用。请确保此绕过是严格必要的,并且上游检查(例如服务帐户类型、所需范围)充分涵盖了授权要求。

@x22x22 x22x22 force-pushed the feat/llm-api-key-create branch from b757ad9 to f5ae8ec Compare January 21, 2026 07:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds the ability for service account API keys to create LLM-specific API keys with restricted scopes for safer distribution and usage. The feature enables service accounts with write_api_keys permission to generate limited-scope API keys (with only read_channels and write_requests scopes) for LLM calls, supporting better security practices and lifecycle management.

Changes:

  • Added new API endpoint POST /admin/llm/api-keys with API key authentication middleware
  • Implemented CreateLLMAPIKey business logic with validation for service account type and required scopes
  • Added comprehensive test coverage for all validation scenarios and success cases

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
internal/server/routes.go Added new route group /admin/llm with API key authentication and registered the CreateLLMAPIKey handler
internal/server/biz/errors.go Defined new error constants for API key validation (owner required, service account required, scope required, name required)
internal/server/biz/api_key.go Implemented CreateLLMAPIKey method with security validations and LLM-specific scope assignment
internal/server/biz/api_key_test.go Added comprehensive test suite covering success case and all error scenarios (nil owner, non-service account, missing scope, empty name)
internal/server/api/api_key.go Created new API handler with request/response models and proper error mapping to HTTP status codes
internal/server/api/fx_module.go Registered APIKeyHandlers in the dependency injection module

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@internal/server/api/api_key.go`:
- Around line 84-86: The default branch currently returns the raw err in the 500
response (JSONError(c, http.StatusInternalServerError, err)), which can leak
internal details; change it to return a generic error message to the client
(e.g., "internal server error") while logging the original err server-side;
update the default case in the same function handling API key errors to call
JSONError with the generic message and send the detailed err to your logger (or
c.Error/internal log) instead of returning err to the response.
🧹 Nitpick comments (2)
internal/server/biz/api_key_test.go (1)

972-1064: Good test coverage for the new CreateLLMAPIKey functionality.

The test suite covers:

  • Successful creation with name trimming verification
  • All four error conditions (nil owner, non-service account, missing scope, empty name)
  • Proper field assertions including type, scopes, and ownership inheritance

Consider adding a test case for verifying the generated key format (starts with "ah-") similar to TestAPIKeyService_CreateAPIKey_Type at line 947-969.

🧪 Optional: Add key format verification
t.Run("creates llm api key", func(t *testing.T) {
    apiKey, err := apiKeyService.CreateLLMAPIKey(ctx, ownerAPIKey, "  LLM Key  ")
    require.NoError(t, err)
    require.NotNil(t, apiKey)
    require.Equal(t, "LLM Key", apiKey.Name)
    require.Equal(t, ownerUser.ID, apiKey.UserID)
    require.Equal(t, ownerProject.ID, apiKey.ProjectID)
    require.Equal(t, apikey.TypeUser, apiKey.Type)
    require.ElementsMatch(t, []string{string(scopes.ScopeReadChannels), string(scopes.ScopeWriteRequests)}, apiKey.Scopes)
    require.NotEmpty(t, apiKey.Key)
+   require.True(t, len(apiKey.Key) > 3)
+   require.Equal(t, "ah-", apiKey.Key[:3])
})
internal/server/api/api_key.go (1)

63-66: Consider returning the actual binding error for better debugging.

The generic "Invalid request format" message doesn't help clients understand what's wrong with their request. The actual binding error would provide more useful feedback.

♻️ Suggested improvement
 	var req CreateLLMAPIKeyRequest
 	if err := c.ShouldBindJSON(&req); err != nil {
-		JSONError(c, http.StatusBadRequest, errors.New("Invalid request format"))
+		JSONError(c, http.StatusBadRequest, err)
 		return
 	}

Comment thread internal/server/api/api_key.go Outdated
Comment on lines +84 to +86
default:
JSONError(c, http.StatusInternalServerError, err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid exposing internal error details in 500 responses.

The default case passes the raw error to the response, which might leak implementation details (database errors, internal paths, etc.) to clients.

🔒 Suggested fix
 		default:
-			JSONError(c, http.StatusInternalServerError, err)
+			JSONError(c, http.StatusInternalServerError, biz.ErrInternal)
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
default:
JSONError(c, http.StatusInternalServerError, err)
}
default:
JSONError(c, http.StatusInternalServerError, biz.ErrInternal)
}
🤖 Prompt for AI Agents
In `@internal/server/api/api_key.go` around lines 84 - 86, The default branch
currently returns the raw err in the 500 response (JSONError(c,
http.StatusInternalServerError, err)), which can leak internal details; change
it to return a generic error message to the client (e.g., "internal server
error") while logging the original err server-side; update the default case in
the same function handling API key errors to call JSONError with the generic
message and send the detailed err to your logger (or c.Error/internal log)
instead of returning err to the response.

x22x22 and others added 2 commits January 21, 2026 16:00
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
@looplj

looplj commented Jan 21, 2026

Copy link
Copy Markdown
Owner

现在的 service account 是个半成品,其实没起作用,做了一半才发现没想清楚,所以就没继续了。
正好你有场景,可以考虑下 service account 应该是全局的还是 project 级别的?

然后,预期给 service account 暴露的 API 也是 graphql,完全复用 Admin 的 API,和 Admin API 一样的能力。

@x22x22

x22x22 commented Jan 21, 2026

Copy link
Copy Markdown
Contributor Author

我思考过是否复用admin的api,发现是不行的,因为这样就可能存在越权申请apikey的情况。即我们用一个仅包含创建apikey scope的key去创建了一个更高权限的apikey。基于这个想法,我才没复用 admin的api。

我的业务场景本身其实只是为了让集成方可以在它自己的项目下创建一个仅仅可以进行大模型调用的apikey。

@looplj

looplj commented Jan 21, 2026

Copy link
Copy Markdown
Owner

PermissionValidator 可以看下这个,现在是有权限校验的,不能越过 user/api key 的权限。

长期来看,应该是提供统一的 OpenAPI。

@x22x22

x22x22 commented Jan 21, 2026

Copy link
Copy Markdown
Contributor Author

可以考虑下 service account 应该是全局的还是 project 级别的?

我们这边是期望这个 service account apikey是项目下的。而不是全局的。这样创建的llm调用apikey也就自然属于这个项目下的。这样来说对于第三方就不需要知道自己属于什么项目。

@x22x22 x22x22 marked this pull request as draft January 21, 2026 14:13
@x22x22

x22x22 commented Jan 21, 2026

Copy link
Copy Markdown
Contributor Author

PermissionValidator 可以看下这个,现在是有权限校验的,不能越过 user/api key 的权限。

长期来看,应该是提供统一的 OpenAPI。

我先按你的要求添加一个新的 createLLMAPIKey mutation

@x22x22

x22x22 commented Jan 21, 2026

Copy link
Copy Markdown
Contributor Author

@looplj

已按建议统一走 GraphQL,并确保不越权:

  • 新增专用 mutation:createLLMAPIKey(name: String!),不允许传 projectID/scopes/type,服务端固定为 LLM scopes 与当前项目。
  • /admin/graphql 仍默认 JWT,仅 createLLMAPIKey 允许使用 API key;其他 GraphQL 仍需 JWT,避免 API key 调用高权限接口。
  • 权限模型为 project 级 service account;创建的 LLM key 强制归属同项目。
  • ent policy 增加 APIKeyProjectScopeWriteRule(校验 write_api_keys + project 一致性),移除 privacy.Allow 绕过;用户规则在无 user 时 Skip 以便 API key 规则生效。
  • 已补单测:API key 仅可调用 createLLMAPIKey,其他 mutation 仍拒绝;service 层单测已覆盖。
  • 关于 PermissionValidator:目前它只基于 user context 做“不可越权授予”,对 API key 场景不直接适用。当前用 ent policy 覆盖 API key 权限约束;若后续要统一 PermissionValidator,可考虑扩展为同时支持 API key。

@x22x22 x22x22 marked this pull request as ready for review January 21, 2026 14:41
@x22x22

x22x22 commented Jan 21, 2026

Copy link
Copy Markdown
Contributor Author

PR 说明:新增 LLM API Key 创建能力

背景

当前系统需要支持服务账号在不暴露主密钥的情况下,为同一项目生成用于 LLM 调用的 API Key,以便更安全地分发与使用。

没有该功能之前的问题

  • 服务账号无法生成受限用途的 API Key,只能复用自身密钥进行调用。
  • 难以对 LLM 调用进行最小权限控制与生命周期管理,存在权限过大与泄露风险。
  • 运维/集成侧缺少标准化接口来批量创建 LLM 调用用的 Key。

新增功能后如何使用

  1. 使用服务账号 API Key 发起 GraphQL mutation:createLLMAPIKey(name: String!)
  2. 返回:包含新生成的 Key 信息(id、projectID、name、key、type、status、scopes)。

GraphQL 示例:

curl -X POST "https://<your-host>/admin/graphql" \
  -H "Authorization: Bearer <SERVICE_ACCOUNT_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"query":"mutation { createLLMAPIKey(name: \"llm-key-for-app\") { id projectID name key type status scopes } }"}'

响应示例:

{
  "data": {
    "createLLMAPIKey": {
      "id": "gid://axonhub/APIKey/8",
      "projectID": "gid://axonhub/Project/2",
      "name": "llm-key-for-app",
      "key": "ah-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "type": "user",
      "status": "enabled",
      "scopes": [
        "read_channels",
        "write_requests"
      ]
    }
  }
}

说明:/admin/graphqlcreateLLMAPIKey 支持 API key,其余 GraphQL 操作仍需 JWT。

权限约束:

  • 仅服务账号 API Key 可调用
  • 需要 write:api_keys 权限

对应效果

  • 为同一项目生成仅用于 LLM 调用的 API Key,默认 scopes 为 read:channelswrite:requests
  • 调用方可安全地将新 Key 分发给下游服务,不必暴露服务账号主 Key。
  • 接口返回标准化 Key 信息,便于后续管理与审计。

单测覆盖

  • Service 层:internal/server/biz/api_key_test.go(CreateLLMAPIKey 成功/失败场景)
  • GraphQL 鉴权:internal/server/middleware/graphql_auth_test.go(仅 createLLMAPIKey 允许 API key)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/server/biz/api_key_test.go (1)

55-77: Fix pipeline failure: missing privacy.DecisionContext for test setup.

The pipeline failure indicates TestAPIKeyService_GetAPIKey fails at line 76 with "no user in context: ent/privacy: deny rule". The test setup creates entities without the privacy bypass that other tests use.

🐛 Proposed fix to restore privacy bypass for entity creation
 func TestAPIKeyService_GetAPIKey(t *testing.T) {
 	// Test with noop cache (no cache configured)
 	cacheConfig := xcache.Config{} // Empty config = noop cache

 	apiKeyService, client := setupTestAPIKeyService(t, cacheConfig)
 	defer client.Close()

 	ctx := context.Background()
 	ctx = ent.NewContext(ctx, client)
+	ctx = privacy.DecisionContext(ctx, privacy.Allow)

 	// Create a test user
 	hashedPassword, err := HashPassword("test-password")
🤖 Fix all issues with AI agents
In `@internal/server/middleware/auth.go`:
- Around line 181-197: The current loop in auth.go (iterating doc.Operations and
checking payload.OperationName, ast.Mutation and field.Name ==
"createLLMAPIKey") returns true as soon as it sees createLLMAPIKey, allowing
additional mutations in the same request; change the logic to verify the entire
request contains only that single mutation: inspect doc.Operations and their
SelectionSets to ensure there is exactly one operation (or if
payload.OperationName is set, that the named operation exists) and that this
operation is of type ast.Mutation and its SelectionSet contains exactly one
top-level field whose Name == "createLLMAPIKey"; if any other operations or
other top-level fields are present, do not authorize. Ensure checks reference
doc.Operations, payload.OperationName, ast.Mutation, SelectionSet and field.Name
== "createLLMAPIKey".

In `@internal/server/middleware/graphql_auth_test.go`:
- Around line 40-42: The test-initialized AuthService is missing SystemService
and UserService, causing tryJWTAuth -> AuthenticateJWTToken to panic when it
calls s.SystemService.SecretKey() and s.UserService.GetUserByID(); fix by
updating the test setup that creates authService to also provide simple stub
implementations for SystemService and UserService (implementing SecretKey() and
GetUserByID(...) respectively) or replace with appropriate mocks so
AuthenticateJWTToken can run without nil derefs; reference AuthService,
tryJWTAuth, AuthenticateJWTToken, SystemService.SecretKey, and
UserService.GetUserByID when locating where to add the stubs/mocks.

In `@internal/server/routes.go`:
- Line 28: The Handlers struct contains an unused field APIKey that is never
referenced by SetupRoutes; either remove the APIKey field from the Handlers
struct (and any places that construct/populate it) or keep it but add a clear
TODO comment on the APIKey field in the Handlers definition explaining its
intended future use and why it is currently unused (e.g., "TODO: reserved for
future API key routes"). Update any constructors or tests that populate
Handlers.APIKey accordingly so the code and comments remain consistent.

Comment on lines +181 to +197
for _, op := range doc.Operations {
if payload.OperationName != "" && op.Name != payload.OperationName {
continue
}
if op.Operation != ast.Mutation {
continue
}
for _, selection := range op.SelectionSet {
field, ok := selection.(*ast.Field)
if !ok {
continue
}
if field.Name == "createLLMAPIKey" {
return true, nil
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Security issue: Multiple mutations in a single request could bypass authorization.

The current logic returns true as soon as it finds createLLMAPIKey in any selection set. This allows a malicious request containing multiple mutations to pass authorization:

mutation {
  createLLMAPIKey(name: "x") { id }
  deleteUser(id: 1)  # This would also be executed!
}

The check should verify that the request contains only the createLLMAPIKey mutation and nothing else.

🔒 Proposed fix to ensure only createLLMAPIKey is allowed
 func isCreateLLMAPIKeyMutation(c *gin.Context) (bool, error) {
 	body, err := io.ReadAll(c.Request.Body)
 	if err != nil {
 		return false, err
 	}
 	if len(body) == 0 {
 		return false, errors.New("empty GraphQL request body")
 	}
 	defer func() {
 		c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
 	}()

 	var payload graphQLPayload
 	if err := json.Unmarshal(body, &payload); err != nil {
 		return false, err
 	}
 	if payload.Query == "" {
 		return false, errors.New("missing GraphQL query")
 	}

 	doc, err := parser.ParseQuery(&ast.Source{Input: payload.Query})
 	if err != nil {
 		return false, err
 	}

+	foundCreateLLMAPIKey := false
 	for _, op := range doc.Operations {
 		if payload.OperationName != "" && op.Name != payload.OperationName {
 			continue
 		}
-		if op.Operation != ast.Mutation {
-			continue
-		}
-		for _, selection := range op.SelectionSet {
-			field, ok := selection.(*ast.Field)
-			if !ok {
-				continue
-			}
-			if field.Name == "createLLMAPIKey" {
-				return true, nil
+		// Reject queries - only mutations allowed for API key auth
+		if op.Operation == ast.Query {
+			return false, nil
+		}
+		if op.Operation == ast.Mutation {
+			for _, selection := range op.SelectionSet {
+				field, ok := selection.(*ast.Field)
+				if !ok {
+					continue
+				}
+				if field.Name == "createLLMAPIKey" {
+					foundCreateLLMAPIKey = true
+				} else {
+					// Any other mutation field disqualifies
+					return false, nil
+				}
 			}
 		}
 	}

-	return false, nil
+	return foundCreateLLMAPIKey, nil
 }
🤖 Prompt for AI Agents
In `@internal/server/middleware/auth.go` around lines 181 - 197, The current loop
in auth.go (iterating doc.Operations and checking payload.OperationName,
ast.Mutation and field.Name == "createLLMAPIKey") returns true as soon as it
sees createLLMAPIKey, allowing additional mutations in the same request; change
the logic to verify the entire request contains only that single mutation:
inspect doc.Operations and their SelectionSets to ensure there is exactly one
operation (or if payload.OperationName is set, that the named operation exists)
and that this operation is of type ast.Mutation and its SelectionSet contains
exactly one top-level field whose Name == "createLLMAPIKey"; if any other
operations or other top-level fields are present, do not authorize. Ensure
checks reference doc.Operations, payload.OperationName, ast.Mutation,
SelectionSet and field.Name == "createLLMAPIKey".

Comment on lines +40 to +42
authService := &biz.AuthService{
APIKeyService: apiKeyService,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the test file
find . -type f -name "graphql_auth_test.go"

Repository: looplj/axonhub

Length of output: 107


🏁 Script executed:

#!/bin/bash
# Examine the test file to see the setup and AuthService initialization
cat -n internal/server/middleware/graphql_auth_test.go | head -120

Repository: looplj/axonhub

Length of output: 4553


🏁 Script executed:

#!/bin/bash
# Find AuthService struct definition
rg "type AuthService struct" -A 20

Repository: looplj/axonhub

Length of output: 1283


🏁 Script executed:

#!/bin/bash
# Examine the middleware implementation
cat -n internal/server/middleware/graphql_auth.go | head -150

Repository: looplj/axonhub

Length of output: 132


🏁 Script executed:

#!/bin/bash
# List all files in middleware directory
ls -la internal/server/middleware/

Repository: looplj/axonhub

Length of output: 1311


🏁 Script executed:

#!/bin/bash
# Find the middleware implementation
rg "WithGraphQLAuthForLLMAPIKey" --type go -B 2 -A 20

Repository: looplj/axonhub

Length of output: 6136


🏁 Script executed:

#!/bin/bash
# Look for auth middleware functions
rg "func.*Auth" internal/server/middleware/ --type go -A 10

Repository: looplj/axonhub

Length of output: 7001


🏁 Script executed:

#!/bin/bash
# Get the complete middleware auth.go
cat -n internal/server/middleware/auth.go

Repository: looplj/axonhub

Length of output: 8045


🏁 Script executed:

#!/bin/bash
# Check AuthService methods
rg "func \(\w+ \*AuthService\)" -A 10 internal/server/biz/auth.go

Repository: looplj/axonhub

Length of output: 1711


🏁 Script executed:

#!/bin/bash
# Get full AuthenticateJWTToken implementation
rg "func \(s \*AuthService\) AuthenticateJWTToken" -A 35 internal/server/biz/auth.go

Repository: looplj/axonhub

Length of output: 1241


🏁 Script executed:

#!/bin/bash
# Get GenerateJWTToken for reference
rg "func \(s \*AuthService\) GenerateJWTToken" -A 15 internal/server/biz/auth.go

Repository: looplj/axonhub

Length of output: 632


🏁 Script executed:

#!/bin/bash
# Full AnthenticateAPIKey
rg "func \(s \*AuthService\) AnthenticateAPIKey" -A 25 internal/server/biz/auth.go

Repository: looplj/axonhub

Length of output: 685


AuthService is incompletely initialized; JWT authentication will crash at runtime.

The AuthService in the test setup (lines 40-42) only initializes APIKeyService, leaving SystemService and UserService as nil. The middleware's tryJWTAuth function calls auth.AuthenticateJWTToken, which unconditionally accesses s.SystemService.SecretKey() and s.UserService.GetUserByID(), causing nil pointer dereferences if JWT authentication is attempted.

The current tests pass because they only send API key headers (not JWT tokens), avoiding the JWT auth path. Initialize the missing services or add explicit nil guards in the test to reflect the actual expected behavior.

🤖 Prompt for AI Agents
In `@internal/server/middleware/graphql_auth_test.go` around lines 40 - 42, The
test-initialized AuthService is missing SystemService and UserService, causing
tryJWTAuth -> AuthenticateJWTToken to panic when it calls
s.SystemService.SecretKey() and s.UserService.GetUserByID(); fix by updating the
test setup that creates authService to also provide simple stub implementations
for SystemService and UserService (implementing SecretKey() and GetUserByID(...)
respectively) or replace with appropriate mocks so AuthenticateJWTToken can run
without nil derefs; reference AuthService, tryJWTAuth, AuthenticateJWTToken,
SystemService.SecretKey, and UserService.GetUserByID when locating where to add
the stubs/mocks.

Comment thread internal/server/routes.go
Playground *api.PlaygroundHandlers
System *api.SystemHandlers
Auth *api.AuthHandlers
APIKey *api.APIKeyHandlers

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if APIKey handler is used anywhere in the codebase
echo "=== Searching for handlers.APIKey usage ==="
rg -n 'handlers\.APIKey' .

echo -e "\n=== Searching for APIKey field usage in routes.go ==="
rg -n 'APIKey' internal/server/routes.go

echo -e "\n=== Searching for APIKey references in entire internal/server directory ==="
rg -n 'APIKey' internal/server/

Repository: looplj/axonhub

Length of output: 50371


🏁 Script executed:

#!/bin/bash
# View the Handlers struct and SetupRoutes function to understand APIKey field usage
echo "=== Viewing routes.go around line 28 (struct definition) ==="
sed -n '1,50p' internal/server/routes.go

echo -e "\n=== Searching for SetupRoutes function ==="
rg -n "func SetupRoutes" internal/server/routes.go

echo -e "\n=== Viewing SetupRoutes function ==="
sed -n '60,130p' internal/server/routes.go

Repository: looplj/axonhub

Length of output: 4199


🏁 Script executed:

#!/bin/bash
# Check if handlers.APIKey is referenced anywhere in routes.go
echo "=== Complete search for APIKey in routes.go ==="
rg -n "APIKey" internal/server/routes.go

echo -e "\n=== Total lines in routes.go ==="
wc -l internal/server/routes.go

echo -e "\n=== Viewing end of routes.go to see if there are more routes ==="
tail -30 internal/server/routes.go

Repository: looplj/axonhub

Length of output: 1337


Remove unused APIKey handler field or add a TODO comment if reserved for future use.

The APIKey field is injected into the Handlers struct but never referenced in SetupRoutes. Either remove it or document its intended purpose with a TODO comment.

🤖 Prompt for AI Agents
In `@internal/server/routes.go` at line 28, The Handlers struct contains an unused
field APIKey that is never referenced by SetupRoutes; either remove the APIKey
field from the Handlers struct (and any places that construct/populate it) or
keep it but add a clear TODO comment on the APIKey field in the Handlers
definition explaining its intended future use and why it is currently unused
(e.g., "TODO: reserved for future API key routes"). Update any constructors or
tests that populate Handlers.APIKey accordingly so the code and comments remain
consistent.

@looplj

looplj commented Jan 21, 2026

Copy link
Copy Markdown
Owner

代码我先合并了,我会做一些调整。

@looplj looplj merged commit b65e5ea into looplj:unstable Jan 21, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants