Feast 基于组(Group)与命名空间(Namespace)的授权:从 Token Access Review 到策略引擎的完整实现
【免费下载链接】feastThe Open Source Feature Store for AI/ML项目地址: https://gitcode.com/GitHub_Trending/fe/feast
导读
本文聚焦 Feast 开源特征平台中基于用户组(Groups)与命名空间(Namespaces)的授权能力,系统梳理其在sdk/python/feast/permissions权限子系统中的完整落地:从User模型扩展、四种策略类型(Role/Group/Namespace/Combined)、Policy.proto的 protobuf 定义,到 Kubernetes Token Access Review 的用户信息提取与客户端 SDK 配置。读完本文,你将掌握如何在 Feast 中为外部用户配置组/命名空间粒度的访问策略,并理解其底层认证数据流与向后兼容设计。
一、背景:为什么需要组与命名空间级别的授权
Feast 早期的权限体系以RoleBasedPolicy(角色策略)为核心:用户在 Kubernetes 集群中以 ServiceAccount 身份接入,通过 RoleBinding 关联一组角色,由KubernetesTokenParser读取角色信息,再与权限策略中的roles字段比对。
但仅靠角色粒度存在两个明显局限:
- 外部用户无法接入:ServiceAccount 令牌的解析路径假定请求来自集群内的服务账号,真实的人类用户(通过 OIDC 等方式获得的用户令牌)难以被准确建模;
- 多项目/多团队隔离不直观:特征平台往往服务于多个数据科学团队,每个团队拥有自己的项目命名空间(如
de-dsp、ml-dsp),按“用户属于哪个组、能访问哪些命名空间”来授权更符合组织模型。
因此 Feast 在 PR #5619 中实现了Groups 与 Namespaces 提取与授权,核心变更覆盖:用户模型、策略类型、protobuf 定义、Token Access Review 集成、客户端 SDK 与配置模型六个模块(原始变更清单见 docs/changelogs/Groups_Namespaces_Auth_implmentation_summary.md)。
二、User 模型增强:承载组与命名空间属性
User类位于 sdk/python/feast/permissions/user.py,本次改动在其原有username、roles基础上新增了两个可选属性:
class User: _username: str _roles: Optional[list[str]] _groups: Optional[list[str]] _namespaces: Optional[list[str]] def __init__( self, username: str, roles: Optional[list[str]] = None, groups: Optional[list[str]] = None, namespaces: Optional[list[str]] = None, ): self._username = username self._roles = roles if roles is not None else [] self._groups = groups if groups is not None else [] self._namespaces = namespaces if namespaces is not None else []新增的匹配方法
has_matching_group(requested_groups):只要用户拥有的组与请求列表存在任意交集即返回True(any(group in self.groups for group in requested_groups));has_matching_namespace(requested_namespaces):语义与组匹配一致,判断命名空间列表是否存在交集。
向后兼容性
groups、namespaces默认退化为空列表,因此沿用旧的User(username=..., roles=[...])构造方式完全不受影响;has_matching_role()原有逻辑保持不变。测试用例 sdk/python/tests/unit/permissions/test_groups_namespaces_auth.py 专门验证了“不带 groups/namespaces 创建用户”的兼容场景:
user = User(username="testuser", roles=["feast-reader"]) assert user.groups == [] assert user.namespaces == []三、策略类型:三种新策略 + 原有角色策略并存
所有策略实现集中在 sdk/python/feast/permissions/policy.py。抽象基类Policy定义了两个核心抽象方法:
validate_user(user: User) -> tuple[bool, str]:校验用户是否满足策略,返回“是否通过”与“失败原因说明”;to_proto()/ 静态方法from_proto():完成策略对象与 protobuf 之间的互转。
Policy.from_proto()通过policy_proto.WhichOneof("policy_type")判别具体类型并分发,目前支持四种:
| 策略类 | 判定逻辑 | 失败说明文案 |
|---|---|---|
RoleBasedPolicy(原有) | user.has_matching_role(roles),命中任一角色即通过 | Requires roles [...] |
GroupBasedPolicy(新增) | user.has_matching_group(groups),命中任一组即通过 | User is not added into the permitted groups |
NamespaceBasedPolicy(新增) | user.has_matching_namespace(namespaces),命中任一命名空间即通过 | User is not added into the permitted namespaces |
CombinedGroupNamespacePolicy(新增) | has_matching_group(...) or has_matching_namespace(...),组或命名空间任一命中即通过 | User must be in at least one of the permitted groups or namespaces |
注意:Combined 策略是 OR 语义
从 policy.py 的实现可以看出,CombinedGroupNamespacePolicy的判定是result = has_matching_group or has_matching_namespace——即“组 OR 命名空间命中任一即可放行”,而不是“同时满足”。如果业务需要 AND 语义(必须同时属于某组且位于某命名空间),需要自行组合或扩展策略。
相等性判定
四种策略都实现了__eq__,基于排序后的列表比较,因此GroupBasedPolicy(groups=["a", "b"])与GroupBasedPolicy(groups=["b", "a"])相等,方便在测试与注册表中做幂等比较。
四、Protobuf 定义:策略的序列化载体
新增策略的 protobuf 定义位于 protos/feast/core/Policy.proto,Policy消息通过oneof policy_type承载全部四种策略:
message Policy { string name = 1; string project = 2; oneof policy_type { RoleBasedPolicy role_based_policy = 3; GroupBasedPolicy group_based_policy = 4; NamespaceBasedPolicy namespace_based_policy = 5; CombinedGroupNamespacePolicy combined_group_namespace_policy = 6; } } message GroupBasedPolicy { repeated string groups = 1; } message NamespaceBasedPolicy { repeated string namespaces = 1; } message CombinedGroupNamespacePolicy { repeated string groups = 1; repeated string namespaces = 2; }Python 侧通过feast.protos.feast.core.Policy_pb2使用这些消息,并由make compile-protos-python重新生成。以CombinedGroupNamespacePolicy为例,其to_proto()与from_proto()的往返序列化在测试中有完整覆盖(见 test_groups_namespaces_auth.py)。
五、Token Access Review 集成:从令牌中提取组与命名空间
这是整个实现最核心的链路。KubernetesTokenParser位于 sdk/python/feast/permissions/auth/kubernetes_token_parser.py,本次改动为其新增了AuthenticationV1Api客户端,并实现_extract_groups_and_namespaces_from_token()。
5.1 整体流程:user_details_from_access_token()
- 先调用
_extract_groups_and_namespaces_from_token(access_token),通过Kubernetes TokenReview API(self.auth_v1.create_token_review(...))获得用户信息; - 尝试把令牌当作ServiceAccount 的 JWT解码(
_decode_token),解析出system:serviceaccount:NAMESPACE:SA_NAME格式的sub声明; - 若 JWT 解码失败(
AuthenticationError),则认定这是外部用户的令牌,回退到_get_username_from_token_review()获取用户名,再调用get_user_roles(username, groups)从 RoleBinding/ClusterRoleBinding 中解析角色。
5.2 组与命名空间的提取细节
_extract_groups_and_namespaces_from_token()的关键逻辑:
- 组:直接取自
response.status.user.groups; - ServiceAccount 的命名空间:从
system:serviceaccount:<ns>:<sa>形式的用户名中切分第 3 段得到,并进一步调用_extract_namespace_access_groups()找出该命名空间下 RoleBinding/ClusterRoleBinding 中引用的Group主体; - 普通用户的命名空间:调用
_extract_user_project_namespaces(),扫描全集群 RoleBinding,筛选出dashboard-permissions-*(带opendatahub.io/dashboard=true标签)或绑定adminClusterRole的 RoleBinding,将其所在的命名空间作为用户的“数据科学项目”; - ServiceAccount 组中的命名空间:对
system:serviceaccounts:<ns>形式的组名切分提取命名空间; - 最终对 groups 与 namespaces 做去重 + 排序(
sorted(list(set(...))))。
response = self.auth_v1.create_token_review(token_review) if response.status.authenticated: if response.status.user and hasattr(response.status.user, "groups"): groups = response.status.user.groups or [] # 根据 username 前缀区分 SA 与普通用户,分别补充 namespaces ... return groups, namespaces5.3 角色解析的扩展
新增的get_user_roles(username, groups)同时检查命名空间级 RoleBinding与集群级 ClusterRoleBinding,匹配subject.kind == "User"(用户直绑)或subject.kind == "Group"且组名在用户组列表内(组派生),从而让外部用户也能通过 Kubernetes RBAC 获得角色,保持与既有RoleBasedPolicy的兼容。
5.4 运行前提
- 使用
config.load_incluster_config()加载集群内配置,即该组件必须运行在 Kubernetes 集群内部(如 Feast 的 feature server/registry server Pod); - 运行中的部署需被授予查询
TokenReview、RoleBinding、ClusterRoleBinding、ClusterRole等资源的权限,否则组/命名空间提取会静默降级(源码中异常被except Exception捕获并记录日志); - 一个特殊的内部通信通道:当环境变量
INTRA_COMMUNICATION_BASE64与 ServiceAccount 名匹配时,直接返回空角色的User,用于 Feast 内部服务间通信放行。
六、客户端 SDK 与配置模型:外部用户如何携带令牌
6.1 配置模型新增user_token字段
在 sdk/python/feast/permissions/auth_model.py 中,KubernetesAuthConfig新增可选字段:
class KubernetesAuthConfig(AuthConfig): user_token: Optional[str] = None model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow")user_token用于承载外部用户(非 ServiceAccount)的令牌,同时保持旧配置(仅 ServiceAccount 令牌)的完全兼容。
6.2 客户端取令牌逻辑
KubernetesAuthClientManager.get_token()(见 sdk/python/feast/permissions/client/kubernetes_auth_client_manager.py)的取令牌优先级为:
- 内部通信令牌:若设置了
INTRA_COMMUNICATION_BASE64,构造一个sub: ":::<base64>"的无签名 JWT 直接返回; - 配置中的用户令牌:
auth_config.user_token非空时直接使用(外部用户场景); - ServiceAccount 令牌文件:默认路径
/var/run/secrets/kubernetes.io/serviceaccount/token; - 环境变量:
LOCAL_K8S_TOKEN(本地开发调试用)。
这一优先级设计保证了:外部用户通过配置注入令牌,集群内 ServiceAccount 走文件挂载,本地开发走环境变量,互不干扰。
七、使用示例:配置组/命名空间权限
以下示例完整取自原始变更文档,并补充了可运行上下文(Permission与AuthzedAction的定义见 sdk/python/feast/permissions/permission.py 与 sdk/python/feast/permissions/action.py)。
7.1 基于组的权限
from feast.permissions.policy import GroupBasedPolicy from feast.permissions.permission import Permission policy = GroupBasedPolicy(groups=["data-team", "ml-engineers"]) permission = Permission( name="data_team_access", types=ALL_RESOURCE_TYPES, policy=policy, actions=[AuthzedAction.DESCRIBE] + READ )7.2 基于命名空间的权限
from feast.permissions.policy import NamespaceBasedPolicy from feast.permissions.permission import Permission policy = NamespaceBasedPolicy(namespaces=["de-dsp", "ml-dsp"]) permission = Permission( name="data_team_access", types=ALL_RESOURCE_TYPES, policy=policy, actions=[AuthzedAction.DESCRIBE] + READ )7.3 组 + 命名空间组合权限(OR 语义)
from feast.permissions.policy import CombinedGroupNamespacePolicy policy = CombinedGroupNamespacePolicy( groups=["data-team"], namespaces=["production"] )7.4 客户端配置用户令牌
from feast.permissions.auth_model import KubernetesAuthConfig auth_config = KubernetesAuthConfig( type="kubernetes", user_token="your-kubernetes-user-token" # For external users )参数说明:
types:受策略约束的资源类型;ALL_RESOURCE_TYPES表示所有被 Feast 管理的对象类型(见 sdk/python/feast/feast_object.py 中的常量定义);actions:允许的动作,AuthzedAction.DESCRIBE描述对象,READ是读取类动作列表(见 sdk/python/feast/permissions/action.py);- 权限对象的完整字段与校验逻辑可参考 docs/getting-started/concepts/permission.md。
八、测试覆盖:15 个用例验证新功能
原始变更文档声明的测试文件实际位于 sdk/python/tests/unit/permissions/test_groups_namespaces_auth.py,共分 5 个测试类:
| 测试类 | 覆盖点 |
|---|---|
TestUserGroupsNamespaces | 带/不带 groups、namespaces 的用户创建;has_matching_group/has_matching_namespace的命中与未命中、空列表边界 |
TestGroupBasedPolicy | 组策略的通过/拒绝判定(含失败说明文案)、策略相等性 |
TestNamespaceBasedPolicy | 命名空间策略的通过/拒绝判定、策略相等性 |
TestCombinedGroupNamespacePolicy | 双命中、仅组命中、仅命名空间命中、双未命中四种组合,以及to_proto/from_proto序列化往返 |
TestBackwardCompatibility | 原有RoleBasedPolicy依然生效;带 groups/namespaces 的用户在角色策略下正常通过 |
这些用例同时印证了第四节中Combined 策略的 OR 语义:例如test_combined_policy_validation_group_matches_namespace_doesnt中用户组命中但命名空间未命中,断言结果仍为True。
九、最佳实践与安全考量
- 按团队建模组,按项目建模命名空间:组适合表达横向的组织归属(如
data-team),命名空间适合表达纵向的项目隔离(如production、de-dsp),二者结合可实现矩阵式授权; - 理解 Combined 的 OR 语义:使用
CombinedGroupNamespacePolicy时务必向团队成员说明“组或命名空间任一命中即放行”,避免误以为要求同时满足; - 为 Kubernetes API 授予最小权限:
KubernetesTokenParser依赖 TokenReview、RBAC 资源的读取权限,请通过最小化的 RBAC 规则授予,避免过度授权; - 令牌安全:
user_token是敏感凭据,应通过 Feast 配置中的密钥注入机制管理,避免明文落盘;本地调试使用的LOCAL_K8S_TOKEN仅应出现在开发环境; - 迁移路径:新增策略与
RoleBasedPolicy完全并存,存量用户无需改造即可平滑迁移;Policy.from_proto()对未知策略类型抛出NotImplementedError,升级时注意服务端与客户端 protobuf 版本一致。
十、总结
Feast 的组与命名空间授权是一次在不破坏既有角色体系前提下的能力扩展:User模型新增组/命名空间属性并保持默认空值兼容;policy.py新增三种策略且统一遵循validate_user契约;Policy.proto通过oneof平滑扩展;KubernetesTokenParser借助 Token Access Review 打通了外部用户令牌到组/命名空间/角色的完整提取链路;客户端通过user_token配置即可接入。相关实现证据均可从 sdk/python/feast/permissions、protos/feast/core/Policy.proto 与 sdk/python/tests/unit/permissions/test_groups_namespaces_auth.py 中直接查阅,完整的变更摘要见 docs/changelogs/Groups_Namespaces_Auth_implmentation_summary.md。
【免费下载链接】feastThe Open Source Feature Store for AI/ML项目地址: https://gitcode.com/GitHub_Trending/fe/feast
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考