Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ API_SPECS := \
components/renku_data_services/search/apispec.py \
components/renku_data_services/notifications/apispec.py \
components/renku_data_services/capacity_reservation/apispec.py \
components/renku_data_services/resource_usage/apispec.py
components/renku_data_services/resource_usage/apispec.py \
components/renku_data_services/authn/api/apispec.py

schemas: ${API_SPECS} ## Generate pydantic classes from apispec yaml files
@echo "generated classes based on ApiSpec"
Expand Down
14 changes: 13 additions & 1 deletion bases/renku_data_services/data_api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from ulid import ULID

from renku_data_services import errors
from renku_data_services.authn.api.blueprints import InternalAuthenticationBP
from renku_data_services.base_api.error_handler import CustomErrorHandler
from renku_data_services.base_api.misc import MiscBP
from renku_data_services.base_models.core import Slug
Expand Down Expand Up @@ -192,7 +193,9 @@ def register_all_handlers(app: Sanic, dm: DependencyManager) -> Sanic:
connected_services_repo=dm.connected_services_repo,
oauth_client_factory=dm.oauth_http_client_factory,
authenticator=dm.authenticator,
nb_config=dm.config.nb_config,
internal_authenticator=dm.internal_authenticator,
internal_token_mint=dm.internal_token_mint,
internal_scope_verifier=dm.internal_scope_verifier,
)
repositories = RepositoriesBP(
name="repositories",
Expand Down Expand Up @@ -221,6 +224,7 @@ def register_all_handlers(app: Sanic, dm: DependencyManager) -> Sanic:
session_repo=dm.session_repo,
storage_repo=dm.storage_repo,
user_repo=dm.kc_user_repo,
internal_token_mint=dm.internal_token_mint,
)
platform_config = PlatformConfigBP(
name="platform_config",
Expand Down Expand Up @@ -290,6 +294,13 @@ def register_all_handlers(app: Sanic, dm: DependencyManager) -> Sanic:
rr_svc=dm.resource_usage_service,
authenticator=dm.authenticator,
)
internal_authentication = InternalAuthenticationBP(
name="internal_authentication",
url_prefix=url_prefix,
internal_authenticator=dm.internal_authenticator,
internal_token_mint=dm.internal_token_mint,
internal_scope_verifier=dm.internal_scope_verifier,
)
app.blueprint(
[
resource_pools.blueprint(),
Expand Down Expand Up @@ -320,6 +331,7 @@ def register_all_handlers(app: Sanic, dm: DependencyManager) -> Sanic:
notifications.blueprint(),
capacity_reservation.blueprint(),
resource_usage.blueprint(),
internal_authentication.blueprint(),
]
)
if builds is not None:
Expand Down
10 changes: 9 additions & 1 deletion bases/renku_data_services/data_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
from typing import Self

from renku_data_services import errors
from renku_data_services.app_config.config import KeycloakConfig, PosthogConfig, SentryConfig, TrustedProxiesConfig
from renku_data_services.app_config.config import (
InternalAuthenticationConfig,
KeycloakConfig,
PosthogConfig,
SentryConfig,
TrustedProxiesConfig,
)
from renku_data_services.app_config.logging import Config as LoggingConfig
from renku_data_services.authz.config import AuthzConfig
from renku_data_services.data_connectors.config import DepositConfig
Expand Down Expand Up @@ -36,6 +42,7 @@ class Config:
trusted_proxies: TrustedProxiesConfig
keycloak: KeycloakConfig | None
user_preferences: UserPreferencesConfig
internal_authn_config: InternalAuthenticationConfig
gitlab_url: str | None
log_cfg: LoggingConfig
version: str
Expand Down Expand Up @@ -80,6 +87,7 @@ def from_env(cls, db: DBConfig | None = None) -> Self:
trusted_proxies=TrustedProxiesConfig.from_env(),
keycloak=keycloak,
user_preferences=UserPreferencesConfig.from_env(),
internal_authn_config=InternalAuthenticationConfig.from_env(),
gitlab_url=gitlab_url,
log_cfg=LoggingConfig.from_env(),
alertmanager_webhook_role=os.environ.get("ALERTMANAGER_WEBHOOK_ROLE", "alertmanager-webhook"),
Expand Down
17 changes: 17 additions & 0 deletions bases/renku_data_services/data_api/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
import renku_data_services.search
import renku_data_services.storage
import renku_data_services.users
from renku_data_services.authn.api.core import ScopeVerifier
from renku_data_services.authn.dummy import DummyAuthenticator, DummyUserStore
from renku_data_services.authn.gitlab import EmptyGitlabAuthenticator, GitlabAuthenticator
from renku_data_services.authn.keycloak import KcUserStore, KeycloakAuthenticator
from renku_data_services.authn.renku import RenkuSelfAuthenticator, RenkuSelfTokenMint
from renku_data_services.authz.authz import Authz
from renku_data_services.capacity_reservation.db import CapacityReservationRepository, OccurrenceRepository
from renku_data_services.connected_services.db import ConnectedServicesRepository
Expand Down Expand Up @@ -128,6 +130,7 @@ class DependencyManager:
user_store: base_models.UserStore
authenticator: base_models.Authenticator
gitlab_authenticator: base_models.Authenticator
internal_authenticator: RenkuSelfAuthenticator
quota_repo: QuotaRepository
gitlab_client: base_models.GitlabAPIProtocol
kc_api: IKeycloakAPI
Expand Down Expand Up @@ -170,6 +173,8 @@ class DependencyManager:
zenodo_client: ZenodoAPIClient
job_client: DepositUploadJobClient
secret_client: K8sSecretClient
internal_token_mint: RenkuSelfTokenMint
internal_scope_verifier: ScopeVerifier

