forked from feast-dev/feast
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request feast-dev#35 from dmartinol/feast-rbac-auth
Feast RBAC Authorization Manager
- Loading branch information
Showing
27 changed files
with
957 additions
and
142 deletions.
There are no files selected for viewing
Binary file modified
BIN
+3 Bytes
(100%)
examples/remote-offline-store/offline_server/feature_repo/data/driver_stats.parquet
Binary file not shown.
Binary file modified
BIN
+0 Bytes
(100%)
examples/remote-offline-store/offline_server/feature_repo/data/online_store.db
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
from abc import ABC | ||
from typing import Optional | ||
|
||
from .token_extractor import NoAuthTokenExtractor, TokenExtractor | ||
from .token_parser import NoAuthTokenParser, TokenParser | ||
|
||
|
||
class AuthManager(ABC): | ||
""" | ||
The authorization manager offers services to manage authorization tokens from client requests | ||
to extract user details before injecting them in the security context. | ||
""" | ||
|
||
_token_parser: TokenParser | ||
_token_extractor: TokenExtractor | ||
|
||
def __init__(self, token_parser: TokenParser, token_extractor: TokenExtractor): | ||
self._token_parser = token_parser | ||
self._token_extractor = token_extractor | ||
|
||
@property | ||
def token_parser(self) -> TokenParser: | ||
return self._token_parser | ||
|
||
@property | ||
def token_extractor(self) -> TokenExtractor: | ||
return self._token_extractor | ||
|
||
|
||
""" | ||
The possibly empty global instance of `AuthManager`. | ||
""" | ||
_auth_manager: Optional[AuthManager] = None | ||
|
||
|
||
def get_auth_manager() -> AuthManager: | ||
""" | ||
Return the global instance of `AuthManager`. | ||
Raises: | ||
RuntimeError if the clobal instance is not set. | ||
""" | ||
global _auth_manager | ||
if _auth_manager is None: | ||
raise RuntimeError( | ||
"AuthManager is not initialized. Call 'set_auth_manager' first." | ||
) | ||
return _auth_manager | ||
|
||
|
||
def set_auth_manager(auth_manager: AuthManager): | ||
""" | ||
Initialize the global instance of `AuthManager`. | ||
""" | ||
|
||
global _auth_manager | ||
_auth_manager = auth_manager | ||
|
||
|
||
class AllowAll(AuthManager): | ||
""" | ||
An AuthManager not extracting nor parsing the authorization token. | ||
""" | ||
|
||
def __init__(self): | ||
super().__init__( | ||
token_extractor=NoAuthTokenExtractor(), token_parser=NoAuthTokenParser() | ||
) |
103 changes: 103 additions & 0 deletions
103
sdk/python/feast/permissions/auth/kubernetes_token_parser.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import logging | ||
|
||
import jwt | ||
from kubernetes import client, config | ||
from starlette.authentication import ( | ||
AuthenticationError, | ||
) | ||
|
||
from feast.permissions.auth.token_parser import TokenParser | ||
from feast.permissions.user import User | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class KubernetesTokenParser(TokenParser): | ||
""" | ||
A `TokenParser` implementation to use Kubernetes RBAC resources to retrieve the user details. | ||
The assumption is that the request header includes an authorization bearer with the token of the | ||
client `ServiceAccount`. | ||
By inspecting the role bindings, this `TokenParser` extracts the associated `Role`s. | ||
The client `ServiceAccount` is instead used as the user name, together with the current namespace. | ||
""" | ||
|
||
def __init__(self): | ||
config.load_incluster_config() | ||
self.v1 = client.CoreV1Api() | ||
self.rbac_v1 = client.RbacAuthorizationV1Api() | ||
|
||
async def user_details_from_access_token(self, access_token: str) -> User: | ||
""" | ||
Extract the service account from the token and search the roles associated with it. | ||
Returns: | ||
User: Current user, with associated roles. The `username` is the `:` separated concatenation of `namespace` and `service account name`. | ||
Raises: | ||
AuthenticationError if any error happens. | ||
""" | ||
sa_namespace, sa_name = _decode_token(access_token) | ||
current_user = f"{sa_namespace}:{sa_name}" | ||
logging.info(f"Received request from {sa_name} in {sa_namespace}") | ||
|
||
roles = self.get_roles(sa_namespace, sa_name) | ||
logging.info(f"SA roles are: {roles}") | ||
|
||
return User(username=current_user, roles=roles) | ||
|
||
def get_roles(self, namespace: str, service_account_name: str) -> list[str]: | ||
""" | ||
Fetches the Kubernetes `Role`s associated to the given `ServiceAccount` in the given `namespace`. | ||
The research also includes the `ClusterRole`s, so the running deployment must be granted enough permissions to query | ||
for such instances in all the namespaces. | ||
Returns: | ||
list[str]: Name of the `Role`s and `ClusterRole`s associated to the service account. No string manipulation is performed on the role name. | ||
""" | ||
role_bindings = self.rbac_v1.list_namespaced_role_binding(namespace) | ||
cluster_role_bindings = self.rbac_v1.list_cluster_role_binding() | ||
|
||
roles: set[str] = set() | ||
|
||
for binding in role_bindings.items: | ||
if binding.subjects is not None: | ||
for subject in binding.subjects: | ||
if ( | ||
subject.kind == "ServiceAccount" | ||
and subject.name == service_account_name | ||
): | ||
roles.add(binding.role_ref.name) | ||
|
||
for binding in cluster_role_bindings.items: | ||
if binding.subjects is not None: | ||
for subject in binding.subjects: | ||
if ( | ||
subject.kind == "ServiceAccount" | ||
and subject.name == service_account_name | ||
and subject.namespace == namespace | ||
): | ||
roles.add(binding.role_ref.name) | ||
|
||
return list(roles) | ||
|
||
|
||
def _decode_token(access_token: str) -> tuple[str, str]: | ||
""" | ||
The `sub` portion of the decoded token includes the service account name in the format: `system:serviceaccount:NAMESPACE:SA_NAME` | ||
Returns: | ||
str: the namespace name. | ||
str: the `ServiceAccount` name. | ||
""" | ||
try: | ||
decoded_token = jwt.decode(access_token, options={"verify_signature": False}) | ||
if "sub" in decoded_token: | ||
subject: str = decoded_token["sub"] | ||
_, _, sa_namespace, sa_name = subject.split(":") | ||
return (sa_namespace, sa_name) | ||
else: | ||
raise AuthenticationError("Missing sub section in received token.") | ||
except jwt.DecodeError as e: | ||
raise AuthenticationError(f"Error decoding JWT token: {e}") |
Oops, something went wrong.