Skip to content

Commit 93d5de8

Browse files
committed
feat(identity): add HTTP filter to bypass field selector validation
Add IdentityFieldSelectorPassthrough filter that intercepts requests to UserIdentities and Sessions APIs before the API server validates field selectors. For virtual/proxy resources that delegate to external backends, field selector validation must happen at the backend, not the API server. This filter extracts the raw field selector from the query string and stores it in RequestInfo, bypassing Kubernetes API server validation. The filter is registered in the handler chain before WithRequestInfo, ensuring field selectors like status.userUID pass through to auth-provider-zitadel for validation and authorization.
1 parent 93df6d1 commit 93d5de8

3 files changed

Lines changed: 71 additions & 10 deletions

File tree

cmd/milo/apiserver/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ func DefaultBuildHandlerChain(apiHandler http.Handler, c *server.Config, loopbac
403403
handler = datumfilters.UserContactGroupMembershipListConstraintDecorator(handler)
404404
handler = datumfilters.UserContactGroupMembershipRemovalListConstraintDecorator(handler)
405405
handler = datumfilters.ContactGroupVisibilityWithoutPrivateDecorator(handler)
406+
handler = datumfilters.IdentityFieldSelectorPassthrough(handler)
406407
handler = genericapifilters.WithRequestInfo(handler, c.RequestInfoResolver)
407408
handler = genericapifilters.WithRequestReceivedTimestamp(handler)
408409
// handler = genericapifilters.WithMuxAndDiscoveryComplete(handler, c.lifecycleSignals.MuxAndDiscoveryComplete.Signaled())

internal/apiserver/storage/identity/storageprovider.go

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,17 @@ func (p StorageProvider) NewRESTStorage(
2626
_ serverstorage.APIResourceConfigSource,
2727
_ generic.RESTOptionsGetter,
2828
) (genericapiserver.APIGroupInfo, error) {
29-
// Create a custom scheme that includes identity types but NO field label conversion functions
30-
// This allows field selectors to pass through without validation at the API server level
31-
customScheme := runtime.NewScheme()
32-
identityv1alpha1.AddToScheme(customScheme)
33-
metav1.AddToGroupVersion(customScheme, identityv1alpha1.SchemeGroupVersion)
34-
35-
// Use the custom scheme for both the main scheme and parameter codec
36-
// This ensures field selectors are not validated by the API server framework
37-
parameterCodec := runtime.NewParameterCodec(customScheme)
29+
// For virtual/proxy resources, we use the legacy scheme but with a custom ParameterCodec
30+
// that includes identity types without field label conversion functions.
31+
// This allows field selectors to pass through to the backend without API server validation.
32+
parameterScheme := runtime.NewScheme()
33+
identityv1alpha1.AddToScheme(parameterScheme)
34+
metav1.AddToGroupVersion(parameterScheme, identityv1alpha1.SchemeGroupVersion)
35+
parameterCodec := runtime.NewParameterCodec(parameterScheme)
3836

3937
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(
4038
identityv1alpha1.SchemeGroupVersion.Group,
41-
customScheme,
39+
legacyscheme.Scheme,
4240
parameterCodec,
4341
legacyscheme.Codecs,
4442
)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package filters
2+
3+
import (
4+
"net/http"
5+
"net/url"
6+
7+
identityv1alpha1 "go.miloapis.com/milo/pkg/apis/identity/v1alpha1"
8+
"k8s.io/apiserver/pkg/endpoints/request"
9+
)
10+
11+
// IdentityFieldSelectorPassthrough is a filter that allows field selectors to pass through
12+
// to the identity API backend without validation by the API server framework.
13+
//
14+
// This is necessary because UserIdentity and Session are virtual/proxy resources that
15+
// delegate to an external backend (auth-provider-zitadel). The backend handles field
16+
// selector validation and authorization, not the Milo API server.
17+
//
18+
// Without this filter, the API server would validate field selectors against registered
19+
// field label conversion functions and reject unknown selectors like status.userUID.
20+
func IdentityFieldSelectorPassthrough(handler http.Handler) http.Handler {
21+
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
22+
info, ok := request.RequestInfoFrom(req.Context())
23+
if !ok {
24+
handler.ServeHTTP(w, req)
25+
return
26+
}
27+
28+
// Only intercept identity API list requests
29+
if info.APIGroup != identityv1alpha1.SchemeGroupVersion.Group || info.Verb != "list" {
30+
handler.ServeHTTP(w, req)
31+
return
32+
}
33+
34+
// Only handle useridentities and sessions resources
35+
if info.Resource != "useridentities" && info.Resource != "sessions" {
36+
handler.ServeHTTP(w, req)
37+
return
38+
}
39+
40+
// Extract field selector from query string before API server validation
41+
query, err := url.ParseQuery(req.URL.RawQuery)
42+
if err != nil {
43+
handler.ServeHTTP(w, req)
44+
return
45+
}
46+
47+
fieldSelector := query.Get("fieldSelector")
48+
if fieldSelector == "" {
49+
handler.ServeHTTP(w, req)
50+
return
51+
}
52+
53+
// Store the raw field selector in RequestInfo so it bypasses validation
54+
// The API server will use this value instead of parsing/validating it
55+
info.FieldSelector = fieldSelector
56+
57+
// Update the request context with modified RequestInfo
58+
req = req.WithContext(request.WithRequestInfo(req.Context(), info))
59+
60+
handler.ServeHTTP(w, req)
61+
})
62+
}

0 commit comments

Comments
 (0)