Skip to content

Commit ced8e61

Browse files
committed
Groups and Namespaces authorization
Signed-off-by: jyejare <[email protected]>
1 parent 02c3006 commit ced8e61

File tree

10 files changed

+747
-15
lines changed

10 files changed

+747
-15
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Example permissions configuration with groups and namespaces support
2+
# This demonstrates how to use the new group-based and namespace-based policies
3+
# in addition to the existing role-based policies
4+
5+
from feast.feast_object import ALL_RESOURCE_TYPES
6+
from feast.permissions.action import READ, AuthzedAction, ALL_ACTIONS
7+
from feast.permissions.permission import Permission
8+
from feast.permissions.policy import RoleBasedPolicy, GroupBasedPolicy, NamespaceBasedPolicy, CombinedGroupNamespacePolicy
9+
10+
# Define K8s roles (existing functionality)
11+
admin_roles = ["feast-writer"] # Full access (can create, update, delete) Feast Resources
12+
user_roles = ["feast-reader"] # Read-only access on Feast Resources
13+
14+
# Define groups for different teams
15+
data_team_groups = ["data-team", "ml-engineers"]
16+
dev_team_groups = ["dev-team", "developers"]
17+
admin_groups = ["feast-admins", "platform-admins"]
18+
19+
# Define namespaces for different environments
20+
prod_namespaces = ["production", "prod"]
21+
staging_namespaces = ["staging", "dev"]
22+
test_namespaces = ["test", "testing"]
23+
24+
# Role-based permissions (existing functionality)
25+
# - Grants read and describing Feast objects access
26+
user_perm = Permission(
27+
name="feast_user_permission",
28+
types=ALL_RESOURCE_TYPES,
29+
policy=RoleBasedPolicy(roles=user_roles),
30+
actions=[AuthzedAction.DESCRIBE] + READ # Read access (READ_ONLINE, READ_OFFLINE) + describe other Feast Resources.
31+
)
32+
33+
# Admin permissions (existing functionality)
34+
# - Grants full control over all resources
35+
admin_perm = Permission(
36+
name="feast_admin_permission",
37+
types=ALL_RESOURCE_TYPES,
38+
policy=RoleBasedPolicy(roles=admin_roles),
39+
actions=ALL_ACTIONS # Full permissions: CREATE, UPDATE, DELETE, READ, WRITE
40+
)
41+
42+
# Group-based permissions (new functionality)
43+
# - Grants read access to data team members
44+
data_team_perm = Permission(
45+
name="data_team_read_permission",
46+
types=ALL_RESOURCE_TYPES,
47+
policy=GroupBasedPolicy(groups=data_team_groups),
48+
actions=[AuthzedAction.DESCRIBE] + READ
49+
)
50+
51+
# - Grants full access to admin groups
52+
admin_group_perm = Permission(
53+
name="admin_group_permission",
54+
types=ALL_RESOURCE_TYPES,
55+
policy=GroupBasedPolicy(groups=admin_groups),
56+
actions=ALL_ACTIONS
57+
)
58+
59+
# Namespace-based permissions (new functionality)
60+
# - Grants read access to production namespace users
61+
prod_read_perm = Permission(
62+
name="production_read_permission",
63+
types=ALL_RESOURCE_TYPES,
64+
policy=NamespaceBasedPolicy(namespaces=prod_namespaces),
65+
actions=[AuthzedAction.DESCRIBE] + READ
66+
)
67+
68+
# - Grants full access to staging namespace users
69+
staging_full_perm = Permission(
70+
name="staging_full_permission",
71+
types=ALL_RESOURCE_TYPES,
72+
policy=NamespaceBasedPolicy(namespaces=staging_namespaces),
73+
actions=ALL_ACTIONS
74+
)
75+
76+
# Combined permissions (using combined policy type)
77+
# - Grants read access to dev team members in test namespaces
78+
dev_test_perm = Permission(
79+
name="dev_test_permission",
80+
types=ALL_RESOURCE_TYPES,
81+
policy=CombinedGroupNamespacePolicy(groups=dev_team_groups, namespaces=test_namespaces),
82+
actions=[AuthzedAction.DESCRIBE] + READ
83+
)
84+
85+
# - Grants full access to data team members in staging namespaces
86+
data_staging_perm = Permission(
87+
name="data_staging_permission",
88+
types=ALL_RESOURCE_TYPES,
89+
policy=CombinedGroupNamespacePolicy(groups=data_team_groups, namespaces=staging_namespaces),
90+
actions=ALL_ACTIONS
91+
)
92+
93+
# # Export all permissions
94+
# permissions = [
95+
# # Role-based permissions (existing)
96+
# user_perm,
97+
# admin_perm,
98+
99+
# # Group-based permissions (new)
100+
# data_team_perm,
101+
# admin_group_perm,
102+
103+
# # Namespace-based permissions (new)
104+
# prod_read_perm,
105+
# staging_full_perm,
106+
107+
# # Combined permissions
108+
# dev_test_perm,
109+
# data_staging_perm,
110+
# ]

protos/feast/core/Policy.proto

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,22 @@ message Policy {
1414

1515
oneof policy_type {
1616
RoleBasedPolicy role_based_policy = 3;
17+
GroupBasedPolicy group_based_policy = 4;
18+
NamespaceBasedPolicy namespace_based_policy = 5;
1719
}
1820
}
1921

