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
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ metadata:
}
]
capabilities: Basic Install
createdAt: "2026-04-08T14:26:31Z"
createdAt: "2026-04-08T12:09:54Z"
operators.operatorframework.io/builder: operator-sdk-v1.38.0
operators.operatorframework.io/project_layout: go.kubebuilder.io/v4
name: feast-operator.v0.62.0
Expand Down
74 changes: 72 additions & 2 deletions infra/feast-operator/internal/controller/authz/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
// Deploy the feast authorization
func (authz *FeastAuthorization) Deploy() error {
if authz.isKubernetesAuth() {
authz.cleanupOidcRbac()
return authz.deployKubernetesAuth()
}

Expand All @@ -27,12 +28,14 @@ func (authz *FeastAuthorization) Deploy() error {
apimeta.RemoveStatusCondition(&authz.Handler.FeatureStore.Status.Conditions, feastKubernetesAuthConditions[metav1.ConditionTrue].Type)

if authz.isOidcAuth() {
if err := authz.createFeastClusterRole(); err != nil {
if err := authz.createOidcClusterRole(); err != nil {
return err
}
if err := authz.createFeastClusterRoleBinding(); err != nil {
if err := authz.createOidcClusterRoleBinding(); err != nil {
return err
}
} else {
authz.cleanupOidcRbac()
}
Comment on lines 30 to 39
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Orphaned Kubernetes auth ClusterRoleBinding when transitioning from Kubernetes auth to OIDC auth

Before this PR, the OIDC auth path in Deploy() called createFeastClusterRoleBinding(), which used the same CRB name as Kubernetes auth (GetFeastName(fs) + "-cluster-binding" at infra/feast-operator/internal/controller/authz/authz.go:444). Switching from Kubernetes to OIDC would therefore update the existing CRB in place via CreateOrUpdate. After this PR, the OIDC path creates a separate CRB with a different name (GetFeastName(fs) + "-oidc-token-review" at line 397). When transitioning from Kubernetes auth to OIDC, the old Kubernetes CRB (feast-{name}-cluster-binding) is never deleted — it has no owner reference (setFeastClusterRoleBinding at line 202 returns nil instead of calling SetControllerReference), so it won't be garbage collected either. The orphaned CRB continues to bind the service account to the broad Kubernetes auth ClusterRole (which grants access to subjectaccessreviews, rolebindings, namespaces, clusterroles, clusterrolebindings per lines 142-178), even though OIDC only needs tokenreviews:create. The PR correctly adds cleanupOidcRbac() for the OIDC→Kubernetes transition (line 21), but there is no symmetric cleanup of the Kubernetes CRB when transitioning away from Kubernetes auth.

Prompt for agents
In Deploy(), when the code enters the non-Kubernetes-auth branch (lines 25-41), it should also clean up the Kubernetes auth ClusterRoleBinding before proceeding with OIDC or no-auth setup. Add a cleanup step similar to cleanupOidcRbac() but targeting the Kubernetes auth CRB. For example, add a new method cleanupKubernetesClusterRbac() that deletes the ClusterRoleBinding named getFeastClusterRoleBindingName() (i.e. GetFeastName(fs) + "-cluster-binding"), and call it right after the existing namespace-scoped cleanup (after line 27, before the isOidcAuth check). This mirrors the pattern already established by cleanupOidcRbac() which is called in the Kubernetes auth path at line 21.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


return nil
Expand Down Expand Up @@ -327,6 +330,73 @@ func (authz *FeastAuthorization) setAuthRole(role *rbacv1.Role) error {
return controllerutil.SetControllerReference(authz.Handler.FeatureStore, role, authz.Handler.Scheme)
}

func (authz *FeastAuthorization) createOidcClusterRole() error {
logger := log.FromContext(authz.Handler.Context)
clusterRole := &rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{Name: authz.getOidcClusterRoleName()},
}
clusterRole.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("ClusterRole"))
if op, err := controllerutil.CreateOrUpdate(authz.Handler.Context, authz.Handler.Client, clusterRole, controllerutil.MutateFn(func() error {
clusterRole.Labels = authz.getLabels()
clusterRole.Rules = []rbacv1.PolicyRule{
{
APIGroups: []string{"authentication.k8s.io"},
Resources: []string{"tokenreviews"},
Verbs: []string{"create"},
},
}
return nil
})); err != nil {
return err
} else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated {
logger.Info("Successfully reconciled", "ClusterRole", clusterRole.Name, "operation", op)
}
return nil
}

func (authz *FeastAuthorization) createOidcClusterRoleBinding() error {
logger := log.FromContext(authz.Handler.Context)
crb := &rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{Name: authz.getOidcClusterRoleBindingName()},
}
crb.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("ClusterRoleBinding"))
if op, err := controllerutil.CreateOrUpdate(authz.Handler.Context, authz.Handler.Client, crb, controllerutil.MutateFn(func() error {
crb.Labels = authz.getLabels()
crb.Subjects = []rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: authz.getFeastServiceAccountName(),
Namespace: authz.Handler.FeatureStore.Namespace,
},
}
crb.RoleRef = rbacv1.RoleRef{
APIGroup: rbacv1.GroupName,
Kind: "ClusterRole",
Name: authz.getOidcClusterRoleName(),
}
return nil
})); err != nil {
return err
} else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated {
logger.Info("Successfully reconciled", "ClusterRoleBinding", crb.Name, "operation", op)
}
return nil
}

func (authz *FeastAuthorization) cleanupOidcRbac() {
crb := &rbacv1.ClusterRoleBinding{ObjectMeta: metav1.ObjectMeta{Name: authz.getOidcClusterRoleBindingName()}}
crb.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("ClusterRoleBinding"))
_ = authz.Handler.Client.Delete(authz.Handler.Context, crb)
}

func (authz *FeastAuthorization) getOidcClusterRoleName() string {
return "feast-oidc-token-review"
}

func (authz *FeastAuthorization) getOidcClusterRoleBindingName() string {
return services.GetFeastName(authz.Handler.FeatureStore) + "-oidc-token-review"
}

func (authz *FeastAuthorization) getLabels() map[string]string {
return map[string]string{
services.NameLabelKey: authz.Handler.FeatureStore.Name,
Comment on lines 400 to 402
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getLabels() stamps the FeatureStore-specific name. But the ClusterRole feast-oidc-token-review is shared across all OIDC FeatureStore instances. The last instance to reconcile overwrites the labels with its own name. This creates misleading audit trails — the ClusterRole appears to belong to one FeatureStore when it actually serves all of them.
Recommendation: Either use instance-independent labels for the shared ClusterRole, or use an aggregated label approach.

Expand Down
Loading