Skip to content

Commit 5f6396d

Browse files
committed
squashme: minor fixes
1 parent 3445eb8 commit 5f6396d

16 files changed

Lines changed: 146 additions & 76 deletions

File tree

components/renku_data_services/authn/dummy.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,5 @@ async def authenticate(access_token: str, request: Request) -> base_models.APIUs
6464
last_name=user_props.get("last_name", "Doe") if is_set else None,
6565
email=user_props.get("email", "john.doe@gmail.com") if is_set else None,
6666
full_name=user_props.get("full_name", "John Doe") if is_set else None,
67+
refresh_token=request.headers.get("Renku-Auth-Refresh-Token"),
6768
)

components/renku_data_services/authn/gitlab.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
"""Gitlab authenticator."""
22

3+
import base64
34
import contextlib
5+
import json
6+
import re
47
import urllib.parse as parse
8+
from contextlib import suppress
59
from dataclasses import dataclass
10+
from datetime import datetime
11+
from typing import Any
612

713
import gitlab
814
from sanic import Request
15+
from sanic.compat import Header
916

1017
import renku_data_services.base_models as base_models
1118
from renku_data_services import errors
@@ -36,10 +43,10 @@ async def authenticate(self, access_token: str, request: Request) -> base_models
3643
if self.token_field != "Authorization": # nosec: B105
3744
access_token = str(request.headers.get(self.token_field))
3845

39-
result = await self._get_gitlab_api_user(access_token)
46+
result = await self._get_gitlab_api_user(access_token, request.headers)
4047
return result
4148

42-
async def _get_gitlab_api_user(self, access_token: str) -> base_models.APIUser:
49+
async def _get_gitlab_api_user(self, access_token: str, headers: Header) -> base_models.APIUser:
4350
"""Get and validate a Gitlab API User."""
4451
client = gitlab.Gitlab(self.gitlab_url, oauth_token=access_token)
4552
try:
@@ -69,11 +76,35 @@ async def _get_gitlab_api_user(self, access_token: str) -> base_models.APIUser:
6976
if len(name_parts) >= 1:
7077
last_name = " ".join(name_parts)
7178

79+
_, _, _, expires_at = self.git_creds_from_headers(headers)
7280
return base_models.APIUser(
7381
id=str(user_id),
7482
access_token=access_token,
7583
first_name=first_name,
7684
last_name=last_name,
7785
email=email,
7886
full_name=full_name,
87+
access_token_expires_at=expires_at,
88+
)
89+
90+
@staticmethod
91+
def git_creds_from_headers(headers: Header) -> tuple[Any, Any, Any, datetime | None]:
92+
"""Extract git credentials from the encoded header sent by the gateway."""
93+
parsed_dict = json.loads(base64.decodebytes(headers["Renku-Auth-Git-Credentials"].encode()))
94+
git_url, git_credentials = next(iter(parsed_dict.items()))
95+
token_match = re.match(r"^[^\s]+\ ([^\s]+)$", git_credentials["AuthorizationHeader"])
96+
git_token = token_match.group(1) if token_match is not None else None
97+
git_token_expires_at_raw = git_credentials["AccessTokenExpiresAt"]
98+
git_token_expires_at_num: float | None = None
99+
with suppress(ValueError, TypeError):
100+
git_token_expires_at_num = float(git_token_expires_at_raw)
101+
git_token_expires_at: datetime | None = None
102+
if git_token_expires_at_num is not None and git_token_expires_at_num > 0:
103+
with suppress(ValueError):
104+
git_token_expires_at = datetime.fromtimestamp(git_token_expires_at_num)
105+
return (
106+
git_url,
107+
git_credentials["AuthorizationHeader"],
108+
git_token,
109+
git_token_expires_at,
79110
)

