From df47c3363ba10b00f36c31b6edaa767ae8317033 Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Thu, 28 Nov 2024 09:55:24 +0100 Subject: [PATCH 1/6] feat: make the session secret mount location configurable --- .../SessionSecrets/ProjectSessionSecrets.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/ProjectSessionSecrets.tsx b/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/ProjectSessionSecrets.tsx index 2f0565979..4a5bf3609 100644 --- a/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/ProjectSessionSecrets.tsx +++ b/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/ProjectSessionSecrets.tsx @@ -22,6 +22,7 @@ import { useMemo } from "react"; import { Key, Lock, ShieldLock } from "react-bootstrap-icons"; import { Badge, + Button, Card, CardBody, CardHeader, @@ -99,7 +100,7 @@ export default function ProjectSessionSecrets() { )} >
-

+

Session Secrets

@@ -116,10 +117,18 @@ export default function ProjectSessionSecrets() {
-

+

Use session secrets to connect to resources from inside a session that require a password or credential.

+
+

+ Session secrets will be mounted at {"/secrets"}. +

+ +
{!userLogged && ( Date: Thu, 28 Nov 2024 11:11:34 +0100 Subject: [PATCH 2/6] wip: configure secrets location --- .../SessionSecrets/AddSessionSecretButton.tsx | 10 +- .../SessionSecrets/ProjectSessionSecrets.tsx | 37 ++-- .../SessionSecrets/SessionSecretActions.tsx | 11 +- .../UpdateSecretsMountDirectoryButton.tsx | 158 ++++++++++++++++++ .../SessionSecrets/fields/FilenameField.tsx | 23 ++- .../features/projectsV2/api/projectV2.api.ts | 5 + .../projectsV2/api/projectV2.openapi.json | 26 ++- .../fields/SecretsMountDirectoryField.tsx | 69 ++++++++ 8 files changed, 319 insertions(+), 20 deletions(-) create mode 100644 client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/UpdateSecretsMountDirectoryButton.tsx create mode 100644 client/src/features/projectsV2/fields/SecretsMountDirectoryField.tsx diff --git a/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/AddSessionSecretButton.tsx b/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/AddSessionSecretButton.tsx index 78f7b6d71..1fb3e71a3 100644 --- a/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/AddSessionSecretButton.tsx +++ b/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/AddSessionSecretButton.tsx @@ -65,7 +65,8 @@ interface AddSessionSecretModalProps { function AddSessionSecretModal({ isOpen, toggle }: AddSessionSecretModalProps) { const { project } = useProject(); - const { id: projectId } = project; + const { id: projectId, secrets_mount_directory: secretsMountDirectory } = + project; const [postSessionSecretSlot, result] = usePostSessionSecretSlotsMutation(); @@ -132,7 +133,12 @@ function AddSessionSecretModal({ isOpen, toggle }: AddSessionSecretModalProps) { errors={errors} name="description" /> - + + } + requestedPermission="write" + userPermissions={permissions} + /> {!userLogged && ( @@ -150,11 +157,13 @@ export default function ProjectSessionSecrets() { } interface ProjectSessionSecretsContentProps { + secretsMountDirectory: string; sessionSecretSlots: SessionSecretSlot[]; sessionSecrets: SessionSecret[]; } function ProjectSessionSecretsContent({ + secretsMountDirectory, sessionSecretSlots, sessionSecrets, }: ProjectSessionSecretsContentProps) { @@ -173,6 +182,7 @@ function ProjectSessionSecretsContent({ {sessionSecretSlotsWithSecrets.map((secretSlot) => ( ))} @@ -181,11 +191,16 @@ function ProjectSessionSecretsContent({ } interface SessionSecretSlotItemProps { + secretsMountDirectory: string; secretSlot: SessionSecretSlotWithSecret; } -function SessionSecretSlotItem({ secretSlot }: SessionSecretSlotItemProps) { +function SessionSecretSlotItem({ + secretsMountDirectory, + secretSlot, +}: SessionSecretSlotItemProps) { const { filename, name, description } = secretSlot.secretSlot; + const fullPath = `${secretsMountDirectory}/${filename}`; return ( @@ -222,7 +237,7 @@ function SessionSecretSlotItem({ secretSlot }: SessionSecretSlotItemProps) { )}
- filename: {filename} + Location in sessions: {fullPath}
{description &&

{description}

} diff --git a/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/SessionSecretActions.tsx b/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/SessionSecretActions.tsx index 76738f051..2a635e9bb 100644 --- a/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/SessionSecretActions.tsx +++ b/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/SessionSecretActions.tsx @@ -52,6 +52,7 @@ import { usePatchProjectsByProjectIdSessionSecretsMutation, usePatchSessionSecretSlotsBySlotIdMutation, } from "../../../projectsV2/api/projectV2.enhanced-api"; +import { useProject } from "../../ProjectPageContainer/ProjectPageContainer"; import useProjectPermissions from "../../utils/useProjectPermissions.hook"; import DescriptionField from "./fields/DescriptionField"; import FilenameField from "./fields/FilenameField"; @@ -242,6 +243,9 @@ function EditSessionSecretModal({ }: EditSessionSecretModalProps) { const { id: slotId } = secretSlot; + const { project } = useProject(); + const { secrets_mount_directory: secretsMountDirectory } = project; + const [patchSessionSecretSlot, result] = usePatchSessionSecretSlotsBySlotIdMutation(); @@ -321,7 +325,12 @@ function EditSessionSecretModal({ errors={errors} name="description" /> - + + + + ); +} + +interface UpdateSecretsMountDirectoryModalProps { + isOpen: boolean; + toggle: () => void; +} + +function UpdateSecretsMountDirectoryModal({ + isOpen, + toggle, +}: UpdateSecretsMountDirectoryModalProps) { + const { project } = useProject(); + const { id: projectId } = project; + + const [patchProject, result] = usePatchProjectsByProjectIdMutation(); + + const { + control, + formState: { errors, isDirty }, + handleSubmit, + reset, + } = useForm({ + defaultValues: { + secretsMountDirectory: project.secrets_mount_directory, + }, + }); + + const submitHandler = useCallback( + (data: UpdateSecretsMountDirectoryFrom) => { + patchProject({ + "If-Match": project.etag ?? "", + projectId, + projectPatch: { + secrets_mount_directory: data.secretsMountDirectory, + }, + }); + }, + [patchProject, project.etag, projectId] + ); + const onSubmit = useMemo( + () => handleSubmit(submitHandler), + [handleSubmit, submitHandler] + ); + + useEffect(() => { + reset({ + secretsMountDirectory: project.secrets_mount_directory, + }); + }, [project, reset]); + + useEffect(() => { + if (!isOpen) { + reset(); + result.reset(); + } + }, [isOpen, reset, result]); + + useEffect(() => { + if (result.isSuccess) { + toggle(); + } + }, [result.isSuccess, toggle]); + + return ( + +
+ Update secrets mount location + +

+ Change the location where secrets will be mounted in sessions. Note + that the change will only apply to new sessions. +

+ + {result.error && ( + + )} + + +
+ + + + +
+
+ ); +} + +interface UpdateSecretsMountDirectoryFrom { + secretsMountDirectory: string; +} diff --git a/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/fields/FilenameField.tsx b/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/fields/FilenameField.tsx index 8b4b71e5a..e58ee1d8b 100644 --- a/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/fields/FilenameField.tsx +++ b/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/fields/FilenameField.tsx @@ -17,21 +17,30 @@ */ import cx from "classnames"; -import { Controller, type FieldValues } from "react-hook-form"; +import { Controller, useWatch, type FieldValues } from "react-hook-form"; import { FormText, Input, Label } from "reactstrap"; import type { SessionSecretFormFieldProps } from "./fields.types"; -type FilenameFieldProps = SessionSecretFormFieldProps; +interface FilenameFieldProps + extends SessionSecretFormFieldProps { + secretsMountDirectory: string; +} export default function FilenameField({ control, errors, name, + secretsMountDirectory, }: FilenameFieldProps) { const fieldId = `session-secret-${name}`; const fieldHelpId = `${fieldId}-help`; + const watch = useWatch({ control, name }); + const fullPath = watch + ? `${secretsMountDirectory}/${watch}` + : `${secretsMountDirectory}/`; + return (
@@ -66,8 +75,14 @@ export default function FilenameField({ )}
- This is the filename which will be used when mounting the secret inside - sessions. +

+ This is the filename which will be used when mounting the secret + inside sessions. +

+

+ The secret will be populated at:{" "} + {fullPath}. +

); diff --git a/client/src/features/projectsV2/api/projectV2.api.ts b/client/src/features/projectsV2/api/projectV2.api.ts index 00c99f683..9d79665a8 100644 --- a/client/src/features/projectsV2/api/projectV2.api.ts +++ b/client/src/features/projectsV2/api/projectV2.api.ts @@ -328,6 +328,7 @@ export type Keyword = string; export type KeywordsList = Keyword[]; export type ProjectDocumentation = string; export type IsTemplate = boolean; +export type SecretsMountDirectory = string; export type Project = { id: Ulid; name: ProjectName; @@ -344,6 +345,7 @@ export type Project = { documentation?: ProjectDocumentation; template_id?: Ulid; is_template?: IsTemplate; + secrets_mount_directory: SecretsMountDirectory; }; export type ProjectsList = Project[]; export type ErrorResponse = { @@ -374,8 +376,10 @@ export type ProjectPost = { description?: Description; keywords?: KeywordsList; documentation?: ProjectDocumentation; + secrets_mount_directory?: SecretsMountDirectory; }; export type WithDocumentation = boolean; +export type SecretsMountDirectoryPatch = string; export type ProjectPatch = { name?: ProjectName; namespace?: Slug; @@ -387,6 +391,7 @@ export type ProjectPatch = { /** template_id is set when copying a project from a template project and it cannot be modified. This field can be either null or an empty string; a null value won't change it while an empty string value will delete it, meaning that the project is unlinked from its template */ template_id?: string; is_template?: IsTemplate; + secrets_mount_directory?: SecretsMountDirectoryPatch; }; export type UserFirstLastName = string; export type Role = "viewer" | "editor" | "owner"; diff --git a/client/src/features/projectsV2/api/projectV2.openapi.json b/client/src/features/projectsV2/api/projectV2.openapi.json index 61937e8b9..fe8a71495 100644 --- a/client/src/features/projectsV2/api/projectV2.openapi.json +++ b/client/src/features/projectsV2/api/projectV2.openapi.json @@ -904,6 +904,9 @@ "is_template": { "$ref": "#/components/schemas/IsTemplate", "default": false + }, + "secrets_mount_directory": { + "$ref": "#/components/schemas/SecretsMountDirectory" } }, "required": [ @@ -913,7 +916,8 @@ "slug", "created_by", "creation_date", - "visibility" + "visibility", + "secrets_mount_directory" ], "example": { "id": "01AN4Z79ZS5XN0F25N3DB94T4R", @@ -933,7 +937,8 @@ } ], "keywords": ["keyword 1", "keyword 2"], - "template_id": "01JC3CB5426KC7P5STS5X3KSS8" + "template_id": "01JC3CB5426KC7P5STS5X3KSS8", + "secrets_mount_directory": "/secrets" } }, "ProjectPost": { @@ -964,6 +969,9 @@ }, "documentation": { "$ref": "#/components/schemas/ProjectDocumentation" + }, + "secrets_mount_directory": { + "$ref": "#/components/schemas/SecretsMountDirectory" } }, "required": ["name", "namespace"] @@ -1002,6 +1010,9 @@ }, "is_template": { "$ref": "#/components/schemas/IsTemplate" + }, + "secrets_mount_directory": { + "$ref": "#/components/schemas/SecretsMountDirectoryPatch" } } }, @@ -1105,6 +1116,17 @@ "description": "Shows if a project is a template or not", "type": "boolean" }, + "SecretsMountDirectory": { + "description": "The location where the secrets will be provided inside sessions, if left unset it will default to `/secrets`.", + "type": "string", + "minLength": 1, + "default": "/secrets", + "example": "/secrets" + }, + "SecretsMountDirectoryPatch": { + "type": "string", + "example": "/secrets" + }, "ProjectMemberListPatchRequest": { "description": "List of members and their access level to the project", "type": "array", diff --git a/client/src/features/projectsV2/fields/SecretsMountDirectoryField.tsx b/client/src/features/projectsV2/fields/SecretsMountDirectoryField.tsx new file mode 100644 index 000000000..867e611eb --- /dev/null +++ b/client/src/features/projectsV2/fields/SecretsMountDirectoryField.tsx @@ -0,0 +1,69 @@ +/*! + * Copyright 2024 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import cx from "classnames"; +import { Controller, type FieldValues } from "react-hook-form"; +import { FormText, Input, Label } from "reactstrap"; + +import { GenericProjectFormFieldProps } from "./formField.types"; + +type SecretsMountDirectoryFieldProps = + GenericProjectFormFieldProps; + +export default function SecretsMountDirectoryField({ + control, + errors, + name, +}: SecretsMountDirectoryFieldProps) { + const fieldId = `project-${name}`; + const fieldHelpId = `${fieldId}-help`; + + return ( +
+ + ( + + )} + rules={{ + required: "Please provide a location for the secrets", + }} + /> +
+ {errors[name]?.message ? ( + <>{errors[name]?.message} + ) : ( + <>Invalid location + )} +
+ + This is the location which will be used when mounting secrets inside + sessions. + +
+ ); +} From f046dcd0a2462d68d51aef17ddd91f39ba2b7dec Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Thu, 28 Nov 2024 11:49:14 +0100 Subject: [PATCH 3/6] allow reset --- .../UpdateSecretsMountDirectoryButton.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/UpdateSecretsMountDirectoryButton.tsx b/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/UpdateSecretsMountDirectoryButton.tsx index 6154020d2..9c82528f4 100644 --- a/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/UpdateSecretsMountDirectoryButton.tsx +++ b/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/UpdateSecretsMountDirectoryButton.tsx @@ -18,7 +18,7 @@ import cx from "classnames"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { Pencil, XLg } from "react-bootstrap-icons"; +import { ArrowCounterclockwise, Pencil, XLg } from "react-bootstrap-icons"; import { useForm } from "react-hook-form"; import { Button, Form, ModalBody, ModalFooter, ModalHeader } from "reactstrap"; import { RtkOrNotebooksError } from "../../../../components/errors/RtkErrorAlert"; @@ -85,6 +85,16 @@ function UpdateSecretsMountDirectoryModal({ [handleSubmit, submitHandler] ); + const onReset = useCallback(() => { + patchProject({ + "If-Match": project.etag ?? "", + projectId, + projectPatch: { + secrets_mount_directory: "", + }, + }); + }, [patchProject, project.etag, projectId]); + useEffect(() => { reset({ secretsMountDirectory: project.secrets_mount_directory, @@ -135,6 +145,10 @@ function UpdateSecretsMountDirectoryModal({ Close +