spec: dict[str, Any] = field(init=False, repr=False, default_factory=dict)
app_name: str = "renku_data_services"
Expand Down Expand Up @@ -200,6 +205,7 @@ def load_apispec() -> dict[str, Any]:
renku_data_services.notifications.__file__,
renku_data_services.capacity_reservation.__file__,
renku_data_services.resource_usage.__file__,
renku_data_services.authn.api.__file__,
]

api_specs = []
Expand Down Expand Up @@ -306,6 +312,13 @@ def from_env(cls) -> DependencyManager:
)

authz = Authz(config.authz_config)
internal_authenticator = RenkuSelfAuthenticator.from_config(config=config.internal_authn_config)
internal_token_mint = RenkuSelfTokenMint.from_config(config=config.internal_authn_config)
internal_scope_verifier = ScopeVerifier(
deposit_config=config.deposit_config,
notebook_k8s_client=config.nb_config.k8s_v2_client,
job_client=job_client,
)
search_updates_repo = SearchUpdatesRepo(session_maker=config.db.async_session_maker)
metrics_repo = MetricsRepository(session_maker=config.db.async_session_maker)
metrics = StagingMetricsService(enabled=config.posthog.enabled, metrics_repo=metrics_repo)
Expand Down Expand Up @@ -406,6 +419,7 @@ def from_env(cls) -> DependencyManager:
user_repo=kc_user_repo,
connected_services_repo=connected_services_repo,
oauth_client_factory=oauth_http_client_factory,
internal_token_mint=internal_token_mint,
)
image_check_repo = ImageCheckRepository(
nb_config=config.nb_config,
Expand Down Expand Up @@ -441,6 +455,7 @@ def from_env(cls) -> DependencyManager:
k8s_client=client,
authenticator=authenticator,
gitlab_authenticator=gitlab_authenticator,
internal_authenticator=internal_authenticator,
gitlab_client=gitlab_client,
user_store=user_store,
quota_repo=quota_repo,
Expand Down Expand Up @@ -484,4 +499,6 @@ def from_env(cls) -> DependencyManager:
zenodo_client=ZenodoAPIClient(),
job_client=job_client,
secret_client=secret_client,
internal_token_mint=internal_token_mint,
internal_scope_verifier=internal_scope_verifier,
)
71 changes: 70 additions & 1 deletion components/renku_data_services/app_config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@

from __future__ import annotations

import base64
import os
from dataclasses import dataclass
import random
from dataclasses import dataclass, field
from datetime import timedelta
from pathlib import Path

from renku_data_services import errors

Expand Down Expand Up @@ -99,3 +103,68 @@ def from_env(cls) -> TrustedProxiesConfig:
proxies_count = int(os.environ.get("PROXIES_COUNT") or "0")
real_ip_header = os.environ.get("REAL_IP_HEADER")
return cls(proxies_count=proxies_count or None, real_ip_header=real_ip_header or None)


@dataclass
class InternalAuthenticationConfig:
"""Configuration for internal authentication.

Internal authentication tokens are injected in sessions.
"""

secret_key: bytes = field(repr=False)
default_access_token_expiration: timedelta
default_refresh_token_expiration: timedelta
long_refresh_token_expiration: timedelta
issuer: str
audience: str

