From 1781b7139927730b8f7d38863380875e2e4e1d3f Mon Sep 17 00:00:00 2001 From: Mohammed Date: Fri, 3 Feb 2023 22:33:39 +0100 Subject: [PATCH 01/13] add new modal to compare secrets across environments --- .../dashboard/CompareSecretsModal.tsx | 75 +++++++++++++++++++ frontend/src/components/dashboard/SideBar.tsx | 60 +++++++++++---- 2 files changed, 119 insertions(+), 16 deletions(-) create mode 100644 frontend/src/components/dashboard/CompareSecretsModal.tsx diff --git a/frontend/src/components/dashboard/CompareSecretsModal.tsx b/frontend/src/components/dashboard/CompareSecretsModal.tsx new file mode 100644 index 0000000000..8c41ae8cab --- /dev/null +++ b/frontend/src/components/dashboard/CompareSecretsModal.tsx @@ -0,0 +1,75 @@ +import { SetStateAction, useEffect, useState } from 'react'; + +import { WorkspaceEnv } from '~/pages/dashboard/[id]'; + +import getSecretsForProject from '../utilities/secrets/getSecretsForProject'; +import { Modal, ModalContent } from '../v2'; + +interface Secrets { + label: string; + secret: string; +} + +interface CompareSecretsModalProps { + compareModal: boolean; + setCompareModal: React.Dispatch>; + selectedEnv: WorkspaceEnv; + workspaceEnvs: WorkspaceEnv[]; + workspaceId: string; + currentSecret: { + key: string; + value: string; + }; +} + +const CompareSecretsModal = ({ + compareModal, + setCompareModal, + selectedEnv, + workspaceEnvs, + workspaceId, + currentSecret +}: CompareSecretsModalProps) => { + const [secrets, setSecrets] = useState([]); + + const getEnvSecrets = async () => { + const workspaceEnvironments = workspaceEnvs.filter((env) => env !== selectedEnv); + const newSecrets = await Promise.all( + workspaceEnvironments.map(async (env) => { + const allSecrets = await getSecretsForProject({ env: env.slug, workspaceId }); + const secret = + allSecrets.find((item) => item.key === currentSecret.key)?.value ?? 'Not found'; + return { label: env.name, secret }; + }) + ); + setSecrets([{ label: selectedEnv.name, secret: currentSecret.value }, ...newSecrets]); + }; + + useEffect(() => { + if (compareModal) { + (async () => { + await getEnvSecrets(); + })(); + } + }, [compareModal]); + + return ( + + e.preventDefault()}> +
+ {secrets.map((item) => ( +
+

{item.label}

+ +
+ ))} +
+
+
+ ); +}; +export default CompareSecretsModal; diff --git a/frontend/src/components/dashboard/SideBar.tsx b/frontend/src/components/dashboard/SideBar.tsx index 1a3f99a297..b6c20d8d63 100644 --- a/frontend/src/components/dashboard/SideBar.tsx +++ b/frontend/src/components/dashboard/SideBar.tsx @@ -7,9 +7,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import SecretVersionList from '@app/ee/components/SecretVersionList'; +import { WorkspaceEnv } from '~/pages/dashboard/[id]'; + import Button from '../basic/buttons/Button'; import Toggle from '../basic/Toggle'; import CommentField from './CommentField'; +import CompareSecretsModal from './CompareSecretsModal'; import DashboardInputField from './DashboardInputField'; import { DeleteActionButton } from './DeleteActionButton'; import GenerateSecretMenu from './GenerateSecretMenu'; @@ -40,6 +43,9 @@ interface SideBarProps { sharedToHide: string[]; setSharedToHide: (values: string[]) => void; deleteRow: (props: DeleteRowFunctionProps) => void; + workspaceEnvs: WorkspaceEnv[]; + selectedEnv: WorkspaceEnv; + workspaceId: string; } /** @@ -63,11 +69,15 @@ const SideBar = ({ modifyComment, buttonReady, savePush, - deleteRow + deleteRow, + workspaceEnvs, + selectedEnv, + workspaceId }: SideBarProps) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [isLoading, setIsLoading] = useState(false); - const [overrideEnabled, setOverrideEnabled] = useState(data[0].valueOverride !== undefined); + const [overrideEnabled, setOverrideEnabled] = useState(data[0]?.valueOverride !== undefined); + const [compareModal, setCompareModal] = useState(false); const { t } = useTranslation(); return ( @@ -171,20 +181,38 @@ const SideBar = ({ /> )} -
-
+
+
); From 16883cf168047826603dbf94d116f776327564c2 Mon Sep 17 00:00:00 2001 From: Mohammed Date: Fri, 3 Feb 2023 22:34:18 +0100 Subject: [PATCH 02/13] make some params optional --- .../utilities/secrets/getSecretsForProject.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/utilities/secrets/getSecretsForProject.ts b/frontend/src/components/utilities/secrets/getSecretsForProject.ts index c00f13d04a..62c1e2e2d6 100644 --- a/frontend/src/components/utilities/secrets/getSecretsForProject.ts +++ b/frontend/src/components/utilities/secrets/getSecretsForProject.ts @@ -29,8 +29,8 @@ interface SecretProps { interface FunctionProps { env: string; - setIsKeyAvailable: any; - setData: any; + setIsKeyAvailable?: any; + setData?: any; workspaceId: string; } @@ -58,7 +58,9 @@ const getSecretsForProject = async ({ const latestKey = await getLatestFileKey({ workspaceId }); // This is called isKeyAvailable but what it really means is if a person is able to create new key pairs - setIsKeyAvailable(!latestKey ? encryptedSecrets.length === 0 : true); + if (typeof setIsKeyAvailable === 'function') { + setIsKeyAvailable(!latestKey ? encryptedSecrets.length === 0 : true); + } const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; @@ -131,7 +133,10 @@ const getSecretsForProject = async ({ )[0]?.comment })); - setData(result); + if (typeof setData === 'function') { + setData(result); + } + return result; } catch (error) { console.log('Something went wrong during accessing or decripting secrets.'); From 0d57a26925c4d2c716cac7f4d37c32232d5e7d01 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Fri, 3 Feb 2023 20:54:14 -0800 Subject: [PATCH 03/13] Add token flag to export command --- cli/packages/cmd/export.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/packages/cmd/export.go b/cli/packages/cmd/export.go index 089c407b07..c2963f6bdf 100644 --- a/cli/packages/cmd/export.go +++ b/cli/packages/cmd/export.go @@ -96,6 +96,7 @@ func init() { exportCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)") exportCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") + exportCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") } // Format according to the format flag From e16c0e53ff96601172bfd83e004a3a20333d9c05 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Fri, 3 Feb 2023 21:02:23 -0800 Subject: [PATCH 04/13] Add offline secrets fetch feature --- cli/packages/models/cli.go | 6 +- cli/packages/util/common.go | 9 +++ cli/packages/util/secrets.go | 123 +++++++++++++++++++++++++++++++++-- 3 files changed, 129 insertions(+), 9 deletions(-) diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 5c40e23f16..67b89b27fe 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -34,9 +34,9 @@ type WorkspaceConfigFile struct { } type SymmetricEncryptionResult struct { - CipherText []byte - Nonce []byte - AuthTag []byte + CipherText []byte `json:"CipherText"` + Nonce []byte `json:"Nonce"` + AuthTag []byte `json:"AuthTag"` } type GetAllSecretsParameters struct { diff --git a/cli/packages/util/common.go b/cli/packages/util/common.go index 2d420ac6ff..13e76046d9 100644 --- a/cli/packages/util/common.go +++ b/cli/packages/util/common.go @@ -2,6 +2,7 @@ package util import ( "fmt" + "net/http" "os" ) @@ -19,3 +20,11 @@ func WriteToFile(fileName string, dataToWrite []byte, filePerm os.FileMode) erro return nil } + +func CheckIsConnectedToInternet() (ok bool) { + _, err := http.Get("http://clients3.google.com/generate_204") + if err != nil { + return false + } + return true +} diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index c20bd94e73..57b105de34 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -2,6 +2,8 @@ package util import ( "encoding/base64" + "encoding/json" + "errors" "fmt" "os" "regexp" @@ -105,10 +107,18 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models infisicalToken = params.InfisicalToken } + isConnected := CheckIsConnectedToInternet() + var secretsToReturn []models.SingleEnvironmentVariable + var errorToReturn error + if infisicalToken == "" { - RequireLocalWorkspaceFile() - RequireLogin() - log.Debug("Trying to fetch secrets using logged in details") + if isConnected { + log.Debug("GetAllEnvironmentVariables: Connected to internet, checking logged in creds") + RequireLocalWorkspaceFile() + RequireLogin() + } + + log.Debug("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details") loggedInUserDetails, err := GetCurrentLoggedInUserDetails() if err != nil { @@ -120,13 +130,30 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models return nil, err } - secrets, err := GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment) - return secrets, err + secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment) + log.Debugf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn) + + backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32] + if errorToReturn == nil { + WriteBackupSecrets(workspaceFile.WorkspaceId, params.Environment, backupSecretsEncryptionKey, secretsToReturn) + } + + // only attempt to serve cached secrets if no internet connection and if at least one secret cached + if !isConnected { + backedSecrets, err := ReadBackupSecrets(workspaceFile.WorkspaceId, params.Environment, backupSecretsEncryptionKey) + if len(backedSecrets) > 0 { + PrintWarning("Unable to fetch latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug") + secretsToReturn = backedSecrets + errorToReturn = err + } + } } else { log.Debug("Trying to fetch secrets using service token") - return GetPlainTextSecretsViaServiceToken(infisicalToken) + secretsToReturn, errorToReturn = GetPlainTextSecretsViaServiceToken(infisicalToken) } + + return secretsToReturn, errorToReturn } func getExpandedEnvVariable(secrets []models.SingleEnvironmentVariable, variableWeAreLookingFor string, hashMapOfCompleteVariables map[string]string, hashMapOfSelfRefs map[string]string) string { @@ -300,3 +327,87 @@ func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV2R return plainTextSecrets, nil } + +func WriteBackupSecrets(workspace string, environment string, encryptionKey []byte, secrets []models.SingleEnvironmentVariable) error { + fileName := fmt.Sprintf("secrets_%s_%s", workspace, environment) + secrets_backup_folder_name := "secrets-backup" + + _, fullConfigFileDirPath, err := GetFullConfigFilePath() + if err != nil { + return fmt.Errorf("WriteBackupSecrets: unable to get full config folder path [err=%s]", err) + } + + // create secrets backup directory + fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name) + if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) { + err := os.Mkdir(fullPathToSecretsBackupFolder, os.ModePerm) + if err != nil { + return err + } + } + + var encryptedSecrets []models.SymmetricEncryptionResult + for _, secret := range secrets { + marshaledSecrets, _ := json.Marshal(secret) + result, err := crypto.EncryptSymmetric(marshaledSecrets, encryptionKey) + if err != nil { + return err + } + + encryptedSecrets = append(encryptedSecrets, result) + } + + listOfSecretsMarshalled, _ := json.Marshal(encryptedSecrets) + err = os.WriteFile(fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName), listOfSecretsMarshalled, os.ModePerm) + if err != nil { + return fmt.Errorf("WriteBackupSecrets: Unable to write backup secrets to file [err=%s]", err) + } + + return nil +} + +func ReadBackupSecrets(workspace string, environment string, encryptionKey []byte) ([]models.SingleEnvironmentVariable, error) { + fileName := fmt.Sprintf("secrets_%s_%s", workspace, environment) + secrets_backup_folder_name := "secrets-backup" + + _, fullConfigFileDirPath, err := GetFullConfigFilePath() + if err != nil { + return nil, fmt.Errorf("ReadBackupSecrets: unable to write config file because an error occurred when getting config file path [err=%s]", err) + } + + fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name) + if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) { + return nil, nil + } + + encryptedBackupSecretsFilePath := fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName) + + encryptedBackupSecretsAsBytes, err := os.ReadFile(encryptedBackupSecretsFilePath) + if err != nil { + return nil, err + } + + var listOfEncryptedBackupSecrets []models.SymmetricEncryptionResult + + _ = json.Unmarshal(encryptedBackupSecretsAsBytes, &listOfEncryptedBackupSecrets) + + var plainTextSecrets []models.SingleEnvironmentVariable + for _, encryptedSecret := range listOfEncryptedBackupSecrets { + result, err := crypto.DecryptSymmetric(encryptionKey, encryptedSecret.CipherText, encryptedSecret.AuthTag, encryptedSecret.Nonce) + if err != nil { + return nil, err + } + + var plainTextSecret models.SingleEnvironmentVariable + + err = json.Unmarshal(result, &plainTextSecret) + if err != nil { + return nil, err + } + + plainTextSecrets = append(plainTextSecrets, plainTextSecret) + } + + return plainTextSecrets, nil + +} From 1e9118df333b0895c5b676d91133b278ba2eba39 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Fri, 3 Feb 2023 21:14:56 -0800 Subject: [PATCH 05/13] delete backup secrets when new user login --- cli/packages/cmd/login.go | 3 +++ cli/packages/util/secrets.go | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index 6a1dc23130..c37bdda080 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -101,6 +101,9 @@ var loginCmd = &cobra.Command{ util.HandleError(err, "Unable to write write to Infisical Config file. Please try again") } + // clear backed up secrets from prev account + util.DeleteBackupSecrets() + color.Green("Nice! You are logged in as: %v", email) }, diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 57b105de34..86f4a5e05a 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -411,3 +411,16 @@ func ReadBackupSecrets(workspace string, environment string, encryptionKey []byt return plainTextSecrets, nil } + +func DeleteBackupSecrets() error { + secrets_backup_folder_name := "secrets-backup" + + _, fullConfigFileDirPath, err := GetFullConfigFilePath() + if err != nil { + return fmt.Errorf("ReadBackupSecrets: unable to write config file because an error occurred when getting config file path [err=%s]", err) + } + + fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name) + + return os.RemoveAll(fullPathToSecretsBackupFolder) +} From 56710657bd508f5c5fd52c56a7179d48a479dcf0 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Fri, 3 Feb 2023 23:49:03 -0800 Subject: [PATCH 06/13] Minor styling updates --- .../dashboard/CompareSecretsModal.tsx | 33 +++++++++++++------ frontend/src/components/dashboard/SideBar.tsx | 5 ++- frontend/src/components/v2/Modal/Modal.tsx | 2 +- frontend/src/pages/dashboard/[id].tsx | 4 ++- frontend/src/pages/users/[id].tsx | 1 - 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/dashboard/CompareSecretsModal.tsx b/frontend/src/components/dashboard/CompareSecretsModal.tsx index 8c41ae8cab..b5d81bc04f 100644 --- a/frontend/src/components/dashboard/CompareSecretsModal.tsx +++ b/frontend/src/components/dashboard/CompareSecretsModal.tsx @@ -1,6 +1,7 @@ import { SetStateAction, useEffect, useState } from 'react'; +import Image from 'next/image'; -import { WorkspaceEnv } from '~/pages/dashboard/[id]'; +import { WorkspaceEnv } from '@app/hooks/api/types'; import getSecretsForProject from '../utilities/secrets/getSecretsForProject'; import { Modal, ModalContent } from '../v2'; @@ -33,9 +34,10 @@ const CompareSecretsModal = ({ const [secrets, setSecrets] = useState([]); const getEnvSecrets = async () => { - const workspaceEnvironments = workspaceEnvs.filter((env) => env !== selectedEnv); + const workspaceEnvironments = workspaceEnvs?.filter((env) => env !== selectedEnv); const newSecrets = await Promise.all( workspaceEnvironments.map(async (env) => { + // #TODO: optimize this query somehow... const allSecrets = await getSecretsForProject({ env: env.slug, workspaceId }); const secret = allSecrets.find((item) => item.key === currentSecret.key)?.value ?? 'Not found'; @@ -57,16 +59,27 @@ const CompareSecretsModal = ({ e.preventDefault()}>
- {secrets.map((item) => ( -
-

{item.label}

- + infisical loading indicator
- ))} + ) : ( + secrets.map((item) => ( +
+

{item.label}

+ +
+ )) + )}
diff --git a/frontend/src/components/dashboard/SideBar.tsx b/frontend/src/components/dashboard/SideBar.tsx index d04055af04..d602e2c93c 100644 --- a/frontend/src/components/dashboard/SideBar.tsx +++ b/frontend/src/components/dashboard/SideBar.tsx @@ -6,8 +6,7 @@ import { faX } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import SecretVersionList from '@app/ee/components/SecretVersionList'; - -import { WorkspaceEnv } from '~/pages/dashboard/[id]'; +import { WorkspaceEnv } from '@app/hooks/api/types'; import Button from '../basic/buttons/Button'; import Toggle from '../basic/Toggle'; @@ -181,7 +180,7 @@ const SideBar = ({ /> )} -
+
) : (
-
loading animation
); diff --git a/frontend/src/pages/users/[id].tsx b/frontend/src/pages/users/[id].tsx index abd1cfc6f4..27b531444a 100644 --- a/frontend/src/pages/users/[id].tsx +++ b/frontend/src/pages/users/[id].tsx @@ -207,7 +207,6 @@ export default function Users() {
) : (
-
loading animation
); From e72e6cf2b7289f6d155d3d9140571ff1eb8bc58f Mon Sep 17 00:00:00 2001 From: akhilmhdh Date: Sat, 4 Feb 2023 14:24:10 +0530 Subject: [PATCH 07/13] feat(ui): removed workspace context redirect and added redirect when project is deleted --- .../src/context/WorkspaceContext/WorkspaceContext.tsx | 11 +---------- .../ProjectSettingsPage/ProjectSettingsPage.tsx | 8 ++++++-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx b/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx index ea82fbbe7b..4edef8e6ab 100644 --- a/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx +++ b/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx @@ -1,4 +1,4 @@ -import { createContext, ReactNode, useContext, useEffect, useMemo } from 'react'; +import { createContext, ReactNode, useContext, useMemo } from 'react'; import { useRouter } from 'next/router'; import { useGetUserWorkspaces } from '@app/hooks/api'; @@ -30,15 +30,6 @@ export const WorkspaceProvider = ({ children }: Props): JSX.Element => { }; }, [ws, workspaceId, isLoading]); - useEffect(() => { - // not loading and current workspace is empty - // ws empty means user has no access to the ws - // push to the first workspace - if (!isLoading && !value?.currentWorkspace?._id) { - router.push(`/dashboard/${value.workspaces?.[0]?._id}`); - } - }, [value?.currentWorkspace?._id, isLoading, value.workspaces?.[0]?._id, router.pathname]); - return {children}; }; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx index e6e8f317d2..953a3d1401 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx @@ -2,6 +2,7 @@ import crypto from 'crypto'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useRouter } from 'next/router'; import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider'; import NavHeader from '@app/components/navigation/NavHeader'; @@ -37,7 +38,8 @@ import { export const ProjectSettingsPage = () => { const { t } = useTranslation(); - const { currentWorkspace } = useWorkspace(); + const { currentWorkspace, workspaces } = useWorkspace(); + const router = useRouter(); const { data: serviceTokens } = useGetUserWsServiceTokens({ workspaceID: currentWorkspace?._id || '' }); @@ -64,7 +66,6 @@ export const ProjectSettingsPage = () => { const host = window.location.origin; const isEnvServiceAllowed = subscriptionPlan !== plans.starter || host !== 'https://app.infisical.com'; - const onRenameWorkspace = async (name: string) => { try { @@ -86,6 +87,9 @@ export const ProjectSettingsPage = () => { setIsDeleting.on(); try { await deleteWorkspace.mutateAsync({ workspaceID }); + // redirect user to first workspace user is part of + const ws = workspaces.find(({ _id }) => _id !== workspaceID); + router.push(`/dashboard/${ws?._id}`); createNotification({ text: 'Successfully deleted workspace', type: 'success' From 1d72d310e598f8269926b77c58bdc5429c46eabc Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sat, 4 Feb 2023 08:47:52 -0800 Subject: [PATCH 08/13] Add offline support to faq --- docs/cli/faq.mdx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/cli/faq.mdx b/docs/cli/faq.mdx index 6a98d8dc30..7eb73258e4 100644 --- a/docs/cli/faq.mdx +++ b/docs/cli/faq.mdx @@ -13,4 +13,9 @@ If none of the available stores work for you, you can try using the `file` store If you are still experiencing trouble, please seek support. [Learn more about vault command](./commands/vault) + + + +Yes. If you have previously retrieved secrets for a specific project and environment (such as dev, staging, or prod), the `run`/`secret` command will utilize the saved secrets, even when offline, on subsequent fetch attempts. + \ No newline at end of file From 9f9273bb0201d585aa6fde326c22378833294a7b Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 5 Feb 2023 12:54:27 -0800 Subject: [PATCH 09/13] Add tags support for secrets --- backend/src/app.ts | 2 + backend/src/controllers/v2/index.ts | 4 +- .../src/controllers/v2/secretsController.ts | 58 +++++++++------- backend/src/controllers/v2/tagController.ts | 66 +++++++++++++++++++ backend/src/ee/models/secretVersion.ts | 8 ++- backend/src/models/secret.ts | 6 ++ backend/src/models/tag.ts | 49 ++++++++++++++ backend/src/routes/v2/index.ts | 4 +- backend/src/routes/v2/tags.ts | 50 ++++++++++++++ 9 files changed, 221 insertions(+), 26 deletions(-) create mode 100644 backend/src/controllers/v2/tagController.ts create mode 100644 backend/src/models/tag.ts create mode 100644 backend/src/routes/v2/tags.ts diff --git a/backend/src/app.ts b/backend/src/app.ts index 79561d00c0..cf44804e98 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -50,6 +50,7 @@ import { serviceTokenData as v2ServiceTokenDataRouter, apiKeyData as v2APIKeyDataRouter, environment as v2EnvironmentRouter, + tags as v2TagsRouter, } from './routes/v2'; import { healthCheck } from './routes/status'; @@ -112,6 +113,7 @@ app.use('/api/v1/integration-auth', v1IntegrationAuthRouter); app.use('/api/v2/users', v2UsersRouter); app.use('/api/v2/organizations', v2OrganizationsRouter); app.use('/api/v2/workspace', v2EnvironmentRouter); +app.use('/api/v2/workspace', v2TagsRouter); app.use('/api/v2/workspace', v2WorkspaceRouter); app.use('/api/v2/secret', v2SecretRouter); // deprecated app.use('/api/v2/secrets', v2SecretsRouter); diff --git a/backend/src/controllers/v2/index.ts b/backend/src/controllers/v2/index.ts index 936f5e2819..3183ac60f5 100644 --- a/backend/src/controllers/v2/index.ts +++ b/backend/src/controllers/v2/index.ts @@ -6,6 +6,7 @@ import * as apiKeyDataController from './apiKeyDataController'; import * as secretController from './secretController'; import * as secretsController from './secretsController'; import * as environmentController from './environmentController'; +import * as tagController from './tagController'; export { usersController, @@ -15,5 +16,6 @@ export { apiKeyDataController, secretController, secretsController, - environmentController + environmentController, + tagController } diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 5c6f5898e9..5ac86e9039 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -86,17 +86,28 @@ export const createSecrets = async (req: Request, res: Response) => { throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) } - let toAdd; + let listOfSecretsToCreate; if (Array.isArray(req.body.secrets)) { // case: create multiple secrets - toAdd = req.body.secrets; + listOfSecretsToCreate = req.body.secrets; } else if (typeof req.body.secrets === 'object') { // case: create 1 secret - toAdd = [req.body.secrets]; + listOfSecretsToCreate = [req.body.secrets]; } - const newSecrets = await Secret.insertMany( - toAdd.map(({ + type secretsToCreateType = { + type: string; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + tags: string[] + } + + const newlyCreatedSecrets = await Secret.insertMany( + listOfSecretsToCreate.map(({ type, secretKeyCiphertext, secretKeyIV, @@ -104,15 +115,8 @@ export const createSecrets = async (req: Request, res: Response) => { secretValueCiphertext, secretValueIV, secretValueTag, - }: { - type: string; - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - }) => { + tags + }: secretsToCreateType) => { return ({ version: 1, workspace: new Types.ObjectId(workspaceId), @@ -124,7 +128,8 @@ export const createSecrets = async (req: Request, res: Response) => { secretKeyTag, secretValueCiphertext, secretValueIV, - secretValueTag + secretValueTag, + tags }); }) ); @@ -140,7 +145,7 @@ export const createSecrets = async (req: Request, res: Response) => { // (EE) add secret versions for new secrets await EESecretService.addSecretVersions({ - secretVersions: newSecrets.map(({ + secretVersions: newlyCreatedSecrets.map(({ _id, version, workspace, @@ -154,7 +159,8 @@ export const createSecrets = async (req: Request, res: Response) => { secretValueCiphertext, secretValueIV, secretValueTag, - secretValueHash + secretValueHash, + tags }) => ({ _id: new Types.ObjectId(), secret: _id, @@ -171,7 +177,8 @@ export const createSecrets = async (req: Request, res: Response) => { secretValueCiphertext, secretValueIV, secretValueTag, - secretValueHash + secretValueHash, + tags })) }); @@ -179,7 +186,7 @@ export const createSecrets = async (req: Request, res: Response) => { name: ACTION_ADD_SECRETS, userId: req.user._id, workspaceId: new Types.ObjectId(workspaceId), - secretIds: newSecrets.map((n) => n._id) + secretIds: newlyCreatedSecrets.map((n) => n._id) }); // (EE) create (audit) log @@ -201,7 +208,7 @@ export const createSecrets = async (req: Request, res: Response) => { event: 'secrets added', distinctId: req.user.email, properties: { - numberOfSecrets: toAdd.length, + numberOfSecrets: listOfSecretsToCreate.length, environment, workspaceId, channel: channel, @@ -211,7 +218,7 @@ export const createSecrets = async (req: Request, res: Response) => { } return res.status(200).send({ - secrets: newSecrets + secrets: newlyCreatedSecrets }); } @@ -294,7 +301,7 @@ export const getSecrets = async (req: Request, res: Response) => { ], type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } } - ).then()) + ).populate("tags").then()) if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack }); @@ -398,6 +405,7 @@ export const updateSecrets = async (req: Request, res: Response) => { secretCommentCiphertext: string; secretCommentIV: string; secretCommentTag: string; + tags: string[] } const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => { @@ -410,7 +418,8 @@ export const updateSecrets = async (req: Request, res: Response) => { secretValueTag, secretCommentCiphertext, secretCommentIV, - secretCommentTag + secretCommentTag, + tags } = secret; return ({ @@ -426,6 +435,7 @@ export const updateSecrets = async (req: Request, res: Response) => { secretValueCiphertext, secretValueIV, secretValueTag, + tags, ...(( secretCommentCiphertext && secretCommentIV && @@ -460,6 +470,7 @@ export const updateSecrets = async (req: Request, res: Response) => { secretCommentCiphertext, secretCommentIV, secretCommentTag, + tags } = secretModificationsBySecretId[secret._id.toString()] return ({ @@ -477,6 +488,7 @@ export const updateSecrets = async (req: Request, res: Response) => { secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext, secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV, secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag, + tags: tags ? tags : secret.tags }); }) } diff --git a/backend/src/controllers/v2/tagController.ts b/backend/src/controllers/v2/tagController.ts new file mode 100644 index 0000000000..250ee08a5b --- /dev/null +++ b/backend/src/controllers/v2/tagController.ts @@ -0,0 +1,66 @@ +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import { Types } from 'mongoose'; +import { + Membership, +} from '../../models'; +import Tag, { ITag } from '../../models/tag'; +import { Builder } from "builder-pattern" +import to from 'await-to-js'; +import { BadRequestError, UnauthorizedRequestError } from '../../utils/errors'; +import { MongoError } from 'mongodb'; +import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions'; + +export const createWorkspaceTag = async (req: Request, res: Response) => { + const { workspaceId } = req.params + const { name, slug } = req.body + const sanitizedTagToCreate = Builder() + .name(name) + .workspace(new Types.ObjectId(workspaceId)) + .slug(slug) + .user(new Types.ObjectId(req.user._id)) + .build(); + + const [err, createdTag] = await to(Tag.create(sanitizedTagToCreate)) + + if (err) { + if ((err as MongoError).code === 11000) { + throw BadRequestError({ message: "Tags must be unique in a workspace" }) + } + + throw err + } + + res.json(createdTag) +} + +export const deleteWorkspaceTag = async (req: Request, res: Response) => { + const { tagId } = req.params + + const tagFromDB = await Tag.findById(tagId) + if (!tagFromDB) { + throw BadRequestError() + } + + // can only delete if the request user is one that belongs to the same workspace as the tag + const membership = await Membership.findOne({ + user: req.user, + workspace: tagFromDB.workspace + }); + + if (!membership) { + UnauthorizedRequestError({ message: 'Failed to validate membership' }); + } + + await Tag.findByIdAndDelete(tagId) + + res.sendStatus(200) +} + +export const getWorkspaceTags = async (req: Request, res: Response) => { + const { workspaceId } = req.params + const workspaceTags = await Tag.find({ workspace: workspaceId }) + return res.json({ + workspaceTags + }) +} \ No newline at end of file diff --git a/backend/src/ee/models/secretVersion.ts b/backend/src/ee/models/secretVersion.ts index 1af4aff2c3..efa042765f 100644 --- a/backend/src/ee/models/secretVersion.ts +++ b/backend/src/ee/models/secretVersion.ts @@ -21,6 +21,7 @@ export interface ISecretVersion { secretValueIV: string; secretValueTag: string; secretValueHash: string; + tags?: string[]; } const secretVersionSchema = new Schema( @@ -88,7 +89,12 @@ const secretVersionSchema = new Schema( }, secretValueHash: { type: String - } + }, + tags: { + ref: 'Tag', + type: [Schema.Types.ObjectId], + default: [] + }, }, { timestamps: true diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index 6887c8b0f6..4ac6c768d0 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -23,6 +23,7 @@ export interface ISecret { secretCommentIV?: string; secretCommentTag?: string; secretCommentHash?: string; + tags?: string[]; } const secretSchema = new Schema( @@ -47,6 +48,11 @@ const secretSchema = new Schema( type: Schema.Types.ObjectId, ref: 'User' }, + tags: { + ref: 'Tag', + type: [Schema.Types.ObjectId], + default: [] + }, environment: { type: String, required: true diff --git a/backend/src/models/tag.ts b/backend/src/models/tag.ts new file mode 100644 index 0000000000..6b02c8b1bc --- /dev/null +++ b/backend/src/models/tag.ts @@ -0,0 +1,49 @@ +import { Schema, model, Types } from 'mongoose'; + +export interface ITag { + _id: Types.ObjectId; + name: string; + slug: string; + user: Types.ObjectId; + workspace: Types.ObjectId; +} + +const tagSchema = new Schema( + { + name: { + type: String, + required: true, + trim: true, + }, + slug: { + type: String, + required: true, + trim: true, + lowercase: true, + validate: [ + function (value: any) { + return value.indexOf(' ') === -1; + }, + 'slug cannot contain spaces' + ] + }, + user: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace' + }, + }, + { + timestamps: true + } +); + +tagSchema.index({ slug: 1, workspace: 1 }, { unique: true }) +tagSchema.index({ workspace: 1 }) + +const Tag = model('Tag', tagSchema); + +export default Tag; diff --git a/backend/src/routes/v2/index.ts b/backend/src/routes/v2/index.ts index 6f698e316a..dfc9ee617d 100644 --- a/backend/src/routes/v2/index.ts +++ b/backend/src/routes/v2/index.ts @@ -6,6 +6,7 @@ import secrets from './secrets'; import serviceTokenData from './serviceTokenData'; import apiKeyData from './apiKeyData'; import environment from "./environment" +import tags from "./tags" export { users, @@ -15,5 +16,6 @@ export { secrets, serviceTokenData, apiKeyData, - environment + environment, + tags } \ No newline at end of file diff --git a/backend/src/routes/v2/tags.ts b/backend/src/routes/v2/tags.ts new file mode 100644 index 0000000000..d78e1e0f1e --- /dev/null +++ b/backend/src/routes/v2/tags.ts @@ -0,0 +1,50 @@ +import express, { Response, Request } from 'express'; +const router = express.Router(); +import { body, param } from 'express-validator'; +import { tagController } from '../../controllers/v2'; +import { + requireAuth, + requireWorkspaceAuth, + validateRequest, +} from '../../middleware'; +import { ADMIN, MEMBER } from '../../variables'; + +router.get( + '/:workspaceId/tags', + requireAuth({ + acceptedAuthModes: ['jwt'], + }), + requireWorkspaceAuth({ + acceptedRoles: [MEMBER, ADMIN], + }), + param('workspaceId').exists().trim(), + validateRequest, + tagController.getWorkspaceTags +); + +router.delete( + '/tags/:tagId', + requireAuth({ + acceptedAuthModes: ['jwt'], + }), + param('tagId').exists().trim(), + validateRequest, + tagController.deleteWorkspaceTag +); + +router.post( + '/:workspaceId/tags', + requireAuth({ + acceptedAuthModes: ['jwt'], + }), + requireWorkspaceAuth({ + acceptedRoles: [MEMBER, ADMIN], + }), + param('workspaceId').exists().trim(), + body('name').exists().trim(), + body('slug').exists().trim(), + validateRequest, + tagController.createWorkspaceTag +); + +export default router; From 31df4a26fac566c20bec3d7f8812ad17fae772c0 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 5 Feb 2023 16:05:34 -0800 Subject: [PATCH 10/13] Update cli docs to be more clear and consistent --- docs/cli/commands/init.mdx | 6 ++-- docs/cli/commands/run.mdx | 61 ++++++++++++++++++++++++++++++----- docs/cli/commands/secrets.mdx | 30 +++-------------- 3 files changed, 61 insertions(+), 36 deletions(-) diff --git a/docs/cli/commands/init.mdx b/docs/cli/commands/init.mdx index d477541b80..ef33e1d04a 100644 --- a/docs/cli/commands/init.mdx +++ b/docs/cli/commands/init.mdx @@ -9,6 +9,8 @@ infisical init ## Description -Link a local project to the platform +Link a local project to your Infisical project. Once connected, you can then access the secrets locally from the connected Infisical project. -The command creates a `infisical.json` file containing your Project ID. + +This command creates a `infisical.json` file containing your Project ID. + diff --git a/docs/cli/commands/run.mdx b/docs/cli/commands/run.mdx index a3f02cf444..fcc9be5493 100644 --- a/docs/cli/commands/run.mdx +++ b/docs/cli/commands/run.mdx @@ -25,13 +25,58 @@ description: "The command that injects your secrets into local environment" ## Description -Inject environment variables from the platform into an application process. +Inject secrets from Infisical into your application process. -## Options -| Option | Description | Default value | -| -------------- | ----------------------------------------------------------------------------------------------------------- | ------------- | -| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` | -| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` | -| `--command` | Pass secrets into chained commands (e.g., `"first-command && second-command; more-commands..."`) | None | -| `--secret-overriding`| Prioritizes personal secrets with the same name over shared secrets | `true` | +## Subcommands & flags + + + Use this command to inject secrets into your applications process + + ```bash + $ infisical run -- + + # Example + $ infisical run -- npm run dev + ``` + + ### flags + + Pass secrets into multiple commands at once + + ```bash + # Example + infisical run --command="npm run build && npm run dev; more-commands..." + ``` + + + + If you are using a [service token](../../getting-started/dashboard/token) to authenticate, you can pass the token as a flag + + ```bash + # Example + infisical run --token="st.63e03c4a97cb4a747186c71e.ed5b46a34c078a8f94e8228f4ab0ff97.4f7f38034811995997d72badf44b42ec" -- npm run start + ``` + + You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the run command. This will have the same effect as setting the token with `--token` flag + + + + Turn on or off the shell parameter expansion in your secrets. If you have used shell parameters in your secret(s), activating this feature will populate them before injecting them into your application process. + + Default value: `true` + + + + This is used to specify the environment from which secrets should be retrieved. The accepted values are the environment slugs defined for your project, such as `dev`, `staging`, `test`, and `prod`. + + Default value: `dev` + + + + Prioritizes personal secrets with the same name over shared secrets + + Default value: `true` + + + diff --git a/docs/cli/commands/secrets.mdx b/docs/cli/commands/secrets.mdx index 0556e1866a..0e4190291c 100644 --- a/docs/cli/commands/secrets.mdx +++ b/docs/cli/commands/secrets.mdx @@ -14,17 +14,8 @@ This command enables you to perform CRUD (create, read, update, delete) operatio Use this command to print out all of the secrets in your project - ``` - $ infisical secrets - - ## Example + ```bash $ infisical secrets - ┌─────────────┬──────────────┬─────────────┐ - │ SECRET NAME │ SECRET VALUE │ SECRET TYPE │ - ├─────────────┼──────────────┼─────────────┤ - │ DOMAIN │ example.com │ shared │ - │ HASH │ jebhfbwe │ shared │ - └─────────────┴──────────────┴─────────────┘ ``` ### flags @@ -45,16 +36,11 @@ This command enables you to perform CRUD (create, read, update, delete) operatio This command allows you selectively print the requested secrets by name - ``` + ```bash $ infisical secrets get ... # Example $ infisical secrets get DOMAIN - ┌─────────────┬──────────────┬─────────────┐ - │ SECRET NAME │ SECRET VALUE │ SECRET TYPE │ - ├─────────────┼──────────────┼─────────────┤ - │ DOMAIN │ example.com │ shared │ - └─────────────┴──────────────┴─────────────┘ ``` @@ -70,18 +56,11 @@ This command enables you to perform CRUD (create, read, update, delete) operatio This command allows you to set or update secrets in your environment. If the secret key provided already exists, its value will be updated with the new value. If the secret key does not exist, a new secret will be created using both the key and value provided. -``` +```bash $ infisical secrets set ... ## Example $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jebhfbwe -┌────────────────┬───────────────┬────────────────────────┐ -│ SECRET NAME │ SECRET VALUE │ STATUS │ -├────────────────┼───────────────┼────────────────────────┤ -│ STRIPE_API_KEY │ sjdgwkeudyjwe │ SECRET VALUE UNCHANGED │ -│ DOMAIN │ example.com │ SECRET VALUE MODIFIED │ -│ HASH │ jebhfbwe │ SECRET CREATED │ -└────────────────┴───────────────┴────────────────────────┘ ``` ### Flags @@ -95,12 +74,11 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb This command allows you to delete secrets by their name(s). - ``` + ```bash $ infisical secrets delete ... ## Example $ infisical secrets delete STRIPE_API_KEY DOMAIN HASH - secret name(s) [STRIPE_API_KEY, DOMAIN, HASH] have been deleted from your project ``` ### Flags From c13cb2394273449d21c1e2f82e0fa83647ff4028 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 5 Feb 2023 19:21:07 -0800 Subject: [PATCH 11/13] Add gitlab integ docs --- docs/integrations/cicd/gitlab.mdx | 34 +++++++++++++++++++++++++++++++ docs/mint.json | 1 + 2 files changed, 35 insertions(+) create mode 100644 docs/integrations/cicd/gitlab.mdx diff --git a/docs/integrations/cicd/gitlab.mdx b/docs/integrations/cicd/gitlab.mdx new file mode 100644 index 0000000000..b84d90e078 --- /dev/null +++ b/docs/integrations/cicd/gitlab.mdx @@ -0,0 +1,34 @@ +--- +title: "Gitlab Pipeline" +--- + +To integrate Infisical secrets into your Gitlab CI/CD setup, three steps are required. + +## Generate service token +To expose Infisical secrets in Gitlab CI/CD, you must generate a service token for the specific project and environment in Infisical. For instructions on how to generate a service token, refer to [this page](../../getting-started/dashboard/token) + +## Set Infisical service token in Gitlab +To provide Infisical CLI with the service token generated in the previous step, go to **Settings > CI/CD > Variables** in Gitlab and create a new **INFISICAL_TOKEN** variable. Enter the generated service token as its value. + +## Configure Infisical in your pipeline +Edit your .gitlab-ci.yml to include the installation of the Infisical CLI. This will allow you to use the CLI for fetching and injecting secrets into any script or command within your Gitlab CI/CD process. + +#### Example +```yaml +image: ubuntu + +stages: + - build + - test + - deploy + +build-job: + stage: build + script: + - apt update && apt install -y curl + - curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash + - apt-get update && apt-get install -y infisical + - infisical run -- npm run build + +... +``` \ No newline at end of file diff --git a/docs/mint.json b/docs/mint.json index 4417691523..fd334b5ab6 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -227,6 +227,7 @@ "group": "CI/CD", "pages": [ "integrations/cicd/githubactions", + "integrations/cicd/gitlab", "integrations/cicd/circleci" ] }, From 56a14925daf8678b1fea9aa6bb1d134fe14566b3 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 5 Feb 2023 19:23:52 -0800 Subject: [PATCH 12/13] Add githlab to integ overview --- docs/integrations/overview.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/integrations/overview.mdx b/docs/integrations/overview.mdx index 36423e787c..eb99ca79d6 100644 --- a/docs/integrations/overview.mdx +++ b/docs/integrations/overview.mdx @@ -37,7 +37,7 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi | GCP | Cloud | Coming soon | | Azure | Cloud | Coming soon | | DigitalOcean | Cloud | Coming soon | -| GitLab | CI/CD | Coming soon | +| [GitLab Pipeline](/integrations/cicd/gitlab) | CI/CD | Available | | [CircleCI](/integrations/cicd/circleci) | CI/CD | Coming soon | | TravisCI | CI/CD | Coming soon | | GitHub Actions | CI/CD | Coming soon | From 086dd621b53c53d2fbbf8f38bf1ba1dfb78cf051 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Sun, 5 Feb 2023 20:29:27 -0800 Subject: [PATCH 13/13] Revamped the dashabord look --- frontend/package-lock.json | 39 +++++ frontend/package.json | 1 + frontend/public/locales/en/common.json | 2 +- frontend/src/components/basic/Listbox.tsx | 2 +- .../components/basic/dialog/DeleteEnvVar.tsx | 2 +- .../dashboard/DashboardInputField.tsx | 126 ++++++++++++---- .../dashboard/DeleteActionButton.tsx | 26 +++- .../dashboard/DownloadSecretsMenu.tsx | 2 +- .../src/components/dashboard/DropZone.tsx | 2 +- frontend/src/components/dashboard/KeyPair.tsx | 77 ++++++---- frontend/src/components/dashboard/SideBar.tsx | 60 ++++---- .../src/components/v2/HoverCard/HoverCard.tsx | 42 ++++++ .../src/components/v2/HoverCard/index.tsx | 2 + .../components/v2/IconButton/IconButton.tsx | 2 +- frontend/src/components/v2/Modal/Modal.tsx | 4 +- .../src/ee/components/PITRecoverySidebar.tsx | 6 +- .../src/ee/components/SecretVersionList.tsx | 2 +- frontend/src/pages/dashboard/[id].tsx | 139 +++++++++++------- 18 files changed, 388 insertions(+), 148 deletions(-) create mode 100644 frontend/src/components/v2/HoverCard/HoverCard.tsx create mode 100644 frontend/src/components/v2/HoverCard/index.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 283c69b532..7f52e04aeb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20,6 +20,7 @@ "@radix-ui/react-checkbox": "^1.0.1", "@radix-ui/react-dialog": "^1.0.2", "@radix-ui/react-dropdown-menu": "^2.0.2", + "@radix-ui/react-hover-card": "^1.0.3", "@radix-ui/react-label": "^2.0.0", "@radix-ui/react-popover": "^1.0.3", "@radix-ui/react-progress": "^1.0.1", @@ -3858,6 +3859,27 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.0.3.tgz", + "integrity": "sha512-rr2+DxPlMhR57IPcNvZ85X8chytdfj7kyVToyR5Ge0r4IJEFiyPs0Cs8/K8oe5zt+yo0F8f29vtC8tNNK+ZIkA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.2", + "@radix-ui/react-popper": "1.1.0", + "@radix-ui/react-portal": "1.0.1", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-id": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz", @@ -25050,6 +25072,23 @@ "@radix-ui/react-use-callback-ref": "1.0.0" } }, + "@radix-ui/react-hover-card": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.0.3.tgz", + "integrity": "sha512-rr2+DxPlMhR57IPcNvZ85X8chytdfj7kyVToyR5Ge0r4IJEFiyPs0Cs8/K8oe5zt+yo0F8f29vtC8tNNK+ZIkA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.2", + "@radix-ui/react-popper": "1.1.0", + "@radix-ui/react-portal": "1.0.1", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.0" + } + }, "@radix-ui/react-id": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 322e60f311..db8153a0a9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,6 +27,7 @@ "@radix-ui/react-checkbox": "^1.0.1", "@radix-ui/react-dialog": "^1.0.2", "@radix-ui/react-dropdown-menu": "^2.0.2", + "@radix-ui/react-hover-card": "^1.0.3", "@radix-ui/react-label": "^2.0.0", "@radix-ui/react-popover": "^1.0.3", "@radix-ui/react-progress": "^1.0.1", diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index e02c4f474e..3d840c6635 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -14,7 +14,7 @@ "save-changes": "Save Changes", "saved": "Saved", "drop-zone": "Drag and drop a .env or .yml file here.", - "drop-zone-keys": "Drag and drop a .env or .yml file here to add more keys.", + "drop-zone-keys": "Drag and drop a .env or .yml file here to add more secrets.", "role": "Role", "role_admin": "admin", "display-name": "Display Name", diff --git a/frontend/src/components/basic/Listbox.tsx b/frontend/src/components/basic/Listbox.tsx index a2f08d9b94..19bdd1dd55 100644 --- a/frontend/src/components/basic/Listbox.tsx +++ b/frontend/src/components/basic/Listbox.tsx @@ -58,7 +58,7 @@ const ListBox = ({ leaveFrom="opacity-100" leaveTo="opacity-0" > - + {data.map((person, personIdx) => ( { return (
- {}}> + {}}>
void; value: string | undefined; - type: 'varName' | 'value'; + type: 'varName' | 'value' | 'comment'; blurred?: boolean; isDuplicate?: boolean; - override?: boolean; + overrideEnabled?: boolean; + modifyValueOverride?: (value: string | undefined, position: number) => void; + isSideBarOpen?: boolean; } /** @@ -26,6 +30,8 @@ interface DashboardInputFieldProps { * @param {boolean} obj.blurred - whether the input field should be blurred (behind the gray dots) or not; this can be turned on/off in the dashboard * @param {boolean} obj.isDuplicate - if the key name is duplicated * @param {boolean} obj.override - whether a secret/row should be displalyed as overriden + * + * * @returns */ @@ -36,7 +42,9 @@ const DashboardInputField = ({ value, blurred, isDuplicate, - override + overrideEnabled, + modifyValueOverride, + isSideBarOpen }: DashboardInputFieldProps) => { const ref = useRef(null); const syncScroll = (e: SyntheticEvent) => { @@ -51,41 +59,97 @@ const DashboardInputField = ({ const error = startsWithNumber || isDuplicate; return ( -
+
onChangeHandler(e.target.value.toUpperCase(), position)} type={type} value={value} - className={`z-10 peer font-mono ph-no-capture bg-bunker-800 rounded-md caret-white text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 ${ - error ? 'focus:ring-red/50' : 'focus:ring-primary/50' + className={`z-10 peer font-mono ph-no-capture bg-transparent h-full caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none ${ + error ? 'text-red-600 focus:text-red-500' : 'text-bunker-300 focus:text-bunker-100' } duration-200`} spellCheck="false" />
{startsWithNumber && ( -

- Should not start with a number -

+
+ +
)} - {isDuplicate && !startsWithNumber && ( -

- Secret names should be unique -

+ {isDuplicate && value !== '' && !startsWithNumber && ( +
+ +
)} + {!error &&
+ +
} +
+ ); + } + if (type === 'comment') { + const startsWithNumber = !Number.isNaN(Number(value?.charAt(0))) && value !== ''; + const error = startsWithNumber || isDuplicate; + + return ( +
+
+ onChangeHandler(e.target.value, position)} + type={type} + value={value} + className='z-10 peer font-mono ph-no-capture bg-transparent py-2.5 caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200' + spellCheck="false" + placeholder='–' + /> +
); } if (type === 'value') { return (
-
- {override === true && ( -
+
+ {overrideEnabled === true && ( +
Override enabled
)} @@ -95,19 +159,19 @@ const DashboardInputField = ({ onScroll={syncScroll} className={`${ blurred - ? 'text-transparent group-hover:text-transparent focus:text-transparent active:text-transparent' + ? 'text-transparent focus:text-transparent active:text-transparent' : '' - } z-10 peer font-mono ph-no-capture bg-transparent rounded-md caret-white text-transparent text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-200 no-scrollbar no-scrollbar::-webkit-scrollbar`} + } z-10 peer font-mono ph-no-capture bg-transparent caret-white text-transparent text-sm px-2 py-2 w-full min-w-16 outline-none duration-200 no-scrollbar no-scrollbar::-webkit-scrollbar`} spellCheck="false" />
{value?.split(REGEX).map((word, id) => { if (word.match(REGEX) !== null) { @@ -137,7 +201,9 @@ const DashboardInputField = ({ })}
{blurred && ( -
+
{value?.split('').map(() => ( ))} + {value?.split('').length === 0 && EMPTY}
+
)}
@@ -163,8 +231,8 @@ function inputPropsAreEqual(prev: DashboardInputFieldProps, next: DashboardInput prev.type === next.type && prev.position === next.position && prev.blurred === next.blurred && - prev.override === next.override && - prev.isDuplicate === next.isDuplicate + prev.overrideEnabled === next.overrideEnabled && + prev.isDuplicate === next.isDuplicate ); } diff --git a/frontend/src/components/dashboard/DeleteActionButton.tsx b/frontend/src/components/dashboard/DeleteActionButton.tsx index e4d56dd950..d0708f239f 100644 --- a/frontend/src/components/dashboard/DeleteActionButton.tsx +++ b/frontend/src/components/dashboard/DeleteActionButton.tsx @@ -1,26 +1,42 @@ import React, { useState } from 'react' import { useTranslation } from 'react-i18next'; +import { faXmark } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import Button from '../basic/buttons/Button'; import { DeleteEnvVar } from '../basic/dialog/DeleteEnvVar'; type Props = { - onSubmit: () => void + onSubmit: () => void; + isPlain?: boolean; } -export const DeleteActionButton = ({ onSubmit }: Props) => { +export const DeleteActionButton = ({ onSubmit, isPlain }: Props) => { const { t } = useTranslation(); const [open, setOpen] = useState(false) return ( -
-
- {!snapshotData && data?.length === 0 && ( + {!snapshotData && data?.length === 0 && selectedEnv && ( name)} @@ -629,7 +646,7 @@ export default function Dashboard() { />
)} - {snapshotData && ( + {snapshotData && selectedEnv && (
-
+
- {(snapshotData || data?.length !== 0) && ( + {(snapshotData || data?.length !== 0) && selectedEnv && ( <> {!snapshotData ? ( )} -
+
setSearchKeys(e.target.value)} placeholder={String(t('dashboard:search-keys'))} />
- {!snapshotData && ( -
-
- )} {!snapshotData && (
@@ -765,13 +773,30 @@ export default function Dashboard() { />
) : data?.length !== 0 ? ( -
+
-
+
+
+
+ Key + {!snapshotData && reorderRows(1)} + > + {sortMethod === 'alphabetical' ? : } + } +
+
Value
+
Comment
+ {!snapshotData &&
} +
+
{!snapshotData && data ?.filter((row) => row.key?.toUpperCase().includes(searchKeys.toUpperCase())) @@ -783,6 +808,7 @@ export default function Dashboard() { modifyValue={listenChangeValue} modifyValueOverride={listenChangeValueOverride} modifyKey={listenChangeKey} + modifyComment={listenChangeComment} isBlurred={blurred} isDuplicate={findDuplicates(data?.map((item) => item.key))?.includes( keyPair.key @@ -790,6 +816,7 @@ export default function Dashboard() { toggleSidebar={toggleSidebar} sidebarSecretId={sidebarSecretId} isSnapshot={false} + deleteRow={deleteCertainRow} /> ))} {snapshotData && @@ -820,6 +847,7 @@ export default function Dashboard() { modifyValue={listenChangeValue} modifyValueOverride={listenChangeValueOverride} modifyKey={listenChangeKey} + modifyComment={listenChangeComment} isBlurred={blurred} isDuplicate={findDuplicates(data?.map((item) => item.key))?.includes( keyPair.key @@ -829,9 +857,20 @@ export default function Dashboard() { isSnapshot /> ))} +
+
+ +
{!snapshotData && ( -
+
) : ( -
+
{isKeyAvailable && !snapshotData && (