Skip to content

Commit baf5254

Browse files
committed
WIP
1 parent 69de16c commit baf5254

File tree

3 files changed

+134
-22
lines changed

3 files changed

+134
-22
lines changed

pulp-glue/pulp_glue/common/openapi.py

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,46 @@ class AuthProviderBase:
6767
Returned auth objects need to be compatible with `requests.auth.AuthBase`.
6868
"""
6969

70+
def can_complete_http_basic(self) -> bool:
71+
return False
72+
73+
def can_complete_mutualTLS(self) -> bool:
74+
return False
75+
76+
def can_complete_oauth2_client_credentials(self, scopes: list[str]) -> bool:
77+
return False
78+
79+
def can_complete_scheme(self, scheme: dict[str, t.Any], scopes: list[str]) -> bool:
80+
if scheme["type"] == "http":
81+
if scheme["scheme"] == "basic":
82+
return self.can_complete_http_basic()
83+
elif scheme["type"] == "mutualTLS":
84+
return self.can_complete_mutualTLS()
85+
elif scheme["type"] == "oauth2":
86+
for flow_name, flow in scheme["flows"].items():
87+
if (
88+
flow_name == "clientCredentials"
89+
and self.can_complete_oauth2_client_credentials(flow["scopes"])
90+
):
91+
return True
92+
return False
93+
94+
def can_complete(
95+
self, proposal: dict[str, list[str]], security_schemes: dict[str, dict[str, t.Any]]
96+
) -> bool:
97+
for name, scopes in proposal.items():
98+
scheme = security_schemes.get(name)
99+
if scheme is None or not self.can_complete_scheme(scheme, scopes):
100+
return False
101+
# This covers the case where `[]` allows for no auth at all.
102+
return True
103+
104+
async def http_basic_credentials(self) -> tuple[bytes, bytes]:
105+
raise NotImplementedError()
106+
107+
async def oauth2_client_credentials(self) -> tuple[bytes, bytes]:
108+
raise NotImplementedError()
109+
70110
def basic_auth(self, scopes: list[str]) -> requests.auth.AuthBase | None:
71111
"""Implement this to provide means of http basic auth."""
72112
return None
@@ -145,11 +185,17 @@ class BasicAuthProvider(AuthProviderBase):
145185
Implementation for AuthProviderBase providing basic auth with fixed `username`, `password`.
146186
"""
147187

148-
def __init__(self, username: str, password: str):
149-
self.username = username
150-
self.password = password
188+
def __init__(self, username: t.AnyStr, password: t.AnyStr):
189+
self.username: bytes = username.encode("latin1") if isinstance(username, str) else username
190+
self.password: bytes = password.encode("latin1") if isinstance(password, str) else password
151191
self.auth = requests.auth.HTTPBasicAuth(username, password)
152192

193+
def can_complete_http_basic(self) -> bool:
194+
return True
195+
196+
async def http_basic_credentials(self) -> tuple[bytes, bytes]:
197+
return self.username, self.password
198+
153199
def basic_auth(self, scopes: list[str]) -> requests.auth.AuthBase | None:
154200
return self.auth
155201

@@ -171,22 +217,41 @@ async def __call__(
171217
) -> aiohttp.ClientResponse:
172218
if self._security:
173219
assert self._openapi._auth_provider is not None
174-
auth = self._openapi._auth_provider(
175-
self._security, self._openapi.api_spec["components"]["securitySchemes"]
176-
)
177-
if isinstance(auth, requests.auth.HTTPBasicAuth):
178-
username = (
179-
auth.username.encode("latin1")
180-
if isinstance(auth.username, str)
181-
else auth.username
182-
)
183-
password = (
184-
auth.password.encode("latin1")
185-
if isinstance(auth.password, str)
186-
else auth.password
187-
)
188-
secret = b64encode(username + b":" + password)
189-
request.headers["Authorization"] = "Basic " + secret.decode()
220+
security_schemes: dict[str, dict[str, t.Any]] = self._openapi.api_spec["components"][
221+
"securitySchemes"
222+
]
223+
for proposal in self._security:
224+
if self._openapi._auth_provider.can_complete(proposal, security_schemes):
225+
break
226+
else:
227+
raise OpenAPIError(_("No suitable auth scheme found."))
228+
229+
assert proposal is not None
230+
for scheme_name, scopes in proposal.items():
231+
scheme = security_schemes[scheme_name]
232+
if scheme["type"] == "http":
233+
if scheme["scheme"] == "basic":
234+
username, password = (
235+
await self._openapi._auth_provider.http_basic_credentials()
236+
)
237+
secret = b64encode(username + b":" + password)
238+
request.headers.add("Authorization", "Basic " + secret.decode())
239+
else:
240+
raise NotImplementedError("Auth scheme: http " + scheme["scheme"])
241+
elif scheme["type"] == "mutualTLS":
242+
# At this point, we assume the cert has already been loaded into the sslcontext.
243+
pass
244+
elif scheme["type"] == "oauth2":
245+
flow = scheme["flows"].get("clientCredentials")
246+
if flow is None:
247+
raise NotImplementedError(
248+
"OAuth2: Only client credential flow is available."
249+
)
250+
token = "DEADBEEF"
251+
request.headers.add("Authorization", f"Bearer {token}")
252+
else:
253+
raise NotImplementedError("Auth type: " + scheme["type"])
254+
190255
response = await handler(request)
191256