@classmethod
def from_env(cls) -> InternalAuthenticationConfig:
"""Create a config from environment variables."""
default_access_token_expiration_str = os.environ.get("INTERNAL_AUTHN_DEFAULT_ACCESS_TOKEN_EXPIRATION_SECONDS")
default_access_token_expiration = (
timedelta(seconds=int(default_access_token_expiration_str))
if default_access_token_expiration_str
else timedelta(minutes=15)
)
default_refresh_token_expiration_str = os.environ.get("INTERNAL_AUTHN_DEFAULT_REFRESH_TOKEN_EXPIRATION_SECONDS")
default_refresh_token_expiration = (
timedelta(seconds=int(default_refresh_token_expiration_str))
if default_refresh_token_expiration_str
else timedelta(hours=1)
)
long_refresh_token_expiration_str = os.environ.get("INTERNAL_AUTHN_LONG_REFRESH_TOKEN_EXPIRATION_SECONDS")
long_refresh_token_expiration = (
timedelta(seconds=int(long_refresh_token_expiration_str))
if long_refresh_token_expiration_str
else timedelta(hours=24)
)
issuer = os.environ.get("INTERNAL_AUTHN_ISSUER") or "renku-self"
audience = os.environ.get("INTERNAL_AUTHN_AUDIENCE") or "renku-self"

dummy_stores = os.environ.get("DUMMY_STORES", "false").lower() == "true"
if dummy_stores:
rand = random.SystemRandom()
secret_key = rand.randbytes(64)
return cls(
secret_key=secret_key,
default_access_token_expiration=default_access_token_expiration,
default_refresh_token_expiration=default_refresh_token_expiration,
long_refresh_token_expiration=long_refresh_token_expiration,
issuer=issuer,
audience=audience,
)

secret_key_path = os.environ.get("INTERNAL_AUTHN_SECRET_KEY_PATH", "")
if not secret_key_path:
raise errors.ConfigurationError(message="The secret key for internal authentication has to be specified.")
secret_key = base64.urlsafe_b64decode(Path(secret_key_path).read_bytes())
return cls(
secret_key=secret_key,
default_access_token_expiration=default_access_token_expiration,
default_refresh_token_expiration=default_refresh_token_expiration,
long_refresh_token_expiration=long_refresh_token_expiration,
issuer=issuer,
audience=audience,
)
1 change: 1 addition & 0 deletions components/renku_data_services/authn/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Blueprint for the internal authentication API."""
108 changes: 108 additions & 0 deletions components/renku_data_services/authn/api/api.spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
openapi: 3.0.2
info:
title: Renku Data Services API
description: |
This service is the main backend for Renku. It provides information about users, projects,
cloud storage, access to compute resources and many other things.
version: v1
servers:
- url: /api/data
paths:
/internal/authentication/token:
post:
summary: Token endpoint for internal authentication
description: |
Supports refreshing internal authentication tokens.
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
$ref: "#/components/schemas/PostTokenRequest"
responses:
"200":
description: A new internal access token was generated
content:
application/json:
schema:
$ref: "#/components/schemas/PostTokenResponse"
default:
$ref: "#/components/responses/Error"
tags:
- internal_authentication
components:
schemas:
PostTokenRequest:
type: object
additionalProperties: true
properties:
grant_type:
$ref: "#/components/schemas/PostTokenGrantType"
refresh_token:
type: string
required:
- grant_type
- refresh_token
PostTokenResponse:
type: object
additionalProperties: true
properties:
access_token:
type: string
example: "some_access_token"
token_type:
type: string
example: "Bearer"
expires_in:
type: integer
example: 600
refresh_token:
type: string
example: "some_refresh_token"
refresh_expires_in:
type: integer
example: 3600
scope:
type: string
example: "api"
required:
- access_token
- token_type
- expires_in
- refresh_token
PostTokenGrantType:
type: string
description: A grant type for OAuth 2.0 (see RFC 6749).
enum:
- refresh_token
ErrorResponse:
type: object
properties:
error:
type: object
properties:
code:
type: integer
minimum: 0
exclusiveMinimum: true
example: 1404
detail:
type: string
example: "A more detailed optional message showing what the problem was"
message:
type: string
example: "Something went wrong - please try again later"
trace_id:
type: string
example: "ac93950e9e114a55c67fb8e5ef519bbe"
description: Sentry trace ID for linking to corresponding log entries
required: ["code", "message"]
required: ["error"]

responses:
Error:
description: The schema for all 4xx and 5xx responses
content:
"application/json":
schema:
$ref: "#/components/schemas/ErrorResponse"
Loading
Loading