components/renku_data_services/authn/keycloak.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Keycloak user store."""
22

33
from dataclasses import dataclass
4+
from datetime import datetime
45
from typing import Any, Optional, cast
56

67
import httpx
@@ -42,6 +43,7 @@ class KeycloakAuthenticator(Authenticator):
4243
algorithms: list[str]
4344
admin_role: str = "renku-admin"
4445
token_field: str = "Authorization"
46+
refresh_token_header: str = "Renku-Auth-Refresh-Token"
4547

4648
def __post_init__(self) -> None:
4749
if len(self.algorithms) == 0:
@@ -76,6 +78,7 @@ async def authenticate(self, access_token: str, request: Request) -> base_models
7678

7779
parsed = self._validate(access_token)
7880
is_admin = self.admin_role in parsed.get("realm_access", {}).get("roles", [])
81+
exp = parsed.get("exp")
7982
return base_models.APIUser(
8083
is_admin_init=is_admin,
8184
id=parsed.get("sub"),
@@ -84,4 +87,6 @@ async def authenticate(self, access_token: str, request: Request) -> base_models
8487
first_name=parsed.get("given_name"),
8588
last_name=parsed.get("family_name"),
8689
email=parsed.get("email"),
90+
refresh_token=request.headers.get(self.refresh_token_header),
91+
access_token_expires_at=datetime.fromtimestamp(exp) if exp is not None else None,
8792
)

components/renku_data_services/base_api/auth.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ async def _authenticate(authenticator: Authenticator, request: Request) -> Authe
5252

5353
token = token.removeprefix("Bearer ").removeprefix("bearer ")
5454
user = await authenticator.authenticate(token, request)
55-
if not user.is_authenticated or user.id is None or user.access_token is None:
55+
if not user.is_authenticated or user.id is None or user.access_token is None or user.refresh_token is None:
5656
raise errors.UnauthorizedError(message="You have to log in to access this endpoint.", quiet=True)
5757
if not user.email:
5858
raise errors.ProgrammingError(
@@ -67,6 +67,7 @@ async def _authenticate(authenticator: Authenticator, request: Request) -> Authe
6767
last_name=user.last_name,
6868
email=user.email,
6969
is_admin_init=user.is_admin,
70+
refresh_token=user.refresh_token,
7071
)
7172

7273

components/renku_data_services/base_models/core.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import re
44
import unicodedata
55
from dataclasses import InitVar, dataclass, field
6+
from datetime import datetime
67
from enum import Enum, StrEnum
78
from typing import ClassVar, Optional, Protocol
89

@@ -27,10 +28,12 @@ class APIUser:
2728

2829
id: Optional[str] = None # the sub claim in the access token - i.e. the Keycloak user ID
2930
access_token: Optional[str] = field(repr=False, default=None)
31+
refresh_token: Optional[str] = field(repr=False, default=None)
3032
full_name: Optional[str] = None
3133
first_name: Optional[str] = None
3234
last_name: Optional[str] = None
3335
email: Optional[str] = None
36+
access_token_expires_at: datetime | None = None
3437
is_admin_init: InitVar[bool] = False
3538
__is_admin: bool = field(init=False, repr=False)
3639

@@ -55,6 +58,7 @@ class AuthenticatedAPIUser(APIUser):
5558
id: str
5659
email: str
5760
access_token: str = field(repr=False)
61+
refresh_token: str = field(repr=False)
5862
full_name: str | None = None
5963
first_name: str | None = None
6064
last_name: str | None = None
@@ -70,6 +74,7 @@ class AnonymousAPIUser(APIUser):
7074
first_name = None
7175
last_name = None
7276
email = None
77+
refresh_token = None
7378

7479
@property
7580
def is_authenticated(self) -> bool:

components/renku_data_services/notebooks/api/amalthea_patches/cloudstorage.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,20 @@
88
from renku_data_services.notebooks.api.classes.server import UserServer
99

1010

11-
def main(server: "UserServer") -> list[dict[str, Any]]:
11+
async def main(server: "UserServer") -> list[dict[str, Any]]:
1212
"""Cloud storage patches."""
1313
cloud_storage_patches: list[dict[str, Any]] = []
1414
cloud_storage_request: ICloudStorageRequest
1515
if not server.cloudstorage:
1616
return []
17+
repositories = await server.repositories()
1718
for i, cloud_storage_request in enumerate(server.cloudstorage):
1819
cloud_storage_patches.extend(
1920
cloud_storage_request.get_manifest_patch(
2021
f"{server.server_name}-ds-{i}", server.k8s_client.preferred_namespace
2122
)
2223
)
23-
if server.repositories:
24+
if repositories:
2425
cloud_storage_patches.append(
2526
{
2627
"type": "application/json-patch+json",

components/renku_data_services/notebooks/api/amalthea_patches/git_proxy.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
from renku_data_services.notebooks.api.classes.server import UserServer
1414

1515

16-
def main_container(server: "UserServer") -> client.V1Container | None:
16+
async def main_container(server: "UserServer") -> client.V1Container | None:
1717
"""The patch that adds the git proxy container to a session statefulset."""
18-
if not server.user.is_authenticated or not server.repositories:
18+
repositories = await server.repositories()
19+
if not server.user.is_authenticated or not repositories:
1920
return None
2021

2122
etc_cert_volume_mount = get_certificates_volume_mounts(
@@ -26,6 +27,8 @@ def main_container(server: "UserServer") -> client.V1Container | None:
2627
)
2728

2829
prefix = "GIT_PROXY_"
30+
git_providers = await server.git_providers()
31+
repositories = await server.repositories()
2932
env = [
3033
client.V1EnvVar(name=f"{prefix}PORT", value=str(server.config.sessions.git_proxy.port)),
3134
client.V1EnvVar(name=f"{prefix}HEALTH_PORT", value=str(server.config.sessions.git_proxy.health_port)),
@@ -47,18 +50,18 @@ def main_container(server: "UserServer") -> client.V1Container | None:
4750
client.V1EnvVar(name=f"{prefix}RENKU_URL", value="https://" + server.config.sessions.ingress.host),
4851
client.V1EnvVar(
4952
name=f"{prefix}REPOSITORIES",
50-
value=json.dumps([asdict(repo) for repo in server.repositories]),
53+
value=json.dumps([asdict(repo) for repo in repositories]),
5154
),
5255
client.V1EnvVar(
5356
name=f"{prefix}PROVIDERS",
5457
value=json.dumps(
55-
[dict(id=provider.id, access_token_url=provider.access_token_url) for provider in server.git_providers]
58+
[dict(id=provider.id, access_token_url=provider.access_token_url) for provider in git_providers]
5659
),
5760
),
5861
]
5962
container = client.V1Container(
6063
image=server.config.sessions.git_proxy.image,
61-
securityContext={
64+
security_context={
6265
"fsGroup": 100,
6366
"runAsGroup": 1000,
6467
"runAsUser": 1000,
@@ -67,34 +70,35 @@ def main_container(server: "UserServer") -> client.V1Container | None:
6770
},
6871
name="git-proxy",
6972
env=env,
70-
livenessProbe={
73+
liveness_probe={
7174
"httpGet": {
7275
"path": "/health",
7376
"port": server.config.sessions.git_proxy.health_port,
7477
},
7578
"initialDelaySeconds": 3,
7679
},
77-
readinessProbe={
80+
readiness_probe={
7881
"httpGet": {
7982
"path": "/health",
8083
"port": server.config.sessions.git_proxy.health_port,
8184
},
8285
"initialDelaySeconds": 3,
8386
},
84-
volumeMounts=etc_cert_volume_mount,
87+
volume_mounts=etc_cert_volume_mount,
8588
resources={
8689
"requests": {"memory": "16Mi", "cpu": "50m"},
8790
},
8891
)
8992
return container
9093

9194

92-
def main(server: "UserServer") -> list[dict[str, Any]]:
95+
async def main(server: "UserServer") -> list[dict[str, Any]]:
9396
"""The patch that adds the git proxy container to a session statefulset."""
94-
if not server.user.is_authenticated or not server.repositories:
97+
repositories = await server.repositories()
98+
if not server.user.is_authenticated or not repositories:
9599
return []
96100

97-
container = main_container(server)
101+
container = await main_container(server)
98102
if not container:
99103
return []
100104

components/renku_data_services/notebooks/api/amalthea_patches/git_sidecar.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88
from renku_data_services.notebooks.api.classes.server import UserServer
99

1010

11-
def main(server: "UserServer") -> list[dict[str, Any]]:
11+
async def main(server: "UserServer") -> list[dict[str, Any]]:
1212
"""Adds the git sidecar container to the session statefulset."""
1313
# NOTE: Sessions can be persisted only for registered users
1414
if not server.user.is_authenticated:
1515
return []
16-
if not server.repositories:
16+
repositories = await server.repositories()
17+
if not repositories:
1718
return []
1819

1920
gitlab_project = getattr(server, "gitlab_project", None)

components/renku_data_services/notebooks/api/amalthea_patches/init_containers.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515
# NOTE: If these are directly imported then you get circular imports.
1616
from renku_data_services.notebooks.api.classes.server import UserServer
1717

18-
def git_clone_container_v2(server: "UserServer") -> dict[str, Any] | None:
18+
async def git_clone_container_v2(server: "UserServer") -> dict[str, Any] | None:
1919
"""Returns the specification for the container that clones the user's repositories for new operator."""
2020
amalthea_session_work_volume: str = "amalthea-volume"
21-
if not server.repositories:
21+
repositories = await server.repositories()
22+
if not repositories:
2223
return None
2324

