Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions internal/apiserver/identity/sessions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<user-id> — 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=<target-user-id>

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

5 changes: 4 additions & 1 deletion internal/apiserver/identity/sessions/rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
27 changes: 27 additions & 0 deletions internal/apiserver/identity/useridentities/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<user-id> — 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=<target-user-id>

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
Expand Down
5 changes: 4 additions & 1 deletion internal/apiserver/identity/useridentities/rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
34 changes: 31 additions & 3 deletions internal/apiserver/storage/identity/storageprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,44 @@ 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
Expand All @@ -25,11 +51,13 @@ func (p StorageProvider) NewRESTStorage(
_ serverstorage.APIResourceConfigSource,
_ generic.RESTOptionsGetter,
) (genericapiserver.APIGroupInfo, error) {
// 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,
Scheme,
metav1.ParameterCodec,
legacyscheme.Codecs,
Codecs,
)

storage := map[string]rest.Storage{
Expand Down
14 changes: 14 additions & 0 deletions pkg/apis/identity/install/install.go
Original file line number Diff line number Diff line change
@@ -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))
}
47 changes: 47 additions & 0 deletions pkg/apis/identity/v1alpha1/register.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -35,5 +37,50 @@ func addKnownTypes(scheme *runtime.Scheme) error {
&UserIdentityList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)

// Register field label conversions for UserIdentity
// This enables field selectors like status.userUID=<user-id> 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=<user-id> 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 {
case "metadata.name",
"metadata.namespace",
"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 {
case "metadata.name",
"metadata.namespace",
"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"})
}
}
Loading
Loading