2022
message RoleBasedPolicy {
2123
// List of roles in this policy.
2224
repeated string roles = 1;
2325
}
26+
27+
message GroupBasedPolicy {
28+
// List of groups in this policy.
29+
repeated string groups = 1;
30+
}
31+
32+
message NamespaceBasedPolicy {
33+
// List of namespaces in this policy.
34+
repeated string namespaces = 1;
35+
}

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

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ def __init__(self):
2828
config.load_incluster_config()
2929
self.v1 = client.CoreV1Api()
3030
self.rbac_v1 = client.RbacAuthorizationV1Api()
31+
self.auth_v1 = client.AuthenticationV1Api()
3132

3233
async def user_details_from_access_token(self, access_token: str) -> User:
3334
"""
3435
Extract the service account from the token and search the roles associated with it.
36+
Also extract groups and namespaces using Token Access Review.
3537
3638
Returns:
37-
User: Current user, with associated roles. The `username` is the `:` separated concatenation of `namespace` and `service account name`.
39+
User: Current user, with associated roles, groups, and namespaces. The `username` is the `:` separated concatenation of `namespace` and `service account name`.
3840
3941
Raises:
4042
AuthenticationError if any error happens.
@@ -47,20 +49,30 @@ async def user_details_from_access_token(self, access_token: str) -> User:
4749

4850
intra_communication_base64 = os.getenv("INTRA_COMMUNICATION_BASE64")
4951
if sa_name is not None and sa_name == intra_communication_base64:
50-
return User(username=sa_name, roles=[])
52+
return User(username=sa_name, roles=[], groups=[], namespaces=[])
5153
else:
5254
current_namespace = self._read_namespace_from_file()
5355
logger.info(
5456
f"Looking for ServiceAccount roles of {sa_namespace}:{sa_name} in {current_namespace}"
5557
)
58+
59+
# Get roles using existing method
5660
roles = self.get_roles(
5761
current_namespace=current_namespace,
5862
service_account_namespace=sa_namespace,
5963
service_account_name=sa_name,
6064
)
6165
logger.info(f"Roles: {roles}")
6266

63-
return User(username=current_user, roles=roles)
67+
# Extract groups and namespaces using Token Access Review
68+
groups, namespaces = self._extract_groups_and_namespaces_from_token(
69+
access_token
70+
)
71+
logger.info(f"Groups: {groups}, Namespaces: {namespaces}")
72+
73+
return User(
74+
username=current_user, roles=roles, groups=groups, namespaces=namespaces
75+
)
6476

6577
def _read_namespace_from_file(self):
6678
try:
@@ -99,6 +111,65 @@ def get_roles(
99111

100112
return list(roles)
101113

114+
def _extract_groups_and_namespaces_from_token(
115+
self, access_token: str
116+
) -> tuple[list[str], list[str]]:
117+
"""
118+
Extract groups and namespaces from the token using Kubernetes Token Access Review.
119+
120+
Args:
121+
access_token: The JWT token to analyze
122+
123+
Returns:
124+
tuple[list[str], list[str]]: A tuple containing (groups, namespaces)
125+
"""
126+
try:
127+
# Create TokenReview object
128+
token_review = client.V1TokenReview(
129+
spec=client.V1TokenReviewSpec(token=access_token)
130+
)
131+
groups = []
132+
namespaces = []
133+
134+
# Call Token Access Review API
135+
response = self.auth_v1.create_token_review(token_review)
136+
137+
if response.status.authenticated:
138+
# Extract groups and namespaces from the response
139+
groups = response.status.groups
140+
141+
# Extract namespaces from the user info
142+
if response.status.user:
143+
# For service accounts, the namespace is typically in the username
144+
# For regular users, we might need to extract from groups or other fields
145+
username = response.status.user.get("username", "")
146+
if ":" in username and username.startswith(
147+
"system:serviceaccount:"
148+
):
149+
# Extract namespace from service account username
150+
parts = username.split(":")
151+
if len(parts) >= 4:
152+
namespaces.append(parts[2]) # namespace is the 3rd part
153+
154+
# Also check if there are namespace-specific groups
155+
for group in groups:
156+
if group.startswith("system:serviceaccounts:"):
157+
# Extract namespace from service account group
158+
parts = group.split(":")
159+
if len(parts) >= 3:
160+
namespaces.append(parts[2])
161+
162+
logger.debug(
163+
f"Token Access Review successful. Groups: {groups}, Namespaces: {namespaces}"
164+
)
165+
else:
166+
logger.warning(f"Token Access Review failed: {response.status.error}")
167+
168+
except Exception as e:
169+
logger.error(f"Failed to perform Token Access Review: {e}")
170+
# We dont need to extract groups and namespaces from jwt decoding, not ideal for kubernetes auth
171+
return groups, namespaces
172+
102173

103174
def _decode_token(access_token: str) -> tuple[str, str]:
104175
"""

sdk/python/feast/permissions/auth_model.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,5 @@ class NoAuthConfig(AuthConfig):
6767

6868

6969
class KubernetesAuthConfig(AuthConfig):
70-
pass
70+
# Optional user token for users (not service accounts)
71+
user_token: Optional[str] = None

sdk/python/feast/permissions/client/kubernetes_auth_client_manager.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ def get_token(self):
2424

2525
return jwt.encode(payload, "")
2626

27+
# Check if user token is provided in config (for external users)
28+
if hasattr(self.auth_config, "user_token") and self.auth_config.user_token:
29+
logger.info("Using user token from configuration")
30+
return self.auth_config.user_token
31+
2732
try:
2833
token = self._read_token_from_file()
2934
return token

0 commit comments

Comments
 (0)