diff --git a/backend/capellacollab/sessions/hooks/__init__.py b/backend/capellacollab/sessions/hooks/__init__.py index 6032bbfce..1ff256f83 100644 --- a/backend/capellacollab/sessions/hooks/__init__.py +++ b/backend/capellacollab/sessions/hooks/__init__.py @@ -14,6 +14,7 @@ pure_variants, read_only_workspace, session_preparation, + session_token, t4c, ) @@ -23,15 +24,16 @@ "pure_variants": pure_variants.PureVariantsIntegration(), } -REGISTER_HOOKS_AUTO_USE: dict[str, interface.HookRegistration] = { - "persistent_workspace": persistent_workspace.PersistentWorkspaceHook(), - "guacamole": guacamole.GuacamoleIntegration(), - "http": http.HTTPIntegration(), - "read_only_hook": read_only_workspace.ReadOnlyWorkspaceHook(), - "provisioning": provisioning.ProvisionWorkspaceHook(), - "session_preparation": session_preparation.GitRepositoryCloningHook(), - "networking": networking.NetworkingIntegration(), -} +REGISTER_HOOKS_AUTO_USE: list[interface.HookRegistration] = [ + persistent_workspace.PersistentWorkspaceHook(), + session_token.SessionTokenIntegration(), + guacamole.GuacamoleIntegration(), + http.HTTPIntegration(), + read_only_workspace.ReadOnlyWorkspaceHook(), + provisioning.ProvisionWorkspaceHook(), + session_preparation.GitRepositoryCloningHook(), + networking.NetworkingIntegration(), +] def get_activated_integration_hooks( @@ -42,4 +44,4 @@ def get_activated_integration_hooks( hook for integration, hook in REGISTERED_HOOKS.items() if getattr(tool.integrations, integration, False) - ] + list(REGISTER_HOOKS_AUTO_USE.values()) + ] + list(REGISTER_HOOKS_AUTO_USE) diff --git a/backend/capellacollab/sessions/hooks/interface.py b/backend/capellacollab/sessions/hooks/interface.py index fefbd60f4..fde6c8838 100644 --- a/backend/capellacollab/sessions/hooks/interface.py +++ b/backend/capellacollab/sessions/hooks/interface.py @@ -40,6 +40,7 @@ class ConfigurationHookResult(t.TypedDict): warnings: t.NotRequired[list[core_models.Message]] init_volumes: t.NotRequired[list[operators_models.Volume]] init_environment: t.NotRequired[t.Mapping] + config: t.NotRequired[t.Mapping] class PostSessionCreationHookResult(t.TypedDict): diff --git a/backend/capellacollab/sessions/hooks/session_token.py b/backend/capellacollab/sessions/hooks/session_token.py new file mode 100644 index 000000000..f89bf8201 --- /dev/null +++ b/backend/capellacollab/sessions/hooks/session_token.py @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + + +import datetime + +from sqlalchemy import orm + +from capellacollab.users import models as users_models +from capellacollab.users.tokens import crud as tokens_crud + +from .. import models as sessions_models +from . import interface + + +class SessionTokenIntegration(interface.HookRegistration): + """Create a PAT valid for the duration of the session.""" + + def configuration_hook( # type: ignore + self, + db: orm.Session, + user: users_models.DatabaseUser, + session_id: str, + **kwargs, + ) -> interface.ConfigurationHookResult: + token, password = tokens_crud.create_token( + db, + user, + f"Session token for session {session_id}. Will be revoked when the session is terminated.", + datetime.date.today() + + datetime.timedelta( + days=1 + ), # Maximum duration is until end of the next day. + "SessionTokenIssuer", + ) + + return interface.ConfigurationHookResult( + environment={"CAPELLACOLLAB_SESSION_TOKEN": password}, + config={"session_token_id": token.id}, + ) + + def pre_session_termination_hook( # type: ignore + self, + db: orm.Session, + session: sessions_models.DatabaseSession, + **kwargs, + ) -> interface.PreSessionTerminationHookResult: + token_id = session.config.get("session_token_id") + if not token_id: + return interface.PreSessionTerminationHookResult() + + token = tokens_crud.get_token_by_user_and_id( + db, session.owner.id, int(token_id) + ) + if not token: + return interface.PreSessionTerminationHookResult() + + tokens_crud.delete_token(db, token) + return interface.PreSessionTerminationHookResult() diff --git a/backend/capellacollab/sessions/routes.py b/backend/capellacollab/sessions/routes.py index bd4032647..3fdf2983d 100644 --- a/backend/capellacollab/sessions/routes.py +++ b/backend/capellacollab/sessions/routes.py @@ -123,6 +123,7 @@ def request_session( init_volumes: list[operators_models.Volume] = [] init_environment: dict[str, str] = {} + hook_config: dict[str, str] = {} for hook in hooks.get_activated_integration_hooks(tool): hook_result = hook.configuration_hook( db=db, @@ -141,6 +142,7 @@ def request_session( volumes += hook_result.get("volumes", []) init_volumes += hook_result.get("init_volumes", []) warnings += hook_result.get("warnings", []) + hook_config |= hook_result.get("config", {}) local_env, local_warnings = util.resolve_environment_variables( logger, @@ -219,7 +221,6 @@ def request_session( ), ) - hook_config: dict[str, str] = {} for hook in hooks.get_activated_integration_hooks(tool): result = hook.post_session_creation_hook( session_id=session_id, diff --git a/backend/tests/sessions/test_session_hooks.py b/backend/tests/sessions/test_session_hooks.py index 27565138a..6546f5d8a 100644 --- a/backend/tests/sessions/test_session_hooks.py +++ b/backend/tests/sessions/test_session_hooks.py @@ -100,9 +100,7 @@ def pre_session_termination_hook( def fixture_session_hook(monkeypatch: pytest.MonkeyPatch) -> TestSessionHook: hook = TestSessionHook() - REGISTER_HOOKS_AUTO_USE: dict[str, hooks_interface.HookRegistration] = { - "test": hook, - } + REGISTER_HOOKS_AUTO_USE: list[hooks_interface.HookRegistration] = [hook] monkeypatch.setattr( sessions_hooks, "REGISTER_HOOKS_AUTO_USE", REGISTER_HOOKS_AUTO_USE diff --git a/backend/tests/sessions/test_session_routes.py b/backend/tests/sessions/test_session_routes.py index 8472083b7..492234845 100644 --- a/backend/tests/sessions/test_session_routes.py +++ b/backend/tests/sessions/test_session_routes.py @@ -58,7 +58,7 @@ def get_mock_operator(): @pytest.fixture(autouse=True, name="session_hook") def fixture_session_hook(monkeypatch: pytest.MonkeyPatch): - monkeypatch.setattr(sessions_hooks, "REGISTER_HOOKS_AUTO_USE", {}) + monkeypatch.setattr(sessions_hooks, "REGISTER_HOOKS_AUTO_USE", []) @pytest.mark.usefixtures("user", "session") diff --git a/frontend/src/app/users/basic-auth-token/basic-auth-token.component.html b/frontend/src/app/users/basic-auth-token/basic-auth-token.component.html index a03109556..fa153a9ad 100644 --- a/frontend/src/app/users/basic-auth-token/basic-auth-token.component.html +++ b/frontend/src/app/users/basic-auth-token/basic-auth-token.component.html @@ -67,7 +67,7 @@

Token overview

Description: {{ token.description }}
Expiration date: {{ token.expiration_date | date }}
- Creation location: {{ token.source }} + Source: {{ token.source }} @if (isTokenExpired(token.expiration_date)) { warning This token has expired! diff --git a/frontend/src/app/users/basic-auth-token/basic-auth-token.component.ts b/frontend/src/app/users/basic-auth-token/basic-auth-token.component.ts index 59486bed2..ed88140f4 100644 --- a/frontend/src/app/users/basic-auth-token/basic-auth-token.component.ts +++ b/frontend/src/app/users/basic-auth-token/basic-auth-token.component.ts @@ -103,11 +103,13 @@ export class BasicAuthTokenComponent implements OnInit { } deleteToken(token: Token) { - this.tokenService.deleteToken(token).subscribe(); - this.toastService.showSuccess( - 'Token deleted', - `The token ${token.description} was successfully deleted!`, - ); + this.tokenService.deleteToken(token).subscribe(() => { + this.toastService.showSuccess( + 'Token deleted', + `The token ${token.description} was successfully deleted!`, + ); + this.password = undefined; + }); } isTokenExpired(expirationDate: string): boolean {