From dd18f7dc52e4d4599304cf114c838ae4ea4f92e0 Mon Sep 17 00:00:00 2001 From: OscarLlamas6 Date: Fri, 20 Feb 2026 21:36:40 -0600 Subject: [PATCH 01/11] feat(identity): enable field selector support for staff users Pass field selectors from REST layer to backend provider to enable staff users to query other users' identities and sessions. This is required for the staff portal to view user identity provider links and active sessions for support purposes. Changes: - Pass field selector from ListOptions to backend in useridentities - Pass field selector from ListOptions to backend in sessions - Update README documentation with field selector usage and security model - Add test documentation for field selector authorization scenarios Authorization model: 1. Milo RBAC: PolicyBinding grants access to identity resources 2. Backend authorization: Provider validates group membership - Regular users: Can only see their own data (no field selector) - Staff users: Can use field selectors to query other users - Groups: staff-users, fraud-manager (configured in Zitadel) Security: - Backward compatible: Regular users unaffected - Defense in depth: Two layers of authorization - Audit logging: All requests logged with user context - Explicit deny: Non-staff users cannot use field selectors This changes the previous 'self-scoped' design where field selectors were intentionally ignored. The new behavior maintains self-scoped access for regular users while enabling cross-user queries for staff. Related: datum-cloud/staff-portal (staff user authorization) Related: datum-cloud/auth-provider-zitadel (backend authorization) --- .../apiserver/identity/sessions/README.md | 27 ++++ internal/apiserver/identity/sessions/rest.go | 5 +- .../identity/useridentities/README.md | 27 ++++ .../apiserver/identity/useridentities/rest.go | 5 +- .../field-selector-authorization/README.md | 121 ++++++++++++++++++ 5 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 test/identity/field-selector-authorization/README.md diff --git a/internal/apiserver/identity/sessions/README.md b/internal/apiserver/identity/sessions/README.md index 882dca02..ff63b723 100644 --- a/internal/apiserver/identity/sessions/README.md +++ b/internal/apiserver/identity/sessions/README.md @@ -38,3 +38,30 @@ Naming & structure - internal/apiserver/identity/sessions/rest.go — REST storage - internal/apiserver/identity/sessions/dynamic.go — provider implementation +Field Selector Support for Staff Users +The Sessions API supports field selectors to enable staff users to query +other users' active sessions. This is required for support and administrative +purposes in the staff portal. + +Supported field selectors: +- status.userUID= — Query sessions for a specific user + +Authorization: +- Regular users: Can only list their own sessions (field selectors are ignored) +- Staff users: Can use field selectors to query other users' sessions + - Must be members of privileged groups in the identity provider (e.g., staff-users, fraud-manager) + - Authorization is enforced by the backend provider (auth-provider-zitadel) + - Requires appropriate PolicyBinding in Milo for RBAC + +Example usage: + # Regular user (sees only their own sessions) + kubectl get sessions + + # Staff user (can query specific user's sessions) + kubectl get sessions --field-selector=status.userUID= + +Security model: +1. Milo RBAC: PolicyBinding grants access to sessions resource +2. Backend authorization: Provider validates group membership for field selector usage +3. Audit logging: All requests are logged with user context for compliance + diff --git a/internal/apiserver/identity/sessions/rest.go b/internal/apiserver/identity/sessions/rest.go index ce1dc8c4..30f652a3 100644 --- a/internal/apiserver/identity/sessions/rest.go +++ b/internal/apiserver/identity/sessions/rest.go @@ -49,8 +49,11 @@ func (r *REST) List(ctx context.Context, opts *metainternalversion.ListOptions) groups = u.GetGroups() } logger.V(4).Info("Listing sessions", "username", username, "uid", uid, "groups", groups) - // ignore selectors; self-scoped list delegated to provider + // Pass field selector to backend provider for staff user queries lo := metav1.ListOptions{} + if opts != nil && opts.FieldSelector != nil { + lo.FieldSelector = opts.FieldSelector.String() + } res, err := r.backend.ListSessions(ctx, u, &lo) if err != nil { logger.Error(err, "List sessions failed") diff --git a/internal/apiserver/identity/useridentities/README.md b/internal/apiserver/identity/useridentities/README.md index aa7a065e..9a22d0e0 100644 --- a/internal/apiserver/identity/useridentities/README.md +++ b/internal/apiserver/identity/useridentities/README.md @@ -38,6 +38,33 @@ Naming & structure - internal/apiserver/identity/useridentities/rest.go — REST storage - internal/apiserver/identity/useridentities/dynamic.go — provider implementation +Field Selector Support for Staff Users +The UserIdentities API supports field selectors to enable staff users to query +other users' identity provider links. This is required for support and administrative +purposes in the staff portal. + +Supported field selectors: +- status.userUID= — Query identities for a specific user + +Authorization: +- Regular users: Can only list their own identities (field selectors are ignored) +- Staff users: Can use field selectors to query other users' identities + - Must be members of privileged groups in the identity provider (e.g., staff-users, fraud-manager) + - Authorization is enforced by the backend provider (auth-provider-zitadel) + - Requires appropriate PolicyBinding in Milo for RBAC + +Example usage: + # Regular user (sees only their own identities) + kubectl get useridentities + + # Staff user (can query specific user's identities) + kubectl get useridentities --field-selector=status.userUID= + +Security model: +1. Milo RBAC: PolicyBinding grants access to useridentities resource +2. Backend authorization: Provider validates group membership for field selector usage +3. Audit logging: All requests are logged with user context for compliance + Read-only resource and admission webhook for deletion Unlike sessions, useridentities is a read-only resource. Users cannot create, update, or delete user identities through the Kubernetes API. Identity linking diff --git a/internal/apiserver/identity/useridentities/rest.go b/internal/apiserver/identity/useridentities/rest.go index d1631e4c..50153255 100644 --- a/internal/apiserver/identity/useridentities/rest.go +++ b/internal/apiserver/identity/useridentities/rest.go @@ -47,8 +47,11 @@ func (r *REST) List(ctx context.Context, opts *metainternalversion.ListOptions) groups = u.GetGroups() } logger.V(4).Info("Listing user identities", "username", username, "uid", uid, "groups", groups) - // ignore selectors; self-scoped list delegated to provider + // Pass field selector to backend provider for staff user queries lo := metav1.ListOptions{} + if opts != nil && opts.FieldSelector != nil { + lo.FieldSelector = opts.FieldSelector.String() + } res, err := r.backend.ListUserIdentities(ctx, u, &lo) if err != nil { logger.Error(err, "List user identities failed") diff --git a/test/identity/field-selector-authorization/README.md b/test/identity/field-selector-authorization/README.md new file mode 100644 index 00000000..15c05d36 --- /dev/null +++ b/test/identity/field-selector-authorization/README.md @@ -0,0 +1,121 @@ +# Field Selector Authorization Tests + +This test suite validates that field selector authorization works correctly for +UserIdentities and Sessions resources in the Identity API. + +## Test Scenarios + +### 1. Regular User - Self-Scoped Access (Default Behavior) +**Given:** A regular user without staff privileges +**When:** User lists useridentities without field selector +**Then:** User sees only their own identity provider links + +**When:** User attempts to use field selector for another user +**Then:** Request is rejected with 403 Forbidden error + +### 2. Staff User - Cross-User Access with Field Selector +**Given:** A user in the staff-users group +**When:** User lists useridentities with field selector for another user +**Then:** User successfully retrieves the target user's identity provider links + +### 3. Field Selector Validation +**Given:** Any authenticated user +**When:** User provides invalid field selector (e.g., metadata.name) +**Then:** Request is rejected with appropriate error message + +## Authorization Model + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 1. Milo RBAC Check │ +│ - PolicyBinding grants access to useridentities resource │ +│ - Required for both regular and staff users │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 2. Field Selector Passed to Backend │ +│ - Milo passes field selector to auth-provider-zitadel │ +│ - No validation at Milo layer │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 3. Backend Authorization (auth-provider-zitadel) │ +│ - If no field selector: use authenticated user's UID │ +│ - If field selector with different UID: │ +│ → Check user groups (staff-users, fraud-manager) │ +│ → Allow if staff, deny if not │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Required Setup + +### PolicyBindings +```yaml +# Grant staff-users access to useridentities +apiVersion: iam.miloapis.com/v1alpha1 +kind: PolicyBinding +metadata: + name: staff-useridentities-viewer + namespace: milo-system +spec: + resourceSelector: + resourceKind: + apiGroup: identity.miloapis.com + kind: UserIdentity + roleRef: + name: identity-user-session-viewer + namespace: milo-system + subjects: + - kind: Group + name: staff-users + namespace: milo-system + uid: +``` + +### Zitadel Configuration +- Create group: `staff-users` +- Assign users to group via project roles +- Configure JWT claims to include groups + +## Manual Testing + +### Test 1: Regular User Cannot Use Field Selector +```bash +# As regular user +kubectl get useridentities --field-selector=status.userUID= + +# Expected: 403 Forbidden +# Error: "only staff users can query other users' identities" +``` + +### Test 2: Staff User Can Use Field Selector +```bash +# As staff user (member of staff-users group) +kubectl get useridentities --field-selector=status.userUID= + +# Expected: 200 OK +# Response: List of target user's identity provider links +``` + +### Test 3: Regular User Can See Own Data +```bash +# As regular user +kubectl get useridentities + +# Expected: 200 OK +# Response: List of own identity provider links +``` + +## Security Considerations + +1. **Defense in Depth**: Two layers of authorization (Milo RBAC + Backend groups) +2. **Audit Logging**: All requests logged with user context +3. **Principle of Least Privilege**: Regular users cannot access others' data +4. **Explicit Deny**: Field selector attempts by non-staff users are explicitly denied + +## Future Enhancements + +- [ ] Add automated E2E tests using Chainsaw +- [ ] Add rate limiting for staff user queries +- [ ] Add metrics for field selector usage +- [ ] Consider adding SubjectAccessReview checks in Milo layer From bdb3aae305b99bbdddf9248fa9479294b3df365a Mon Sep 17 00:00:00 2001 From: OscarLlamas6 Date: Fri, 20 Feb 2026 21:40:02 -0600 Subject: [PATCH 02/11] chore: tests adjustments --- .../field-selector-authorization/README.md | 121 ------------------ 1 file changed, 121 deletions(-) delete mode 100644 test/identity/field-selector-authorization/README.md diff --git a/test/identity/field-selector-authorization/README.md b/test/identity/field-selector-authorization/README.md deleted file mode 100644 index 15c05d36..00000000 --- a/test/identity/field-selector-authorization/README.md +++ /dev/null @@ -1,121 +0,0 @@ -# Field Selector Authorization Tests - -This test suite validates that field selector authorization works correctly for -UserIdentities and Sessions resources in the Identity API. - -## Test Scenarios - -### 1. Regular User - Self-Scoped Access (Default Behavior) -**Given:** A regular user without staff privileges -**When:** User lists useridentities without field selector -**Then:** User sees only their own identity provider links - -**When:** User attempts to use field selector for another user -**Then:** Request is rejected with 403 Forbidden error - -### 2. Staff User - Cross-User Access with Field Selector -**Given:** A user in the staff-users group -**When:** User lists useridentities with field selector for another user -**Then:** User successfully retrieves the target user's identity provider links - -### 3. Field Selector Validation -**Given:** Any authenticated user -**When:** User provides invalid field selector (e.g., metadata.name) -**Then:** Request is rejected with appropriate error message - -## Authorization Model - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 1. Milo RBAC Check │ -│ - PolicyBinding grants access to useridentities resource │ -│ - Required for both regular and staff users │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 2. Field Selector Passed to Backend │ -│ - Milo passes field selector to auth-provider-zitadel │ -│ - No validation at Milo layer │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 3. Backend Authorization (auth-provider-zitadel) │ -│ - If no field selector: use authenticated user's UID │ -│ - If field selector with different UID: │ -│ → Check user groups (staff-users, fraud-manager) │ -│ → Allow if staff, deny if not │ -└─────────────────────────────────────────────────────────────┘ -``` - -## Required Setup - -### PolicyBindings -```yaml -# Grant staff-users access to useridentities -apiVersion: iam.miloapis.com/v1alpha1 -kind: PolicyBinding -metadata: - name: staff-useridentities-viewer - namespace: milo-system -spec: - resourceSelector: - resourceKind: - apiGroup: identity.miloapis.com - kind: UserIdentity - roleRef: - name: identity-user-session-viewer - namespace: milo-system - subjects: - - kind: Group - name: staff-users - namespace: milo-system - uid: -``` - -### Zitadel Configuration -- Create group: `staff-users` -- Assign users to group via project roles -- Configure JWT claims to include groups - -## Manual Testing - -### Test 1: Regular User Cannot Use Field Selector -```bash -# As regular user -kubectl get useridentities --field-selector=status.userUID= - -# Expected: 403 Forbidden -# Error: "only staff users can query other users' identities" -``` - -### Test 2: Staff User Can Use Field Selector -```bash -# As staff user (member of staff-users group) -kubectl get useridentities --field-selector=status.userUID= - -# Expected: 200 OK -# Response: List of target user's identity provider links -``` - -### Test 3: Regular User Can See Own Data -```bash -# As regular user -kubectl get useridentities - -# Expected: 200 OK -# Response: List of own identity provider links -``` - -## Security Considerations - -1. **Defense in Depth**: Two layers of authorization (Milo RBAC + Backend groups) -2. **Audit Logging**: All requests logged with user context -3. **Principle of Least Privilege**: Regular users cannot access others' data -4. **Explicit Deny**: Field selector attempts by non-staff users are explicitly denied - -## Future Enhancements - -- [ ] Add automated E2E tests using Chainsaw -- [ ] Add rate limiting for staff user queries -- [ ] Add metrics for field selector usage -- [ ] Consider adding SubjectAccessReview checks in Milo layer From a8be41d152bc2de1681a1c4e9372d7d47faf8634 Mon Sep 17 00:00:00 2001 From: OscarLlamas6 Date: Tue, 24 Feb 2026 15:37:59 -0600 Subject: [PATCH 03/11] feat(identity): register status.userUID as valid field selector Add field label conversion functions for UserIdentity and Session resources to enable status.userUID as a supported field selector. This allows the API server to validate field selector queries before passing them to the backend provider. Changes: - Add UserIdentityFieldLabelConversionFunc for UserIdentity resources - Add SessionFieldLabelConversionFunc for Session resources - Register both conversion functions in addKnownTypes - Support field selectors: metadata.name, metadata.namespace, status.userUID This fixes the error: "status.userUID" is not a known field selector: only "metadata.name", "metadata.namespace" Now staff users can successfully query: kubectl get useridentities --field-selector=status.userUID= Related: PR feedback on field selector validation --- pkg/apis/identity/v1alpha1/register.go | 57 ++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/pkg/apis/identity/v1alpha1/register.go b/pkg/apis/identity/v1alpha1/register.go index 66560647..94b5c08e 100644 --- a/pkg/apis/identity/v1alpha1/register.go +++ b/pkg/apis/identity/v1alpha1/register.go @@ -1,6 +1,8 @@ package v1alpha1 import ( + "fmt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -35,5 +37,60 @@ func addKnownTypes(scheme *runtime.Scheme) error { &UserIdentityList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + + // Register field label conversions for UserIdentity + // This enables field selectors like status.userUID= for staff users + userIdentityGVK := SchemeGroupVersion.WithKind("UserIdentity") + if err := scheme.AddFieldLabelConversionFunc(userIdentityGVK, + UserIdentityFieldLabelConversionFunc); err != nil { + return err + } + + // Register field label conversions for Session + // This enables field selectors like status.userUID= for staff users + sessionGVK := SchemeGroupVersion.WithKind("Session") + if err := scheme.AddFieldLabelConversionFunc(sessionGVK, + SessionFieldLabelConversionFunc); err != nil { + return err + } + return nil } + +// UserIdentityFieldLabelConversionFunc converts field selectors for UserIdentity resources. +// This allows staff users to filter user identities by fields beyond the default metadata.name. +func UserIdentityFieldLabelConversionFunc(label, value string) (string, string, error) { + switch label { + // Metadata fields (default Kubernetes fields) + case "metadata.name", + "metadata.namespace": + return label, value, nil + + // Status fields (custom field selector for staff users) + case "status.userUID": + return label, value, nil + + default: + return "", "", fmt.Errorf("%q is not a known field selector: only %q are supported", + label, []string{"metadata.name", "metadata.namespace", "status.userUID"}) + } +} + +// SessionFieldLabelConversionFunc converts field selectors for Session resources. +// This allows staff users to filter sessions by fields beyond the default metadata.name. +func SessionFieldLabelConversionFunc(label, value string) (string, string, error) { + switch label { + // Metadata fields (default Kubernetes fields) + case "metadata.name", + "metadata.namespace": + return label, value, nil + + // Status fields (custom field selector for staff users) + case "status.userUID": + return label, value, nil + + default: + return "", "", fmt.Errorf("%q is not a known field selector: only %q are supported", + label, []string{"metadata.name", "metadata.namespace", "status.userUID"}) + } +} From 0b3cd56938a35626de9c5f6a6c8d3308a10c7b7a Mon Sep 17 00:00:00 2001 From: OscarLlamas6 Date: Wed, 25 Feb 2026 21:07:51 -0600 Subject: [PATCH 04/11] fix(identity): use legacyscheme for ParameterCodec Use runtime.NewParameterCodec(legacyscheme.Scheme) instead of metav1.ParameterCodec. The legacyscheme already has identity scheme installed with field label conversions. Add unit tests for field selector validation. --- .../storage/identity/storageprovider.go | 9 +- pkg/apis/identity/v1alpha1/register_test.go | 189 ++++++++++++++++++ 2 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 pkg/apis/identity/v1alpha1/register_test.go diff --git a/internal/apiserver/storage/identity/storageprovider.go b/internal/apiserver/storage/identity/storageprovider.go index 286b97c4..daa94e73 100644 --- a/internal/apiserver/storage/identity/storageprovider.go +++ b/internal/apiserver/storage/identity/storageprovider.go @@ -1,7 +1,7 @@ package identity import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" generic "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" @@ -25,10 +25,15 @@ func (p StorageProvider) NewRESTStorage( _ serverstorage.APIResourceConfigSource, _ generic.RESTOptionsGetter, ) (genericapiserver.APIGroupInfo, error) { + // Create ParameterCodec using legacyscheme.Scheme which has identity scheme installed + // with field label conversion functions. This is critical for field selector validation. + // The identity scheme is installed in cmd/milo/apiserver/config.go via identityapi.Install(legacyscheme.Scheme) + parameterCodec := runtime.NewParameterCodec(legacyscheme.Scheme) + apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo( identityv1alpha1.SchemeGroupVersion.Group, legacyscheme.Scheme, - metav1.ParameterCodec, + parameterCodec, legacyscheme.Codecs, ) diff --git a/pkg/apis/identity/v1alpha1/register_test.go b/pkg/apis/identity/v1alpha1/register_test.go new file mode 100644 index 00000000..ed1d4f9a --- /dev/null +++ b/pkg/apis/identity/v1alpha1/register_test.go @@ -0,0 +1,189 @@ +package v1alpha1 + +import ( + "testing" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func TestUserIdentityFieldLabelConversion(t *testing.T) { + tests := []struct { + name string + label string + value string + wantLabel string + wantValue string + wantErr bool + errContains string + }{ + { + name: "metadata.name is valid", + label: "metadata.name", + value: "test-identity", + wantLabel: "metadata.name", + wantValue: "test-identity", + wantErr: false, + }, + { + name: "metadata.namespace is valid", + label: "metadata.namespace", + value: "default", + wantLabel: "metadata.namespace", + wantValue: "default", + wantErr: false, + }, + { + name: "status.userUID is valid", + label: "status.userUID", + value: "340583683847098197", + wantLabel: "status.userUID", + wantValue: "340583683847098197", + wantErr: false, + }, + { + name: "invalid field selector", + label: "status.invalidField", + value: "test", + wantErr: true, + errContains: "not a known field selector", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotLabel, gotValue, err := UserIdentityFieldLabelConversionFunc(tt.label, tt.value) + + if tt.wantErr { + if err == nil { + t.Errorf("expected error but got none") + return + } + if tt.errContains != "" && !contains(err.Error(), tt.errContains) { + t.Errorf("error %q does not contain %q", err.Error(), tt.errContains) + } + return + } + + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + if gotLabel != tt.wantLabel { + t.Errorf("label = %q, want %q", gotLabel, tt.wantLabel) + } + + if gotValue != tt.wantValue { + t.Errorf("value = %q, want %q", gotValue, tt.wantValue) + } + }) + } +} + +func TestSessionFieldLabelConversion(t *testing.T) { + tests := []struct { + name string + label string + value string + wantLabel string + wantValue string + wantErr bool + errContains string + }{ + { + name: "metadata.name is valid", + label: "metadata.name", + value: "test-session", + wantLabel: "metadata.name", + wantValue: "test-session", + wantErr: false, + }, + { + name: "status.userUID is valid", + label: "status.userUID", + value: "340583683847098197", + wantLabel: "status.userUID", + wantValue: "340583683847098197", + wantErr: false, + }, + { + name: "invalid field selector", + label: "spec.invalid", + value: "test", + wantErr: true, + errContains: "not a known field selector", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotLabel, gotValue, err := SessionFieldLabelConversionFunc(tt.label, tt.value) + + if tt.wantErr { + if err == nil { + t.Errorf("expected error but got none") + return + } + if tt.errContains != "" && !contains(err.Error(), tt.errContains) { + t.Errorf("error %q does not contain %q", err.Error(), tt.errContains) + } + return + } + + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + if gotLabel != tt.wantLabel { + t.Errorf("label = %q, want %q", gotLabel, tt.wantLabel) + } + + if gotValue != tt.wantValue { + t.Errorf("value = %q, want %q", gotValue, tt.wantValue) + } + }) + } +} + +func TestFieldLabelConversionRegistration(t *testing.T) { + scheme := runtime.NewScheme() + err := addKnownTypes(scheme) + if err != nil { + t.Fatalf("addKnownTypes failed: %v", err) + } + + // Test that UserIdentity field label conversion is registered + userIdentityGVK := schema.GroupVersion{Group: "identity.miloapis.com", Version: "v1alpha1"}.WithKind("UserIdentity") + + // Try to convert a valid field selector + converter := scheme.Converter() + if converter == nil { + t.Fatal("scheme converter is nil") + } + + // Test that Session field label conversion is registered + sessionGVK := schema.GroupVersion{Group: "identity.miloapis.com", Version: "v1alpha1"}.WithKind("Session") + + // Verify the GVKs are registered + if !scheme.Recognizes(userIdentityGVK) { + t.Errorf("UserIdentity GVK not recognized by scheme") + } + if !scheme.Recognizes(sessionGVK) { + t.Errorf("Session GVK not recognized by scheme") + } +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && containsSubstring(s, substr)) +} + +func containsSubstring(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} From 40fd512c014b4cd7492f3660918d3eb68b727118 Mon Sep 17 00:00:00 2001 From: OscarLlamas6 Date: Wed, 25 Feb 2026 22:31:47 -0600 Subject: [PATCH 05/11] debug: add logging to field label conversion functions --- pkg/apis/identity/v1alpha1/register.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pkg/apis/identity/v1alpha1/register.go b/pkg/apis/identity/v1alpha1/register.go index 94b5c08e..ba24b551 100644 --- a/pkg/apis/identity/v1alpha1/register.go +++ b/pkg/apis/identity/v1alpha1/register.go @@ -6,6 +6,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/klog/v2" ) // SchemeGroupVersion is group version used to register these objects @@ -60,37 +61,47 @@ func addKnownTypes(scheme *runtime.Scheme) error { // UserIdentityFieldLabelConversionFunc converts field selectors for UserIdentity resources. // This allows staff users to filter user identities by fields beyond the default metadata.name. func UserIdentityFieldLabelConversionFunc(label, value string) (string, string, error) { + klog.V(2).InfoS("UserIdentity field label conversion called", "label", label, "value", value) switch label { // Metadata fields (default Kubernetes fields) case "metadata.name", "metadata.namespace": + klog.V(2).InfoS("UserIdentity field label conversion: accepted metadata field", "label", label) return label, value, nil // Status fields (custom field selector for staff users) case "status.userUID": + klog.V(2).InfoS("UserIdentity field label conversion: accepted status.userUID", "label", label, "value", value) return label, value, nil default: - return "", "", fmt.Errorf("%q is not a known field selector: only %q are supported", + err := fmt.Errorf("%q is not a known field selector: only %q are supported", label, []string{"metadata.name", "metadata.namespace", "status.userUID"}) + klog.V(2).InfoS("UserIdentity field label conversion: rejected field", "label", label, "error", err) + return "", "", err } } // SessionFieldLabelConversionFunc converts field selectors for Session resources. // This allows staff users to filter sessions by fields beyond the default metadata.name. func SessionFieldLabelConversionFunc(label, value string) (string, string, error) { + klog.V(2).InfoS("Session field label conversion called", "label", label, "value", value) switch label { // Metadata fields (default Kubernetes fields) case "metadata.name", "metadata.namespace": + klog.V(2).InfoS("Session field label conversion: accepted metadata field", "label", label) return label, value, nil // Status fields (custom field selector for staff users) case "status.userUID": + klog.V(2).InfoS("Session field label conversion: accepted status.userUID", "label", label, "value", value) return label, value, nil default: - return "", "", fmt.Errorf("%q is not a known field selector: only %q are supported", + err := fmt.Errorf("%q is not a known field selector: only %q are supported", label, []string{"metadata.name", "metadata.namespace", "status.userUID"}) + klog.V(2).InfoS("Session field label conversion: rejected field", "label", label, "error", err) + return "", "", err } } From 47e67bcbae1a24c80786f5e789e7e25a9dc4dcb8 Mon Sep 17 00:00:00 2001 From: OscarLlamas6 Date: Wed, 25 Feb 2026 22:53:15 -0600 Subject: [PATCH 06/11] fix(identity): use dedicated Scheme for field selector support Create dedicated Identity Scheme following Activity API pattern. This ensures field label conversion functions are properly registered and used by the API server for field selector validation. - Add pkg/apis/identity/install package - Create dedicated Scheme in storageprovider with identity types only - Use metav1.ParameterCodec with dedicated Scheme --- .../storage/identity/storageprovider.go | 41 +++++++++++++++---- pkg/apis/identity/install/install.go | 14 +++++++ 2 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 pkg/apis/identity/install/install.go diff --git a/internal/apiserver/storage/identity/storageprovider.go b/internal/apiserver/storage/identity/storageprovider.go index daa94e73..103c48fe 100644 --- a/internal/apiserver/storage/identity/storageprovider.go +++ b/internal/apiserver/storage/identity/storageprovider.go @@ -1,19 +1,45 @@ package identity import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" generic "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" serverstorage "k8s.io/apiserver/pkg/server/storage" - "k8s.io/kubernetes/pkg/api/legacyscheme" controlplaneapiserver "k8s.io/kubernetes/pkg/controlplane/apiserver" sessionsregistry "go.miloapis.com/milo/internal/apiserver/identity/sessions" useridentitiesregistry "go.miloapis.com/milo/internal/apiserver/identity/useridentities" + identityinstall "go.miloapis.com/milo/pkg/apis/identity/install" identityv1alpha1 "go.miloapis.com/milo/pkg/apis/identity/v1alpha1" ) +var ( + // Scheme defines the runtime type system for Identity API object serialization. + Scheme = runtime.NewScheme() + // Codecs provides serializers for Identity API objects. + Codecs = serializer.NewCodecFactory(Scheme) +) + +func init() { + identityinstall.Install(Scheme) + + metav1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) + + // Register unversioned meta types required by the API machinery. + unversioned := schema.GroupVersion{Group: "", Version: "v1"} + Scheme.AddUnversionedTypes(unversioned, + &metav1.Status{}, + &metav1.APIVersions{}, + &metav1.APIGroupList{}, + &metav1.APIGroup{}, + &metav1.APIResourceList{}, + ) +} + type StorageProvider struct { Sessions sessionsregistry.Backend UserIdentities useridentitiesregistry.Backend @@ -25,16 +51,13 @@ func (p StorageProvider) NewRESTStorage( _ serverstorage.APIResourceConfigSource, _ generic.RESTOptionsGetter, ) (genericapiserver.APIGroupInfo, error) { - // Create ParameterCodec using legacyscheme.Scheme which has identity scheme installed - // with field label conversion functions. This is critical for field selector validation. - // The identity scheme is installed in cmd/milo/apiserver/config.go via identityapi.Install(legacyscheme.Scheme) - parameterCodec := runtime.NewParameterCodec(legacyscheme.Scheme) - + // Use dedicated Identity Scheme with field label conversion functions registered + // This follows the same pattern as Activity API apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo( identityv1alpha1.SchemeGroupVersion.Group, - legacyscheme.Scheme, - parameterCodec, - legacyscheme.Codecs, + Scheme, + metav1.ParameterCodec, + Codecs, ) storage := map[string]rest.Storage{ diff --git a/pkg/apis/identity/install/install.go b/pkg/apis/identity/install/install.go new file mode 100644 index 00000000..7a8c1071 --- /dev/null +++ b/pkg/apis/identity/install/install.go @@ -0,0 +1,14 @@ +package install + +import ( + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + + "go.miloapis.com/milo/pkg/apis/identity/v1alpha1" +) + +// Install registers the API group and adds types to a scheme +func Install(scheme *runtime.Scheme) { + utilruntime.Must(v1alpha1.AddToScheme(scheme)) + utilruntime.Must(scheme.SetVersionPriority(v1alpha1.SchemeGroupVersion)) +} From fa76dffdce3d7748bf6b3b7522991fab1d91baca Mon Sep 17 00:00:00 2001 From: OscarLlamas6 Date: Wed, 25 Feb 2026 23:07:36 -0600 Subject: [PATCH 07/11] debug: add logging for field label conversion registration --- pkg/apis/identity/v1alpha1/register.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/apis/identity/v1alpha1/register.go b/pkg/apis/identity/v1alpha1/register.go index ba24b551..cab88708 100644 --- a/pkg/apis/identity/v1alpha1/register.go +++ b/pkg/apis/identity/v1alpha1/register.go @@ -42,16 +42,20 @@ func addKnownTypes(scheme *runtime.Scheme) error { // Register field label conversions for UserIdentity // This enables field selectors like status.userUID= for staff users userIdentityGVK := SchemeGroupVersion.WithKind("UserIdentity") + klog.InfoS("Registering field label conversion for UserIdentity", "gvk", userIdentityGVK.String()) if err := scheme.AddFieldLabelConversionFunc(userIdentityGVK, UserIdentityFieldLabelConversionFunc); err != nil { + klog.ErrorS(err, "Failed to register UserIdentity field label conversion") return err } // Register field label conversions for Session // This enables field selectors like status.userUID= for staff users sessionGVK := SchemeGroupVersion.WithKind("Session") + klog.InfoS("Registering field label conversion for Session", "gvk", sessionGVK.String()) if err := scheme.AddFieldLabelConversionFunc(sessionGVK, SessionFieldLabelConversionFunc); err != nil { + klog.ErrorS(err, "Failed to register Session field label conversion") return err } From a588049a9e39c5bd6533660770c07399879a0463 Mon Sep 17 00:00:00 2001 From: OscarLlamas6 Date: Wed, 25 Feb 2026 23:19:47 -0600 Subject: [PATCH 08/11] debug: test field label conversion in init --- .../storage/identity/storageprovider.go | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/internal/apiserver/storage/identity/storageprovider.go b/internal/apiserver/storage/identity/storageprovider.go index 103c48fe..9d4252cd 100644 --- a/internal/apiserver/storage/identity/storageprovider.go +++ b/internal/apiserver/storage/identity/storageprovider.go @@ -9,6 +9,7 @@ import ( "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" serverstorage "k8s.io/apiserver/pkg/server/storage" + "k8s.io/klog/v2" controlplaneapiserver "k8s.io/kubernetes/pkg/controlplane/apiserver" sessionsregistry "go.miloapis.com/milo/internal/apiserver/identity/sessions" @@ -38,6 +39,28 @@ func init() { &metav1.APIGroup{}, &metav1.APIResourceList{}, ) + + // Wrap the scheme's ConvertFieldLabel to add logging + originalConverter := Scheme.Converter() + if originalConverter != nil { + // Note: We can't actually wrap ConvertFieldLabel easily, but we can verify registration + // by checking if the scheme recognizes our GVKs + userIdentityGVK := identityv1alpha1.SchemeGroupVersion.WithKind("UserIdentity") + sessionGVK := identityv1alpha1.SchemeGroupVersion.WithKind("Session") + + // Test if field label conversion works + if _, _, err := Scheme.ConvertFieldLabel(userIdentityGVK, "status.userUID", "test"); err == nil { + klog.InfoS("UserIdentity field label conversion is working in init", "gvk", userIdentityGVK.String()) + } else { + klog.ErrorS(err, "UserIdentity field label conversion FAILED in init", "gvk", userIdentityGVK.String()) + } + + if _, _, err := Scheme.ConvertFieldLabel(sessionGVK, "status.userUID", "test"); err == nil { + klog.InfoS("Session field label conversion is working in init", "gvk", sessionGVK.String()) + } else { + klog.ErrorS(err, "Session field label conversion FAILED in init", "gvk", sessionGVK.String()) + } + } } type StorageProvider struct { From 132f603f3795fc96dd45c3f3ebc73b50a63d5da9 Mon Sep 17 00:00:00 2001 From: OscarLlamas6 Date: Wed, 25 Feb 2026 23:30:15 -0600 Subject: [PATCH 09/11] debug: simplify field label conversion test --- .../apiserver/storage/identity/storageprovider.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/internal/apiserver/storage/identity/storageprovider.go b/internal/apiserver/storage/identity/storageprovider.go index 9d4252cd..716b921c 100644 --- a/internal/apiserver/storage/identity/storageprovider.go +++ b/internal/apiserver/storage/identity/storageprovider.go @@ -48,17 +48,20 @@ func init() { userIdentityGVK := identityv1alpha1.SchemeGroupVersion.WithKind("UserIdentity") sessionGVK := identityv1alpha1.SchemeGroupVersion.WithKind("Session") - // Test if field label conversion works + // Test if field label conversion functions are registered correctly + + // Test UserIdentity field label conversion if _, _, err := Scheme.ConvertFieldLabel(userIdentityGVK, "status.userUID", "test"); err == nil { - klog.InfoS("UserIdentity field label conversion is working in init", "gvk", userIdentityGVK.String()) + klog.InfoS("✓ UserIdentity field label conversion WORKS", "gvk", userIdentityGVK.String()) } else { - klog.ErrorS(err, "UserIdentity field label conversion FAILED in init", "gvk", userIdentityGVK.String()) + klog.ErrorS(err, "✗ UserIdentity field label conversion FAILED", "gvk", userIdentityGVK.String()) } + // Test Session field label conversion if _, _, err := Scheme.ConvertFieldLabel(sessionGVK, "status.userUID", "test"); err == nil { - klog.InfoS("Session field label conversion is working in init", "gvk", sessionGVK.String()) + klog.InfoS("✓ Session field label conversion WORKS", "gvk", sessionGVK.String()) } else { - klog.ErrorS(err, "Session field label conversion FAILED in init", "gvk", sessionGVK.String()) + klog.ErrorS(err, "✗ Session field label conversion FAILED", "gvk", sessionGVK.String()) } } } From 0d9c1d378aab6fd89d91498abd7d5bb371caf74a Mon Sep 17 00:00:00 2001 From: OscarLlamas6 Date: Wed, 25 Feb 2026 23:39:21 -0600 Subject: [PATCH 10/11] debug: use klog.Error for visibility in init --- .../storage/identity/storageprovider.go | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/internal/apiserver/storage/identity/storageprovider.go b/internal/apiserver/storage/identity/storageprovider.go index 716b921c..9db064e3 100644 --- a/internal/apiserver/storage/identity/storageprovider.go +++ b/internal/apiserver/storage/identity/storageprovider.go @@ -26,7 +26,10 @@ var ( ) func init() { + klog.Error("========== IDENTITY STORAGE INIT START ==========") + identityinstall.Install(Scheme) + klog.Error("Identity install complete") metav1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) @@ -40,30 +43,27 @@ func init() { &metav1.APIResourceList{}, ) - // Wrap the scheme's ConvertFieldLabel to add logging - originalConverter := Scheme.Converter() - if originalConverter != nil { - // Note: We can't actually wrap ConvertFieldLabel easily, but we can verify registration - // by checking if the scheme recognizes our GVKs - userIdentityGVK := identityv1alpha1.SchemeGroupVersion.WithKind("UserIdentity") - sessionGVK := identityv1alpha1.SchemeGroupVersion.WithKind("Session") - - // Test if field label conversion functions are registered correctly - - // Test UserIdentity field label conversion - if _, _, err := Scheme.ConvertFieldLabel(userIdentityGVK, "status.userUID", "test"); err == nil { - klog.InfoS("✓ UserIdentity field label conversion WORKS", "gvk", userIdentityGVK.String()) - } else { - klog.ErrorS(err, "✗ UserIdentity field label conversion FAILED", "gvk", userIdentityGVK.String()) - } - - // Test Session field label conversion - if _, _, err := Scheme.ConvertFieldLabel(sessionGVK, "status.userUID", "test"); err == nil { - klog.InfoS("✓ Session field label conversion WORKS", "gvk", sessionGVK.String()) - } else { - klog.ErrorS(err, "✗ Session field label conversion FAILED", "gvk", sessionGVK.String()) - } + // Test if field label conversion functions are registered correctly + userIdentityGVK := identityv1alpha1.SchemeGroupVersion.WithKind("UserIdentity") + sessionGVK := identityv1alpha1.SchemeGroupVersion.WithKind("Session") + + klog.Errorf("Testing UserIdentity GVK: %s", userIdentityGVK.String()) + + // Test UserIdentity field label conversion + if _, _, err := Scheme.ConvertFieldLabel(userIdentityGVK, "status.userUID", "test"); err == nil { + klog.Error("✓ UserIdentity field label conversion WORKS") + } else { + klog.Errorf("✗ UserIdentity field label conversion FAILED: %v", err) } + + // Test Session field label conversion + if _, _, err := Scheme.ConvertFieldLabel(sessionGVK, "status.userUID", "test"); err == nil { + klog.Error("✓ Session field label conversion WORKS") + } else { + klog.Errorf("✗ Session field label conversion FAILED: %v", err) + } + + klog.Error("========== IDENTITY STORAGE INIT END ==========") } type StorageProvider struct { From ca3133384acf171db964b1bdc767b3891ccada5b Mon Sep 17 00:00:00 2001 From: OscarLlamas6 Date: Thu, 26 Feb 2026 22:27:31 -0600 Subject: [PATCH 11/11] refactor: remove debug logging from field selector implementation Clean up debug logging that was added during investigation: - Remove klog.Error debug messages from storageprovider init() - Remove verbose logging from field label conversion functions - Keep only essential code for field selector support All functionality remains intact and tested. --- .../storage/identity/storageprovider.go | 26 ------------- pkg/apis/identity/v1alpha1/register.go | 37 +++---------------- 2 files changed, 6 insertions(+), 57 deletions(-) diff --git a/internal/apiserver/storage/identity/storageprovider.go b/internal/apiserver/storage/identity/storageprovider.go index 9db064e3..103c48fe 100644 --- a/internal/apiserver/storage/identity/storageprovider.go +++ b/internal/apiserver/storage/identity/storageprovider.go @@ -9,7 +9,6 @@ import ( "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" serverstorage "k8s.io/apiserver/pkg/server/storage" - "k8s.io/klog/v2" controlplaneapiserver "k8s.io/kubernetes/pkg/controlplane/apiserver" sessionsregistry "go.miloapis.com/milo/internal/apiserver/identity/sessions" @@ -26,10 +25,7 @@ var ( ) func init() { - klog.Error("========== IDENTITY STORAGE INIT START ==========") - identityinstall.Install(Scheme) - klog.Error("Identity install complete") metav1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) @@ -42,28 +38,6 @@ func init() { &metav1.APIGroup{}, &metav1.APIResourceList{}, ) - - // Test if field label conversion functions are registered correctly - userIdentityGVK := identityv1alpha1.SchemeGroupVersion.WithKind("UserIdentity") - sessionGVK := identityv1alpha1.SchemeGroupVersion.WithKind("Session") - - klog.Errorf("Testing UserIdentity GVK: %s", userIdentityGVK.String()) - - // Test UserIdentity field label conversion - if _, _, err := Scheme.ConvertFieldLabel(userIdentityGVK, "status.userUID", "test"); err == nil { - klog.Error("✓ UserIdentity field label conversion WORKS") - } else { - klog.Errorf("✗ UserIdentity field label conversion FAILED: %v", err) - } - - // Test Session field label conversion - if _, _, err := Scheme.ConvertFieldLabel(sessionGVK, "status.userUID", "test"); err == nil { - klog.Error("✓ Session field label conversion WORKS") - } else { - klog.Errorf("✗ Session field label conversion FAILED: %v", err) - } - - klog.Error("========== IDENTITY STORAGE INIT END ==========") } type StorageProvider struct { diff --git a/pkg/apis/identity/v1alpha1/register.go b/pkg/apis/identity/v1alpha1/register.go index cab88708..bb44e8cd 100644 --- a/pkg/apis/identity/v1alpha1/register.go +++ b/pkg/apis/identity/v1alpha1/register.go @@ -6,7 +6,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/klog/v2" ) // SchemeGroupVersion is group version used to register these objects @@ -42,20 +41,16 @@ func addKnownTypes(scheme *runtime.Scheme) error { // Register field label conversions for UserIdentity // This enables field selectors like status.userUID= for staff users userIdentityGVK := SchemeGroupVersion.WithKind("UserIdentity") - klog.InfoS("Registering field label conversion for UserIdentity", "gvk", userIdentityGVK.String()) if err := scheme.AddFieldLabelConversionFunc(userIdentityGVK, UserIdentityFieldLabelConversionFunc); err != nil { - klog.ErrorS(err, "Failed to register UserIdentity field label conversion") return err } // Register field label conversions for Session // This enables field selectors like status.userUID= for staff users sessionGVK := SchemeGroupVersion.WithKind("Session") - klog.InfoS("Registering field label conversion for Session", "gvk", sessionGVK.String()) if err := scheme.AddFieldLabelConversionFunc(sessionGVK, SessionFieldLabelConversionFunc); err != nil { - klog.ErrorS(err, "Failed to register Session field label conversion") return err } @@ -65,47 +60,27 @@ func addKnownTypes(scheme *runtime.Scheme) error { // UserIdentityFieldLabelConversionFunc converts field selectors for UserIdentity resources. // This allows staff users to filter user identities by fields beyond the default metadata.name. func UserIdentityFieldLabelConversionFunc(label, value string) (string, string, error) { - klog.V(2).InfoS("UserIdentity field label conversion called", "label", label, "value", value) switch label { - // Metadata fields (default Kubernetes fields) case "metadata.name", - "metadata.namespace": - klog.V(2).InfoS("UserIdentity field label conversion: accepted metadata field", "label", label) + "metadata.namespace", + "status.userUID": return label, value, nil - - // Status fields (custom field selector for staff users) - case "status.userUID": - klog.V(2).InfoS("UserIdentity field label conversion: accepted status.userUID", "label", label, "value", value) - return label, value, nil - default: - err := fmt.Errorf("%q is not a known field selector: only %q are supported", + return "", "", fmt.Errorf("%q is not a known field selector: only %q are supported", label, []string{"metadata.name", "metadata.namespace", "status.userUID"}) - klog.V(2).InfoS("UserIdentity field label conversion: rejected field", "label", label, "error", err) - return "", "", err } } // SessionFieldLabelConversionFunc converts field selectors for Session resources. // This allows staff users to filter sessions by fields beyond the default metadata.name. func SessionFieldLabelConversionFunc(label, value string) (string, string, error) { - klog.V(2).InfoS("Session field label conversion called", "label", label, "value", value) switch label { - // Metadata fields (default Kubernetes fields) case "metadata.name", - "metadata.namespace": - klog.V(2).InfoS("Session field label conversion: accepted metadata field", "label", label) + "metadata.namespace", + "status.userUID": return label, value, nil - - // Status fields (custom field selector for staff users) - case "status.userUID": - klog.V(2).InfoS("Session field label conversion: accepted status.userUID", "label", label, "value", value) - return label, value, nil - default: - err := fmt.Errorf("%q is not a known field selector: only %q are supported", + return "", "", fmt.Errorf("%q is not a known field selector: only %q are supported", label, []string{"metadata.name", "metadata.namespace", "status.userUID"}) - klog.V(2).InfoS("Session field label conversion: rejected field", "label", label, "error", err) - return "", "", err } }