Skip to content

Commit 3546ba1

Browse files
lokeshrangineniredhatHameed
authored andcommitted
Implementation of oidc client authentication. (feast-dev#40)
* Adding initial draft code to manage the oidc client authentication. Signed-off-by: Lokesh Rangineni <lokeshforjava@gmail.com> * Adding initial draft code to manage the oidc client authentication. Signed-off-by: Lokesh Rangineni <lokeshforjava@gmail.com> * Incorporating code review comments. Signed-off-by: Lokesh Rangineni <lokeshforjava@gmail.com> --------- Signed-off-by: Lokesh Rangineni <lokeshforjava@gmail.com>
1 parent eafeeee commit 3546ba1

8 files changed

Lines changed: 137 additions & 9 deletions

File tree

sdk/python/feast/infra/online_stores/remote.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616
from datetime import datetime
1717
from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple
1818

19-
import requests
2019
from pydantic import StrictStr
2120

2221
from feast import Entity, FeatureView, RepoConfig
2322
from feast.infra.online_stores.online_store import OnlineStore
23+
from feast.permissions.client.http_auth_requests_wrapper import (
24+
get_http_auth_requests_session,
25+
)
2426
from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto
2527
from feast.protos.feast.types.Value_pb2 import Value as ValueProto
2628
from feast.repo_config import FeastConfigBaseModel
@@ -70,7 +72,7 @@ def online_read(
7072
req_body = self._construct_online_read_api_json_request(
7173
entity_keys, table, requested_features
7274
)
73-
response = requests.post(
75+
response = get_http_auth_requests_session(config.auth_config).post(
7476
f"{config.online_store.path}/get-online-features", data=req_body
7577
)
7678
if response.status_code == 200:
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Literal
1+
from typing import Literal, Optional
22

33
from feast.repo_config import FeastConfigBaseModel
44

@@ -8,9 +8,10 @@ class AuthConfig(FeastConfigBaseModel):
88

99

1010
class OidcAuthConfig(AuthConfig):
11-
auth_server_url: str
11+
auth_server_url: Optional[str] = None
12+
auth_discovery_url: str
1213
client_id: str
13-
client_secret: str
14+
client_secret: Optional[str] = None
1415
username: str
1516
password: str
1617
realm: str = "master"
@@ -20,5 +21,5 @@ class NoAuthConfig(AuthConfig):
2021
pass
2122

2223

23-
class K8AuthConfig(AuthConfig):
24+
class KubernetesAuthConfig(AuthConfig):
2425
pass
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from abc import ABC, abstractmethod
2+
3+
4+
class AuthenticationClientManager(ABC):
5+
@abstractmethod
6+
def get_token(self) -> str:
7+
"""Retrieves the token based on the authentication type configuration"""
8+
pass
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import requests
2+
from requests import Session
3+
4+
from feast.permissions.auth_model import (
5+
AuthConfig,
6+
KubernetesAuthConfig,
7+
OidcAuthConfig,
8+
)
9+
from feast.permissions.client.kubernetes_auth_client_manager import (
10+
KubernetesAuthClientManager,
11+
)
12+
from feast.permissions.client.oidc_authentication_client_manager import (
13+
OidcAuthClientManager,
14+
)
15+
16+
17+
class AuthenticatedRequestsSession(Session):
18+
def __init__(self, auth_token: str):
19+
super().__init__()
20+
self.auth_token = auth_token
21+
self.headers.update({"Authorization": f"Bearer {self.auth_token}"})
22+
23+
24+
def get_auth_client_manager(auth_config: AuthConfig):
25+
if auth_config.type == "oidc":
26+
assert isinstance(auth_config, OidcAuthConfig)
27+
return OidcAuthClientManager(auth_config)
28+
elif auth_config.type == "kubernetes":
29+
assert isinstance(auth_config, KubernetesAuthConfig)
30+
return KubernetesAuthClientManager(auth_config)
31+
else:
32+
raise RuntimeError(
33+
f"No Auth client manager implemented for the auth type:${auth_config.type}"
34+
)
35+
36+
37+
def get_http_auth_requests_session(auth_config: AuthConfig) -> Session:
38+
if auth_config.type == "no_auth":
39+
request_session = requests.session()
40+
else:
41+
auth_client_manager = get_auth_client_manager(auth_config)
42+
request_session = AuthenticatedRequestsSession(auth_client_manager.get_token())
43+
return request_session
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from feast.permissions.auth_model import KubernetesAuthConfig
2+
from feast.permissions.client.auth_client_manager import AuthenticationClientManager
3+
4+
5+
class KubernetesAuthClientManager(AuthenticationClientManager):
6+
def __init__(self, auth_config: KubernetesAuthConfig):
7+
self.auth_config = auth_config
8+
9+
# TODO: needs to implement this for k8 auth.
10+
def get_token(self):
11+
return ""
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import logging
2+
3+
import requests
4+
5+
from feast.permissions.auth_model import OidcAuthConfig
6+
from feast.permissions.client.auth_client_manager import AuthenticationClientManager
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
class OidcAuthClientManager(AuthenticationClientManager):
12+
def __init__(self, auth_config: OidcAuthConfig):
13+
self.auth_config = auth_config
14+
15+
def _get_token_endpoint(self):
16+
response = requests.get(self.auth_config.auth_discovery_url)
17+
if response.status_code == 200:
18+
oidc_config = response.json()
19+
if not oidc_config["token_endpoint"]:
20+
raise RuntimeError(
21+
" OIDC token_endpoint is not available from discovery url response."
22+
)
23+
return oidc_config["token_endpoint"]
24+
else:
25+
raise RuntimeError(
26+
f"Error fetching OIDC token endpoint configuration: {response.status_code} - {response.text}"
27+
)
28+
29+
def get_token(self):
30+
# Fetch the token endpoint from the discovery URL
31+
token_endpoint = self._get_token_endpoint()
32+
33+
token_request_body = {
34+
"grant_type": "password",
35+
"client_id": self.auth_config.client_id,
36+
"username": self.auth_config.username,
37+
"password": self.auth_config.password,
38+
}
39+
40+
token_response = requests.post(token_endpoint, data=token_request_body)
41+
if token_response.status_code == 200:
42+
access_token = token_response.json()["access_token"]
43+
if not access_token:
44+
logger.debug(
45+
f"access_token is empty for the client_id=${self.auth_config.client_id}"
46+
)
47+
raise RuntimeError("access token is empty")
48+
return access_token
49+
else:
50+
raise RuntimeError(
51+
"Failed to obtain access token: {token_response.status_code} - {token_response.text}"
52+
)

sdk/python/feast/repo_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989

9090
AUTH_CONFIGS_CLASS_FOR_TYPE = {
9191
"no_auth": "feast.permissions.auth_model.NoAuthConfig",
92-
"kubernetes": "feast.permissions.auth_model.K8AuthConfig",
92+
"kubernetes": "feast.permissions.auth_model.KubernetesAuthConfig",
9393
"oidc": "feast.permissions.auth_model.OidcAuthConfig",
9494
}
9595

sdk/python/tests/unit/infra/scaffolding/test_repo_config.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
from typing import Optional
55

66
from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig
7-
from feast.permissions.auth_model import K8AuthConfig, NoAuthConfig, OidcAuthConfig
7+
from feast.permissions.auth_model import (
8+
KubernetesAuthConfig,
9+
NoAuthConfig,
10+
OidcAuthConfig,
11+
)
812
from feast.repo_config import FeastConfigError, load_repo_config
913

1014

@@ -210,6 +214,7 @@ def test_auth_config():
210214
password: test_password
211215
realm: master
212216
auth_server_url: http://localhost:8712
217+
auth_discovery_url: http://localhost:8080/realms/master/.well-known/openid-configuration
213218
registry: "registry.db"
214219
provider: local
215220
online_store:
@@ -232,6 +237,7 @@ def test_auth_config():
232237
password: test_password
233238
realm: master
234239
auth_server_url: http://localhost:8712
240+
auth_discovery_url: http://localhost:8080/realms/master/.well-known/openid-configuration
235241
registry: "registry.db"
236242
provider: local
237243
online_store:
@@ -254,6 +260,7 @@ def test_auth_config():
254260
password: test_password
255261
realm: master
256262
auth_server_url: http://localhost:8712
263+
auth_discovery_url: http://localhost:8080/realms/master/.well-known/openid-configuration
257264
registry: "registry.db"
258265
provider: local
259266
online_store:
@@ -271,6 +278,10 @@ def test_auth_config():
271278
assert oidc_repo_config.auth_config.password == "test_password"
272279
assert oidc_repo_config.auth_config.realm == "master"
273280
assert oidc_repo_config.auth_config.auth_server_url == "http://localhost:8712"
281+
assert (
282+
oidc_repo_config.auth_config.auth_discovery_url
283+
== "http://localhost:8080/realms/master/.well-known/openid-configuration"
284+
)
274285

275286
no_auth_repo_config = _test_config(
276287
dedent(
@@ -304,4 +315,4 @@ def test_auth_config():
304315
expect_error=None,
305316
)
306317
assert k8_repo_config.auth.get("type") == "kubernetes"
307-
assert isinstance(k8_repo_config.auth_config, K8AuthConfig)
318+
assert isinstance(k8_repo_config.auth_config, KubernetesAuthConfig)

0 commit comments

Comments
 (0)