Skip to content

Commit e833ca6

Browse files
committed
feat(auth): support client-credentials & static token for OIDC client auth
Signed-off-by: allenov <allenov@webshark34.ru>
1 parent 9f85892 commit e833ca6

File tree

3 files changed

+89
-14
lines changed

3 files changed

+89
-14
lines changed

sdk/python/feast/permissions/auth_model.py

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,30 @@
1-
from typing import Literal
1+
# --------------------------------------------------------------------
2+
# Extends OIDC client auth model with an optional `token` field.
3+
# Works on Pydantic v1 and v2.
4+
#
5+
# Accepted credential sets (exactly **one** of):
6+
# 1 pre-issued `token`
7+
# 2 `client_secret` (client-credentials flow)
8+
# 3 `username` + `password` + `client_secret` (ROPG)
9+
# --------------------------------------------------------------------
10+
from __future__ import annotations
11+
12+
from typing import Literal, Optional
213

314
from feast.repo_config import FeastConfigBaseModel
415

16+
# pick the correct validator decorator for current Pydantic version
17+
try: # Pydantic ≥ 2.0
18+
from pydantic import model_validator as _v2 # type: ignore
19+
20+
def _cred_validator(fn):
21+
return _v2(mode="after")(fn) # run after field validation
22+
except ImportError: # Pydantic 1.x
23+
from pydantic import root_validator as _v1 # type: ignore
24+
25+
def _cred_validator(fn):
26+
return _v1(skip_on_failure=True)(fn)
27+
528

629
class AuthConfig(FeastConfigBaseModel):
730
type: Literal["oidc", "kubernetes", "no_auth"] = "no_auth"
@@ -13,9 +36,40 @@ class OidcAuthConfig(AuthConfig):
1336

1437

1538
class OidcClientAuthConfig(OidcAuthConfig):
16-
username: str
17-
password: str
18-
client_secret: str
39+
# any **one** of the four fields below is sufficient
40+
username: Optional[str] = None
41+
password: Optional[str] = None
42+
client_secret: Optional[str] = None
43+
token: Optional[str] = None # pre-issued `token`
44+
45+
@_cred_validator
46+
def _validate_credentials(cls, values):
47+
"""Enforce exactly one valid credential set."""
48+
d = values.__dict__ if hasattr(values, "__dict__") else values
49+
50+
has_user_pass = bool(d.get("username")) and bool(d.get("password"))
51+
has_secret = bool(d.get("client_secret"))
52+
has_token = bool(d.get("token"))
53+
54+
# 1 static token
55+
if has_token and not (has_user_pass or has_secret):
56+
return values
57+
58+
# 2 client_credentials
59+
if has_secret and not has_user_pass and not has_token:
60+
return values
61+
62+
# 3 ROPG
63+
if has_user_pass and has_secret and not has_token:
64+
return values
65+
66+
raise ValueError(
67+
"Invalid OIDC client auth combination: "
68+
"provide either\n"
69+
" • token\n"
70+
" • client_secret (without username/password)\n"
71+
" • username + password + client_secret"
72+
)
1973

2074

2175
class NoAuthConfig(AuthConfig):

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

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,28 @@ def get_token(self):
3030
self.auth_config.auth_discovery_url
3131
).get_token_url()
3232

33-
token_request_body = {
34-
"grant_type": "password",
35-
"client_id": self.auth_config.client_id,
36-
"client_secret": self.auth_config.client_secret,
37-
"username": self.auth_config.username,
38-
"password": self.auth_config.password,
39-
}
33+
# 1) pre-issued JWT supplied in config
34+
if getattr(self.auth_config, "token", None):
35+
return self.auth_config.token
36+
37+
# 2) client_credentials
38+
if self.auth_config.client_secret and not (
39+
self.auth_config.username and self.auth_config.password
40+
):
41+
token_request_body = {
42+
"grant_type": "client_credentials",
43+
"client_id": self.auth_config.client_id,
44+
"client_secret": self.auth_config.client_secret,
45+
}
46+
# 3) ROPG (username + password + client_secret)
47+
else:
48+
token_request_body = {
49+
"grant_type": "password",
50+
"client_id": self.auth_config.client_id,
51+
"client_secret": self.auth_config.client_secret,
52+
"username": self.auth_config.username,
53+
"password": self.auth_config.password,
54+
}
4055
headers = {"Content-Type": "application/x-www-form-urlencoded"}
4156

4257
token_response = requests.post(

sdk/python/feast/repo_config.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -308,11 +308,17 @@ def offline_store(self):
308308
def auth_config(self):
309309
if not self._auth:
310310
if isinstance(self.auth, Dict):
311+
# treat this auth block as *client-side* OIDC when it matches
312+
# 1) ROPG – username + password + client_secret
313+
# 2) client-credentials – client_secret only
314+
# 3) static token – token
311315
is_oidc_client = (
312316
self.auth.get("type") == AuthType.OIDC.value
313-
and "username" in self.auth
314-
and "password" in self.auth
315-
and "client_secret" in self.auth
317+
and (
318+
("username" in self.auth and "password" in self.auth and "client_secret" in self.auth) # 1
319+
or ("client_secret" in self.auth and "username" not in self.auth and "password" not in self.auth) # 2
320+
or ("token" in self.auth) # 3
321+
)
316322
)
317323
self._auth = get_auth_config_from_type(
318324
"oidc_client" if is_oidc_client else self.auth.get("type")

0 commit comments

Comments
 (0)