-
Notifications
You must be signed in to change notification settings - Fork 0
feat: introspection authentication #65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughRemoved the external RoundTripper factory from manager and targetcluster APIs, moved introspection auth into the nested Gateway config, updated roundtripper internals to accept a base transport and clone requests, and updated related constructors and tests to match the new signatures and wiring. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
if !rt.appCfg.Gateway.ShouldImpersonate { | ||
rt.log.Debug().Str("path", req.URL.Path).Msg("Using bearer token authentication") | ||
|
||
return rt.adminRT.RoundTrip(req) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We used adminRT here and it worked with any token passed since adminRT works without tokens.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
gateway/manager/roundtripper/roundtripper.go (1)
121-144
: Verify discovery request path handling is exhaustive.The updated
isDiscoveryRequest
logic handles path prefixes (/services/.../clusters/...
and/clusters/...
) before checking discovery endpoints. The logic looks correct, but consider the following edge cases:
- Paths with trailing slashes are trimmed (line 121), which is good
- Empty paths return false (lines 122-124), which is correct
- Prefix stripping logic (lines 127-131) correctly removes 5 parts for services prefix and 3 parts for clusters prefix
However, ensure that:
- Virtual workspace paths (
/services/{service}/clusters/{workspace}/api
) are correctly handled- KCP workspace paths (
/clusters/{workspace}/api
) are correctly handled- All discovery endpoints (
/api
,/apis
,/api/v1
,/apis/group
,/apis/group/v1
) are recognizedConsider adding test cases for virtual workspace and KCP workspace discovery paths to ensure the prefix stripping logic works correctly:
// Test cases to add to roundtripper_test.go { name: "virtual_workspace_with_trailing_components", path: "/services/myservice/clusters/workspace1/api/v1", isDiscovery: true, }, { name: "kcp_workspace_with_version", path: "/clusters/workspace1/api/v1", isDiscovery: true, },
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
gateway/manager/manager.go
(1 hunks)gateway/manager/roundtripper/roundtripper.go
(4 hunks)gateway/manager/roundtripper/roundtripper_test.go
(8 hunks)gateway/manager/targetcluster/cluster.go
(5 hunks)gateway/manager/targetcluster/registry.go
(2 hunks)gateway/manager/targetcluster/registry_test.go
(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
gateway/manager/roundtripper/roundtripper_test.go (1)
gateway/manager/roundtripper/roundtripper.go (1)
New
(26-34)
gateway/manager/manager.go (1)
gateway/manager/targetcluster/registry.go (1)
NewClusterRegistry
(34-43)
gateway/manager/targetcluster/cluster.go (2)
common/config/config.go (1)
Config
(3-36)gateway/manager/roundtripper/roundtripper.go (2)
New
(26-34)NewUnauthorizedRoundTripper
(37-39)
gateway/manager/roundtripper/roundtripper.go (1)
common/config/config.go (1)
Config
(3-36)
gateway/manager/targetcluster/registry.go (1)
gateway/manager/targetcluster/cluster.go (2)
TargetCluster
(52-60)NewTargetCluster
(63-96)
gateway/manager/targetcluster/registry_test.go (1)
gateway/manager/targetcluster/registry.go (1)
NewClusterRegistry
(34-43)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: pipe / testSource / test
- GitHub Check: pipe / dockerBuild / docker
- GitHub Check: Analyze (go)
🔇 Additional comments (16)
gateway/manager/targetcluster/registry_test.go (1)
21-21
: LGTM!The test correctly uses the updated
NewClusterRegistry
signature, removing the thirdnil
argument that previously represented theRoundTripperFactory
.gateway/manager/manager.go (1)
25-25
: LGTM!The
NewClusterRegistry
call correctly uses the simplified signature without theRoundTripperFactory
parameter.gateway/manager/roundtripper/roundtripper_test.go (3)
80-80
: LGTM!All test cases correctly updated to use the new
roundtripper.New
signature with the additionalbaseRoundTripper
parameter. PassingmockAdmin
as bothadminRoundTripper
andbaseRoundTripper
is appropriate for these test scenarios.Also applies to: 265-265, 379-379, 454-454, 503-503, 534-534
514-514
: LGTM!Skipping this test is appropriate since it would require mocking
baseRT
, which is now an internal implementation detail.
557-563
: Add assertion verifying admin Authorization header
The test removes the original Authorization header and setsImpersonate-User
but doesn’t confirm the adminRT token is applied. Add an assertion, for example:assert.Equal(t, "Bearer <admin-token>", capturedRequest.Header.Get("Authorization"))or update the test documentation to clarify the expected authentication flow.
gateway/manager/targetcluster/registry.go (3)
27-31
: LGTM!The
ClusterRegistry
struct correctly removes theroundTripperFactory
field, aligning with the refactor that moves RoundTripper wiring into the cluster connection logic.
34-42
: LGTM!The
NewClusterRegistry
signature and initialization correctly simplified by removing theRoundTripperFactory
parameter.
59-59
: LGTM!The
NewTargetCluster
call correctly updated to remove theroundTripperFactory
argument, consistent with the new cluster creation flow.gateway/manager/targetcluster/cluster.go (4)
18-18
: LGTM!The import of the
roundtripper
package is necessary for the new internal RoundTripper construction logic.
63-68
: LGTM!The function signatures correctly updated to remove the
roundTripperFactory
parameter, aligning with the new approach where RoundTripper construction is handled internally.Also applies to: 81-81
117-126
: LGTM!The new
Wrap
implementation correctly constructs the composite RoundTripper:
- Derives the base transport via
unwrapToBaseTransport
- Constructs the custom RoundTripper with
adminRT
,baseRT
, andunauthorizedRT
This centralizes RoundTripper construction and eliminates the need for an external factory.
172-185
: Verify client-go transport wrappers implement WrappedRoundTripper
Ensure every RoundTripper decorator in your client-go version (e.g. BearerAuthRoundTripper, ImpersonatingRoundTripper) defines func WrappedRoundTripper() http.RoundTripper so unwrapToBaseTransport can recurse through the chain. For vendored code, you can run:grep -R "func .*WrappedRoundTripper" -n vendor/k8s.io/client-go/transport
gateway/manager/roundtripper/roundtripper.go (4)
9-9
: LGTM!The import of
utilnet
and addition of thebaseRT
field are necessary for the new request cloning and bearer authentication flow.Also applies to: 20-20
26-33
: LGTM!The
New
constructor correctly updated to accept and store thebaseRoundTripper
parameter, enabling the bearer token authentication path to use the unwrapped base transport.
70-71
: LGTM!Cloning the request before modifying headers is essential to prevent unintended side effects on the original request. This is a good security practice.
73-76
: Verify non-impersonation auth behavior
Non-impersonation now wrapsbaseRT
with the user’s bearer token instead of using admin credentials. Confirm this matches the intended authentication model and add or update documentation/config comments accordingly.
…orm-mesh/kubernetes-graphql-gateway into config/IntrospectionAuthentication
v.SetDefault("openapi-definitions-path", "./bin/definitions") | ||
v.SetDefault("enable-kcp", true) | ||
v.SetDefault("local-development", false) | ||
v.SetDefault("introspection-authentication", false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
moved under the gateway section
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (1)
gateway/manager/roundtripper/roundtripper_test.go (1)
80-80
: Add a dedicated baseRoundTripper mock.Now that
New
takes a real base transport, these scenarios should assert that the non-impersonation path hits the base RT instead of the admin RT. PassingmockAdmin
in both slots masks that distinction, so a regression in wiring would still go green. Please introduce a separatemockBase
(with its own expectations) or reuse an existing mock configured for the bearer path so the tests fail if the wrong transport is used.Also applies to: 265-265, 379-379, 454-454, 503-503, 530-530
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
cmd/root.go
(1 hunks)common/config/config.go
(2 hunks)common/config/config_test.go
(4 hunks)gateway/manager/roundtripper/roundtripper.go
(3 hunks)gateway/manager/roundtripper/roundtripper_test.go
(7 hunks)gateway/manager/targetcluster/registry.go
(3 hunks)tests/gateway_test/suite_test.go
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- gateway/manager/roundtripper/roundtripper.go
🧰 Additional context used
🧬 Code graph analysis (3)
gateway/manager/targetcluster/registry.go (3)
gateway/manager/targetcluster/cluster.go (2)
TargetCluster
(52-60)NewTargetCluster
(63-96)common/config/config.go (1)
Config
(3-36)gateway/manager/targetcluster/graphql.go (1)
IsIntrospectionQuery
(81-97)
gateway/manager/roundtripper/roundtripper_test.go (1)
gateway/manager/roundtripper/roundtripper.go (1)
New
(26-34)
common/config/config_test.go (1)
gateway/schema/schema.go (1)
Gateway
(24-37)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: pipe / testSource / test
- GitHub Check: pipe / dockerBuild / docker
- GitHub Check: Analyze (go)
🔇 Additional comments (7)
cmd/root.go (1)
69-69
: Default relocation looks good.
gateway-introspection-authentication
aligns with the new nested config field.common/config/config_test.go (1)
16-16
: Tests now target the nested introspection flag correctly.Initialization, assignment, and assertions all reference
cfg.Gateway.IntrospectionAuthentication
, matching the struct changes.Also applies to: 40-42, 54-54, 68-68
tests/gateway_test/suite_test.go (1)
127-127
: Suite setup tracks the relocated flag.Propagating
AuthenticateSchemaRequests
viaappCfg.Gateway.IntrospectionAuthentication
keeps the tests aligned with the new config shape.common/config/config.go (1)
4-36
: Config struct reshaping is consistent.Moving
IntrospectionAuthentication
underGateway
withgateway-
mapstructure tags matches the new defaults and test expectations.gateway/manager/targetcluster/registry.go (3)
26-31
: LGTM! ClusterRegistry simplified.The removal of the
roundTripperFactory
field simplifies the struct and aligns with the PR objectives.
34-43
: LGTM! Signature simplified.The removal of the
roundTripperFactory
parameter is consistent with the struct changes and simplifies the constructor.
59-59
: LGTM! Call updated to match new signature.The call to
NewTargetCluster
correctly omits theroundTripperFactory
argument, matching the updated function signature.
func (cr *ClusterRegistry) handleAuth(w http.ResponseWriter, r *http.Request, token string, cluster *TargetCluster) bool { | ||
if !cr.appCfg.LocalDevelopment { | ||
if cr.appCfg.LocalDevelopment { | ||
return true | ||
} | ||
|
||
if cr.appCfg.Gateway.IntrospectionAuthentication { | ||
if token == "" { | ||
http.Error(w, "Authorization header is required", http.StatusUnauthorized) | ||
return false | ||
} | ||
|
||
if cr.appCfg.IntrospectionAuthentication { | ||
if IsIntrospectionQuery(r) { | ||
valid, err := cr.validateToken(r.Context(), token, cluster) | ||
if err != nil { | ||
cr.log.Error().Err(err).Str("cluster", cluster.name).Msg("Error validating token") | ||
http.Error(w, "Token validation failed", http.StatusUnauthorized) | ||
return false | ||
} | ||
if !valid { | ||
cr.log.Debug().Str("cluster", cluster.name).Msg("Invalid token for introspection query") | ||
http.Error(w, "Invalid token", http.StatusUnauthorized) | ||
return false | ||
} | ||
if IsIntrospectionQuery(r) { | ||
valid, err := cr.validateToken(r.Context(), token, cluster) | ||
if err != nil { | ||
cr.log.Error().Err(err).Str("cluster", cluster.name).Msg("Error validating token") | ||
http.Error(w, "Token validation failed", http.StatusUnauthorized) | ||
return false | ||
} | ||
if !valid { | ||
cr.log.Debug().Str("cluster", cluster.name).Msg("Invalid token for introspection query") | ||
http.Error(w, "Invalid token", http.StatusUnauthorized) | ||
return false | ||
} | ||
} | ||
} | ||
|
||
return true | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical logic flaw: token required for all requests when IntrospectionAuthentication is enabled.
The current implementation at lines 199-202 rejects all requests when IntrospectionAuthentication
is true
and the token is empty, regardless of whether the request is an introspection query. This contradicts the expected behavior where authentication should only be required for introspection queries when IntrospectionAuthentication
is enabled.
Current flow:
- If
IntrospectionAuthentication=true
andtoken=""
→ reject all requests - If
IntrospectionAuthentication=true
andtoken!=""
andIsIntrospectionQuery
→ validate token - Otherwise → allow
Expected flow:
- If
IntrospectionAuthentication=true
ANDIsIntrospectionQuery
→ require and validate token - Otherwise → allow
Non-introspection queries should not require a token when IntrospectionAuthentication
is enabled.
Apply this diff to fix the authentication logic:
func (cr *ClusterRegistry) handleAuth(w http.ResponseWriter, r *http.Request, token string, cluster *TargetCluster) bool {
if cr.appCfg.LocalDevelopment {
return true
}
- if cr.appCfg.Gateway.IntrospectionAuthentication {
+ if cr.appCfg.Gateway.IntrospectionAuthentication && IsIntrospectionQuery(r) {
if token == "" {
http.Error(w, "Authorization header is required", http.StatusUnauthorized)
return false
}
- if IsIntrospectionQuery(r) {
- valid, err := cr.validateToken(r.Context(), token, cluster)
- if err != nil {
- cr.log.Error().Err(err).Str("cluster", cluster.name).Msg("Error validating token")
- http.Error(w, "Token validation failed", http.StatusUnauthorized)
- return false
- }
- if !valid {
- cr.log.Debug().Str("cluster", cluster.name).Msg("Invalid token for introspection query")
- http.Error(w, "Invalid token", http.StatusUnauthorized)
- return false
- }
+ valid, err := cr.validateToken(r.Context(), token, cluster)
+ if err != nil {
+ cr.log.Error().Err(err).Str("cluster", cluster.name).Msg("Error validating token")
+ http.Error(w, "Token validation failed", http.StatusUnauthorized)
+ return false
+ }
+ if !valid {
+ cr.log.Debug().Str("cluster", cluster.name).Msg("Invalid token for introspection query")
+ http.Error(w, "Invalid token", http.StatusUnauthorized)
+ return false
}
}
return true
}
📝 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.
func (cr *ClusterRegistry) handleAuth(w http.ResponseWriter, r *http.Request, token string, cluster *TargetCluster) bool { | |
if !cr.appCfg.LocalDevelopment { | |
if cr.appCfg.LocalDevelopment { | |
return true | |
} | |
if cr.appCfg.Gateway.IntrospectionAuthentication { | |
if token == "" { | |
http.Error(w, "Authorization header is required", http.StatusUnauthorized) | |
return false | |
} | |
if cr.appCfg.IntrospectionAuthentication { | |
if IsIntrospectionQuery(r) { | |
valid, err := cr.validateToken(r.Context(), token, cluster) | |
if err != nil { | |
cr.log.Error().Err(err).Str("cluster", cluster.name).Msg("Error validating token") | |
http.Error(w, "Token validation failed", http.StatusUnauthorized) | |
return false | |
} | |
if !valid { | |
cr.log.Debug().Str("cluster", cluster.name).Msg("Invalid token for introspection query") | |
http.Error(w, "Invalid token", http.StatusUnauthorized) | |
return false | |
} | |
if IsIntrospectionQuery(r) { | |
valid, err := cr.validateToken(r.Context(), token, cluster) | |
if err != nil { | |
cr.log.Error().Err(err).Str("cluster", cluster.name).Msg("Error validating token") | |
http.Error(w, "Token validation failed", http.StatusUnauthorized) | |
return false | |
} | |
if !valid { | |
cr.log.Debug().Str("cluster", cluster.name).Msg("Invalid token for introspection query") | |
http.Error(w, "Invalid token", http.StatusUnauthorized) | |
return false | |
} | |
} | |
} | |
return true | |
} | |
func (cr *ClusterRegistry) handleAuth(w http.ResponseWriter, r *http.Request, token string, cluster *TargetCluster) bool { | |
if cr.appCfg.LocalDevelopment { | |
return true | |
} | |
if cr.appCfg.Gateway.IntrospectionAuthentication && IsIntrospectionQuery(r) { | |
if token == "" { | |
http.Error(w, "Authorization header is required", http.StatusUnauthorized) | |
return false | |
} | |
valid, err := cr.validateToken(r.Context(), token, cluster) | |
if err != nil { | |
cr.log.Error().Err(err).Str("cluster", cluster.name).Msg("Error validating token") | |
http.Error(w, "Token validation failed", http.StatusUnauthorized) | |
return false | |
} | |
if !valid { | |
cr.log.Debug().Str("cluster", cluster.name).Msg("Invalid token for introspection query") | |
http.Error(w, "Invalid token", http.StatusUnauthorized) | |
return false | |
} | |
} | |
return true | |
} |
🤖 Prompt for AI Agents
In gateway/manager/targetcluster/registry.go around lines 193 to 220, the
current logic incorrectly requires a token for all requests whenever
IntrospectionAuthentication is enabled; change the flow so token presence and
validation are only enforced for introspection queries: first check if
IntrospectionAuthentication is true AND IsIntrospectionQuery(r) is true, and
only then require token (return 401 if empty) and call cr.validateToken, logging
and returning on errors or invalid tokens; for all other cases
(non-introspection queries or when IntrospectionAuthentication is false) return
true.
Changes
Summary by CodeRabbit
Refactor
CLI
Tests