2425
etc_cert_volume_mount = get_certificates_volume_mounts(
@@ -105,7 +106,7 @@ def git_clone_container_v2(server: "UserServer") -> dict[str, Any] | None:
105106

106107

107108
# Set up git repositories
108-
for idx, repo in enumerate(server.repositories):
109+
for idx, repo in enumerate(repositories):
109110
obj_env = f"{prefix}REPOSITORIES_{idx}_"
110111
env.append(
111112
{
@@ -115,7 +116,8 @@ def git_clone_container_v2(server: "UserServer") -> dict[str, Any] | None:
115116
)
116117

117118
# Set up git providers
118-
for idx, provider in enumerate(server.required_git_providers):
119+
required_git_providers = await server.required_git_providers()
120+
for idx, provider in enumerate(required_git_providers):
119121
obj_env = f"{prefix}GIT_PROVIDERS_{idx}_"
120122
data = dict(id=provider.id, access_token_url=provider.access_token_url)
121123
env.append(
@@ -151,9 +153,10 @@ def git_clone_container_v2(server: "UserServer") -> dict[str, Any] | None:
151153
"env": env,
152154
}
153155

154-
def git_clone_container(server: "UserServer") -> dict[str, Any] | None:
156+
async def git_clone_container(server: "UserServer") -> dict[str, Any] | None:
155157
"""Returns the specification for the container that clones the user's repositories."""
156-
if not server.repositories:
158+
repositories = await server.repositories()
159+
if not repositories:
157160
return None
158161

159162
etc_cert_volume_mount = get_certificates_volume_mounts(
@@ -240,7 +243,7 @@ def git_clone_container(server: "UserServer") -> dict[str, Any] | None:
240243

241244

242245
# Set up git repositories
243-
for idx, repo in enumerate(server.repositories):
246+
for idx, repo in enumerate(repositories):
244247
obj_env = f"{prefix}REPOSITORIES_{idx}_"
245248
env.append(
246249
{
@@ -250,7 +253,8 @@ def git_clone_container(server: "UserServer") -> dict[str, Any] | None:
250253
)
251254

252255
# Set up git providers
253-
for idx, provider in enumerate(server.required_git_providers):
256+
required_git_providers = await server.required_git_providers()
257+
for idx, provider in enumerate(required_git_providers):
254258
obj_env = f"{prefix}GIT_PROVIDERS_{idx}_"
255259
data = dict(id=provider.id, access_token_url=provider.access_token_url)
256260
env.append(
@@ -287,9 +291,9 @@ def git_clone_container(server: "UserServer") -> dict[str, Any] | None:
287291
}
288292

289293

290-
def git_clone(server: "UserServer") -> list[dict[str, Any]]:
294+
async def git_clone(server: "UserServer") -> list[dict[str, Any]]:
291295
"""The patch for the init container that clones the git repository."""
292-
container = git_clone_container(server)
296+
container = await git_clone_container(server)
293297
if not container:
294298
return []
295299
return [

0 commit comments

Comments
 (0)