192257
if "Correlation-Id" in response.headers:

pulp-glue/tests/test_auth_provider.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import typing as t
2+
import asyncio
23

34
import pytest
45
from requests.auth import AuthBase
56

67
from pulp_glue.common.exceptions import OpenAPIError
7-
from pulp_glue.common.openapi import AuthProviderBase
8+
from pulp_glue.common.openapi import AuthProviderBase, BasicAuthProvider
89

910
pytestmark = pytest.mark.glue
1011

@@ -51,9 +52,34 @@
5152
},
5253
},
5354
},
55+
"E": {"type": "mutualTLS"},
5456
}
5557

5658

59+
class TestBasicAuthProvider:
60+
@pytest.fixture(scope="class")
61+
def provider(self) -> AuthProviderBase:
62+
return BasicAuthProvider(username="user1", password="password1")
63+
64+
def test_can_complete_basic(self, provider: AuthProviderBase) -> None:
65+
assert provider.can_complete_http_basic()
66+
67+
def test_provides_username_and_password(self, provider: AuthProviderBase) -> None:
68+
assert asyncio.run(provider.http_basic_credentials()) == (b"user1", b"password1")
69+
70+
def test_cannot_complete_mutualTLS(self, provider: AuthProviderBase) -> None:
71+
assert not provider.can_complete_mutualTLS()
72+
73+
def test_can_complete_basic_proposal(self, provider: AuthProviderBase) -> None:
74+
assert provider.can_complete({"B": []}, security_schemes=SECURITY_SCHEMES)
75+
76+
def test_cannot_complete_bearer_proposal(self, provider: AuthProviderBase) -> None:
77+
assert not provider.can_complete({"A": []}, security_schemes=SECURITY_SCHEMES)
78+
79+
def test_cannot_complete_combined_proposal(self, provider: AuthProviderBase) -> None:
80+
assert not provider.can_complete({"A": [], "B": []}, security_schemes=SECURITY_SCHEMES)
81+
82+
5783
class MockBasicAuth(AuthBase):
5884
pass
5985

pulp_cli/generic.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,7 @@ def __init__(
160160
self.password = password
161161
self.oauth2_client_id = oauth2_client_id
162162
self.oauth2_client_secret = oauth2_client_secret
163-
if not api_kwargs.get("cert"):
164-
api_kwargs["auth_provider"] = PulpCLIAuthProvider(pulp_ctx=self)
163+
api_kwargs["auth_provider"] = PulpCLIAuthProvider(pulp_ctx=self)
165164

166165
verify_ssl: bool | None = api_kwargs.pop("verify_ssl", None)
167166
super().__init__(
@@ -253,6 +252,28 @@ def __init__(self, pulp_ctx: PulpCLIContext):
253252
self.pulp_ctx = pulp_ctx
254253
self._memoized: dict[str, requests.auth.AuthBase | None] = {}
255254

255+
def can_complete_http_basic(self) -> bool:
256+
return self.pulp_ctx.username is not None
257+
258+
def can_complete_oauth2_client_credentials(self, scopes: list[str]) -> bool:
259+
return self.pulp_ctx.oauth2_client_id is not None
260+
261+
async def http_basic_credentials(self) -> tuple[bytes, bytes]:
262+
username = (
263+
self.pulp_ctx.username.encode("latin1")
264+
if isinstance(self.pulp_ctx.username, str)
265+
else self.pulp_ctx.username
266+
)
267+
password = (
268+
self.pulp_ctx.password.encode("latin1")
269+
if isinstance(self.pulp_ctx.password, str)
270+
else self.pulp_ctx.password
271+
)
272+
return username, password
273+
274+
async def oauth2_client_credentials(self, scopes: list[str]) -> tuple[str, str]:
275+
return self.pulp_ctx.client_id, self.pulp_ctx.client_secret
276+
256277
def basic_auth(self, scopes: list[str]) -> requests.auth.AuthBase | None:
257278
if "BASIC_AUTH" not in self._memoized:
258279
if self.pulp_ctx.username is None:

0 commit comments

Comments
 (0)