Skip to content

Commit ca9fb9b

Browse files
authored
fix: Replaced ClusterRoles with local RoleBindings (feast-dev#4625)
* replaced fetching ClusterRoles with local RoleBindings Signed-off-by: Daniele Martinoli <dmartino@redhat.com> * added missing namespace configurations Signed-off-by: Daniele Martinoli <dmartino@redhat.com> --------- Signed-off-by: Daniele Martinoli <dmartino@redhat.com>
1 parent c0a1026 commit ca9fb9b

File tree

6 files changed

+85
-88
lines changed

6 files changed

+85
-88
lines changed

docs/getting-started/components/authz_manager.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,14 @@ auth:
114114

115115
In case the client cannot run on the same cluster as the servers, the client token can be injected using the `LOCAL_K8S_TOKEN`
116116
environment variable on the client side. The value must refer to the token of a service account created on the servers cluster
117-
and linked to the desired RBAC roles.
117+
and linked to the desired RBAC roles.
118+
119+
#### Setting Up Kubernetes RBAC for Feast
120+
121+
To ensure the Kubernetes RBAC environment aligns with Feast's RBAC configuration, follow these guidelines:
122+
* The roles defined in Feast `Permission` instances must have corresponding Kubernetes RBAC `Role` names.
123+
* The Kubernetes RBAC `Role` must reside in the same namespace as the Feast service.
124+
* The client application can run in a different namespace, using its own dedicated `ServiceAccount`.
125+
* Finally, the `RoleBinding` that links the client `ServiceAccount` to the RBAC `Role` must be defined in the namespace of the Feast service.
126+
127+
If the above rules are satisfied, the Feast service must be granted permissions to fetch `RoleBinding` instances from the local namespace.

examples/rbac-remote/server/k8s/server_resources.yaml

+9-7
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,25 @@ metadata:
55
namespace: feast-dev
66
---
77
apiVersion: rbac.authorization.k8s.io/v1
8-
kind: ClusterRole
8+
kind: Role
99
metadata:
10-
name: feast-cluster-role
10+
name: feast-role
11+
namespace: feast-dev
1112
rules:
1213
- apiGroups: ["rbac.authorization.k8s.io"]
13-
resources: ["roles", "rolebindings", "clusterrolebindings"]
14+
resources: ["roles", "rolebindings"]
1415
verbs: ["get", "list", "watch"]
1516
---
1617
apiVersion: rbac.authorization.k8s.io/v1
17-
kind: ClusterRoleBinding
18+
kind: RoleBinding
1819
metadata:
19-
name: feast-cluster-rolebinding
20+
name: feast-rolebinding
21+
namespace: feast-dev
2022
subjects:
2123
- kind: ServiceAccount
2224
name: feast-sa
2325
namespace: feast-dev
2426
roleRef:
2527
apiGroup: rbac.authorization.k8s.io
26-
kind: ClusterRole
27-
name: feast-cluster-role
28+
kind: Role
29+
name: feast-role

sdk/python/feast/permissions/auth/kubernetes_token_parser.py

+30-20
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from feast.permissions.user import User
1212

1313
logger = logging.getLogger(__name__)
14+
_namespace_file_path = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
1415

1516

1617
class KubernetesTokenParser(TokenParser):
@@ -48,24 +49,42 @@ async def user_details_from_access_token(self, access_token: str) -> User:
4849
if sa_name is not None and sa_name == intra_communication_base64:
4950
return User(username=sa_name, roles=[])
5051
else:
51-
roles = self.get_roles(sa_namespace, sa_name)
52-
logger.info(f"Roles for ServiceAccount {sa_name}: {roles}")
52+
current_namespace = self._read_namespace_from_file()
53+
logger.info(
54+
f"Looking for ServiceAccount roles of {sa_namespace}:{sa_name} in {current_namespace}"
55+
)
56+
roles = self.get_roles(
57+
current_namespace=current_namespace,
58+
service_account_namespace=sa_namespace,
59+
service_account_name=sa_name,
60+
)
61+
logger.info(f"Roles: {roles}")
5362

5463
return User(username=current_user, roles=roles)
5564

56-
def get_roles(self, namespace: str, service_account_name: str) -> list[str]:
65+
def _read_namespace_from_file(self):
66+
try:
67+
with open(_namespace_file_path, "r") as file:
68+
namespace = file.read().strip()
69+
return namespace
70+
except Exception as e:
71+
raise e
72+
73+
def get_roles(
74+
self,
75+
current_namespace: str,
76+
service_account_namespace: str,
77+
service_account_name: str,
78+
) -> list[str]:
5779
"""
58-
Fetches the Kubernetes `Role`s associated to the given `ServiceAccount` in the given `namespace`.
80+
Fetches the Kubernetes `Role`s associated to the given `ServiceAccount` in `current_namespace` namespace.
5981
60-
The research also includes the `ClusterRole`s, so the running deployment must be granted enough permissions to query
61-
for such instances in all the namespaces.
82+
The running deployment must be granted enough permissions to query for such instances in this namespace.
6283
6384
Returns:
64-
list[str]: Name of the `Role`s and `ClusterRole`s associated to the service account. No string manipulation is performed on the role name.
85+
list[str]: Name of the `Role`s associated to the service account. No string manipulation is performed on the role name.
6586
"""
66-
role_bindings = self.rbac_v1.list_namespaced_role_binding(namespace)
67-
cluster_role_bindings = self.rbac_v1.list_cluster_role_binding()
68-
87+
role_bindings = self.rbac_v1.list_namespaced_role_binding(current_namespace)
6988
roles: set[str] = set()
7089

7190
for binding in role_bindings.items:
@@ -74,16 +93,7 @@ def get_roles(self, namespace: str, service_account_name: str) -> list[str]:
7493
if (
7594
subject.kind == "ServiceAccount"
7695
and subject.name == service_account_name
77-
):
78-
roles.add(binding.role_ref.name)
79-
80-
for binding in cluster_role_bindings.items:
81-
if binding.subjects is not None:
82-
for subject in binding.subjects:
83-
if (
84-
subject.kind == "ServiceAccount"
85-
and subject.name == service_account_name
86-
and subject.namespace == namespace
96+
and subject.namespace == service_account_namespace
8797
):
8898
roles.add(binding.role_ref.name)
8999

sdk/python/tests/unit/permissions/auth/conftest.py

+8-26
Original file line numberDiff line numberDiff line change
@@ -20,46 +20,28 @@ def sa_name():
2020

2121

2222
@pytest.fixture
23-
def namespace():
23+
def my_namespace():
2424
return "my-ns"
2525

2626

2727
@pytest.fixture
28-
def rolebindings(sa_name, namespace) -> dict:
29-
roles = ["reader", "writer"]
30-
items = []
31-
for r in roles:
32-
items.append(
33-
client.V1RoleBinding(
34-
metadata=client.V1ObjectMeta(name=r, namespace=namespace),
35-
subjects=[
36-
client.V1Subject(
37-
kind="ServiceAccount",
38-
name=sa_name,
39-
api_group="rbac.authorization.k8s.io",
40-
)
41-
],
42-
role_ref=client.V1RoleRef(
43-
kind="Role", name=r, api_group="rbac.authorization.k8s.io"
44-
),
45-
)
46-
)
47-
return {"items": client.V1RoleBindingList(items=items), "roles": roles}
28+
def sa_namespace():
29+
return "sa-ns"
4830

4931

5032
@pytest.fixture
51-
def clusterrolebindings(sa_name, namespace) -> dict:
52-
roles = ["updater"]
33+
def rolebindings(my_namespace, sa_name, sa_namespace) -> dict:
34+
roles = ["reader", "writer"]
5335
items = []
5436
for r in roles:
5537
items.append(
56-
client.V1ClusterRoleBinding(
57-
metadata=client.V1ObjectMeta(name=r, namespace=namespace),
38+
client.V1RoleBinding(
39+
metadata=client.V1ObjectMeta(name=r, namespace=my_namespace),
5840
subjects=[
5941
client.V1Subject(
6042
kind="ServiceAccount",
6143
name=sa_name,
62-
namespace=namespace,
44+
namespace=sa_namespace,
6345
api_group="rbac.authorization.k8s.io",
6446
)
6547
],

sdk/python/tests/unit/permissions/auth/server/mock_utils.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ async def mock_oath2(self, request):
5353

5454

5555
def mock_kubernetes(request, monkeypatch):
56+
my_namespace = request.getfixturevalue("my_namespace")
5657
sa_name = request.getfixturevalue("sa_name")
57-
namespace = request.getfixturevalue("namespace")
58-
subject = f"system:serviceaccount:{namespace}:{sa_name}"
58+
sa_namespace = request.getfixturevalue("sa_namespace")
59+
subject = f"system:serviceaccount:{sa_namespace}:{sa_name}"
5960
rolebindings = request.getfixturevalue("rolebindings")
60-
clusterrolebindings = request.getfixturevalue("clusterrolebindings")
6161

6262
monkeypatch.setattr(
6363
"feast.permissions.auth.kubernetes_token_parser.config.load_incluster_config",
@@ -71,11 +71,11 @@ def mock_kubernetes(request, monkeypatch):
7171
"feast.permissions.auth.kubernetes_token_parser.client.RbacAuthorizationV1Api.list_namespaced_role_binding",
7272
lambda *args, **kwargs: rolebindings["items"],
7373
)
74-
monkeypatch.setattr(
75-
"feast.permissions.auth.kubernetes_token_parser.client.RbacAuthorizationV1Api.list_cluster_role_binding",
76-
lambda *args, **kwargs: clusterrolebindings["items"],
77-
)
7874
monkeypatch.setattr(
7975
"feast.permissions.client.kubernetes_auth_client_manager.KubernetesAuthClientManager.get_token",
8076
lambda self: "my-token",
8177
)
78+
monkeypatch.setattr(
79+
"feast.permissions.auth.kubernetes_token_parser.KubernetesTokenParser._read_namespace_from_file",
80+
lambda self: my_namespace,
81+
)

sdk/python/tests/unit/permissions/auth/test_token_parser.py

+20-27
Original file line numberDiff line numberDiff line change
@@ -145,27 +145,26 @@ async def mock_oath2(self, request):
145145
@patch(
146146
"feast.permissions.auth.kubernetes_token_parser.client.RbacAuthorizationV1Api.list_namespaced_role_binding"
147147
)
148-
@patch(
149-
"feast.permissions.auth.kubernetes_token_parser.client.RbacAuthorizationV1Api.list_cluster_role_binding"
150-
)
151148
def test_k8s_token_validation_success(
152-
mock_crb,
153149
mock_rb,
154150
mock_jwt,
155151
mock_config,
156152
rolebindings,
157-
clusterrolebindings,
153+
monkeypatch,
154+
my_namespace,
155+
sa_name,
156+
sa_namespace,
158157
):
159-
sa_name = "my-name"
160-
namespace = "my-ns"
161-
subject = f"system:serviceaccount:{namespace}:{sa_name}"
158+
monkeypatch.setattr(
159+
"feast.permissions.auth.kubernetes_token_parser.KubernetesTokenParser._read_namespace_from_file",
160+
lambda self: my_namespace,
161+
)
162+
subject = f"system:serviceaccount:{sa_namespace}:{sa_name}"
162163
mock_jwt.return_value = {"sub": subject}
163164

164165
mock_rb.return_value = rolebindings["items"]
165-
mock_crb.return_value = clusterrolebindings["items"]
166166

167167
roles = rolebindings["roles"]
168-
croles = clusterrolebindings["roles"]
169168

170169
access_token = "aaa-bbb-ccc"
171170
token_parser = KubernetesTokenParser()
@@ -175,12 +174,10 @@ def test_k8s_token_validation_success(
175174

176175
assertpy.assert_that(user).is_type_of(User)
177176
if isinstance(user, User):
178-
assertpy.assert_that(user.username).is_equal_to(f"{namespace}:{sa_name}")
179-
assertpy.assert_that(user.roles.sort()).is_equal_to((roles + croles).sort())
177+
assertpy.assert_that(user.username).is_equal_to(f"{sa_namespace}:{sa_name}")
178+
assertpy.assert_that(user.roles.sort()).is_equal_to(roles.sort())
180179
for r in roles:
181180
assertpy.assert_that(user.has_matching_role([r])).is_true()
182-
for cr in croles:
183-
assertpy.assert_that(user.has_matching_role([cr])).is_true()
184181
assertpy.assert_that(user.has_matching_role(["foo"])).is_false()
185182

186183

@@ -212,30 +209,29 @@ def test_k8s_inter_server_comm(
212209
oidc_config,
213210
request,
214211
rolebindings,
215-
clusterrolebindings,
216212
monkeypatch,
217213
):
218214
if is_intra_server:
219215
subject = f":::{intra_communication_val}"
220216
else:
221217
sa_name = request.getfixturevalue("sa_name")
222-
namespace = request.getfixturevalue("namespace")
223-
subject = f"system:serviceaccount:{namespace}:{sa_name}"
218+
sa_namespace = request.getfixturevalue("sa_namespace")
219+
my_namespace = request.getfixturevalue("my_namespace")
220+
subject = f"system:serviceaccount:{sa_namespace}:{sa_name}"
224221
rolebindings = request.getfixturevalue("rolebindings")
225-
clusterrolebindings = request.getfixturevalue("clusterrolebindings")
226222

227223
monkeypatch.setattr(
228224
"feast.permissions.auth.kubernetes_token_parser.client.RbacAuthorizationV1Api.list_namespaced_role_binding",
229225
lambda *args, **kwargs: rolebindings["items"],
230226
)
231-
monkeypatch.setattr(
232-
"feast.permissions.auth.kubernetes_token_parser.client.RbacAuthorizationV1Api.list_cluster_role_binding",
233-
lambda *args, **kwargs: clusterrolebindings["items"],
234-
)
235227
monkeypatch.setattr(
236228
"feast.permissions.client.kubernetes_auth_client_manager.KubernetesAuthClientManager.get_token",
237229
lambda self: "my-token",
238230
)
231+
monkeypatch.setattr(
232+
"feast.permissions.auth.kubernetes_token_parser.KubernetesTokenParser._read_namespace_from_file",
233+
lambda self: my_namespace,
234+
)
239235

240236
monkeypatch.setattr(
241237
"feast.permissions.auth.kubernetes_token_parser.config.load_incluster_config",
@@ -248,7 +244,6 @@ def test_k8s_inter_server_comm(
248244
)
249245

250246
roles = rolebindings["roles"]
251-
croles = clusterrolebindings["roles"]
252247

253248
access_token = "aaa-bbb-ccc"
254249
token_parser = KubernetesTokenParser()
@@ -263,10 +258,8 @@ def test_k8s_inter_server_comm(
263258
else:
264259
assertpy.assert_that(user).is_type_of(User)
265260
if isinstance(user, User):
266-
assertpy.assert_that(user.username).is_equal_to(f"{namespace}:{sa_name}")
267-
assertpy.assert_that(user.roles.sort()).is_equal_to((roles + croles).sort())
261+
assertpy.assert_that(user.username).is_equal_to(f"{sa_namespace}:{sa_name}")
262+
assertpy.assert_that(user.roles.sort()).is_equal_to(roles.sort())
268263
for r in roles:
269264
assertpy.assert_that(user.has_matching_role([r])).is_true()
270-
for cr in croles:
271-
assertpy.assert_that(user.has_matching_role([cr])).is_true()
272265
assertpy.assert_that(user.has_matching_role(["foo"])).is_false()

0 commit comments

Comments
 (0)