From c64c0a8e3750b52701ac88791d58730de8560f43 Mon Sep 17 00:00:00 2001 From: ntkathole Date: Thu, 13 Aug 2026 22:53:30 +0530 Subject: [PATCH] fix: Default authentication to kubernetes auth Signed-off-by: ntkathole Co-authored-by: Cursor Signed-off-by: ntkathole Co-authored-by: Cursor --- .secrets.baseline | 20 +++--- .../components/authz_manager.md | 70 ++++++++++++++++++- docs/reference/auth/kubernetes_auth_setup.md | 54 +++++++++++++- .../api/v1/featurestore_types.go | 7 +- .../api/v1/zz_generated.deepcopy.go | 5 ++ .../api/v1alpha1/featurestore_types.go | 7 +- .../api/v1alpha1/zz_generated.deepcopy.go | 5 ++ .../manifests/feast.dev_featurestores.yaml | 40 ++++++++--- .../crd/bases/feast.dev_featurestores.yaml | 34 ++++++--- infra/feast-operator/dist/install.yaml | 34 ++++++--- infra/feast-operator/docs/api/markdown/ref.md | 3 + .../featurestore_controller_ephemeral_test.go | 8 ++- ...restore_controller_kubernetes_auth_test.go | 9 ++- ...eaturestore_controller_objectstore_test.go | 8 ++- .../featurestore_controller_oidc_auth_test.go | 7 +- .../featurestore_controller_pvc_test.go | 8 ++- .../featurestore_controller_test.go | 34 ++++++--- .../controller/services/repo_config.go | 8 ++- .../controller/services/repo_config_test.go | 28 ++++++-- .../internal/controller/services/util.go | 9 +++ .../test/api/featurestore_types_test.go | 7 +- sdk/python/feast/permissions/enforcer.py | 17 +++-- sdk/python/feast/permissions/server/utils.py | 5 ++ .../auth/server/test_auth_registry_server.py | 34 ++------- .../resource/feast_config_credit_scoring.yaml | 2 + .../resource/feast_config_driver_ranking.yaml | 2 + .../rest_api/resource/feast_config_rhoai.yaml | 2 + 27 files changed, 355 insertions(+), 112 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 62d7fd33f37..b16f006df82 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -957,7 +957,7 @@ "filename": "infra/feast-operator/api/v1/featurestore_types.go", "hashed_secret": "44e17306b837162269a410204daaa5ecee4ec22c", "is_verified": false, - "line_number": 958 + "line_number": 963 } ], "infra/feast-operator/api/v1/zz_generated.deepcopy.go": [ @@ -966,21 +966,21 @@ "filename": "infra/feast-operator/api/v1/zz_generated.deepcopy.go", "hashed_secret": "f914fc9324de1bec1ad13dec94a8ea2ddb41fc87", "is_verified": false, - "line_number": 842 + "line_number": 847 }, { "type": "Secret Keyword", "filename": "infra/feast-operator/api/v1/zz_generated.deepcopy.go", "hashed_secret": "44e17306b837162269a410204daaa5ecee4ec22c", "is_verified": false, - "line_number": 903 + "line_number": 908 }, { "type": "Secret Keyword", "filename": "infra/feast-operator/api/v1/zz_generated.deepcopy.go", "hashed_secret": "c2028031c154bbe86fd69bef740855c74b927dcf", "is_verified": false, - "line_number": 1595 + "line_number": 1600 } ], "infra/feast-operator/api/v1alpha1/featurestore_types.go": [ @@ -989,7 +989,7 @@ "filename": "infra/feast-operator/api/v1alpha1/featurestore_types.go", "hashed_secret": "44e17306b837162269a410204daaa5ecee4ec22c", "is_verified": false, - "line_number": 663 + "line_number": 668 } ], "infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go": [ @@ -998,21 +998,21 @@ "filename": "infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go", "hashed_secret": "f914fc9324de1bec1ad13dec94a8ea2ddb41fc87", "is_verified": false, - "line_number": 615 + "line_number": 620 }, { "type": "Secret Keyword", "filename": "infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go", "hashed_secret": "44e17306b837162269a410204daaa5ecee4ec22c", "is_verified": false, - "line_number": 1123 + "line_number": 1128 }, { "type": "Secret Keyword", "filename": "infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go", "hashed_secret": "c2028031c154bbe86fd69bef740855c74b927dcf", "is_verified": false, - "line_number": 1128 + "line_number": 1133 } ], "infra/feast-operator/bundle/manifests/openlineage-secret_v1_secret.yaml": [ @@ -1147,7 +1147,7 @@ "filename": "infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go", "hashed_secret": "a1f14fc6f33ba39a8b6d006fefa6fe0fe8d60ae2", "is_verified": false, - "line_number": 450 + "line_number": 449 } ], "infra/feast-operator/internal/controller/featurestore_controller_test_utils_test.go": [ @@ -1564,5 +1564,5 @@ } ] }, - "generated_at": "2026-07-31T05:29:18Z" + "generated_at": "2026-08-13T17:14:16Z" } diff --git a/docs/getting-started/components/authz_manager.md b/docs/getting-started/components/authz_manager.md index 12fdf2f39e5..f9e1d8af104 100644 --- a/docs/getting-started/components/authz_manager.md +++ b/docs/getting-started/components/authz_manager.md @@ -21,6 +21,70 @@ the authorization tokens that the server can properly identify and use to enforc The server-side implementation of the authorization functionality is defined [here](./../../../sdk/python/feast/permissions/server). Few of the key models, classes to understand the authorization implementation on the client side can be found [here](./../../../sdk/python/feast/permissions/client). +## Default Authorization Behavior + +### Feast Operator (Kubernetes Deployments) + +When deploying Feast using the [Feast operator](../../../infra/feast-operator/docs/api/markdown/ref.md), **Kubernetes authentication is enabled by default**. If no `authz` section is specified in the `FeatureStore` CR, the operator automatically configures `kubernetes` auth for all deployed services. + +This follows an **"Authenticated by Default, Authorized Gradually"** security model: +- All Feast endpoints require a valid Kubernetes bearer token by default. +- If no explicit `Permission` objects are defined (via `permissions.py` + `feast apply`), **all authenticated users are granted full access**. A warning is logged to remind administrators to define fine-grained permissions. +- Unauthenticated requests are rejected. + +This ensures that Feast deployments are never accidentally exposed without authentication, while allowing teams to incrementally adopt fine-grained RBAC. + +#### Disabling Authentication with `noAuth` + +For development, testing, or environments where authentication is handled externally, you can explicitly disable authentication using the `noAuth` option in the `FeatureStore` CR: + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: my-feature-store +spec: + feastProject: my_project + authz: + noAuth: true +``` + +{% hint style="warning" %} +Setting `noAuth: true` disables all authentication and authorization. All endpoints become publicly accessible without any identity checks. Only use this for local development or testing environments. For production, use `kubernetes` or `oidc` authentication. +{% endhint %} + +#### Explicit Kubernetes Auth (Default) + +This is equivalent to the default behavior when no `authz` section is provided: + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: my-feature-store +spec: + feastProject: my_project + authz: + kubernetes: {} +``` + +#### OIDC Auth via Operator + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: my-feature-store +spec: + feastProject: my_project + authz: + oidc: + secretRef: + name: feast-oidc-secret +``` + +### Standalone Deployments (feature_store.yaml) + ## Configuring Authorization The authorization is configured using a dedicated `auth` section in the `feature_store.yaml` configuration. @@ -28,7 +92,7 @@ The authorization is configured using a dedicated `auth` section in the `feature the `feature_store_yaml_base64` value must include the `auth` section to specify the authorization configuration. ### No Authorization -This configuration applies the default `no_auth` authorization: +This configuration applies the `no_auth` authorization: ```yaml project: my-project auth: @@ -36,6 +100,10 @@ auth: ... ``` +{% hint style="warning" %} +Running with `auth.type: no_auth` leaves all endpoints unauthenticated. This is suitable for local development only. For production deployments, configure `kubernetes` or `oidc` authentication. +{% endhint %} + ### OIDC Authorization With OIDC authorization, the Feast client proxies retrieve the JWT token from an OIDC server (or [Identity Provider](https://openid.net/developers/how-connect-works/)) and append it in every request to a Feast server, using an [Authorization Bearer Token](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#bearer). diff --git a/docs/reference/auth/kubernetes_auth_setup.md b/docs/reference/auth/kubernetes_auth_setup.md index 447e1d5a684..7bb0ed6208c 100644 --- a/docs/reference/auth/kubernetes_auth_setup.md +++ b/docs/reference/auth/kubernetes_auth_setup.md @@ -10,6 +10,45 @@ Feast supports extracting user groups, namespaces and roles of both Service Acco - **Namespaces**: Kubernetes namespaces associated with User/SA - **Roles**: Kubernetes roles associated with User/SA +## Operator Default Behavior + +When deploying Feast using the Feast operator, **Kubernetes authentication is enabled by default**. You do not need to explicitly configure `authz` in the `FeatureStore` CR — the operator automatically applies `kubernetes` auth to all deployed services. + +### What This Means + +- All HTTP/gRPC requests to Feast services must include a valid Kubernetes bearer token in the `Authorization` header. +- The server validates the token via the Kubernetes Token Access Review API and extracts user identity (username, groups, namespaces, roles). +- If no `Permission` objects are defined, authenticated users get full access (with a warning logged). +- Unauthenticated requests receive a `401 Unauthorized` response. + +### Disabling Authentication + +If you need to run Feast without authentication (e.g., for local development or testing), explicitly set `noAuth: true` in the `FeatureStore` CR: + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: my-feature-store +spec: + feastProject: my_project + authz: + noAuth: true +``` + +{% hint style="warning" %} +`noAuth: true` disables all authentication and authorization checks. All endpoints become publicly accessible. Use only in non-production environments. +{% endhint %} + +### Choosing an Auth Mode + +| `spec.authz` Setting | Behavior | +| --- | --- | +| _(not specified)_ | Kubernetes auth enabled (default) | +| `kubernetes: {}` | Kubernetes auth enabled (explicit) | +| `oidc: { ... }` | OIDC auth enabled | +| `noAuth: true` | All auth disabled | + ## Key Features ### Setting Up Kubernetes RBAC for Feast @@ -142,20 +181,29 @@ Run `feast apply` from CLI/API/SDK on server or from client(if permitted) to app ### Common Issues -1. **Token Access Review Fails** +1. **401 Unauthorized After Upgrading** + - The Feast operator now defaults to Kubernetes authentication. If your existing FeatureStore CR did not specify `authz`, the upgrade enables auth automatically. + - **Quick fix for testing**: Add `authz.noAuth: true` to your `FeatureStore` CR to restore the previous unauthenticated behavior. + - **Recommended**: Update your client applications to include a valid Kubernetes bearer token in requests. + +2. **Token Access Review Fails** - Check that the Feast server has the required RBAC permissions - Verify the token is valid and not expired - Check server logs for detailed error messages in debug mode -2. **Groups/Namespaces Not Extracted** +3. **Groups/Namespaces Not Extracted** - Verify the token contains the expected claims - Check that the user is properly configured in Kubernetes/ODH/RHOAI -3. **Permission Denied** +4. **Permission Denied** - Verify the user is added to required groups/namespaces Or has the required role assigned - Check that the policy is correctly configured - Review the permission evaluation logs +5. **"No permissions defined" Warning in Logs** + - This is expected when Kubernetes auth is enabled but no `Permission` objects have been applied. + - Authenticated users get full access by default. Define permissions via `permissions.py` + `feast apply` to enforce fine-grained authorization. + ## Migration Guide ### From Role-Based to Group/Namespace-Based diff --git a/infra/feast-operator/api/v1/featurestore_types.go b/infra/feast-operator/api/v1/featurestore_types.go index 3372e74f63a..d83c005485b 100644 --- a/infra/feast-operator/api/v1/featurestore_types.go +++ b/infra/feast-operator/api/v1/featurestore_types.go @@ -889,10 +889,15 @@ type OptionalCtrConfigs struct { } // AuthzConfig defines the authorization settings for the deployed Feast services. -// +kubebuilder:validation:XValidation:rule="[has(self.kubernetes), has(self.oidc)].exists_one(c, c)",message="One selection required between kubernetes or oidc." +// +kubebuilder:validation:XValidation:rule="[has(self.kubernetes), has(self.oidc), has(self.noAuth)].exists_one(c, c)",message="One selection required between kubernetes, oidc, or noAuth." type AuthzConfig struct { KubernetesAuthz *KubernetesAuthz `json:"kubernetes,omitempty"` OidcAuthz *OidcAuthz `json:"oidc,omitempty"` + // NoAuth explicitly disables authentication and authorization. + // When set to true, Feast services run without any auth checks. + // Use only for development or testing environments. + // +optional + NoAuth *bool `json:"noAuth,omitempty"` } // KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. diff --git a/infra/feast-operator/api/v1/zz_generated.deepcopy.go b/infra/feast-operator/api/v1/zz_generated.deepcopy.go index 3035ed066cf..96fcb9027f0 100644 --- a/infra/feast-operator/api/v1/zz_generated.deepcopy.go +++ b/infra/feast-operator/api/v1/zz_generated.deepcopy.go @@ -43,6 +43,11 @@ func (in *AuthzConfig) DeepCopyInto(out *AuthzConfig) { *out = new(OidcAuthz) (*in).DeepCopyInto(*out) } + if in.NoAuth != nil { + in, out := &in.NoAuth, &out.NoAuth + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthzConfig. diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go index 8ccde377e77..aabb2103598 100644 --- a/infra/feast-operator/api/v1alpha1/featurestore_types.go +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -621,10 +621,15 @@ type OptionalCtrConfigs struct { } // AuthzConfig defines the authorization settings for the deployed Feast services. -// +kubebuilder:validation:XValidation:rule="[has(self.kubernetes), has(self.oidc)].exists_one(c, c)",message="One selection required between kubernetes or oidc." +// +kubebuilder:validation:XValidation:rule="[has(self.kubernetes), has(self.oidc), has(self.noAuth)].exists_one(c, c)",message="One selection required between kubernetes, oidc, or noAuth." type AuthzConfig struct { KubernetesAuthz *KubernetesAuthz `json:"kubernetes,omitempty"` OidcAuthz *OidcAuthz `json:"oidc,omitempty"` + // NoAuth explicitly disables authentication and authorization. + // When set to true, Feast services run without any auth checks. + // Use only for development or testing environments. + // +optional + NoAuth *bool `json:"noAuth,omitempty"` } // KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. diff --git a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go index 17ae4841966..2345d07533a 100644 --- a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -41,6 +41,11 @@ func (in *AuthzConfig) DeepCopyInto(out *AuthzConfig) { *out = new(OidcAuthz) **out = **in } + if in.NoAuth != nil { + in, out := &in.NoAuth, &out.NoAuth + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthzConfig. diff --git a/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml b/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml index 0ab08afef51..94dc71f7c9c 100644 --- a/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml +++ b/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml @@ -57,6 +57,11 @@ spec: type: string type: array type: object + noAuth: + description: NoAuth explicitly disables authentication and authorization. + When set to true, Feast services run without any auth checks. + Use only for development or testing environments. + type: boolean oidc: description: |- OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. @@ -109,8 +114,9 @@ spec: type: object type: object x-kubernetes-validations: - - message: One selection required between kubernetes or oidc. - rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, c)' + - message: One selection required between kubernetes, oidc, or noAuth. + rule: '[has(self.kubernetes), has(self.oidc), has(self.noAuth)].exists_one(c, + c)' batchEngine: description: BatchEngineConfig defines the batch compute engine configuration. properties: @@ -6366,6 +6372,11 @@ spec: type: string type: array type: object + noAuth: + description: NoAuth explicitly disables authentication and + authorization. When set to true, Feast services run without + any auth checks. Use only for development or testing environments. + type: boolean oidc: description: |- OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. @@ -6418,8 +6429,9 @@ spec: type: object type: object x-kubernetes-validations: - - message: One selection required between kubernetes or oidc. - rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, + - message: One selection required between kubernetes, oidc, or + noAuth. + rule: '[has(self.kubernetes), has(self.oidc), has(self.noAuth)].exists_one(c, c)' batchEngine: description: BatchEngineConfig defines the batch compute engine @@ -12883,6 +12895,11 @@ spec: type: string type: array type: object + noAuth: + description: NoAuth explicitly disables authentication and authorization. + When set to true, Feast services run without any auth checks. + Use only for development or testing environments. + type: boolean oidc: description: |- OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. @@ -12907,8 +12924,9 @@ spec: type: object type: object x-kubernetes-validations: - - message: One selection required between kubernetes or oidc. - rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, c)' + - message: One selection required between kubernetes, oidc, or noAuth. + rule: '[has(self.kubernetes), has(self.oidc), has(self.noAuth)].exists_one(c, + c)' cronJob: description: FeastCronJob defines a CronJob to execute against a Feature Store deployment. @@ -17392,6 +17410,11 @@ spec: type: string type: array type: object + noAuth: + description: NoAuth explicitly disables authentication and + authorization. When set to true, Feast services run without + any auth checks. Use only for development or testing environments. + type: boolean oidc: description: |- OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. @@ -17416,8 +17439,9 @@ spec: type: object type: object x-kubernetes-validations: - - message: One selection required between kubernetes or oidc. - rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, + - message: One selection required between kubernetes, oidc, or + noAuth. + rule: '[has(self.kubernetes), has(self.oidc), has(self.noAuth)].exists_one(c, c)' cronJob: description: FeastCronJob defines a CronJob to execute against diff --git a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml index 8184906e14d..23f76ddaa1d 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -57,6 +57,9 @@ spec: type: string type: array type: object + noAuth: + description: NoAuth explicitly disables authentication and authorization. + type: boolean oidc: description: |- OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. @@ -109,8 +112,9 @@ spec: type: object type: object x-kubernetes-validations: - - message: One selection required between kubernetes or oidc. - rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, c)' + - message: One selection required between kubernetes, oidc, or noAuth. + rule: '[has(self.kubernetes), has(self.oidc), has(self.noAuth)].exists_one(c, + c)' batchEngine: description: BatchEngineConfig defines the batch compute engine configuration. properties: @@ -6366,6 +6370,10 @@ spec: type: string type: array type: object + noAuth: + description: NoAuth explicitly disables authentication and + authorization. + type: boolean oidc: description: |- OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. @@ -6418,8 +6426,9 @@ spec: type: object type: object x-kubernetes-validations: - - message: One selection required between kubernetes or oidc. - rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, + - message: One selection required between kubernetes, oidc, or + noAuth. + rule: '[has(self.kubernetes), has(self.oidc), has(self.noAuth)].exists_one(c, c)' batchEngine: description: BatchEngineConfig defines the batch compute engine @@ -12883,6 +12892,9 @@ spec: type: string type: array type: object + noAuth: + description: NoAuth explicitly disables authentication and authorization. + type: boolean oidc: description: |- OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. @@ -12907,8 +12919,9 @@ spec: type: object type: object x-kubernetes-validations: - - message: One selection required between kubernetes or oidc. - rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, c)' + - message: One selection required between kubernetes, oidc, or noAuth. + rule: '[has(self.kubernetes), has(self.oidc), has(self.noAuth)].exists_one(c, + c)' cronJob: description: FeastCronJob defines a CronJob to execute against a Feature Store deployment. @@ -17392,6 +17405,10 @@ spec: type: string type: array type: object + noAuth: + description: NoAuth explicitly disables authentication and + authorization. + type: boolean oidc: description: |- OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. @@ -17416,8 +17433,9 @@ spec: type: object type: object x-kubernetes-validations: - - message: One selection required between kubernetes or oidc. - rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, + - message: One selection required between kubernetes, oidc, or + noAuth. + rule: '[has(self.kubernetes), has(self.oidc), has(self.noAuth)].exists_one(c, c)' cronJob: description: FeastCronJob defines a CronJob to execute against diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index be85a29a7b2..df2ccf1764c 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -65,6 +65,9 @@ spec: type: string type: array type: object + noAuth: + description: NoAuth explicitly disables authentication and authorization. + type: boolean oidc: description: |- OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. @@ -117,8 +120,9 @@ spec: type: object type: object x-kubernetes-validations: - - message: One selection required between kubernetes or oidc. - rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, c)' + - message: One selection required between kubernetes, oidc, or noAuth. + rule: '[has(self.kubernetes), has(self.oidc), has(self.noAuth)].exists_one(c, + c)' batchEngine: description: BatchEngineConfig defines the batch compute engine configuration. properties: @@ -6374,6 +6378,10 @@ spec: type: string type: array type: object + noAuth: + description: NoAuth explicitly disables authentication and + authorization. + type: boolean oidc: description: |- OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. @@ -6426,8 +6434,9 @@ spec: type: object type: object x-kubernetes-validations: - - message: One selection required between kubernetes or oidc. - rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, + - message: One selection required between kubernetes, oidc, or + noAuth. + rule: '[has(self.kubernetes), has(self.oidc), has(self.noAuth)].exists_one(c, c)' batchEngine: description: BatchEngineConfig defines the batch compute engine @@ -12891,6 +12900,9 @@ spec: type: string type: array type: object + noAuth: + description: NoAuth explicitly disables authentication and authorization. + type: boolean oidc: description: |- OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. @@ -12915,8 +12927,9 @@ spec: type: object type: object x-kubernetes-validations: - - message: One selection required between kubernetes or oidc. - rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, c)' + - message: One selection required between kubernetes, oidc, or noAuth. + rule: '[has(self.kubernetes), has(self.oidc), has(self.noAuth)].exists_one(c, + c)' cronJob: description: FeastCronJob defines a CronJob to execute against a Feature Store deployment. @@ -17400,6 +17413,10 @@ spec: type: string type: array type: object + noAuth: + description: NoAuth explicitly disables authentication and + authorization. + type: boolean oidc: description: |- OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. @@ -17424,8 +17441,9 @@ spec: type: object type: object x-kubernetes-validations: - - message: One selection required between kubernetes or oidc. - rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, + - message: One selection required between kubernetes, oidc, or + noAuth. + rule: '[has(self.kubernetes), has(self.oidc), has(self.noAuth)].exists_one(c, c)' cronJob: description: FeastCronJob defines a CronJob to execute against diff --git a/infra/feast-operator/docs/api/markdown/ref.md b/infra/feast-operator/docs/api/markdown/ref.md index cb911ffae22..03ab37e92d1 100644 --- a/infra/feast-operator/docs/api/markdown/ref.md +++ b/infra/feast-operator/docs/api/markdown/ref.md @@ -26,6 +26,9 @@ _Appears in:_ | --- | --- | | `kubernetes` _[KubernetesAuthz](#kubernetesauthz)_ | | | `oidc` _[OidcAuthz](#oidcauthz)_ | | +| `noAuth` _boolean_ | NoAuth explicitly disables authentication and authorization. +When set to true, Feast services run without any auth checks. +Use only for development or testing environments. | #### AutoscalingConfig diff --git a/infra/feast-operator/internal/controller/featurestore_controller_ephemeral_test.go b/infra/feast-operator/internal/controller/featurestore_controller_ephemeral_test.go index 212fa80228b..454bd81f5ee 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_ephemeral_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_ephemeral_test.go @@ -126,7 +126,8 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(resource.Status.FeastVersion).To(Equal(feastversion.FeastVersion)) Expect(resource.Status.ClientConfigMap).To(Equal(feast.GetFeastServiceName(services.ClientFeastType))) Expect(resource.Status.Applied.FeastProject).To(Equal(resource.Spec.FeastProject)) - Expect(resource.Status.Applied.AuthzConfig).To(BeNil()) + Expect(resource.Status.Applied.AuthzConfig).NotTo(BeNil()) + Expect(resource.Status.Applied.AuthzConfig.KubernetesAuthz).NotTo(BeNil()) Expect(resource.Status.Applied.Services).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore.Persistence).NotTo(BeNil()) @@ -166,7 +167,10 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(cond.Message).To(Equal(feastdevv1.DeploymentNotAvailableMessage)) cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType) - Expect(cond).To(BeNil()) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Type).To(Equal(feastdevv1.AuthorizationReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.KubernetesAuthzReadyMessage)) cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType) Expect(cond).ToNot(BeNil()) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_kubernetes_auth_test.go b/infra/feast-operator/internal/controller/featurestore_controller_kubernetes_auth_test.go index 3bfab485e85..3c1adf52279 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_kubernetes_auth_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_kubernetes_auth_test.go @@ -308,7 +308,7 @@ var _ = Describe("FeatureStore Controller-Kubernetes authorization", func() { Expect(err).To(HaveOccurred()) Expect(errors.IsNotFound(err)).To(BeTrue()) - By("Clearing the kubernetes authorization and reconciling") + By("Clearing the kubernetes authorization and reconciling (defaults to Kubernetes auth)") resourceNew = resource.DeepCopy() resourceNew.Spec.AuthzConfig = nil err = k8sClient.Update(ctx, resourceNew) @@ -323,7 +323,7 @@ var _ = Describe("FeatureStore Controller-Kubernetes authorization", func() { Expect(err).NotTo(HaveOccurred()) feast.Handler.FeatureStore = resource - // check no Roles + // custom roles should be cleaned up for _, roleName := range roles { role := &rbacv1.Role{} err = k8sClient.Get(ctx, types.NamespacedName{ @@ -334,15 +334,14 @@ var _ = Describe("FeatureStore Controller-Kubernetes authorization", func() { Expect(err).To(HaveOccurred()) Expect(errors.IsNotFound(err)).To(BeTrue()) } - // check no RoleBinding + // RoleBinding should still exist since nil AuthzConfig defaults to Kubernetes auth roleBinding = &rbacv1.RoleBinding{} err = k8sClient.Get(ctx, types.NamespacedName{ Name: authz.GetFeastRoleName(resource), Namespace: resource.Namespace, }, roleBinding) - Expect(err).To(HaveOccurred()) - Expect(errors.IsNotFound(err)).To(BeTrue()) + Expect(err).NotTo(HaveOccurred()) }) It("should properly encode a feature_store.yaml config", func() { diff --git a/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go b/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go index a326752a78f..58c842c37d7 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go @@ -121,7 +121,8 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(resource.Status.FeastVersion).To(Equal(feastversion.FeastVersion)) Expect(resource.Status.ClientConfigMap).To(Equal(feast.GetFeastServiceName(services.ClientFeastType))) Expect(resource.Status.Applied.FeastProject).To(Equal(resource.Spec.FeastProject)) - Expect(resource.Status.Applied.AuthzConfig).To(BeNil()) + Expect(resource.Status.Applied.AuthzConfig).NotTo(BeNil()) + Expect(resource.Status.Applied.AuthzConfig.KubernetesAuthz).NotTo(BeNil()) Expect(resource.Status.Applied.Services).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore).To(BeNil()) Expect(resource.Status.Applied.Services.OnlineStore).NotTo(BeNil()) @@ -151,7 +152,10 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(cond.Message).To(Equal(feastdevv1.DeploymentNotAvailableMessage)) cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType) - Expect(cond).To(BeNil()) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Type).To(Equal(feastdevv1.AuthorizationReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.KubernetesAuthzReadyMessage)) cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType) Expect(cond).ToNot(BeNil()) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go b/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go index 0f99f6d0479..75d81edc905 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go @@ -263,7 +263,7 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { sa) Expect(err).NotTo(HaveOccurred()) - By("Clearing the OIDC authorization and reconciling") + By("Clearing the OIDC authorization and reconciling (defaults to Kubernetes auth)") resourceNew := resource.DeepCopy() resourceNew.Spec.AuthzConfig = nil err = k8sClient.Update(ctx, resourceNew) @@ -278,15 +278,14 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { Expect(err).NotTo(HaveOccurred()) feast.Handler.FeatureStore = resource - // check no RoleBinding + // With authz cleared, operator defaults to Kubernetes auth, so RoleBinding should exist roleBinding = &rbacv1.RoleBinding{} err = k8sClient.Get(ctx, types.NamespacedName{ Name: authz.GetFeastRoleName(resource), Namespace: resource.Namespace, }, roleBinding) - Expect(err).To(HaveOccurred()) - Expect(errors.IsNotFound(err)).To(BeTrue()) + Expect(err).NotTo(HaveOccurred()) }) It("should properly encode a feature_store.yaml config", func() { diff --git a/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go b/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go index 8e7303cee34..375d7cd613b 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go @@ -160,7 +160,8 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(resource.Status.FeastVersion).To(Equal(feastversion.FeastVersion)) Expect(resource.Status.ClientConfigMap).To(Equal(feast.GetFeastServiceName(services.ClientFeastType))) Expect(resource.Status.Applied.FeastProject).To(Equal(resource.Spec.FeastProject)) - Expect(resource.Status.Applied.AuthzConfig).To(BeNil()) + Expect(resource.Status.Applied.AuthzConfig).NotTo(BeNil()) + Expect(resource.Status.Applied.AuthzConfig.KubernetesAuthz).NotTo(BeNil()) Expect(resource.Status.Applied.Services).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore.Persistence).NotTo(BeNil()) @@ -233,7 +234,10 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(cond.Message).To(Equal(feastdevv1.DeploymentNotAvailableMessage)) cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType) - Expect(cond).To(BeNil()) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Type).To(Equal(feastdevv1.AuthorizationReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.KubernetesAuthzReadyMessage)) cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType) Expect(cond).ToNot(BeNil()) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_test.go b/infra/feast-operator/internal/controller/featurestore_controller_test.go index 712644f7a0c..4f74cca2546 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_test.go @@ -167,7 +167,8 @@ var _ = Describe("FeatureStore Controller", func() { Expect(resource.Status.ServiceHostnames.UI).To(BeEmpty()) Expect(resource.Status.ServiceHostnames.OnlineStore).To(Equal(feast.GetFeastServiceName(services.OnlineFeastType) + "." + resource.Namespace + ".svc.cluster.local:80")) Expect(resource.Status.Applied.FeastProject).To(Equal(resource.Spec.FeastProject)) - Expect(resource.Status.Applied.AuthzConfig).To(BeNil()) + Expect(resource.Status.Applied.AuthzConfig).NotTo(BeNil()) + Expect(resource.Status.Applied.AuthzConfig.KubernetesAuthz).NotTo(BeNil()) Expect(resource.Status.Applied.Services).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore).To(BeNil()) Expect(resource.Status.Applied.Services.Registry).To(BeNil()) @@ -183,7 +184,10 @@ var _ = Describe("FeatureStore Controller", func() { Expect(cond.Type).To(Equal(feastdevv1.ReadyType)) Expect(cond.Message).To(Equal(feastdevv1.ReadyMessage)) cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType) - Expect(cond).To(BeNil()) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Type).To(Equal(feastdevv1.AuthorizationReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.KubernetesAuthzReadyMessage)) cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType) Expect(cond).ToNot(BeNil()) @@ -509,7 +513,7 @@ var _ = Describe("FeatureStore Controller", func() { err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) - Expect(resource.Status.Conditions).To(HaveLen(4)) + Expect(resource.Status.Conditions).To(HaveLen(5)) cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ReadyType) Expect(cond).ToNot(BeNil()) @@ -519,7 +523,9 @@ var _ = Describe("FeatureStore Controller", func() { Expect(cond.Message).To(Equal("Error: Object " + resource.Namespace + "/" + deploy.Name + " is already owned by another Service controller " + name)) cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType) - Expect(cond).To(BeNil()) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Message).To(Equal(feastdevv1.KubernetesAuthzReadyMessage)) cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType) Expect(cond).ToNot(BeNil()) @@ -610,7 +616,8 @@ var _ = Describe("FeatureStore Controller", func() { Expect(resource.Status.FeastVersion).To(Equal(feastversion.FeastVersion)) Expect(resource.Status.ClientConfigMap).To(Equal(feast.GetFeastServiceName(services.ClientFeastType))) Expect(resource.Status.Applied.FeastProject).To(Equal(resource.Spec.FeastProject)) - Expect(resource.Status.Applied.AuthzConfig).To(BeNil()) + Expect(resource.Status.Applied.AuthzConfig).NotTo(BeNil()) + Expect(resource.Status.Applied.AuthzConfig.KubernetesAuthz).NotTo(BeNil()) Expect(resource.Status.Applied.Services).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore.Persistence).NotTo(BeNil()) @@ -656,7 +663,10 @@ var _ = Describe("FeatureStore Controller", func() { Expect(cond.Message).To(Equal(feastdevv1.DeploymentNotAvailableMessage)) cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType) - Expect(cond).To(BeNil()) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Type).To(Equal(feastdevv1.AuthorizationReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.KubernetesAuthzReadyMessage)) cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType) Expect(cond).ToNot(BeNil()) @@ -1157,7 +1167,7 @@ var _ = Describe("FeatureStore Controller", func() { err = k8sClient.Get(ctx, nsName, resource) Expect(err).NotTo(HaveOccurred()) - Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType)).To(BeNil()) + Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1.AuthorizationReadyType)).To(BeTrue()) Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType)).To(BeNil()) Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType)).To(BeTrue()) Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1.OfflineStoreReadyType)).To(BeTrue()) @@ -1258,7 +1268,7 @@ var _ = Describe("FeatureStore Controller", func() { err = k8sClient.Get(ctx, nsName, resource) Expect(err).NotTo(HaveOccurred()) Expect(resource.Status.ServiceHostnames.Registry).To(BeEmpty()) - Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType)).To(BeNil()) + Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1.AuthorizationReadyType)).To(BeTrue()) Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType)).To(BeNil()) Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1.ReadyType)).To(BeFalse()) Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType)).To(BeTrue()) @@ -1688,7 +1698,7 @@ var _ = Describe("FeatureStore Controller", func() { err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) - Expect(resource.Status.Conditions).To(HaveLen(7)) + Expect(resource.Status.Conditions).To(HaveLen(8)) cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ReadyType) Expect(cond).ToNot(BeNil()) @@ -1698,7 +1708,9 @@ var _ = Describe("FeatureStore Controller", func() { Expect(cond.Message).To(Equal("Error: Object " + resource.Namespace + "/" + deploy.Name + " is already owned by another Service controller " + name)) cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType) - Expect(cond).To(BeNil()) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Message).To(Equal(feastdevv1.KubernetesAuthzReadyMessage)) cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType) Expect(cond).ToNot(BeNil()) @@ -1843,7 +1855,7 @@ func getFeatureStoreYamlEnvVar(envs []corev1.EnvVar) *corev1.EnvVar { func noAuthzConfig() services.AuthzConfig { return services.AuthzConfig{ - Type: services.NoAuthAuthType, + Type: services.KubernetesAuthType, } } diff --git a/infra/feast-operator/internal/controller/services/repo_config.go b/infra/feast-operator/internal/controller/services/repo_config.go index b4a01b312b1..bc0009fc51e 100644 --- a/infra/feast-operator/internal/controller/services/repo_config.go +++ b/infra/feast-operator/internal/controller/services/repo_config.go @@ -620,7 +620,11 @@ func getRepoConfig(featureStore *feastdevv1.FeatureStore) RepoConfig { status := featureStore.Status repoConfig := initRepoConfig(status.Applied.FeastProject) if status.Applied.AuthzConfig != nil { - if status.Applied.AuthzConfig.KubernetesAuthz != nil { + if status.Applied.AuthzConfig.NoAuth != nil && *status.Applied.AuthzConfig.NoAuth { + repoConfig.AuthzConfig = AuthzConfig{ + Type: NoAuthAuthType, + } + } else if status.Applied.AuthzConfig.KubernetesAuthz != nil { repoConfig.AuthzConfig = AuthzConfig{ Type: KubernetesAuthType, } @@ -766,7 +770,7 @@ var defaultOfflineStoreConfig = OfflineStoreConfig{ } var defaultAuthzConfig = AuthzConfig{ - Type: NoAuthAuthType, + Type: KubernetesAuthType, } // getCertificatePath returns the appropriate certificate path based on whether a custom CA bundle is available diff --git a/infra/feast-operator/internal/controller/services/repo_config_test.go b/infra/feast-operator/internal/controller/services/repo_config_test.go index e87efdf7dec..01bde57db24 100644 --- a/infra/feast-operator/internal/controller/services/repo_config_test.go +++ b/infra/feast-operator/internal/controller/services/repo_config_test.go @@ -50,7 +50,7 @@ var _ = Describe("Repo Config", func() { repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) - Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) + Expect(repoConfig.AuthzConfig.Type).To(Equal(KubernetesAuthType)) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig)) Expect(repoConfig.OnlineStore).To(Equal(expectedOnlineConfig)) Expect(repoConfig.Registry).To(Equal(expectedRegistryConfig)) @@ -78,7 +78,7 @@ var _ = Describe("Repo Config", func() { repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) - Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) + Expect(repoConfig.AuthzConfig.Type).To(Equal(KubernetesAuthType)) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig)) Expect(repoConfig.OnlineStore).To(Equal(expectedOnlineConfig)) Expect(repoConfig.Registry).To(Equal(expectedRegistryConfig)) @@ -101,7 +101,7 @@ var _ = Describe("Repo Config", func() { repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.OfflineStore).To(Equal(defaultOfflineStoreConfig)) - Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) + Expect(repoConfig.AuthzConfig.Type).To(Equal(KubernetesAuthType)) Expect(repoConfig.Registry).To(Equal(expectedRegistryConfig)) Expect(repoConfig.OnlineStore).To(Equal(expectedOnlineConfig)) @@ -119,7 +119,7 @@ var _ = Describe("Repo Config", func() { ApplyDefaultsToStatus(featureStore) repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) - Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) + Expect(repoConfig.AuthzConfig.Type).To(Equal(KubernetesAuthType)) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig)) Expect(repoConfig.OnlineStore).To(Equal(expectedOnlineConfig)) Expect(repoConfig.Registry).To(Equal(emptyRegistryConfig)) @@ -167,7 +167,7 @@ var _ = Describe("Repo Config", func() { repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) - Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) + Expect(repoConfig.AuthzConfig.Type).To(Equal(KubernetesAuthType)) Expect(repoConfig.OfflineStore).To(Equal(expectedOfflineConfig)) Expect(repoConfig.OnlineStore).To(Equal(expectedOnlineConfig)) Expect(repoConfig.Registry).To(Equal(expectedRegistryConfig)) @@ -197,7 +197,25 @@ var _ = Describe("Repo Config", func() { Expect(repoConfig.OnlineStore).To(Equal(defaultOnlineStoreConfig(featureStore))) Expect(repoConfig.Registry).To(Equal(defaultRegistryConfig(featureStore))) + By("Having noAuth explicitly set") + featureStore = minimalFeatureStore() + featureStore.Spec.AuthzConfig = &feastdevv1.AuthzConfig{ + NoAuth: boolPtr(true), + } + ApplyDefaultsToStatus(featureStore) + repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) + By("Having oidc authorization with Secret") + featureStore = minimalFeatureStore() + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + OfflineStore: &feastdevv1.OfflineStore{}, + OnlineStore: &feastdevv1.OnlineStore{}, + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{}, + }, + } featureStore.Spec.AuthzConfig = &feastdevv1.AuthzConfig{ OidcAuthz: &feastdevv1.OidcAuthz{ SecretRef: &corev1.LocalObjectReference{ diff --git a/infra/feast-operator/internal/controller/services/util.go b/infra/feast-operator/internal/controller/services/util.go index 84951f2077b..76290fe9075 100644 --- a/infra/feast-operator/internal/controller/services/util.go +++ b/infra/feast-operator/internal/controller/services/util.go @@ -91,6 +91,7 @@ func ApplyDefaultsToStatus(cr *feastdevv1.FeatureStore) { cr.Status.FeastVersion = feastversion.FeastVersion applied := &cr.Status.Applied + applyDefaultAuthzConfig(applied) if applied.FeastProjectDir == nil { applied.FeastProjectDir = &feastdevv1.FeastProjectDir{ Init: &feastdevv1.FeastInitOptions{}, @@ -201,6 +202,14 @@ func ApplyDefaultsToStatus(cr *feastdevv1.FeatureStore) { setDefaultCronJobConfigs(applied.CronJob) } +func applyDefaultAuthzConfig(applied *feastdevv1.FeatureStoreSpec) { + if applied.AuthzConfig == nil { + applied.AuthzConfig = &feastdevv1.AuthzConfig{ + KubernetesAuthz: &feastdevv1.KubernetesAuthz{}, + } + } +} + func setDefaultCtrConfigs(defaultConfigs *feastdevv1.DefaultCtrConfigs, defaultImage string) { if defaultConfigs.Image == nil { img := defaultImage diff --git a/infra/feast-operator/test/api/featurestore_types_test.go b/infra/feast-operator/test/api/featurestore_types_test.go index 00312e0fabb..e2f6f1c204e 100644 --- a/infra/feast-operator/test/api/featurestore_types_test.go +++ b/infra/feast-operator/test/api/featurestore_types_test.go @@ -599,16 +599,17 @@ var _ = Describe("FeatureStore API", func() { }) Context("When omitting the AuthzConfig PvcConfig", func() { _, featurestore := initContext() - It("should keep an empty AuthzConfig", func() { + It("should default to Kubernetes AuthzConfig", func() { resource := featurestore services.ApplyDefaultsToStatus(resource) - Expect(resource.Status.Applied.AuthzConfig).To(BeNil()) + Expect(resource.Status.Applied.AuthzConfig).NotTo(BeNil()) + Expect(resource.Status.Applied.AuthzConfig.KubernetesAuthz).NotTo(BeNil()) }) }) Context("When configuring the AuthzConfig", func() { ctx, featurestore := initContext() It("should fail when both kubernetes and oidc settings are given", func() { - attemptInvalidCreationAndAsserts(ctx, authzConfigWithOidc(authzConfigWithKubernetes(featurestore)), "One selection required between kubernetes or oidc") + attemptInvalidCreationAndAsserts(ctx, authzConfigWithOidc(authzConfigWithKubernetes(featurestore)), "One selection required between kubernetes, oidc, or noAuth") }) }) diff --git a/sdk/python/feast/permissions/enforcer.py b/sdk/python/feast/permissions/enforcer.py index 4db0241ef5a..c72c25bb9f1 100644 --- a/sdk/python/feast/permissions/enforcer.py +++ b/sdk/python/feast/permissions/enforcer.py @@ -23,7 +23,7 @@ def enforce_policy( Define the logic to apply the configured permissions when a given action is requested on a protected resource. - If no permissions are defined, the result is to deny the execution. + If no permissions are defined, all authenticated users are granted full access. Args: permissions: The configured set of `Permission`. @@ -40,12 +40,17 @@ def enforce_policy( FeastPermissionError: If the current user is not authorized to eecute the requested actions on the given resources (and `filter_only` is `False`). """ if not permissions: - # If no permissions are defined, deny access to all resources - # This is a security measure to prevent unauthorized access - logger.warning("No permissions defined - denying access to all resources") - raise FeastPermissionError( - "Permissions are not defined - access denied for all resources" + if not user.username: + raise FeastPermissionError( + "No permissions defined and no authenticated user context. " + "Access denied." + ) + logger.warning( + "No permissions defined in registry. All authenticated users are " + "granted full access. Define permissions via permissions.py and " + "'feast apply' to enforce fine-grained authorization." ) + return resources _permitted_resources: list[FeastObject] = [] for resource in resources: diff --git a/sdk/python/feast/permissions/server/utils.py b/sdk/python/feast/permissions/server/utils.py index cd72ae58204..e7ace754d99 100644 --- a/sdk/python/feast/permissions/server/utils.py +++ b/sdk/python/feast/permissions/server/utils.py @@ -73,6 +73,11 @@ def init_security_manager(auth_type: AuthManagerType, fs: "feast.FeatureStore"): registry: The feature store registry. """ if auth_type == AuthManagerType.NONE: + logger.warning( + "Running with auth_type=no_auth. All endpoints are unauthenticated. " + "This is suitable for local development only. For production " + "deployments, configure 'kubernetes' or 'oidc' authentication." + ) no_security_manager() else: # TODO permissions from registry diff --git a/sdk/python/tests/integration/permissions/auth/server/test_auth_registry_server.py b/sdk/python/tests/integration/permissions/auth/server/test_auth_registry_server.py index 40b506cd4db..7a236f590ff 100644 --- a/sdk/python/tests/integration/permissions/auth/server/test_auth_registry_server.py +++ b/sdk/python/tests/integration/permissions/auth/server/test_auth_registry_server.py @@ -150,11 +150,6 @@ def _test_get_historical_features(client_fs: FeatureStore): def _test_get_entity(client_fs: FeatureStore, permissions: list[Permission]): - if _is_auth_enabled(client_fs) and len(permissions) == 0: - with pytest.raises(FeastPermissionError): - client_fs.get_entity("driver") - return - if not _is_auth_enabled(client_fs) or _is_permission_enabled( client_fs, permissions, read_entities_perm ): @@ -169,11 +164,6 @@ def _test_get_entity(client_fs: FeatureStore, permissions: list[Permission]): def _test_list_entities(client_fs: FeatureStore, permissions: list[Permission]): - if _is_auth_enabled(client_fs) and len(permissions) == 0: - with pytest.raises(FeastPermissionError): - client_fs.list_entities() - return - if _is_auth_enabled(client_fs) and _permissions_exist_in_permission_list( [invalid_list_entities_perm], permissions ): @@ -195,8 +185,7 @@ def _test_list_entities(client_fs: FeatureStore, permissions: list[Permission]): def _no_permission_retrieved(permissions: list[Permission]) -> bool: - # With security-first approach, no permissions means access should be denied - return False + return len(permissions) == 0 def _test_list_permissions( @@ -208,10 +197,6 @@ def _test_list_permissions( with pytest.raises(Exception): client_fs.list_permissions() return [] - elif _is_auth_enabled(client_fs) and len(applied_permissions) == 0: - with pytest.raises(FeastPermissionError): - client_fs.list_permissions() - return [] else: permissions = client_fs.list_permissions() @@ -258,11 +243,6 @@ def _is_auth_enabled(client_fs: FeatureStore) -> bool: def _test_get_fv(client_fs: FeatureStore, permissions: list[Permission]): - if _is_auth_enabled(client_fs) and len(permissions) == 0: - with pytest.raises(FeastPermissionError): - client_fs.get_feature_view("driver_hourly_stats") - return - if not _is_auth_enabled(client_fs) or _is_permission_enabled( client_fs, permissions, read_fv_perm ): @@ -283,10 +263,6 @@ def _test_list_fvs(client_fs: FeatureStore, permissions: list[Permission]): with pytest.raises(Exception): client_fs.list_feature_views() return [] - elif _is_auth_enabled(client_fs) and len(permissions) == 0: - with pytest.raises(FeastPermissionError): - client_fs.list_feature_views() - return [] else: fvs = client_fs.list_feature_views() for fv in fvs: @@ -317,15 +293,13 @@ def _is_permission_enabled( permissions: list[Permission], permission: Permission, ): - # With security-first approach, if no permissions are defined, access should be denied if not _is_auth_enabled(client_fs): - return True # No auth enabled, allow access + return True - # If auth is enabled but no permissions are defined, deny access (security-first) + # No permissions defined = full access for authenticated users if len(permissions) == 0: - return False + return True - # Check if the specific permission exists return _permissions_exist_in_permission_list( [read_permissions_perm, permission], permissions ) diff --git a/sdk/python/tests/integration/rest_api/resource/feast_config_credit_scoring.yaml b/sdk/python/tests/integration/rest_api/resource/feast_config_credit_scoring.yaml index f75fb4c031e..ce8d29b0696 100644 --- a/sdk/python/tests/integration/rest_api/resource/feast_config_credit_scoring.yaml +++ b/sdk/python/tests/integration/rest_api/resource/feast_config_credit_scoring.yaml @@ -20,6 +20,8 @@ metadata: namespace: test-ns-feast-rest spec: feastProject: credit_scoring_local + authz: + noAuth: true feastProjectDir: git: url: https://github.com/feast-dev/feast-credit-score-local-tutorial.git diff --git a/sdk/python/tests/integration/rest_api/resource/feast_config_driver_ranking.yaml b/sdk/python/tests/integration/rest_api/resource/feast_config_driver_ranking.yaml index 6341a6e4eed..90cc4e54d85 100644 --- a/sdk/python/tests/integration/rest_api/resource/feast_config_driver_ranking.yaml +++ b/sdk/python/tests/integration/rest_api/resource/feast_config_driver_ranking.yaml @@ -5,6 +5,8 @@ metadata: namespace: test-ns-feast-rest spec: feastProject: driver_ranking + authz: + noAuth: true feastProjectDir: init: template: local diff --git a/sdk/python/tests/integration/rest_api/resource/feast_config_rhoai.yaml b/sdk/python/tests/integration/rest_api/resource/feast_config_rhoai.yaml index 247f23acabf..33cefcea519 100644 --- a/sdk/python/tests/integration/rest_api/resource/feast_config_rhoai.yaml +++ b/sdk/python/tests/integration/rest_api/resource/feast_config_rhoai.yaml @@ -15,6 +15,8 @@ metadata: namespace: NAMESPACE_PLACEHOLDER spec: feastProject: s3_features + authz: + noAuth: true services: onlineStore: server: