diff --git a/examples/README.md b/examples/README.md index ab5288dd5ad..6dac867be43 100644 --- a/examples/README.md +++ b/examples/README.md @@ -18,4 +18,5 @@ The following examples illustrate various **Feast** use cases to enhance underst The examples below showcase how to deploy and manage **Feast on Kubernetes** using the **Feast Go Operator**. 1. **[Operator Quickstart](operator-quickstart)**: Demonstrates how to install and use Feast on Kubernetes with the Feast Go Operator. -1. **[Operator Quickstart with Postgres in TLS](operator-postgres-tls-demo)**: Demonstrates installing and configuring Feast with PostgreSQL in TLS mode on Kubernetes using the Feast Go Operator, with an emphasis on volumes and VolumeMounts support. +1. **[Operator Quickstart with Postgres in TLS](operator-postgres-tls-demo)**: Demonstrates installing and configuring Feast with PostgreSQL in TLS mode on Kubernetes using the Feast Go Operator, with an emphasis on volumes and VolumeMounts support. +1. **[Operator RBAC with Kubernetes](operator-rbac)**: Demonstrates the Feast RBAC example on Kubernetes using the Feast Operator. diff --git a/examples/operator-rbac/03-uninstall.ipynb b/examples/operator-rbac/03-uninstall.ipynb new file mode 100644 index 00000000000..f9c794c03f8 --- /dev/null +++ b/examples/operator-rbac/03-uninstall.ipynb @@ -0,0 +1,175 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "## Uninstall", + "id": "bd1a081f3f7f5752" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Uninstall the Operator and all Feast related objects##", + "id": "1175f3d6c5ee9bf0" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-05T19:09:52.349677Z", + "start_time": "2025-03-05T19:09:46.308482Z" + } + }, + "cell_type": "code", + "source": [ + "!kubectl delete -f ../../infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml\n", + "!kubectl delete -f ../../infra/feast-operator/dist/install.yaml" + ], + "id": "f4b4c6fa4a1fe0a8", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "featurestore.feast.dev \"sample-kubernetes-auth\" deleted\r\n", + "namespace \"feast-operator-system\" deleted\r\n", + "customresourcedefinition.apiextensions.k8s.io \"featurestores.feast.dev\" deleted\r\n", + "serviceaccount \"feast-operator-controller-manager\" deleted\r\n", + "role.rbac.authorization.k8s.io \"feast-operator-leader-election-role\" deleted\r\n", + "clusterrole.rbac.authorization.k8s.io \"feast-operator-featurestore-editor-role\" deleted\r\n", + "clusterrole.rbac.authorization.k8s.io \"feast-operator-featurestore-viewer-role\" deleted\r\n", + "clusterrole.rbac.authorization.k8s.io \"feast-operator-manager-role\" deleted\r\n", + "clusterrole.rbac.authorization.k8s.io \"feast-operator-metrics-auth-role\" deleted\r\n", + "clusterrole.rbac.authorization.k8s.io \"feast-operator-metrics-reader\" deleted\r\n", + "rolebinding.rbac.authorization.k8s.io \"feast-operator-leader-election-rolebinding\" deleted\r\n", + "clusterrolebinding.rbac.authorization.k8s.io \"feast-operator-manager-rolebinding\" deleted\r\n", + "clusterrolebinding.rbac.authorization.k8s.io \"feast-operator-metrics-auth-rolebinding\" deleted\r\n", + "service \"feast-operator-controller-manager-metrics-service\" deleted\r\n", + "deployment.apps \"feast-operator-controller-manager\" deleted\r\n" + ] + } + ], + "execution_count": 6 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Uninstall Client Related Objects", + "id": "2a2aa884aeddfb99" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-05T19:09:54.655575Z", + "start_time": "2025-03-05T19:09:53.553918Z" + } + }, + "cell_type": "code", + "source": [ + "!echo \"Deleting RoleBindings...\"\n", + "!kubectl delete rolebinding feast-user-rolebinding -n feast --ignore-not-found\n", + "!kubectl delete rolebinding feast-admin-rolebinding -n feast --ignore-not-found\n", + "\n", + "!echo \"Deleting ServiceAccounts...\"\n", + "!kubectl delete serviceaccount feast-user-sa -n feast --ignore-not-found\n", + "!kubectl delete serviceaccount feast-admin-sa -n feast --ignore-not-found\n", + "!kubectl delete serviceaccount feast-unauthorized-user-sa -n feast --ignore-not-found\n" + ], + "id": "6ce30879d64bbd06", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Deleting RoleBindings...\r\n", + "rolebinding.rbac.authorization.k8s.io \"feast-user-rolebinding\" deleted\r\n", + "rolebinding.rbac.authorization.k8s.io \"feast-admin-rolebinding\" deleted\r\n", + "Deleting ServiceAccounts...\r\n", + "serviceaccount \"feast-user-sa\" deleted\r\n", + "serviceaccount \"feast-admin-sa\" deleted\r\n", + "serviceaccount \"feast-unauthorized-user-sa\" deleted\r\n" + ] + } + ], + "execution_count": 7 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Ensure everything has been removed, or is in the process of being terminated.", + "id": "638421caa8ff849e" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-05T19:09:59.868383Z", + "start_time": "2025-03-05T19:09:59.611048Z" + } + }, + "cell_type": "code", + "source": "!kubectl get all -n feast\n", + "id": "587eb85352a8a353", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "No resources found in feast namespace.\r\n" + ] + } + ], + "execution_count": 8 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-05T19:10:07.846749Z", + "start_time": "2025-03-05T19:10:02.561070Z" + } + }, + "cell_type": "code", + "source": "!kubectl delete namespace feast", + "id": "7a0ce2d9e4a92828", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "namespace \"feast\" deleted\r\n" + ] + } + ], + "execution_count": 9 + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "", + "id": "10707783148c5f8d" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/operator-rbac/1-setup-operator-rbac.ipynb b/examples/operator-rbac/1-setup-operator-rbac.ipynb new file mode 100644 index 00000000000..69cc285a01c --- /dev/null +++ b/examples/operator-rbac/1-setup-operator-rbac.ipynb @@ -0,0 +1,760 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Feast Operator with RBAC Configuration\n", + "## Objective\n", + "\n", + "This demo provides a reference implementation of a runbook on how to enable Role-Based Access Control (RBAC) for Feast using the Feast Operator with the Kubernetes authentication type. This serves as useful reference material for a cluster admin / MLOps engineer.\n", + "\n", + "The demo steps include deploying the Feast Operator, creating Feast instances with server components (registry, offline store, online store), and Feast client testing locally. The goal is to ensure secure access control for Feast instances deployed by the Feast Operator.\n", + " \n", + "Please read these reference documents for understanding the Feast RBAC framework.\n", + "- [RBAC Architecture](https://docs.feast.dev/v/master/getting-started/architecture/rbac) \n", + "- [RBAC Permission](https://docs.feast.dev/v/master/getting-started/concepts/permission).\n", + "- [RBAC Authorization Manager](https://docs.feast.dev/v/master/getting-started/components/authz_manager)\n" + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Deployment Architecture\n", + "In this notebook, we will deploy a distributed topology of Feast services, which includes:\n", + "\n", + "* `Registry Server`: Handles metadata storage for feature definitions.\n", + "* `Online Store Server`: Uses the `Registry Server` to query metadata and is responsible for low-latency serving of features.\n", + "* `Offline Store Server`: Uses the `Registry Server` to query metadata and provides access to batch data for historical feature retrieval.\n", + "\n", + "Additionally, we will cover:\n", + "* RBAC Configuration with Kubernetes Authentication for Feast resources." + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Prerequisites\n", + "* Kubernetes Cluster\n", + "* [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl) Kubernetes CLI tool." + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Install Prerequisites\n", + "The following commands install and configure all the prerequisites on a MacOS environment. You can find the\n", + "equivalent instructions on the offical documentation pages:\n", + "* Install the `kubectl` cli.\n", + "* Install Kubernetes and Container runtime (e.g. [Colima](https://github.com/abiosoft/colima)).\n", + " * Alternatively, authenticate to an existing Kubernetes or OpenShift cluster.\n", + " \n", + "```bash\n", + "brew install colima kubectl\n", + "colima start -r containerd -k -m 3 -d 100 -c 2 --cpu-type max -a x86_64\n", + "colima list\n", + "```" + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T18:27:31.474254Z", + "start_time": "2025-03-06T18:27:31.012088Z" + } + }, + "cell_type": "code", + "source": [ + "!kubectl create ns feast\n", + "!kubectl config set-context --current --namespace feast" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "namespace/feast created\r\n", + "Context \"kind-kind\" modified.\r\n" + ] + } + ], + "execution_count": 1 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Validate the cluster setup:" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T18:32:23.198122Z", + "start_time": "2025-03-06T18:32:22.930547Z" + } + }, + "cell_type": "code", + "source": "!kubectl get ns feast", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "NAME STATUS AGE\r\n", + "feast Active 4m52s\r\n" + ] + } + ], + "execution_count": 2 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Feast Admin Steps:\n", + "Feast Admins or MLOps Engineers may require Kubernetes Cluster Admin roles when working with OpenShift or Kubernetes clusters. Below is the list of steps Required to set up Feast RBAC with the Operator by an Admin or MLOps Engineer.\n", + "\n", + "1. **Install the Feast Operator**\n", + "2. **Install the Feast services via FeatureStore CR**\n", + "3. **Configure the RBAC Permissions**\n", + "4. **Perform Feast Apply**\n", + "5. **Setting Service Account and Role Binding**\n", + "\n", + "## Install the Feast Operator" + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T18:32:40.721042Z", + "start_time": "2025-03-06T18:32:28.484245Z" + } + }, + "cell_type": "code", + "source": [ + "## Use this install command from a stable branch \n", + "!kubectl apply -f ../../infra/feast-operator/dist/install.yaml\n", + "\n", + "## OR, for the latest code/builds, use one the following commands from the 'master' branch\n", + "# !make -C ../../infra/feast-operator install deploy IMG=quay.io/feastdev-ci/feast-operator:develop FS_IMG=quay.io/feastdev-ci/feature-server:develop\n", + "# !make -C ../../infra/feast-operator install deploy IMG=quay.io/feastdev-ci/feast-operator:$(git rev-parse HEAD) FS_IMG=quay.io/feastdev-ci/feature-server:$(git rev-parse HEAD)\n", + "\n", + "!kubectl wait --for=condition=available --timeout=5m deployment/feast-operator-controller-manager -n feast-operator-system" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "namespace/feast-operator-system created\r\n", + "customresourcedefinition.apiextensions.k8s.io/featurestores.feast.dev created\r\n", + "serviceaccount/feast-operator-controller-manager created\r\n", + "role.rbac.authorization.k8s.io/feast-operator-leader-election-role created\r\n", + "clusterrole.rbac.authorization.k8s.io/feast-operator-featurestore-editor-role created\r\n", + "clusterrole.rbac.authorization.k8s.io/feast-operator-featurestore-viewer-role created\r\n", + "clusterrole.rbac.authorization.k8s.io/feast-operator-manager-role created\r\n", + "clusterrole.rbac.authorization.k8s.io/feast-operator-metrics-auth-role created\r\n", + "clusterrole.rbac.authorization.k8s.io/feast-operator-metrics-reader created\r\n", + "rolebinding.rbac.authorization.k8s.io/feast-operator-leader-election-rolebinding created\r\n", + "clusterrolebinding.rbac.authorization.k8s.io/feast-operator-manager-rolebinding created\r\n", + "clusterrolebinding.rbac.authorization.k8s.io/feast-operator-metrics-auth-rolebinding created\r\n", + "service/feast-operator-controller-manager-metrics-service created\r\n", + "deployment.apps/feast-operator-controller-manager created\r\n", + "deployment.apps/feast-operator-controller-manager condition met\r\n" + ] + } + ], + "execution_count": 3 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Install the Feast services via FeatureStore CR\n", + "Next, we'll use the running Feast Operator to install the feast services with Server components online, offline, registry with kubernetes Authorization set. Apply the included [reference deployment](../../infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml) to install and configure Feast with kubernetes Authorization ." + ] + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T18:34:39.847211Z", + "start_time": "2025-03-06T18:34:39.378680Z" + } + }, + "source": [ + "!cat ../../infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml\n", + "!kubectl apply -f ../../infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml -n feast" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "apiVersion: feast.dev/v1alpha1\r\n", + "kind: FeatureStore\r\n", + "metadata:\r\n", + " name: sample-kubernetes-auth\r\n", + "spec:\r\n", + " feastProject: feast_rbac\r\n", + " authz:\r\n", + " kubernetes:\r\n", + " roles:\r\n", + " - feast-writer\r\n", + " - feast-reader\r\n", + " services:\r\n", + " offlineStore:\r\n", + " server: {}\r\n", + " onlineStore:\r\n", + " server: {}\r\n", + " registry:\r\n", + " local:\r\n", + " server: {}\r\n", + " ui: {}\r\n", + "featurestore.feast.dev/sample-kubernetes-auth created\r\n" + ] + } + ], + "execution_count": 4 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Validate the running FeatureStore deployment\n", + "Validate the deployment status." + ] + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T18:35:05.202176Z", + "start_time": "2025-03-06T18:35:02.498106Z" + } + }, + "source": [ + "!kubectl get all\n", + "!kubectl wait --for=condition=available --timeout=8m deployment/feast-sample-kubernetes-auth" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "NAME READY STATUS RESTARTS AGE\r\n", + "pod/feast-sample-kubernetes-auth-774f6df8df-95nc6 0/4 Running 0 22s\r\n", + "\r\n", + "NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE\r\n", + "service/feast-sample-kubernetes-auth-offline ClusterIP 10.96.38.230 80/TCP 22s\r\n", + "service/feast-sample-kubernetes-auth-online ClusterIP 10.96.140.194 80/TCP 22s\r\n", + "service/feast-sample-kubernetes-auth-registry ClusterIP 10.96.140.31 80/TCP 22s\r\n", + "service/feast-sample-kubernetes-auth-ui ClusterIP 10.96.26.21 80/TCP 22s\r\n", + "\r\n", + "NAME READY UP-TO-DATE AVAILABLE AGE\r\n", + "deployment.apps/feast-sample-kubernetes-auth 0/1 1 0 22s\r\n", + "\r\n", + "NAME DESIRED CURRENT READY AGE\r\n", + "replicaset.apps/feast-sample-kubernetes-auth-774f6df8df 1 1 0 22s\r\n", + "deployment.apps/feast-sample-kubernetes-auth condition met\r\n" + ] + } + ], + "execution_count": 5 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Validate that the FeatureStore CR is in a `Ready` state." + ] + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T18:35:55.728523Z", + "start_time": "2025-03-06T18:35:55.452894Z" + } + }, + "source": [ + "!kubectl get feast" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "NAME STATUS AGE\r\n", + "sample-kubernetes-auth Ready 76s\r\n" + ] + } + ], + "execution_count": 6 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Configure the RBAC Permissions\n", + "As we have created Kubernetes roles in FeatureStore CR to manage access control for Feast objects, the Python script `permissions_apply.py` will apply these roles to configure permissions. See the detailed code example below with comments." + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T18:37:17.062072Z", + "start_time": "2025-03-06T18:37:16.930026Z" + } + }, + "cell_type": "code", + "source": [ + "#view the permissions \n", + "!cat permissions_apply.py" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "# Necessary modules for permissions and policies in Feast for RBAC\r\n", + "from feast.feast_object import ALL_RESOURCE_TYPES\r\n", + "from feast.permissions.action import READ, AuthzedAction, ALL_ACTIONS\r\n", + "from feast.permissions.permission import Permission\r\n", + "from feast.permissions.policy import RoleBasedPolicy\r\n", + "\r\n", + "# Define K8s roles same as created with FeatureStore CR\r\n", + "admin_roles = [\"feast-writer\"] # Full access (can create, update, delete ) Feast Resources\r\n", + "user_roles = [\"feast-reader\"] # Read-only access on Feast Resources\r\n", + "\r\n", + "# User permissions (feast_user_permission)\r\n", + "# - Grants read and describing Feast objects access\r\n", + "user_perm = Permission(\r\n", + " name=\"feast_user_permission\",\r\n", + " types=ALL_RESOURCE_TYPES,\r\n", + " policy=RoleBasedPolicy(roles=user_roles),\r\n", + " actions=[AuthzedAction.DESCRIBE] + READ # Read access (READ_ONLINE, READ_OFFLINE) + describe other Feast Resources.\r\n", + ")\r\n", + "\r\n", + "# Admin permissions (feast_admin_permission)\r\n", + "# - Grants full control over all resources\r\n", + "admin_perm = Permission(\r\n", + " name=\"feast_admin_permission\",\r\n", + " types=ALL_RESOURCE_TYPES,\r\n", + " policy=RoleBasedPolicy(roles=admin_roles),\r\n", + " actions=ALL_ACTIONS # Full permissions: CREATE, UPDATE, DELETE, READ, WRITE\r\n", + ")\r\n" + ] + } + ], + "execution_count": 7 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T18:37:31.662484Z", + "start_time": "2025-03-06T18:37:31.139869Z" + } + }, + "cell_type": "code", + "source": [ + "# Copy the Permissions to the pods under feature_repo directory\n", + "!kubectl cp permissions_apply.py $(kubectl get pods -l 'feast.dev/name=sample-kubernetes-auth' -ojsonpath=\"{.items[*].metadata.name}\"):/feast-data/feast_rbac/feature_repo -c online" + ], + "outputs": [], + "execution_count": 8 + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T18:37:38.003082Z", + "start_time": "2025-03-06T18:37:37.662378Z" + } + }, + "source": [ + "#view the feature_store.yaml configuration \n", + "!kubectl exec deploy/feast-sample-kubernetes-auth -itc online -- cat feature_store.yaml" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "project: feast_rbac\r\n", + "provider: local\r\n", + "offline_store:\r\n", + " type: dask\r\n", + "online_store:\r\n", + " path: /feast-data/online_store.db\r\n", + " type: sqlite\r\n", + "registry:\r\n", + " path: /feast-data/registry.db\r\n", + " registry_type: file\r\n", + "auth:\r\n", + " type: kubernetes\r\n", + "entity_key_serialization_version: 3\r\n" + ] + } + ], + "execution_count": 9 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Apply the Permissions and Feast Object to Registry" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T18:37:56.131390Z", + "start_time": "2025-03-06T18:37:45.483916Z" + } + }, + "cell_type": "code", + "source": "!kubectl exec deploy/feast-sample-kubernetes-auth -itc online -- feast apply", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ": MADV_DONTNEED does not work (memset will be used instead)\r\n", + ": (This is the expected behaviour if you are running under QEMU)\r\n", + "/opt/app-root/lib64/python3.11/site-packages/feast/feature_view.py:48: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\r\n", + " DUMMY_ENTITY = Entity(\r\n", + "/opt/app-root/lib64/python3.11/site-packages/pydantic/_internal/_fields.py:192: UserWarning: Field name \"vector_enabled\" in \"SqliteOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\r\n", + " warnings.warn(\r\n", + "/opt/app-root/lib64/python3.11/site-packages/pydantic/_internal/_fields.py:192: UserWarning: Field name \"vector_len\" in \"SqliteOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\r\n", + " warnings.warn(\r\n", + "/feast-data/feast_rbac/feature_repo/example_repo.py:27: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'driver'.\r\n", + " driver = Entity(name=\"driver\", join_keys=[\"driver_id\"])\r\n", + "Applying changes for project feast_rbac\r\n", + "/opt/app-root/lib64/python3.11/site-packages/feast/feature_store.py:579: RuntimeWarning: On demand feature view is an experimental feature. This API is stable, but the functionality does not scale well for offline retrieval\r\n", + " warnings.warn(\r\n", + "Created project \u001B[1m\u001B[32mfeast_rbac\u001B[0m\r\n", + "Created entity \u001B[1m\u001B[32mdriver\u001B[0m\r\n", + "Created feature view \u001B[1m\u001B[32mdriver_hourly_stats\u001B[0m\r\n", + "Created feature view \u001B[1m\u001B[32mdriver_hourly_stats_fresh\u001B[0m\r\n", + "Created on demand feature view \u001B[1m\u001B[32mtransformed_conv_rate\u001B[0m\r\n", + "Created on demand feature view \u001B[1m\u001B[32mtransformed_conv_rate_fresh\u001B[0m\r\n", + "Created feature service \u001B[1m\u001B[32mdriver_activity_v2\u001B[0m\r\n", + "Created feature service \u001B[1m\u001B[32mdriver_activity_v1\u001B[0m\r\n", + "Created feature service \u001B[1m\u001B[32mdriver_activity_v3\u001B[0m\r\n", + "Created permission \u001B[1m\u001B[32mfeast_admin_permission\u001B[0m\r\n", + "Created permission \u001B[1m\u001B[32mfeast_user_permission\u001B[0m\r\n", + "\r\n", + "Created sqlite table \u001B[1m\u001B[32mfeast_rbac_driver_hourly_stats_fresh\u001B[0m\r\n", + "Created sqlite table \u001B[1m\u001B[32mfeast_rbac_driver_hourly_stats\u001B[0m\r\n", + "\r\n" + ] + } + ], + "execution_count": 10 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "**List the applied permission details permissions on Feast Resources.**" + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T18:38:45.881715Z", + "start_time": "2025-03-06T18:38:04.170364Z" + } + }, + "source": [ + "!kubectl exec deploy/feast-sample-kubernetes-auth -itc online -- feast permissions list-roles\n", + "!kubectl exec deploy/feast-sample-kubernetes-auth -itc online -- feast permissions list\n", + "!kubectl exec deploy/feast-sample-kubernetes-auth -itc online -- feast permissions describe feast_admin_permission\n", + "!kubectl exec deploy/feast-sample-kubernetes-auth -itc online -- feast permissions describe feast_user_permission" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ": MADV_DONTNEED does not work (memset will be used instead)\r\n", + ": (This is the expected behaviour if you are running under QEMU)\r\n", + "/opt/app-root/lib64/python3.11/site-packages/feast/feature_view.py:48: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\r\n", + " DUMMY_ENTITY = Entity(\r\n", + "/opt/app-root/lib64/python3.11/site-packages/pydantic/_internal/_fields.py:192: UserWarning: Field name \"vector_enabled\" in \"SqliteOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\r\n", + " warnings.warn(\r\n", + "/opt/app-root/lib64/python3.11/site-packages/pydantic/_internal/_fields.py:192: UserWarning: Field name \"vector_len\" in \"SqliteOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\r\n", + " warnings.warn(\r\n", + "+--------------+\r\n", + "| ROLE NAME |\r\n", + "+==============+\r\n", + "| feast-reader |\r\n", + "+--------------+\r\n", + "| feast-writer |\r\n", + "+--------------+\r\n", + ": MADV_DONTNEED does not work (memset will be used instead)\r\n", + ": (This is the expected behaviour if you are running under QEMU)\r\n", + "/opt/app-root/lib64/python3.11/site-packages/feast/feature_view.py:48: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\r\n", + " DUMMY_ENTITY = Entity(\r\n", + "/opt/app-root/lib64/python3.11/site-packages/pydantic/_internal/_fields.py:192: UserWarning: Field name \"vector_enabled\" in \"SqliteOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\r\n", + " warnings.warn(\r\n", + "/opt/app-root/lib64/python3.11/site-packages/pydantic/_internal/_fields.py:192: UserWarning: Field name \"vector_len\" in \"SqliteOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\r\n", + " warnings.warn(\r\n", + "NAME TYPES NAME_PATTERNS ACTIONS ROLES REQUIRED_TAGS\r\n", + "feast_admin_permission Project - CREATE feast-writer -\r\n", + " FeatureView DESCRIBE\r\n", + " OnDemandFeatureView UPDATE\r\n", + " BatchFeatureView DELETE\r\n", + " StreamFeatureView READ_ONLINE\r\n", + " Entity READ_OFFLINE\r\n", + " FeatureService WRITE_ONLINE\r\n", + " DataSource WRITE_OFFLINE\r\n", + " ValidationReference\r\n", + " SavedDataset\r\n", + " Permission\r\n", + "feast_user_permission Project - DESCRIBE feast-reader -\r\n", + " FeatureView READ_OFFLINE\r\n", + " OnDemandFeatureView READ_ONLINE\r\n", + " BatchFeatureView\r\n", + " StreamFeatureView\r\n", + " Entity\r\n", + " FeatureService\r\n", + " DataSource\r\n", + " ValidationReference\r\n", + " SavedDataset\r\n", + " Permission\r\n", + ": MADV_DONTNEED does not work (memset will be used instead)\r\n", + ": (This is the expected behaviour if you are running under QEMU)\r\n", + "/opt/app-root/lib64/python3.11/site-packages/feast/feature_view.py:48: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\r\n", + " DUMMY_ENTITY = Entity(\r\n", + "/opt/app-root/lib64/python3.11/site-packages/pydantic/_internal/_fields.py:192: UserWarning: Field name \"vector_enabled\" in \"SqliteOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\r\n", + " warnings.warn(\r\n", + "/opt/app-root/lib64/python3.11/site-packages/pydantic/_internal/_fields.py:192: UserWarning: Field name \"vector_len\" in \"SqliteOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\r\n", + " warnings.warn(\r\n", + "spec:\r\n", + " name: feast_admin_permission\r\n", + " types:\r\n", + " - PROJECT\r\n", + " - FEATURE_VIEW\r\n", + " - ON_DEMAND_FEATURE_VIEW\r\n", + " - BATCH_FEATURE_VIEW\r\n", + " - STREAM_FEATURE_VIEW\r\n", + " - ENTITY\r\n", + " - FEATURE_SERVICE\r\n", + " - DATA_SOURCE\r\n", + " - VALIDATION_REFERENCE\r\n", + " - SAVED_DATASET\r\n", + " - PERMISSION\r\n", + " actions:\r\n", + " - CREATE\r\n", + " - DESCRIBE\r\n", + " - UPDATE\r\n", + " - DELETE\r\n", + " - READ_ONLINE\r\n", + " - READ_OFFLINE\r\n", + " - WRITE_ONLINE\r\n", + " - WRITE_OFFLINE\r\n", + " policy:\r\n", + " roleBasedPolicy:\r\n", + " roles:\r\n", + " - feast-writer\r\n", + "meta:\r\n", + " createdTimestamp: '2025-03-06T18:37:55.742625Z'\r\n", + " lastUpdatedTimestamp: '2025-03-06T18:37:55.742625Z'\r\n", + "\r\n", + ": MADV_DONTNEED does not work (memset will be used instead)\r\n", + ": (This is the expected behaviour if you are running under QEMU)\r\n", + "/opt/app-root/lib64/python3.11/site-packages/feast/feature_view.py:48: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\r\n", + " DUMMY_ENTITY = Entity(\r\n", + "/opt/app-root/lib64/python3.11/site-packages/pydantic/_internal/_fields.py:192: UserWarning: Field name \"vector_enabled\" in \"SqliteOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\r\n", + " warnings.warn(\r\n", + "/opt/app-root/lib64/python3.11/site-packages/pydantic/_internal/_fields.py:192: UserWarning: Field name \"vector_len\" in \"SqliteOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\r\n", + " warnings.warn(\r\n", + "spec:\r\n", + " name: feast_user_permission\r\n", + " types:\r\n", + " - PROJECT\r\n", + " - FEATURE_VIEW\r\n", + " - ON_DEMAND_FEATURE_VIEW\r\n", + " - BATCH_FEATURE_VIEW\r\n", + " - STREAM_FEATURE_VIEW\r\n", + " - ENTITY\r\n", + " - FEATURE_SERVICE\r\n", + " - DATA_SOURCE\r\n", + " - VALIDATION_REFERENCE\r\n", + " - SAVED_DATASET\r\n", + " - PERMISSION\r\n", + " actions:\r\n", + " - DESCRIBE\r\n", + " - READ_OFFLINE\r\n", + " - READ_ONLINE\r\n", + " policy:\r\n", + " roleBasedPolicy:\r\n", + " roles:\r\n", + " - feast-reader\r\n", + "meta:\r\n", + " createdTimestamp: '2025-03-06T18:37:55.743643Z'\r\n", + " lastUpdatedTimestamp: '2025-03-06T18:37:55.743643Z'\r\n", + "\r\n" + ] + } + ], + "execution_count": 11 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Setting Up Service Account and RoleBinding \n", + "The steps below will:\n", + "- Create **three different ServiceAccounts** for Feast.\n", + "- Assign appropriate **RoleBindings** for access control." + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Test Cases\n", + "| User Type | ServiceAccount | RoleBinding Assigned | Expected Behavior in output |\n", + "|----------------|-----------------------------|----------------------|------------------------------------------------------------|\n", + "| **Read-Only** | `feast-user-sa` | `feast-reader` | Can **read** from the feature store, but **cannot write**. |\n", + "| **Unauthorized** | `feast-unauthorized-user-sa` | _None_ | **Access should be denied** in `test.py`. |\n", + "| **Admin** | `feast-admin-sa` | `feast-writer` | Can **read and write** feature store data. |" + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Setup Read-Only Feast User the ServiceAccount and Role Binding (serviceaccount: feast-user-sa, role: feast-reader)" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T18:42:04.122440Z", + "start_time": "2025-03-06T18:42:03.397214Z" + } + }, + "cell_type": "code", + "source": [ + "# Step 1: Create the ServiceAccount\n", + "!echo \"Creating ServiceAccount: feast-user-sa\"\n", + "!kubectl create serviceaccount feast-user-sa -n feast\n", + "\n", + "# Step 2: Assign RoleBinding (Read-Only Access for Feast)\n", + "!echo \"Assigning Read-Only RoleBinding: feast-user-rolebinding\"\n", + "!kubectl create rolebinding feast-user-rolebinding --role=feast-reader --serviceaccount=feast:feast-user-sa -n feast" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Creating ServiceAccount: feast-user-sa\r\n", + "serviceaccount/feast-user-sa created\r\n", + "Assigning Read-Only RoleBinding: feast-user-rolebinding\r\n", + "rolebinding.rbac.authorization.k8s.io/feast-user-rolebinding created\r\n" + ] + } + ], + "execution_count": 12 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Setup Unauthorized Feast User (serviceaccount: feast-unauthorized-user-sa, role: None)" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T18:42:07.992216Z", + "start_time": "2025-03-06T18:42:07.721628Z" + } + }, + "cell_type": "code", + "source": [ + "# Create the ServiceAccount (Without RoleBinding)\n", + "!echo \"Creating Unauthorized ServiceAccount: feast-unauthorized-user-sa\"\n", + "!kubectl create serviceaccount feast-unauthorized-user-sa -n feast\n" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Creating Unauthorized ServiceAccount: feast-unauthorized-user-sa\r\n", + "serviceaccount/feast-unauthorized-user-sa created\r\n" + ] + } + ], + "execution_count": 13 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Setup Test Admin Feast User (serviceaccount: feast-admin-sa, role: feast-writer)" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T18:42:11.651408Z", + "start_time": "2025-03-06T18:42:11.097231Z" + } + }, + "cell_type": "code", + "source": [ + "# Create the ServiceAccount\n", + "!echo \"Creating ServiceAccount: feast-admin-sa\"\n", + "!kubectl create serviceaccount feast-admin-sa -n feast\n", + "\n", + "# Assign RoleBinding (Admin Access for Feast)\n", + "!echo \"Assigning Admin RoleBinding: feast-admin-rolebinding\"\n", + "!kubectl create rolebinding feast-admin-rolebinding --role=feast-writer --serviceaccount=feast:feast-admin-sa -n feast\n" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Creating ServiceAccount: feast-admin-sa\r\n", + "serviceaccount/feast-admin-sa created\r\n", + "Assigning Admin RoleBinding: feast-admin-rolebinding\r\n", + "rolebinding.rbac.authorization.k8s.io/feast-admin-rolebinding created\r\n" + ] + } + ], + "execution_count": 14 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "[Next Run Client notebook](./2-client.ipynb)" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.11" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/operator-rbac/2-client.ipynb b/examples/operator-rbac/2-client.ipynb new file mode 100644 index 00000000000..cf9d57cb5bc --- /dev/null +++ b/examples/operator-rbac/2-client.ipynb @@ -0,0 +1,828 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Feast Client with RBAC\n", + "### Kubernetes RBAC Authorization\n", + "\n", + "## Feast Role-Based Access Control (RBAC) in Kubernetes \n", + "\n", + "Feast **Role-Based Access Control (RBAC)** in Kubernetes supports authentication both **inside a Kubernetes pod** and for **external clients** using the `LOCAL_K8S_TOKEN` environment variable. \n", + "\n", + "\n", + "### Inside a Kubernetes Pod\n", + "Feast automatically retrieves the Kubernetes ServiceAccount token from:\n", + "```\n", + "/var/run/secrets/kubernetes.io/serviceaccount/token\n", + "```\n", + "This means:\n", + "- No manual configuration is needed inside a pod.\n", + "- The token is mounted automatically and used for authentication.\n", + "- Developer just need create the binding with role and service account accordingly.\n", + "- Code Reference: \n", + "[Feast Kubernetes Auth Client Manager (Pod Token Usage)](https://github.com/feast-dev/feast/blob/master/sdk/python/feast/permissions/client/kubernetes_auth_client_manager.py#L15) \n", + "- Using a service account from a pod [Example](https://github.com/feast-dev/feast/blob/master/examples/rbac-remote/client/k8s/)\n", + "\n", + "### Outside a Kubernetes Pod (External Clients & Local Testing)\n", + " \n", + "If running Feast outside of Kubernetes, authentication requires setting the token manually to the environment variable `LOCAL_K8S_TOKEN` :\n", + "```sh\n", + "export LOCAL_K8S_TOKEN=\"your-service-account-token\"\n", + "```\n", + "\n", + "For more details, refer the user guide: [Kubernetes RBAC Authorization](https://docs.feast.dev/master/getting-started/components/authz_manager#kubernetes-rbac-authorization) \n" + ], + "id": "bb0145c9c1f6ebcc" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Test Cases\n", + "| User Type | ServiceAccount | RoleBinding Assigned | Expected Behavior in output |\n", + "|----------------|-----------------------------|----------------------|------------------------------------------------------------|\n", + "| **Read-Only** | `feast-user-sa` | `feast-reader` | Can **read** from the feature store, but **cannot write**. |\n", + "| **Unauthorized** | `feast-unauthorized-user-sa` | _None_ | **Access should be denied** in `test.py`. |\n", + "| **Admin** | `feast-admin-sa` | `feast-writer` | Can **read and write** feature store data. |" + ], + "id": "160681ba4ab3c2c5" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Feature Store settings", + "id": "6590c081efb1fe3c" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T18:47:45.151296Z", + "start_time": "2025-03-06T18:47:45.024854Z" + } + }, + "cell_type": "code", + "source": "!cat client/feature_store.yaml", + "id": "fac5f67ff391b5cf", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "project: feast_rbac\r\n", + "provider: local\r\n", + "offline_store:\r\n", + " host: localhost\r\n", + " type: remote\r\n", + " port: 8081\r\n", + "online_store:\r\n", + " path: http://localhost:8082\r\n", + " type: remote\r\n", + "registry:\r\n", + " path: localhost:8083\r\n", + " registry_type: remote\r\n", + "auth:\r\n", + " type: kubernetes\r\n", + "entity_key_serialization_version: 3\r\n" + ] + } + ], + "execution_count": 1 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "**The Operator client feature store ConfigMap** containing the `feature_store.yaml `settings. We can retrieve it and port froward to local as we are testing locally.", + "id": "84f73e09711bff9f" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T18:46:36.029308Z", + "start_time": "2025-03-06T18:46:35.712532Z" + } + }, + "cell_type": "code", + "source": "!kubectl get configmap feast-sample-kubernetes-auth-client -n feast -o jsonpath='{.data.feature_store\\.yaml}' ", + "id": "456fb4df46f32380", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "project: feast_rbac\r\n", + "provider: local\r\n", + "offline_store:\r\n", + " host: feast-sample-kubernetes-auth-offline.feast.svc.cluster.local\r\n", + " type: remote\r\n", + " port: 80\r\n", + "online_store:\r\n", + " path: http://feast-sample-kubernetes-auth-online.feast.svc.cluster.local:80\r\n", + " type: remote\r\n", + "registry:\r\n", + " path: feast-sample-kubernetes-auth-registry.feast.svc.cluster.local:80\r\n", + " registry_type: remote\r\n", + "auth:\r\n", + " type: kubernetes\r\n", + "entity_key_serialization_version: 3\r\n" + ] + } + ], + "execution_count": 34 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### The function below is executed to support the preparation of client testing.", + "id": "ae61f4dca31f3466" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Run Port Forwarding for All Services for local testing ", + "id": "28636825ae8f676d" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T18:47:55.237205Z", + "start_time": "2025-03-06T18:47:55.226143Z" + } + }, + "cell_type": "code", + "source": [ + "import subprocess\n", + "\n", + "# Define services and their local ports\n", + "services = {\n", + " \"offline_store\": (\"feast-sample-kubernetes-auth-offline\", 8081),\n", + " \"online_store\": (\"feast-sample-kubernetes-auth-online\", 8082),\n", + " \"registry\": (\"feast-sample-kubernetes-auth-registry\", 8083),\n", + "}\n", + "\n", + "# Start port-forwarding for each service\n", + "port_forward_processes = {}\n", + "for name, (service, local_port) in services.items():\n", + " cmd = f\"kubectl port-forward svc/{service} -n feast {local_port}:80\"\n", + " process = subprocess.Popen(cmd, shell=True)\n", + " port_forward_processes[name] = process\n", + " print(f\"Port forwarding {service} -> localhost:{local_port}\")" + ], + "id": "c014248190863e8a", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Port forwarding feast-sample-kubernetes-auth-offline -> localhost:8081\n", + "Port forwarding feast-sample-kubernetes-auth-online -> localhost:8082\n", + "Port forwarding feast-sample-kubernetes-auth-registry -> localhost:8083\n" + ] + } + ], + "execution_count": 2 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Function to retrieve a Kubernetes service account token and set it as an environment variable", + "id": "c0eccef6379f442c" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T18:48:00.150752Z", + "start_time": "2025-03-06T18:48:00.143370Z" + } + }, + "cell_type": "code", + "source": [ + "import subprocess\n", + "import os\n", + "\n", + "def get_k8s_token(service_account):\n", + " namespace = \"feast\"\n", + "\n", + " if not service_account:\n", + " raise ValueError(\"Service account name is required.\")\n", + "\n", + " result = subprocess.run(\n", + " [\"kubectl\", \"create\", \"token\", service_account, \"-n\", namespace],\n", + " capture_output=True, text=True, check=True\n", + " )\n", + "\n", + " token = result.stdout.strip()\n", + "\n", + " if not token:\n", + " return None # Silently return None if token retrieval fails\n", + "\n", + " os.environ[\"LOCAL_K8S_TOKEN\"] = token\n", + " return \"Token Retrieved: ***** (hidden for security)\"\n" + ], + "id": "70bdbcd7b3fe44", + "outputs": [], + "execution_count": 3 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "**Generating training data**. The following test functions were copied from the `test_workflow.py` template but we added `try` blocks to print only \n", + "the relevant error messages, since we expect to receive errors from the permission enforcement modules." + ], + "id": "8c9e27ec4ed8ca2c" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T20:16:04.254201Z", + "start_time": "2025-03-06T20:16:04.245605Z" + } + }, + "cell_type": "code", + "source": [ + "from feast import FeatureStore\n", + "from feast.data_source import PushMode\n", + "from datetime import datetime\n", + "import pandas as pd\n", + "\n", + "# Initialize Feature Store\n", + "store = FeatureStore(repo_path=\"client\")\n", + "\n", + "def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: bool):\n", + " \"\"\"Fetch historical features for training or batch scoring.\"\"\"\n", + " try:\n", + " entity_df = pd.DataFrame.from_dict(\n", + " {\n", + " \"driver_id\": [1001, 1002, 1003],\n", + " \"event_timestamp\": [\n", + " datetime(2021, 4, 12, 10, 59, 42),\n", + " datetime(2021, 4, 12, 8, 12, 10),\n", + " datetime(2021, 4, 12, 16, 40, 26),\n", + " ],\n", + " \"label_driver_reported_satisfaction\": [1, 5, 3],\n", + " \"val_to_add\": [1, 2, 3],\n", + " \"val_to_add_2\": [10, 20, 30],\n", + " }\n", + " )\n", + " if for_batch_scoring:\n", + " entity_df[\"event_timestamp\"] = pd.to_datetime(\"now\", utc=True)\n", + "\n", + " training_df = store.get_historical_features(\n", + " entity_df=entity_df,\n", + " features=[\n", + " \"driver_hourly_stats:conv_rate\",\n", + " \"driver_hourly_stats:acc_rate\",\n", + " \"driver_hourly_stats:avg_daily_trips\",\n", + " \"transformed_conv_rate:conv_rate_plus_val1\",\n", + " \"transformed_conv_rate:conv_rate_plus_val2\",\n", + " ],\n", + " ).to_df()\n", + " print(f\"Successfully fetched {'batch scoring' if for_batch_scoring else 'training'} historical features:\\n\", training_df.head())\n", + "\n", + " except PermissionError:\n", + " print(\"\\n*** PERMISSION DENIED *** Cannot fetch historical features.\")\n", + " except Exception as e:\n", + " print(f\"Unexpected error while fetching historical features: {e}\")\n", + "\n", + "def fetch_online_features(store: FeatureStore, source: str = \"\"):\n", + " \"\"\"Fetch online features from the feature store.\"\"\"\n", + " try:\n", + " entity_rows = [\n", + " {\n", + " \"driver_id\": 1001,\n", + " \"val_to_add\": 1000,\n", + " \"val_to_add_2\": 2000,\n", + " },\n", + " {\n", + " \"driver_id\": 1002,\n", + " \"val_to_add\": 1001,\n", + " \"val_to_add_2\": 2002,\n", + " },\n", + " ]\n", + " if source == \"feature_service\":\n", + " features_to_fetch = store.get_feature_service(\"driver_activity_v1\")\n", + " elif source == \"push\":\n", + " features_to_fetch = store.get_feature_service(\"driver_activity_v3\")\n", + " else:\n", + " features_to_fetch = [\n", + " \"driver_hourly_stats:acc_rate\",\n", + " \"transformed_conv_rate:conv_rate_plus_val1\",\n", + " \"transformed_conv_rate:conv_rate_plus_val2\",\n", + " ]\n", + "\n", + " returned_features = store.get_online_features(\n", + " features=features_to_fetch,\n", + " entity_rows=entity_rows,\n", + " ).to_dict()\n", + "\n", + " print(f\"Successfully fetched online features {'via feature service' if source else 'directly'}:\\n\")\n", + " for key, value in sorted(returned_features.items()):\n", + " print(f\"{key} : {value}\")\n", + "\n", + " except PermissionError:\n", + " print(\"\\n*** PERMISSION DENIED *** Cannot fetch online features.\")\n", + " except Exception as e:\n", + " print(f\"Unexpected error while fetching online features: {e}\")\n", + "\n", + "def check_permissions():\n", + " \"\"\"Check user role, test various Feast operations,.\"\"\"\n", + "\n", + " feature_views = []\n", + "\n", + " # Step 1: List feature views\n", + " print(\"\\n--- List feature views ---\")\n", + " try:\n", + " feature_views = store.list_feature_views()\n", + " if not feature_views:\n", + " print(\"No feature views found. You might not have access or they haven't been created.\")\n", + " has_feature_view_access = False\n", + " else:\n", + " print(f\"Successfully listed {len(feature_views)} feature views:\")\n", + " for fv in feature_views:\n", + " print(f\" - {fv.name}\")\n", + "\n", + " except PermissionError:\n", + " print(\"\\n*** PERMISSION DENIED *** Cannot list feature views.\")\n", + " has_feature_view_access = False\n", + " except Exception as e:\n", + " print(f\"Unexpected error listing feature views: {e}\")\n", + " has_feature_view_access = False\n", + "\n", + " # Step 2: Fetch Historical Features\n", + " print(\"\\n--- Fetching Historical Features for Training ---\")\n", + " fetch_historical_features_entity_df(store, for_batch_scoring=False)\n", + "\n", + " print(\"\\n--- Fetching Historical Features for Batch Scoring ---\")\n", + " fetch_historical_features_entity_df(store, for_batch_scoring=True)\n", + "\n", + " # Step 3: Apply Feature Store\n", + " print(\"\\n--- Write to Feature Store ---\")\n", + " try:\n", + " store.apply(feature_views)\n", + " print(\"User has write access to the feature store.\")\n", + " except PermissionError:\n", + " print(\"\\n*** PERMISSION DENIED *** User lacks permission to modify the feature store.\")\n", + " except Exception as e:\n", + " print(f\"Unexpected error testing write access: {e}\")\n", + "\n", + " # Step 4: Fetch Online Features\n", + " print(\"\\n--- Fetching Online Features ---\")\n", + " fetch_online_features(store)\n", + "\n", + " print(\"\\n--- Fetching Online Features via Feature Service ---\")\n", + " fetch_online_features(store, source=\"feature_service\")\n", + "\n", + " print(\"\\n--- Fetching Online Features via Push Source ---\")\n", + " fetch_online_features(store, source=\"push\")\n", + "\n", + " print(\"\\n--- Performing Push Source ---\")\n", + " # Step 5: Simulate Event Push (Streaming Ingestion)\n", + " try:\n", + " event_df = pd.DataFrame.from_dict(\n", + " {\n", + " \"driver_id\": [1001],\n", + " \"event_timestamp\": [datetime.now()],\n", + " \"created\": [datetime.now()],\n", + " \"conv_rate\": [1.0],\n", + " \"acc_rate\": [1.0],\n", + " \"avg_daily_trips\": [1000],\n", + " }\n", + " )\n", + " store.push(\"driver_stats_push_source\", event_df, to=PushMode.ONLINE_AND_OFFLINE)\n", + " print(\"Successfully pushed a test event.\")\n", + " except PermissionError:\n", + " print(\"\\n*** PERMISSION DENIED *** Cannot push event (no write access).\")\n", + " except Exception as e:\n", + " print(f\"Unexpected error while pushing event: {e}\")\n" + ], + "id": "934963c5f6b18930", + "outputs": [], + "execution_count": 51 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Test Read-Only Feast User \n", + "**Step 1: Set the Token**" + ], + "id": "84e3f83699b8d83" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T20:12:44.771268Z", + "start_time": "2025-03-06T20:12:44.691353Z" + } + }, + "cell_type": "code", + "source": "get_k8s_token(\"feast-user-sa\")", + "id": "f1fe8baa02d27d38", + "outputs": [ + { + "data": { + "text/plain": [ + "'Token Retrieved: ***** (hidden for security)'" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 48 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "**Step 2: Test misc functions from offline, online, materialize_incremental, and others**", + "id": "140c909fa8bcc6ab" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T20:16:16.680582Z", + "start_time": "2025-03-06T20:16:14.930480Z" + } + }, + "cell_type": "code", + "source": [ + "# Run the permission check function\n", + "check_permissions()\n" + ], + "id": "14b7ad38368db767", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "--- List feature views ---\n", + "Successfully listed 2 feature views:\n", + " - driver_hourly_stats\n", + " - driver_hourly_stats_fresh\n", + "\n", + "--- Fetching Historical Features for Training ---\n", + "Handling connection for 8081\n", + "Successfully fetched training historical features:\n", + " driver_id event_timestamp label_driver_reported_satisfaction \\\n", + "0 1001 2021-04-12 10:59:42+00:00 1 \n", + "1 1002 2021-04-12 08:12:10+00:00 5 \n", + "2 1003 2021-04-12 16:40:26+00:00 3 \n", + "\n", + " val_to_add val_to_add_2 conv_rate acc_rate avg_daily_trips \\\n", + "0 1 10 0.677818 0.453707 193 \n", + "1 2 20 0.328160 0.900565 929 \n", + "2 3 30 0.787191 0.958963 571 \n", + "\n", + " conv_rate_plus_val1 conv_rate_plus_val2 \n", + "0 1.677818 10.677818 \n", + "1 2.328160 20.328160 \n", + "2 3.787191 30.787191 \n", + "\n", + "--- Fetching Historical Features for Batch Scoring ---\n", + "Handling connection for 8081\n", + "Successfully fetched batch scoring historical features:\n", + " driver_id event_timestamp \\\n", + "0 1001 2025-03-06 20:16:15.556223+00:00 \n", + "1 1002 2025-03-06 20:16:15.556223+00:00 \n", + "2 1003 2025-03-06 20:16:15.556223+00:00 \n", + "\n", + " label_driver_reported_satisfaction val_to_add val_to_add_2 conv_rate \\\n", + "0 1 1 10 0.782836 \n", + "1 5 2 20 0.731948 \n", + "2 3 3 30 0.613211 \n", + "\n", + " acc_rate avg_daily_trips conv_rate_plus_val1 conv_rate_plus_val2 \n", + "0 0.729726 652 1.782836 10.782836 \n", + "1 0.384902 902 2.731948 20.731948 \n", + "2 0.075386 101 3.613211 30.613211 \n", + "\n", + "--- Write to Feature Store ---\n", + "\n", + "*** PERMISSION DENIED *** User lacks permission to modify the feature store.\n", + "\n", + "--- Fetching Online Features ---\n", + "Handling connection for 8082\n", + "Successfully fetched online features directly:\n", + "\n", + "acc_rate : [None, None]\n", + "conv_rate_plus_val1 : [None, None]\n", + "conv_rate_plus_val2 : [None, None]\n", + "driver_id : [1001, 1002]\n", + "\n", + "--- Fetching Online Features via Feature Service ---\n", + "Handling connection for 8082\n", + "Successfully fetched online features via feature service:\n", + "\n", + "conv_rate : [None, None]\n", + "conv_rate_plus_val1 : [None, None]\n", + "conv_rate_plus_val2 : [None, None]\n", + "driver_id : [1001, 1002]\n", + "\n", + "--- Fetching Online Features via Push Source ---\n", + "Handling connection for 8082\n", + "Successfully fetched online features via feature service:\n", + "\n", + "acc_rate : [None, None]\n", + "avg_daily_trips : [None, None]\n", + "conv_rate : [None, None]\n", + "conv_rate_plus_val1 : [None, None]\n", + "conv_rate_plus_val2 : [None, None]\n", + "driver_id : [1001, 1002]\n", + "\n", + "--- Performing Push Source ---\n", + "Unexpected error while pushing event: \n" + ] + } + ], + "execution_count": 53 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Test Unauthorized Feast User ", + "id": "e5e63a172da6d6d7" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T20:16:38.487573Z", + "start_time": "2025-03-06T20:16:38.351889Z" + } + }, + "cell_type": "code", + "source": [ + "# Retrieve and store the token\n", + "get_k8s_token(\"feast-unauthorized-user-sa\")" + ], + "id": "a7b3a6578fcf5c3c", + "outputs": [ + { + "data": { + "text/plain": [ + "'Token Retrieved: ***** (hidden for security)'" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 54 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T20:16:41.522132Z", + "start_time": "2025-03-06T20:16:41.254668Z" + } + }, + "cell_type": "code", + "source": "check_permissions()", + "id": "7aea5658325ab008", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "--- List feature views ---\n", + "No feature views found. You might not have access or they haven't been created.\n", + "\n", + "--- Fetching Historical Features for Training ---\n", + "\n", + "*** PERMISSION DENIED *** Cannot fetch historical features.\n", + "\n", + "--- Fetching Historical Features for Batch Scoring ---\n", + "\n", + "*** PERMISSION DENIED *** Cannot fetch historical features.\n", + "\n", + "--- Write to Feature Store ---\n", + "\n", + "*** PERMISSION DENIED *** User lacks permission to modify the feature store.\n", + "\n", + "--- Fetching Online Features ---\n", + "\n", + "*** PERMISSION DENIED *** Cannot fetch online features.\n", + "\n", + "--- Fetching Online Features via Feature Service ---\n", + "\n", + "*** PERMISSION DENIED *** Cannot fetch online features.\n", + "\n", + "--- Fetching Online Features via Push Source ---\n", + "\n", + "*** PERMISSION DENIED *** Cannot fetch online features.\n", + "\n", + "--- Performing Push Source ---\n", + "Unexpected error while pushing event: Unable to find push source 'driver_stats_push_source'.\n" + ] + } + ], + "execution_count": 55 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Test Admin Feast User", + "id": "cb78ced7c37ceb4c" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T20:17:02.206503Z", + "start_time": "2025-03-06T20:17:02.137409Z" + } + }, + "cell_type": "code", + "source": [ + "# Retrieve and store the token\n", + "get_k8s_token(\"feast-admin-sa\")" + ], + "id": "4f10aae116825619", + "outputs": [ + { + "data": { + "text/plain": [ + "'Token Retrieved: ***** (hidden for security)'" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 56 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-06T20:17:07.799782Z", + "start_time": "2025-03-06T20:17:05.946696Z" + } + }, + "cell_type": "code", + "source": "check_permissions()", + "id": "7a6133f052b9cfe1", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "--- List feature views ---\n", + "Successfully listed 2 feature views:\n", + " - driver_hourly_stats\n", + " - driver_hourly_stats_fresh\n", + "\n", + "--- Fetching Historical Features for Training ---\n", + "Handling connection for 8081\n", + "Successfully fetched training historical features:\n", + " driver_id event_timestamp label_driver_reported_satisfaction \\\n", + "0 1001 2021-04-12 10:59:42+00:00 1 \n", + "1 1002 2021-04-12 08:12:10+00:00 5 \n", + "2 1003 2021-04-12 16:40:26+00:00 3 \n", + "\n", + " val_to_add val_to_add_2 conv_rate acc_rate avg_daily_trips \\\n", + "0 1 10 0.677818 0.453707 193 \n", + "1 2 20 0.328160 0.900565 929 \n", + "2 3 30 0.787191 0.958963 571 \n", + "\n", + " conv_rate_plus_val1 conv_rate_plus_val2 \n", + "0 1.677818 10.677818 \n", + "1 2.328160 20.328160 \n", + "2 3.787191 30.787191 \n", + "\n", + "--- Fetching Historical Features for Batch Scoring ---\n", + "Handling connection for 8081\n", + "Successfully fetched batch scoring historical features:\n", + " driver_id event_timestamp \\\n", + "0 1001 2025-03-06 20:17:06.566035+00:00 \n", + "1 1002 2025-03-06 20:17:06.566035+00:00 \n", + "2 1003 2025-03-06 20:17:06.566035+00:00 \n", + "\n", + " label_driver_reported_satisfaction val_to_add val_to_add_2 conv_rate \\\n", + "0 1 1 10 0.782836 \n", + "1 5 2 20 0.731948 \n", + "2 3 3 30 0.613211 \n", + "\n", + " acc_rate avg_daily_trips conv_rate_plus_val1 conv_rate_plus_val2 \n", + "0 0.729726 652 1.782836 10.782836 \n", + "1 0.384902 902 2.731948 20.731948 \n", + "2 0.075386 101 3.613211 30.613211 \n", + "\n", + "--- Write to Feature Store ---\n", + "User has write access to the feature store.\n", + "\n", + "--- Fetching Online Features ---\n", + "Handling connection for 8082\n", + "Successfully fetched online features directly:\n", + "\n", + "acc_rate : [None, None]\n", + "conv_rate_plus_val1 : [None, None]\n", + "conv_rate_plus_val2 : [None, None]\n", + "driver_id : [1001, 1002]\n", + "\n", + "--- Fetching Online Features via Feature Service ---\n", + "Handling connection for 8082\n", + "Successfully fetched online features via feature service:\n", + "\n", + "conv_rate : [None, None]\n", + "conv_rate_plus_val1 : [None, None]\n", + "conv_rate_plus_val2 : [None, None]\n", + "driver_id : [1001, 1002]\n", + "\n", + "--- Fetching Online Features via Push Source ---\n", + "Handling connection for 8082\n", + "Successfully fetched online features via feature service:\n", + "\n", + "acc_rate : [None, None]\n", + "avg_daily_trips : [None, None]\n", + "conv_rate : [None, None]\n", + "conv_rate_plus_val1 : [None, None]\n", + "conv_rate_plus_val2 : [None, None]\n", + "driver_id : [1001, 1002]\n", + "\n", + "--- Performing Push Source ---\n", + "Unexpected error while pushing event: \n" + ] + } + ], + "execution_count": 57 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + " **Note:**\n", + "**Currently, remote materialization not available in Feast when using the Remote Client**\n", + "**Workaround: Consider using running it from pod like**\n", + " \n", + " `kubectl exec deploy/feast-sample-kubernetes-auth -itc online -- bash -c 'feast materialize-incremental $(date -u +\"%Y-%m-%dT%H:%M:%S\")`\n" + ], + "id": "e451c30649630b2f" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Terminate the process", + "id": "e88442b1bae2b327" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-05T19:09:29.743583Z", + "start_time": "2025-03-05T19:09:29.734671Z" + } + }, + "cell_type": "code", + "source": [ + "for name, process in port_forward_processes.items():\n", + " process.terminate()\n", + " print(f\"Stopped port forwarding for {name}\")" + ], + "id": "2984d62766da122a", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Stopped port forwarding for offline_store\n", + "Stopped port forwarding for online_store\n", + "Stopped port forwarding for registry\n" + ] + } + ], + "execution_count": 25 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "[Next: Uninstall the Operator and all Feast objects](./03-uninstall.ipynb)", + "id": "38c54e92643e0bda" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/operator-rbac/README.md b/examples/operator-rbac/README.md new file mode 100644 index 00000000000..9c0a0461678 --- /dev/null +++ b/examples/operator-rbac/README.md @@ -0,0 +1,6 @@ +# Running the Feast RBAC example on Kubernetes using the Feast Operator. + +1. [1-setup-operator-rbac.ipynb](1-setup-operator-rbac.ipynb) will guide you through how to setup Role-Based Access Control (RBAC) for Feast using the [Feast Operator](../../infra/feast-operator/) and Kubernetes Authentication. This Feast Admin Step requires you to setup the operator and Feast RBAC on K8s. +2. [2-client.ipynb](2-client.ipynb) Validate the RBAC with the client example using different test cases using a service account token locally. +3. [03-uninstall.ipynb](03-uninstall.ipynb) Clear the installed deployments and K8s Objects. + diff --git a/examples/operator-rbac/client/feature_store.yaml b/examples/operator-rbac/client/feature_store.yaml new file mode 100644 index 00000000000..49a4c426363 --- /dev/null +++ b/examples/operator-rbac/client/feature_store.yaml @@ -0,0 +1,15 @@ +project: feast_rbac +provider: local +offline_store: + host: localhost + type: remote + port: 8081 +online_store: + path: http://localhost:8082 + type: remote +registry: + path: localhost:8083 + registry_type: remote +auth: + type: kubernetes +entity_key_serialization_version: 3 diff --git a/examples/operator-rbac/permissions_apply.py b/examples/operator-rbac/permissions_apply.py new file mode 100644 index 00000000000..0d46ad5260a --- /dev/null +++ b/examples/operator-rbac/permissions_apply.py @@ -0,0 +1,27 @@ +# Necessary modules for permissions and policies in Feast for RBAC +from feast.feast_object import ALL_RESOURCE_TYPES +from feast.permissions.action import READ, AuthzedAction, ALL_ACTIONS +from feast.permissions.permission import Permission +from feast.permissions.policy import RoleBasedPolicy + +# Define K8s roles same as created with FeatureStore CR +admin_roles = ["feast-writer"] # Full access (can create, update, delete ) Feast Resources +user_roles = ["feast-reader"] # Read-only access on Feast Resources + +# User permissions (feast_user_permission) +# - Grants read and describing Feast objects access +user_perm = Permission( + name="feast_user_permission", + types=ALL_RESOURCE_TYPES, + policy=RoleBasedPolicy(roles=user_roles), + actions=[AuthzedAction.DESCRIBE] + READ # Read access (READ_ONLINE, READ_OFFLINE) + describe other Feast Resources. +) + +# Admin permissions (feast_admin_permission) +# - Grants full control over all resources +admin_perm = Permission( + name="feast_admin_permission", + types=ALL_RESOURCE_TYPES, + policy=RoleBasedPolicy(roles=admin_roles), + actions=ALL_ACTIONS # Full permissions: CREATE, UPDATE, DELETE, READ, WRITE +) diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml b/infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml index 6751be60e9c..33225b2edfb 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml +++ b/infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml @@ -3,9 +3,18 @@ kind: FeatureStore metadata: name: sample-kubernetes-auth spec: - feastProject: my_project + feastProject: feast_rbac authz: kubernetes: roles: - - reader - - writer + - feast-writer + - feast-reader + services: + offlineStore: + server: {} + onlineStore: + server: {} + registry: + local: + server: {} + ui: {}