refactor: move openapi to separate graph#637
Conversation
📝 WalkthroughWalkthroughThis PR restructures the LLM API key creation flow by establishing a dedicated OpenAPI GraphQL endpoint at Changes
Sequence DiagramsequenceDiagram
participant Client
participant Router
participant AuthMW as Auth Middleware
participant GraphQL as GraphQL Handler
participant APIKeyService
participant DB as Database
Note over Client,DB: OLD FLOW: Main GraphQL
Client->>Router: POST /graphql (createLLMAPIKey)
Router->>AuthMW: WithGraphQLAuthForLLMAPIKey
AuthMW->>AuthMW: Parse GraphQL payload for mutation type
AuthMW->>GraphQL: Validate & forward context
GraphQL->>GraphQL: Resolve createLLMAPIKey
GraphQL->>APIKeyService: CreateLLMAPIKey(ownerKey, name)
APIKeyService->>DB: Create & persist
DB-->>APIKeyService: APIKey
APIKeyService-->>GraphQL: APIKey
GraphQL-->>Client: APIKey (or error)
Note over Client,DB: NEW FLOW: OpenAPI GraphQL
Client->>Router: POST /openapi/v1/graphql (createLLMAPIKey)
Router->>AuthMW: WithOpenAPIAuth
AuthMW->>AuthMW: Validate API key from Authorization header
AuthMW->>AuthMW: Enforce service account type
AuthMW->>GraphQL: Set context (APIKey, ProjectID)
GraphQL->>GraphQL: Resolve createLLMAPIKey
GraphQL->>APIKeyService: CreateLLMAPIKey(ownerKey, name)
APIKeyService->>DB: Create & persist (no validations)
DB-->>APIKeyService: APIKey
APIKeyService-->>GraphQL: APIKey
GraphQL-->>Client: APIKey (or error)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
Summary of ChangesHello @looplj, 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! This pull request undertakes a significant architectural refactoring by isolating the OpenAPI-related GraphQL functionality into its own dedicated graph. This separation encompasses distinct schema definitions, resolver implementations, code generation, and a specialized authentication middleware. The change enhances modularity, clarifies the purpose of different API endpoints, and allows for more granular control over access to specific mutations like Highlights
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
Code Review
The pull request successfully refactors the OpenAPI GraphQL endpoint into a separate graph, which enhances modularity and separation of concerns. The createLLMAPIKey mutation has been correctly moved to this new graph. However, there are critical concerns regarding the authorization logic for this new endpoint. The explicit scope check for ScopeWriteAPIKeys, previously in the business logic, is now missing in the new middleware setup, posing a security risk. Additionally, some minor improvements can be made to clarity in the Makefile and test coverage for the new authorization middleware.
I am having trouble creating individual review comments. Click here to see my feedback.
internal/server/middleware/auth.go (107-109)
The WithOpenAPIAuth middleware correctly enforces that only TypeServiceAccount API keys can access the OpenAPI endpoints. However, it does not verify if the service account has the necessary ScopeWriteAPIKeys permission to create LLM API keys. This is a critical security oversight, as any service account could potentially create LLM API keys without proper authorization. You will also need to add import "slices" to the file.
if apiKey.Type != apikey.TypeServiceAccount {
AbortWithError(c, http.StatusUnauthorized, errors.New("API key type must be service account"))
return
}
if !slices.Contains(apiKey.Scopes, string(scopes.ScopeWriteAPIKeys)) {
AbortWithError(c, http.StatusUnauthorized, errors.New("API key missing required scope: write_api_keys"))
return
}internal/server/middleware/graphql_auth_test.go (1-124)
The graphql_auth_test.go file was removed, which contained tests for the previous GraphQL authorization middleware. With the introduction of WithOpenAPIAuth middleware and the relocation of authorization logic, it is crucial to create new dedicated tests for WithOpenAPIAuth to ensure its correctness, especially for the critical scope checks (e.g., ScopeWriteAPIKeys for service accounts).
Makefile (13)
The echo message for the generate-openapi target is generic. To improve clarity, it should be more specific to reflect that it's generating OpenAPI-related GraphQL code.
@echo "Generating OpenAPI GraphQL code..."
internal/server/biz/api_key.go (8)
The slices import was removed from this file. This indicates that the slices.Contains check for ScopeWriteAPIKeys is intended to be handled elsewhere, likely in the middleware. Please ensure this critical scope validation is correctly implemented in the WithOpenAPIAuth middleware to prevent unauthorized API key creation.
internal/server/routes.go (28)
The APIKey *api.APIKeyHandlers field was removed from the Handlers struct. Please confirm if this handler was intentionally removed and if its functionality is no longer required or has been fully replaced by other handlers. If it provided other API key related endpoints, those might now be missing.
There was a problem hiding this comment.
Actionable comments posted: 0
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.go (1)
64-81: Add nil check forownerparameter to follow defensive programming practices.The
ownerparameter is dereferenced at lines 80-81 without a nil check. While the single active call site inopenapi.resolvers.govalidates the parameter before calling this function, the method should validate its own inputs rather than relying on caller validation. This prevents potential nil pointer panics if the function is called from other locations in the future.Proposed fix
func (s *APIKeyService) CreateLLMAPIKey(ctx context.Context, owner *ent.APIKey, name string) (*ent.APIKey, error) { + if owner == nil { + return nil, fmt.Errorf("owner API key is required") + } + name = strings.TrimSpace(name) if name == "" { return nil, ErrAPIKeyNameRequired }
🧹 Nitpick comments (7)
internal/server/gql/axonhub.resolvers.go (1)
510-526: Consider removing the legacy “harms way” block once migration is complete.It’s harmless, but leaving commented resolver logic in a generated file can confuse future edits and tooling. If the OpenAPI graph is now the sole owner, it’s safe to delete this block to keep the generated file clean.
🧹 Suggested cleanup
-// !!! WARNING !!! -// The code below was going to be deleted when updating resolvers. It has been copied here so you have -// one last chance to move it out of harms way if you want. There are two reasons this happens: -// - When renaming or deleting a resolver the old code will be put in here. You can safely delete -// it when you're done. -// - You have helper methods in this file. Move them out to keep these resolver files clean. -/* - func (r *mutationResolver) CreateLLMAPIKey(ctx context.Context, name string) (*ent.APIKey, error) { - ownerKey, ok := contexts.GetAPIKey(ctx) - if !ok || ownerKey == nil { - return nil, fmt.Errorf("api key not found in context") - } - - return r.apiKeyService.CreateLLMAPIKey(ctx, ownerKey, name) - } -*/Makefile (1)
12-15: Consider updating the echo message for clarity.The message says "Generating GraphQL and Ent code..." but this target only generates OpenAPI GraphQL code, not Ent schemas. A more accurate message would help avoid confusion.
Suggested improvement
generate-openapi: - `@echo` "Generating GraphQL and Ent code..." + `@echo` "Generating OpenAPI GraphQL code..." cd internal/server/gql/openapi && go generate - `@echo` "Generation completed!" + `@echo` "OpenAPI GraphQL generation completed!"internal/server/gql/openapi/gqlgen.yml (1)
7-10: Remove commented-out configuration.The commented
execsection is unused and adds noise. Consider removing it to keep the configuration clean.Suggested cleanup
omit_gqlgen_version_in_file_notice: true -#exec: -# layout: follow-schema -# dir: ./graph -# package: graph - # resolver reports where the resolver implementations go.internal/server/middleware/auth.go (2)
87-88: Outdated comment doesn't match the function's purpose.The comment states "allows API key auth for createLLMAPIKey only" but the function is a general OpenAPI authentication middleware that restricts access to service account API keys. Consider updating the comment to accurately describe the middleware's purpose.
📝 Suggested comment update
-// WithOpenAPIAuth allows API key auth for createLLMAPIKey only. +// WithOpenAPIAuth middleware validates API key authentication for OpenAPI endpoints, +// restricting access to service account type API keys only. func WithOpenAPIAuth(auth *biz.AuthService) gin.HandlerFunc {
89-119: Consider reducing code duplication withWithAPIKeyConfig.
WithOpenAPIAuthshares most of its logic withWithAPIKeyConfig(API key extraction, authentication, error handling, context propagation). You could refactor to compose the middleware or extract common logic into a helper function.♻️ One possible approach
// Helper to handle common API key authentication logic func authenticateAndSetContext(c *gin.Context, auth *biz.AuthService, config *APIKeyConfig) (*ent.APIKey, error) { key, err := ExtractAPIKeyFromRequest(c.Request, config) if err != nil { AbortWithError(c, http.StatusUnauthorized, err) return nil, err } apiKey, err := auth.AnthenticateAPIKey(c.Request.Context(), key) if err != nil { if ent.IsNotFound(err) || errors.Is(err, biz.ErrInvalidAPIKey) { AbortWithError(c, http.StatusUnauthorized, errors.New("Invalid API key")) } else { AbortWithError(c, http.StatusInternalServerError, errors.New("Failed to validate API key")) } return nil, err } return apiKey, nil }Then
WithOpenAPIAuthcan call this helper and add the service account check.internal/server/gql/openapi/openapi.graphql (1)
7-9: Consider adding schema documentation.The mutation signature is correct. For better API discoverability, consider adding GraphQL descriptions to document the mutation's purpose and requirements.
📝 Example with documentation
""" Creates a new LLM API key associated with the authenticated service account's project. Requires a service account API key with write_api_keys scope for authentication. """ extend type Mutation { createLLMAPIKey( """The display name for the new API key.""" name: String! ): APIKey! }internal/server/gql/openapi/openapi.resolvers.go (1)
16-32: Consider simplifying the redundant nil check.Based on the
contexts.GetAPIKeyimplementation (which returnscontainer.APIKey != nilas the boolean), whenokistrue,ownerKeyis guaranteed to be non-nil. TheownerKey == nilcheck is defensive but redundant.♻️ Suggested simplification
func (r *mutationResolver) CreateLLMAPIKey(ctx context.Context, name string) (*APIKey, error) { ownerKey, ok := contexts.GetAPIKey(ctx) - if !ok || ownerKey == nil { + if !ok { return nil, fmt.Errorf("api key not found in context") }
…#1617) * feat(openapi): add updateAPIKeyProfiles and loadApiKeyProfileTemplate Adds two GraphQL mutations to /openapi/v1/graphql for programmatic LLM API key profile management via service account keys: - updateAPIKeyProfiles: integral state replacement (mirrors admin) - loadApiKeyProfileTemplate: append a project template to a key Both reuse existing admin biz methods directly — zero new business logic. Following the createLLMAPIKey pattern from #637, the OpenAPI schema stays isolated from admin (own gqlgen module, own minimal APIKey output type, scope checks delegated to ent privacy). Privacy model: - New scopes.APIKeyProjectScopeReadRule restricts API-key-principal reads to the caller's project, gated by required scope. Mounted on both APIKey and APIKeyProfileTemplate Query policies so service accounts can resolve the target key and template. - scopes.UserProjectScopeReadRule now skips when no user is in context (matching UserProjectScopeWriteRule's existing convention) so API key principals fall through to APIKeyProjectScopeReadRule instead of being denied at the user-rule layer. Required scopes for the new mutations: read_api_keys + write_api_keys. Frontend compatibility fix: the OpenAPI updateAPIKeyProfiles resolver coerces nil ModelMappings to []. Admin UI's Zod schema strictly requires a non-null array for this single field (sibling array fields are .nullable()), so OpenAPI clients omitting modelMappings would otherwise produce rows the UI can't render. Tests: - internal/scopes: APIKeyProjectScopeReadRule unit tests - internal/server/gql/openapi (new test file): 9 resolver-level e2e tests covering happy paths, cross-project denial, missing-scope denial, and ModelMappings normalization regression Example client (examples/openapi/): - synced server schema, added genqlient operations for new mutations - regenerated client code with Decimal scalar binding - README updated with scope requirements and capability list * feat(openapi): expose id on APIKey output type Add id: ID! to the OpenAPI APIKey output so callers can chain createLLMAPIKey into updateAPIKeyProfiles / loadApiKeyProfileTemplate without a separate lookup. Previously the only way to obtain a freshly-created LLM key's id was via admin UI / direct DB access, which programmatic OpenAPI clients don't have access to. Mechanical changes: - openapi.graphql: APIKey gains id field - toOpenAPIAPIKey lifted into a new helper.go file so subsequent gqlgen regenerations don't sweep it into the warning block as unknown code - examples/openapi/graphql: schema mirror synced; api_key.graphql operations now select id; genqlient client regenerated * review(openapi): tighten privacy rule + non-null profiles list Address greptile-apps review on PR #1617: P2 — APIKeyProjectScopeReadRule now Denyf (was Skipf) when an API key principal lacks the required scope. Aligns with the explicit-deny contract used by APIKeyScopeQueryRule and APIKeyProjectScopeWriteRule. Skip is reserved for "this rule's principal type doesn't apply"; here the principal IS an API key, just unauthorised. P1 — UpdateAPIKeyProfilesInput.profiles is now non-null ([APIKeyProfileInput!]!). Closes the gap where a client could submit profiles: null and rely on biz validateActiveProfile to reject — failing earlier at the GraphQL parse layer is the safer contract. Diverges from admin's looser type intentionally; admin/openapi schemas are independently versioned. * style: gofumpt-fix trailing newline and field alignment
Summary by CodeRabbit
Release Notes
New Features
/openapi/v1/graphqlwith LLM API key creation supportRefactor
Chores
✏️ Tip: You can customize this high-level summary in your review settings.