diff --git a/backend/src/controllers/v1/secretsFolderController.ts b/backend/src/controllers/v1/secretsFolderController.ts index 06d68a41df..c1ce7ffc44 100644 --- a/backend/src/controllers/v1/secretsFolderController.ts +++ b/backend/src/controllers/v1/secretsFolderController.ts @@ -11,6 +11,7 @@ import { validateFolderName, generateFolderId, getParentFromFolderId, + getFolderByPath, } from "../../services/FolderService"; import { ADMIN, MEMBER } from "../../variables"; import { validateMembership } from "../../helpers/membership"; @@ -177,11 +178,13 @@ export const deleteFolder = async (req: Request, res: Response) => { // TODO: validate workspace export const getFolders = async (req: Request, res: Response) => { - const { workspaceId, environment, parentFolderId } = req.query as { - workspaceId: string; - environment: string; - parentFolderId?: string; - }; + const { workspaceId, environment, parentFolderId, parentFolderPath } = + req.query as { + workspaceId: string; + environment: string; + parentFolderId?: string; + parentFolderPath?: string; + }; const folders = await Folder.findOne({ workspace: workspaceId, environment }); if (!folders) { @@ -196,6 +199,20 @@ export const getFolders = async (req: Request, res: Response) => { acceptedRoles: [ADMIN, MEMBER], }); + // if instead of parentFolderId given a path like /folder1/folder2 + if (parentFolderPath) { + const folder = getFolderByPath(folders.nodes, parentFolderPath); + if (!folder) { + res.send({ folders: [], dir: [] }); + return; + } + // dir is not needed at present as this is only used in overview section of secrets + res.send({ + folders: folder.children.map(({ id, name }) => ({ id, name })), + dir: [{ name: folder.name, id: folder.id }], + }); + } + if (!parentFolderId) { const rootFolders = folders.nodes.children.map(({ id, name }) => ({ id, diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index ae1de8cfed..256ad248c2 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -700,11 +700,15 @@ export const getSecrets = async (req: Request, res: Response) => { (!folders && folderId && folderId !== "root") || (!folders && secretPath) ) { - throw BadRequestError({ message: "Folder not found" }); + res.send({ secrets: [] }); + return; } if (folders && folderId !== "root") { const folder = searchByFolderId(folders.nodes, folderId as string); - if (!folder) throw BadRequestError({ message: "Folder not found" }); + if (!folder) { + res.send({ secrets: [] }); + return; + } } if (req.authData.authPayload instanceof ServiceTokenData) { @@ -720,10 +724,11 @@ export const getSecrets = async (req: Request, res: Response) => { } if (folders && secretPath) { - if (!folders) throw BadRequestError({ message: "Folder not found" }); + // avoid throwing error and send empty list const folder = getFolderByPath(folders.nodes, secretPath as string); if (!folder) { - throw BadRequestError({ message: "Secret path not found" }); + res.send({ secrets: [] }); + return; } folderId = folder.id; } diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index 8a9350a361..182d7f14f4 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -34,7 +34,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => { secret, key }); - + return rep; }) }); @@ -88,7 +88,7 @@ export const createSecretRaw = async (req: Request, res: Response) => { secretComment, secretPath = "/" } = req.body; - + const key = await BotService.getWorkspaceKeyWithBot({ workspaceId: new Types.ObjectId(workspaceId) }); @@ -102,12 +102,12 @@ export const createSecretRaw = async (req: Request, res: Response) => { plaintext: secretValue, key }); - + const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8({ plaintext: secretComment, key - }); - + }); + const secret = await SecretService.createSecret({ secretName, workspaceId: new Types.ObjectId(workspaceId), @@ -135,7 +135,7 @@ export const createSecretRaw = async (req: Request, res: Response) => { const secretWithoutBlindIndex = secret.toObject(); delete secretWithoutBlindIndex.secretBlindIndex; - + return res.status(200).send({ secret: repackageSecretToRaw({ secret: secretWithoutBlindIndex, @@ -202,11 +202,11 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => { */ export const deleteSecretByNameRaw = async (req: Request, res: Response) => { const { secretName } = req.params; - const { - workspaceId, - environment, - type, - secretPath = "/" + const { + workspaceId, + environment, + type, + secretPath = "/" } = req.body; const { secret } = await SecretService.deleteSecret({ @@ -391,11 +391,11 @@ export const updateSecretByName = async (req: Request, res: Response) => { */ export const deleteSecretByName = async (req: Request, res: Response) => { const { secretName } = req.params; - const { - workspaceId, - environment, - type, - secretPath = "/" + const { + workspaceId, + environment, + type, + secretPath = "/" } = req.body; const { secret } = await SecretService.deleteSecret({ diff --git a/backend/src/helpers/rateLimiter.ts b/backend/src/helpers/rateLimiter.ts index 9e9d022ebb..123b230a49 100644 --- a/backend/src/helpers/rateLimiter.ts +++ b/backend/src/helpers/rateLimiter.ts @@ -1,16 +1,16 @@ import rateLimit from 'express-rate-limit'; -const MongoStore = require('rate-limit-mongo'); +// const MongoStore = require('rate-limit-mongo'); // 200 per minute export const apiLimiter = rateLimit({ - store: new MongoStore({ - uri: process.env.MONGO_URL, - expireTimeMs: 1000 * 60, - collectionName: "expressRateRecords-apiLimiter", - errorHandler: console.error.bind(null, 'rate-limit-mongo') - }), - windowMs: 1000 * 60, - max: 200, + // store: new MongoStore({ + // uri: process.env.MONGO_URL, + // expireTimeMs: 1000 * 60, + // collectionName: "expressRateRecords-apiLimiter", + // errorHandler: console.error.bind(null, 'rate-limit-mongo') + // }), + windowMs: 60 * 1000, + max: 240, standardHeaders: true, legacyHeaders: false, skip: (request) => { @@ -23,14 +23,14 @@ export const apiLimiter = rateLimit({ // 50 requests per 1 hours const authLimit = rateLimit({ - store: new MongoStore({ - uri: process.env.MONGO_URL, - expireTimeMs: 1000 * 60 * 60, - errorHandler: console.error.bind(null, 'rate-limit-mongo'), - collectionName: "expressRateRecords-authLimit", - }), - windowMs: 1000 * 60 * 60, - max: 50, + // store: new MongoStore({ + // uri: process.env.MONGO_URL, + // expireTimeMs: 1000 * 60 * 60, + // errorHandler: console.error.bind(null, 'rate-limit-mongo'), + // collectionName: "expressRateRecords-authLimit", + // }), + windowMs: 60 * 1000, + max: 10, standardHeaders: true, legacyHeaders: false, keyGenerator: (req, res) => { @@ -40,14 +40,14 @@ const authLimit = rateLimit({ // 5 requests per 1 hour export const passwordLimiter = rateLimit({ - store: new MongoStore({ - uri: process.env.MONGO_URL, - expireTimeMs: 1000 * 60 * 60, - errorHandler: console.error.bind(null, 'rate-limit-mongo'), - collectionName: "expressRateRecords-passwordLimiter", - }), - windowMs: 1000 * 60 * 60, - max: 5, + // store: new MongoStore({ + // uri: process.env.MONGO_URL, + // expireTimeMs: 1000 * 60 * 60, + // errorHandler: console.error.bind(null, 'rate-limit-mongo'), + // collectionName: "expressRateRecords-passwordLimiter", + // }), + windowMs: 60 * 60 * 1000, + max: 10, standardHeaders: true, legacyHeaders: false, keyGenerator: (req, res) => { diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index d7cc31cdae..7c8c69e972 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -57,7 +57,7 @@ import { getFolderIdFromServiceToken } from "../services/FolderService"; export const repackageSecretToRaw = ({ secret, key -}:{ +}: { secret: ISecret; key: string; }) => { @@ -76,8 +76,8 @@ export const repackageSecretToRaw = ({ key }); - let secretComment: string = ''; - + let secretComment: string = ''; + if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) { secretComment = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secret.secretCommentCiphertext, @@ -86,7 +86,7 @@ export const repackageSecretToRaw = ({ key }); } - + return ({ _id: secret._id, version: secret.version, @@ -503,7 +503,7 @@ export const getSecretsHelper = async ({ folder: folderId, type: SECRET_PERSONAL, ...getAuthDataPayloadUserObj(authData), - }).lean(); + }).populate("tags").lean(); // concat with shared secrets secrets = secrets.concat( @@ -515,7 +515,7 @@ export const getSecretsHelper = async ({ secretBlindIndex: { $nin: secrets.map((secret) => secret.secretBlindIndex), }, - }).lean() + }).populate("tags").lean() ); // (EE) create (audit) log @@ -553,7 +553,7 @@ export const getSecretsHelper = async ({ }, }); } - + return secrets; }; @@ -652,7 +652,7 @@ export const getSecretHelper = async ({ }, }); } - + return secret; }; @@ -843,7 +843,7 @@ export const deleteSecretHelper = async ({ // if using service token filter towards the folderId by secretpath if (authData.authPayload instanceof ServiceTokenData) { const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; - + if (secretPath !== serviceTkScopedSecretPath) { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } @@ -909,12 +909,12 @@ export const deleteSecretHelper = async ({ }); action && (await EELogService.createLog({ - ...getAuthDataPayloadIdObj(authData), - workspaceId, - actions: [action], - channel: authData.authChannel, - ipAddress: authData.authIP, - })); + ...getAuthDataPayloadIdObj(authData), + workspaceId, + actions: [action], + channel: authData.authChannel, + ipAddress: authData.authIP, + })); // (EE) take a secret snapshot await EESecretService.takeSecretSnapshot({ @@ -941,7 +941,7 @@ export const deleteSecretHelper = async ({ }, }); } - + return ({ secrets, secret diff --git a/backend/src/routes/v1/secretsFolder.ts b/backend/src/routes/v1/secretsFolder.ts index 2644a835be..83517f1ab0 100644 --- a/backend/src/routes/v1/secretsFolder.ts +++ b/backend/src/routes/v1/secretsFolder.ts @@ -63,6 +63,7 @@ router.get( query("workspaceId").exists().isString().trim(), query("environment").exists().isString().trim(), query("parentFolderId").optional().isString().trim(), + query("parentFolderPath").optional().isString().trim(), validateRequest, getFolders ); diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 87832a5378..a94be0ab0a 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -11,63 +11,6 @@ import ( const USER_AGENT = "cli" -func CallBatchModifySecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchModifySecretsByWorkspaceAndEnvRequest) error { - endpoint := fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL) - response, err := httpClient. - R(). - SetBody(request). - SetHeader("User-Agent", USER_AGENT). - Patch(endpoint) - - if err != nil { - return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err) - } - - if response.IsError() { - return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response) - } - - return nil -} - -func CallBatchCreateSecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchCreateSecretsByWorkspaceAndEnvRequest) error { - endpoint := fmt.Sprintf("%v/v2/secrets/", config.INFISICAL_URL) - response, err := httpClient. - R(). - SetBody(request). - SetHeader("User-Agent", USER_AGENT). - Post(endpoint) - - if err != nil { - return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err) - } - - if response.IsError() { - return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response) - } - - return nil -} - -func CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchDeleteSecretsBySecretIdsRequest) error { - endpoint := fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL) - response, err := httpClient. - R(). - SetBody(request). - SetHeader("User-Agent", USER_AGENT). - Delete(endpoint) - - if err != nil { - return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err) - } - - if response.IsError() { - return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response) - } - - return nil -} - func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request GetEncryptedWorkspaceKeyRequest) (GetEncryptedWorkspaceKeyResponse, error) { endpoint := fmt.Sprintf("%v/v2/workspace/%v/encrypted-key", config.INFISICAL_URL, request.WorkspaceId) var result GetEncryptedWorkspaceKeyResponse @@ -107,28 +50,6 @@ func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDeta return tokenDetailsResponse, nil } -func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Request) (GetEncryptedSecretsV2Response, error) { - var secretsResponse GetEncryptedSecretsV2Response - response, err := httpClient. - R(). - SetResult(&secretsResponse). - SetHeader("User-Agent", USER_AGENT). - SetQueryParam("environment", request.Environment). - SetQueryParam("workspaceId", request.WorkspaceId). - SetQueryParam("tagSlugs", request.TagSlugs). - Get(fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL)) - - if err != nil { - return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unable to complete api request [err=%s]", err) - } - - if response.IsError() { - return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unsuccessful response: [response=%s]", response) - } - - return secretsResponse, nil -} - func CallLogin1V2(httpClient *resty.Client, request GetLoginOneV2Request) (GetLoginOneV2Response, error) { var loginOneV2Response GetLoginOneV2Response response, err := httpClient. @@ -303,3 +224,110 @@ func CallGetNewAccessTokenWithRefreshToken(httpClient *resty.Client, refreshToke return newAccessToken, nil } + +func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Request) (GetEncryptedSecretsV3Response, error) { + var secretsResponse GetEncryptedSecretsV3Response + + httpRequest := httpClient. + R(). + SetResult(&secretsResponse). + SetHeader("User-Agent", USER_AGENT). + SetQueryParam("environment", request.Environment). + SetQueryParam("workspaceId", request.WorkspaceId) + + if request.SecretPath != "" { + httpRequest.SetQueryParam("secretPath", request.SecretPath) + } + + response, err := httpRequest.Get(fmt.Sprintf("%v/v3/secrets", config.INFISICAL_URL)) + + if err != nil { + return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response) + } + + return secretsResponse, nil +} + +func CallCreateSecretsV3(httpClient *resty.Client, request CreateSecretV3Request) error { + var secretsResponse GetEncryptedSecretsV3Response + response, err := httpClient. + R(). + SetResult(&secretsResponse). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName)) + + if err != nil { + return fmt.Errorf("CallCreateSecretsV3: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return fmt.Errorf("CallCreateSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response) + } + + return nil +} + +func CallDeleteSecretsV3(httpClient *resty.Client, request DeleteSecretV3Request) error { + var secretsResponse GetEncryptedSecretsV3Response + response, err := httpClient. + R(). + SetResult(&secretsResponse). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Delete(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName)) + + if err != nil { + return fmt.Errorf("CallDeleteSecretsV3: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return fmt.Errorf("CallDeleteSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response) + } + + return nil +} + +func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request) error { + var secretsResponse GetEncryptedSecretsV3Response + response, err := httpClient. + R(). + SetResult(&secretsResponse). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Patch(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName)) + + if err != nil { + return fmt.Errorf("CallUpdateSecretsV3: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return fmt.Errorf("CallUpdateSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response) + } + + return nil +} + +func CallGetSingleSecretByNameV3(httpClient *resty.Client, request CreateSecretV3Request) error { + var secretsResponse GetEncryptedSecretsV3Response + response, err := httpClient. + R(). + SetResult(&secretsResponse). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName)) + + if err != nil { + return fmt.Errorf("CallGetSingleSecretByNameV3: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return fmt.Errorf("CallGetSingleSecretByNameV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response) + } + + return nil +} diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index 30896b819d..7de7a962b2 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -143,24 +143,7 @@ type Secret struct { SecretCommentHash string `json:"secretCommentHash,omitempty"` Type string `json:"type,omitempty"` ID string `json:"id,omitempty"` -} - -type BatchCreateSecretsByWorkspaceAndEnvRequest struct { - Environment string `json:"environment"` - WorkspaceId string `json:"workspaceId"` - Secrets []Secret `json:"secrets"` -} - -type BatchModifySecretsByWorkspaceAndEnvRequest struct { - Environment string `json:"environment"` - WorkspaceId string `json:"workspaceId"` - Secrets []Secret `json:"secrets"` -} - -type BatchDeleteSecretsBySecretIdsRequest struct { - EnvironmentName string `json:"environmentName"` - WorkspaceId string `json:"workspaceId"` - SecretIds []string `json:"secretIds"` + PlainTextKey string `json:"plainTextKey"` } type GetEncryptedWorkspaceKeyRequest struct { @@ -194,41 +177,6 @@ type GetSecretsByWorkspaceIdAndEnvironmentRequest struct { WorkspaceId string `json:"workspaceId"` } -type GetEncryptedSecretsV2Request struct { - Environment string `json:"environment"` - WorkspaceId string `json:"workspaceId"` - TagSlugs string `json:"tagSlugs"` -} - -type GetEncryptedSecretsV2Response struct { - Secrets []struct { - ID string `json:"_id"` - Version int `json:"version"` - Workspace string `json:"workspace"` - Type string `json:"type"` - Environment string `json:"environment"` - SecretKeyCiphertext string `json:"secretKeyCiphertext"` - SecretKeyIV string `json:"secretKeyIV"` - SecretKeyTag string `json:"secretKeyTag"` - SecretValueCiphertext string `json:"secretValueCiphertext"` - SecretValueIV string `json:"secretValueIV"` - SecretValueTag string `json:"secretValueTag"` - SecretCommentCiphertext string `json:"secretCommentCiphertext"` - SecretCommentIV string `json:"secretCommentIV"` - SecretCommentTag string `json:"secretCommentTag"` - V int `json:"__v"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - User string `json:"user,omitempty"` - Tags []struct { - ID string `json:"_id"` - Name string `json:"name"` - Slug string `json:"slug"` - Workspace string `json:"workspace"` - } `json:"tags"` - } `json:"secrets"` -} - type GetServiceTokenDetailsResponse struct { ID string `json:"_id"` Name string `json:"name"` @@ -320,3 +268,109 @@ type VerifyMfaTokenErrorResponse struct { type GetNewAccessTokenWithRefreshTokenResponse struct { Token string `json:"token"` } + +type GetEncryptedSecretsV3Request struct { + Environment string `json:"environment"` + WorkspaceId string `json:"workspaceId"` + SecretPath string `json:"secretPath"` +} + +type GetEncryptedSecretsV3Response struct { + Secrets []struct { + ID string `json:"_id"` + Version int `json:"version"` + Workspace string `json:"workspace"` + Type string `json:"type"` + Tags []struct { + ID string `json:"_id"` + Name string `json:"name"` + Slug string `json:"slug"` + Workspace string `json:"workspace"` + } `json:"tags"` + Environment string `json:"environment"` + SecretKeyCiphertext string `json:"secretKeyCiphertext"` + SecretKeyIV string `json:"secretKeyIV"` + SecretKeyTag string `json:"secretKeyTag"` + SecretValueCiphertext string `json:"secretValueCiphertext"` + SecretValueIV string `json:"secretValueIV"` + SecretValueTag string `json:"secretValueTag"` + SecretCommentCiphertext string `json:"secretCommentCiphertext"` + SecretCommentIV string `json:"secretCommentIV"` + SecretCommentTag string `json:"secretCommentTag"` + Algorithm string `json:"algorithm"` + KeyEncoding string `json:"keyEncoding"` + Folder string `json:"folder"` + V int `json:"__v"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + } `json:"secrets"` +} + +type CreateSecretV3Request struct { + SecretName string `json:"secretName"` + WorkspaceID string `json:"workspaceId"` + Type string `json:"type"` + Environment string `json:"environment"` + SecretKeyCiphertext string `json:"secretKeyCiphertext"` + SecretKeyIV string `json:"secretKeyIV"` + SecretKeyTag string `json:"secretKeyTag"` + SecretValueCiphertext string `json:"secretValueCiphertext"` + SecretValueIV string `json:"secretValueIV"` + SecretValueTag string `json:"secretValueTag"` + SecretCommentCiphertext string `json:"secretCommentCiphertext"` + SecretCommentIV string `json:"secretCommentIV"` + SecretCommentTag string `json:"secretCommentTag"` + SecretPath string `json:"secretPath"` +} + +type DeleteSecretV3Request struct { + SecretName string `json:"secretName"` + WorkspaceId string `json:"workspaceId"` + Environment string `json:"environment"` + Type string `json:"type"` + SecretPath string `json:"secretPath"` +} + +type UpdateSecretByNameV3Request struct { + SecretName string `json:"secretName"` + WorkspaceID string `json:"workspaceId"` + Environment string `json:"environment"` + Type string `json:"type"` + SecretPath string `json:"secretPath"` + SecretValueCiphertext string `json:"secretValueCiphertext"` + SecretValueIV string `json:"secretValueIV"` + SecretValueTag string `json:"secretValueTag"` +} + +type GetSingleSecretByNameV3Request struct { + SecretName string `json:"secretName"` + WorkspaceId string `json:"workspaceId"` + Environment string `json:"environment"` + Type string `json:"type"` + SecretPath string `json:"secretPath"` +} + +type GetSingleSecretByNameSecretResponse struct { + Secrets []struct { + ID string `json:"_id"` + Version int `json:"version"` + Workspace string `json:"workspace"` + Type string `json:"type"` + Environment string `json:"environment"` + SecretKeyCiphertext string `json:"secretKeyCiphertext"` + SecretKeyIV string `json:"secretKeyIV"` + SecretKeyTag string `json:"secretKeyTag"` + SecretValueCiphertext string `json:"secretValueCiphertext"` + SecretValueIV string `json:"secretValueIV"` + SecretValueTag string `json:"secretValueTag"` + SecretCommentCiphertext string `json:"secretCommentCiphertext"` + SecretCommentIV string `json:"secretCommentIV"` + SecretCommentTag string `json:"secretCommentTag"` + Algorithm string `json:"algorithm"` + KeyEncoding string `json:"keyEncoding"` + Folder string `json:"folder"` + V int `json:"__v"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + } `json:"secrets"` +} diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index afce399d5d..8b82faa65c 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -82,7 +82,12 @@ var runCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs}) + secretsPath, err := cmd.Flags().GetString("path") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}) if err != nil { util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid") @@ -184,6 +189,7 @@ func init() { runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")") runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ") + runCmd.Flags().String("path", "/", "get secrets within a folder path") } // Will execute a single command and pass in the given secrets into the process diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index 26835eff1d..0d6181a565 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -44,6 +44,11 @@ var secretsCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } + secretsPath, err := cmd.Flags().GetString("path") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + shouldExpandSecrets, err := cmd.Flags().GetBool("expand") if err != nil { util.HandleError(err) @@ -54,7 +59,7 @@ var secretsCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs}) + secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}) if err != nil { util.HandleError(err) } @@ -103,6 +108,11 @@ var secretsSetCmd = &cobra.Command{ } } + secretsPath, err := cmd.Flags().GetString("path") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + workspaceFile, err := util.GetWorkSpaceFromFile() if err != nil { util.HandleError(err, "Unable to get your local config details") @@ -140,7 +150,7 @@ var secretsSetCmd = &cobra.Command{ plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) // pull current secrets - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName}) + secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, SecretsPath: secretsPath}) if err != nil { util.HandleError(err, "unable to retrieve secrets") } @@ -191,6 +201,8 @@ var secretsSetCmd = &cobra.Command{ SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), SecretValueHash: hashedValue, + PlainTextKey: key, + Type: existingSecret.Type, } // Only add to modifications if the value is different @@ -222,6 +234,7 @@ var secretsSetCmd = &cobra.Command{ SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), SecretValueHash: hashedValue, Type: util.SECRET_TYPE_SHARED, + PlainTextKey: key, } secretsToCreate = append(secretsToCreate, encryptedSecretDetails) secretOperations = append(secretOperations, SecretSetOperation{ @@ -232,30 +245,43 @@ var secretsSetCmd = &cobra.Command{ } } - if len(secretsToCreate) > 0 { - batchCreateRequest := api.BatchCreateSecretsByWorkspaceAndEnvRequest{ - WorkspaceId: workspaceFile.WorkspaceId, - Environment: environmentName, - Secrets: secretsToCreate, + for _, secret := range secretsToCreate { + createSecretRequest := api.CreateSecretV3Request{ + WorkspaceID: workspaceFile.WorkspaceId, + Environment: environmentName, + SecretName: secret.PlainTextKey, + SecretKeyCiphertext: secret.SecretKeyCiphertext, + SecretKeyIV: secret.SecretKeyIV, + SecretKeyTag: secret.SecretKeyTag, + SecretValueCiphertext: secret.SecretValueCiphertext, + SecretValueIV: secret.SecretValueIV, + SecretValueTag: secret.SecretValueTag, + Type: secret.Type, + SecretPath: secretsPath, } - err = api.CallBatchCreateSecretsByWorkspaceAndEnv(httpClient, batchCreateRequest) + err = api.CallCreateSecretsV3(httpClient, createSecretRequest) if err != nil { util.HandleError(err, "Unable to process new secret creations") return } } - if len(secretsToModify) > 0 { - batchModifyRequest := api.BatchModifySecretsByWorkspaceAndEnvRequest{ - WorkspaceId: workspaceFile.WorkspaceId, - Environment: environmentName, - Secrets: secretsToModify, + for _, secret := range secretsToModify { + updateSecretRequest := api.UpdateSecretByNameV3Request{ + WorkspaceID: workspaceFile.WorkspaceId, + Environment: environmentName, + SecretName: secret.PlainTextKey, + SecretValueCiphertext: secret.SecretValueCiphertext, + SecretValueIV: secret.SecretValueIV, + SecretValueTag: secret.SecretValueTag, + Type: secret.Type, + SecretPath: secretsPath, } - err = api.CallBatchModifySecretsByWorkspaceAndEnv(httpClient, batchModifyRequest) + err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest) if err != nil { - util.HandleError(err, "Unable to process the modifications to your secrets") + util.HandleError(err, "Unable to process secret update request") return } } @@ -288,6 +314,16 @@ var secretsDeleteCmd = &cobra.Command{ } } + secretsPath, err := cmd.Flags().GetString("path") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + secretType, err := cmd.Flags().GetString("type") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() if err != nil { util.HandleError(err, "Unable to authenticate") @@ -298,46 +334,28 @@ var secretsDeleteCmd = &cobra.Command{ util.HandleError(err, "Unable to get local project details") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName}) - if err != nil { - util.HandleError(err, "Unable to fetch secrets") - } - - secretByKey := getSecretsByKeys(secrets) - validSecretIdsToDelete := []string{} - invalidSecretNamesThatDoNotExist := []string{} - - for _, secretKeyFromArg := range args { - if value, ok := secretByKey[strings.ToUpper(secretKeyFromArg)]; ok { - validSecretIdsToDelete = append(validSecretIdsToDelete, value.ID) - } else { - invalidSecretNamesThatDoNotExist = append(invalidSecretNamesThatDoNotExist, secretKeyFromArg) + for _, secretName := range args { + request := api.DeleteSecretV3Request{ + WorkspaceId: workspaceFile.WorkspaceId, + Environment: environmentName, + SecretName: secretName, + Type: secretType, + SecretPath: secretsPath, } - } - if len(invalidSecretNamesThatDoNotExist) != 0 { - message := fmt.Sprintf("secret name(s) [%v] does not exist in your project. To see which secrets exist run [infisical secrets]", strings.Join(invalidSecretNamesThatDoNotExist, ", ")) - util.PrintErrorMessageAndExit(message) - } + httpClient := resty.New(). + SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). + SetHeader("Accept", "application/json") - request := api.BatchDeleteSecretsBySecretIdsRequest{ - WorkspaceId: workspaceFile.WorkspaceId, - EnvironmentName: environmentName, - SecretIds: validSecretIdsToDelete, - } - - httpClient := resty.New(). - SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). - SetHeader("Accept", "application/json") - - err = api.CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient, request) - if err != nil { - util.HandleError(err, "Unable to complete your batch delete request") + err = api.CallDeleteSecretsV3(httpClient, request) + if err != nil { + util.HandleError(err, "Unable to complete your delete request") + } } fmt.Printf("secret name(s) [%v] have been deleted from your project \n", strings.Join(args, ", ")) - Telemetry.CaptureEvent("cli-command:secrets delete", posthog.NewProperties().Set("secretCount", len(secrets)).Set("version", util.CLI_VERSION)) + Telemetry.CaptureEvent("cli-command:secrets delete", posthog.NewProperties().Set("secretCount", len(args)).Set("version", util.CLI_VERSION)) }, } @@ -611,11 +629,15 @@ func init() { secretsCmd.AddCommand(secretsGetCmd) secretsCmd.AddCommand(secretsSetCmd) + secretsSetCmd.Flags().String("path", "/", "get secrets within a folder path") + secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { util.RequireLogin() util.RequireLocalWorkspaceFile() } + secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)") + secretsDeleteCmd.Flags().String("path", "/", "get secrets within a folder path") secretsCmd.AddCommand(secretsDeleteCmd) secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { util.RequireLogin() @@ -626,5 +648,6 @@ func init() { secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on") secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs") + secretsCmd.Flags().String("path", "/", "get secrets within a folder path") rootCmd.AddCommand(secretsCmd) } diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 22f72e2da2..18a64750fd 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -64,4 +64,5 @@ type GetAllSecretsParameters struct { InfisicalToken string TagSlugs string WorkspaceId string + SecretsPath string } diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index a713abddbb..eef691fdca 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -34,7 +34,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to get service token details. [err=%v]", err) } - encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{ + encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{ WorkspaceId: serviceTokenDetails.Workspace, Environment: serviceTokenDetails.Environment, }) @@ -61,7 +61,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl return plainTextSecrets, serviceTokenDetails, nil } -func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string) ([]models.SingleEnvironmentVariable, error) { +func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string) ([]models.SingleEnvironmentVariable, error) { httpClient := resty.New() httpClient.SetAuthToken(JTWToken). SetHeader("Accept", "application/json") @@ -102,11 +102,17 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) - encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{ + getSecretsRequest := api.GetEncryptedSecretsV3Request{ WorkspaceId: workspaceId, Environment: environmentName, - TagSlugs: tagSlugs, - }) + // TagSlugs: tagSlugs, + } + + if secretsPath != "" { + getSecretsRequest.SecretPath = secretsPath + } + + encryptedSecrets, err := api.CallGetSecretsV3(httpClient, getSecretsRequest) if err != nil { return nil, err @@ -162,7 +168,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err) } - secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment, params.TagSlugs) + secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment, params.TagSlugs, params.SecretsPath) log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn) backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32] @@ -333,7 +339,7 @@ func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType stri return secretsToReturn } -func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV2Response) ([]models.SingleEnvironmentVariable, error) { +func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV3Response) ([]models.SingleEnvironmentVariable, error) { plainTextSecrets := []models.SingleEnvironmentVariable{} for _, secret := range encryptedSecrets.Secrets { // Decrypt key diff --git a/frontend/src/hooks/api/secretFolders/index.tsx b/frontend/src/hooks/api/secretFolders/index.tsx index c1cc056d1b..8b48f3e204 100644 --- a/frontend/src/hooks/api/secretFolders/index.tsx +++ b/frontend/src/hooks/api/secretFolders/index.tsx @@ -1 +1,7 @@ -export { useCreateFolder, useDeleteFolder, useGetProjectFolders, useUpdateFolder } from './queries'; +export { + useCreateFolder, + useDeleteFolder, + useGetProjectFolders, + useGetProjectFoldersBatch, + useUpdateFolder +} from './queries'; diff --git a/frontend/src/hooks/api/secretFolders/queries.tsx b/frontend/src/hooks/api/secretFolders/queries.tsx index 008d6f26f5..57b58afea4 100644 --- a/frontend/src/hooks/api/secretFolders/queries.tsx +++ b/frontend/src/hooks/api/secretFolders/queries.tsx @@ -1,5 +1,5 @@ import { useCallback } from 'react'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/react-query'; import { apiRequest } from '@app/config/request'; @@ -7,6 +7,7 @@ import { secretSnapshotKeys } from '../secretSnapshots/queries'; import { CreateFolderDTO, DeleteFolderDTO, + GetProjectFoldersBatchDTO, GetProjectFoldersDTO, TSecretFolder, UpdateFolderDTO @@ -17,6 +18,26 @@ const queryKeys = { ['secret-folders', { workspaceId, environment, parentFolderId }] as const }; +const fetchProjectFolders = async ( + workspaceId: string, + environment: string, + parentFolderId?: string, + parentFolderPath?: string +) => { + const { data } = await apiRequest.get<{ folders: TSecretFolder[]; dir: TSecretFolder[] }>( + '/api/v1/folders', + { + params: { + workspaceId, + environment, + parentFolderId, + parentFolderPath + } + } + ); + return data; +}; + export const useGetProjectFolders = ({ workspaceId, parentFolderId, @@ -27,19 +48,7 @@ export const useGetProjectFolders = ({ useQuery({ queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderId), enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused, - queryFn: async () => { - const { data } = await apiRequest.get<{ folders: TSecretFolder[]; dir: TSecretFolder[] }>( - '/api/v1/folders', - { - params: { - workspaceId, - environment, - parentFolderId - } - } - ); - return data; - }, + queryFn: async () => fetchProjectFolders(workspaceId, environment, parentFolderId), select: useCallback( ({ folders, dir }: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({ dir, @@ -53,6 +62,25 @@ export const useGetProjectFolders = ({ ) }); +export const useGetProjectFoldersBatch = ({ + folders = [], + isPaused, + parentFolderPath +}: GetProjectFoldersBatchDTO) => + useQueries({ + queries: folders.map(({ workspaceId, environment, parentFolderId }) => ({ + queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderPath), + queryFn: async () => + fetchProjectFolders(workspaceId, environment, parentFolderId, parentFolderPath), + enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused, + select: (data: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({ + environment, + folders: data.folders, + dir: data.dir + }) + })) + }); + export const useCreateFolder = () => { const queryClient = useQueryClient(); diff --git a/frontend/src/hooks/api/secretFolders/types.ts b/frontend/src/hooks/api/secretFolders/types.ts index a9ca176411..44a6a0859d 100644 --- a/frontend/src/hooks/api/secretFolders/types.ts +++ b/frontend/src/hooks/api/secretFolders/types.ts @@ -11,6 +11,12 @@ export type GetProjectFoldersDTO = { sortDir?: 'asc' | 'desc'; }; +export type GetProjectFoldersBatchDTO = { + folders: Omit[]; + isPaused?: boolean; + parentFolderPath?: string; +}; + export type CreateFolderDTO = { workspaceId: string; environment: string; diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index e4ad89dd6f..39ac39b5e4 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -1,4 +1,5 @@ /* eslint-disable no-param-reassign */ +import { useCallback } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { @@ -29,14 +30,16 @@ export const secretKeys = { const fetchProjectEncryptedSecrets = async ( workspaceId: string, env: string | string[], - folderId?: string + folderId?: string, + secretPath?: string ) => { if (typeof env === 'string') { const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', { params: { environment: env, workspaceId, - folderId: folderId || undefined + folderId: folderId || undefined, + secretPath } }); return data.secrets; @@ -52,7 +55,8 @@ const fetchProjectEncryptedSecrets = async ( params: { environment: envPoint, workspaceId, - folderId + folderId, + secretPath } }); allEnvData = allEnvData.concat(data.secrets); @@ -77,7 +81,7 @@ export const useGetProjectSecrets = ({ enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused, queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId), queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId), - select: (data) => { + select: useCallback((data: EncryptedSecret[]) => { const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; const latestKey = decryptFileKey; const key = decryptAssymmetric({ @@ -146,21 +150,24 @@ export const useGetProjectSecrets = ({ } }); return { secrets: sharedSecrets }; - } + }, [decryptFileKey]) }); export const useGetProjectSecretsByKey = ({ workspaceId, env, decryptFileKey, - isPaused + isPaused, + folderId, + secretPath }: GetProjectSecretsDTO) => useQuery({ // wait for all values to be available enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused, - queryKey: secretKeys.getProjectSecret(workspaceId, env), - queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env), - select: (data) => { + // right now secretpath is passed as folderid as only this is used in overview + queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath), + queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath), + select: useCallback((data: EncryptedSecret[]) => { const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; const latestKey = decryptFileKey; const key = decryptAssymmetric({ @@ -235,7 +242,7 @@ export const useGetProjectSecretsByKey = ({ }); return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length }; - } + }, [decryptFileKey]) }); const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => { @@ -256,7 +263,7 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) => enabled: Boolean(dto.secretId && dto.decryptFileKey), queryKey: secretKeys.getSecretVersion(dto.secretId), queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit), - select: (data) => { + select: useCallback((data: EncryptedSecretVersion[]) => { const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; const latestKey = dto.decryptFileKey; const key = decryptAssymmetric({ @@ -278,7 +285,7 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) => }) })) .sort((a, b) => b.createdAt.localeCompare(a.createdAt)); - } + }, []) }); export const useBatchSecretsOp = () => { diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 2a5938c681..5bde496e26 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -96,6 +96,7 @@ export type GetProjectSecretsDTO = { env: string | string[]; decryptFileKey: UserWsKeyPair; folderId?: string; + secretPath?: string; isPaused?: boolean; onSuccess?: (data: DecryptedSecret[]) => void; }; diff --git a/frontend/src/pages/dashboard/[id].tsx b/frontend/src/pages/dashboard/[id].tsx index 8d31b42917..3c6fdf9951 100644 --- a/frontend/src/pages/dashboard/[id].tsx +++ b/frontend/src/pages/dashboard/[id].tsx @@ -12,13 +12,6 @@ const Dashboard = () => { const queryEnv = router.query.env as string; const isOverviewMode = !queryEnv; - const onExploreEnv = (slug: string) => { - router.push({ - pathname: router.pathname, - query: { ...router.query, env: slug } - }); - }; - return ( <> @@ -29,11 +22,7 @@ const Dashboard = () => {
- {isOverviewMode ? ( - - ) : ( - - )} + {isOverviewMode ? : }
); diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index feca862f66..de4adfff42 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -4,20 +4,20 @@ @layer utilities { .flex-0 { - flex:0; + flex: 0; } .flex-2 { flex-grow: 2; } .flex-3 { - flex-grow: 3; + flex-grow: 3; } } @layer components { .secret-table { - @apply bg-mineshaft-800 text-left text-bunker-300 w-full; + @apply w-full bg-mineshaft-800 text-left text-bunker-300; } /* padding except for comment column */ @@ -29,13 +29,45 @@ @apply py-1 px-1 pr-2 text-sm; } - .secret-table th:not(:last-child),.secret-table td:not(:last-child) { + .secret-table th:not(:last-child), + .secret-table td:not(:last-child) { @apply border-r border-mineshaft-600; } .secret-table tr { @apply border-b border-mineshaft-600; } + + .breadcrumb::after, + .breadcrumb::before { + content: ''; + height: 60%; + width: 100%; + z-index: -1; + display: block; + position: absolute; + @apply bg-mineshaft-800; + } + + .breadcrumb:hover::before { + @apply bg-mineshaft-600; + } + + .breadcrumb:hover::after { + @apply bg-mineshaft-600; + } + + .breadcrumb::after { + left: 5px; + bottom: -3px; + transform: skew(-30deg); + } + + .breadcrumb::before { + left: 5px; + top: -3px; + transform: skew(30deg); + } } @import '@fontsource/inter/400.css'; diff --git a/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx b/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx index 7427bbc3e2..033f7f1e21 100644 --- a/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx +++ b/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx @@ -1,35 +1,31 @@ import { useEffect, useMemo, useState } from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { useRouter } from 'next/router'; -import { faKey, faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'; +import { faFolderOpen, faKey, faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { yupResolver } from '@hookform/resolvers/yup'; import NavHeader from '@app/components/navigation/NavHeader'; import { Button, Input, TableContainer, Tooltip } from '@app/components/v2'; import { useWorkspace } from '@app/context'; import { + useGetProjectFoldersBatch, useGetProjectSecretsByKey, useGetUserWsEnvironments, useGetUserWsKey } from '@app/hooks/api'; -import { WorkspaceEnv } from '@app/hooks/api/types'; import { EnvComparisonRow } from './components/EnvComparisonRow'; -import { FormData, schema } from './DashboardPage.utils'; +import { FolderComparisonRow } from './components/EnvComparisonRow/FolderComparisonRow'; -export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => { +export const DashboardEnvOverview = () => { const { t } = useTranslation(); const router = useRouter(); - const [selectedEnv, setSelectedEnv] = useState(null); - const { currentWorkspace, isLoading } = useWorkspace(); const workspaceId = currentWorkspace?._id as string; const { data: latestFileKey } = useGetUserWsKey(workspaceId); - const [searchFilter, setSearchFilter] = useState(''); + const secretPath = router.query?.secretPath as string; useEffect(() => { if (!isLoading && !workspaceId && router.isReady) { @@ -38,14 +34,7 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => { }, [isLoading, workspaceId, router.isReady]); const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({ - workspaceId, - onSuccess: (data) => { - // get an env with one of the access available - const env = data.find(({ isReadDenied }) => !isReadDenied); - if (env) { - setSelectedEnv(env); - } - } + workspaceId }); const userAvailableEnvs = wsEnv?.filter(({ isReadDenied }) => !isReadDenied); @@ -54,17 +43,32 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => { workspaceId, env: userAvailableEnvs?.map((env) => env.slug) ?? [], decryptFileKey: latestFileKey!, - isPaused: false + isPaused: false, + secretPath }); - const method = useForm({ - // why any: well yup inferred ts expects other keys to defined as undefined - defaultValues: secrets as any, - values: secrets as any, - mode: 'onBlur', - resolver: yupResolver(schema) + const folders = useGetProjectFoldersBatch({ + folders: + userAvailableEnvs?.map((env) => ({ + environment: env.slug, + workspaceId + })) ?? [], + parentFolderPath: secretPath }); + const foldersGroupedByEnv = useMemo(() => { + const res: Record> = {}; + folders.forEach(({ data }) => { + data?.folders + ?.filter(({ name }) => name.toLowerCase().includes(searchFilter)) + ?.forEach((folder) => { + if (!res?.[folder.name]) res[folder.name] = {}; + res[folder.name][data.environment] = true; + }); + }); + return res; + }, [folders, userAvailableEnvs, searchFilter]); + const numSecretsMissingPerEnv = useMemo(() => { // first get all sec in the env then subtract with total to get missing ones const secPerEnvMissing: Record = Object.fromEntries( @@ -81,7 +85,43 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => { return secPerEnvMissing; }, [secrets, userAvailableEnvs]); - const isReadOnly = selectedEnv?.isWriteDenied; + const onExploreEnv = (slug: string) => { + const query: Record = { ...router.query, env: slug }; + delete query.secretPath; + // the dir return will have the present directory folder id + // use that when clicking on explore to redirect user to there + const envFolder = folders.find(({ data }) => slug === data?.environment); + const dir = envFolder?.data?.dir?.pop(); + if (dir) { + query.folderId = dir.id; + } + + router.push({ + pathname: router.pathname, + query + }); + }; + + const onFolderClick = (path: string) => { + router.push({ + pathname: router.pathname, + query: { + ...router.query, + secretPath: `${router.query?.secretPath || ''}/${path}` + } + }); + }; + + const onFolderCrumbClick = (index: number) => { + const newSecPath = secretPath.split('/').filter(Boolean).slice(0, index).join('/'); + const query = { ...router.query, secretPath: `/${newSecPath}` } as Record; + // root condition + if (index === 0) delete query.secretPath; + router.push({ + pathname: router.pathname, + query + }); + }; if (isSecretsLoading || isEnvListLoading) { return ( @@ -91,165 +131,195 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => { ); } + const filteredSecrets = Object.keys(secrets?.secrets || {})?.filter((secret: any) => + secret.toUpperCase().includes(searchFilter.toUpperCase()) + ); // when secrets is not loading and secrets list is empty - const isDashboardSecretEmpty = !isSecretsLoading && !Object.keys(secrets?.secrets || {})?.filter((secret: any) => secret.toUpperCase().includes(searchFilter.toUpperCase()))?.length; + const isDashboardSecretEmpty = !isSecretsLoading && !filteredSecrets?.length; + const isFoldersEmtpy = + !folders.some(({ isLoading: isFolderLoading }) => isFolderLoading) && + !Object.keys(foldersGroupedByEnv).length; + const isDashboardEmpty = isFoldersEmtpy && isDashboardSecretEmpty; return (
- -
- {/* breadcrumb row */} -
- +
+ +
+
+

Secrets Overview

+

+ Inject your secrets using + + Infisical CLI + + or + + Infisical SDKs + +

+
+
+
+
onFolderCrumbClick(0)} + onKeyDown={() => null} + role="button" + tabIndex={0} + > +
-
-

Secrets Overview

-

- Inject your secrets using - ( +

-
- setSearchFilter(e.target.value)} - leftIcon={} - /> -
-
-
-
-
{0}
+ {path}
-
-
-
Secret
-
-
- {numSecretsMissingPerEnv && - userAvailableEnvs?.map((env) => { - return ( -
-
- {env.name} - {numSecretsMissingPerEnv[env.slug] > 0 && ( -
- - - {numSecretsMissingPerEnv[env.slug]} - - -
- )} + ))} +
+
+ setSearchFilter(e.target.value)} + leftIcon={} + /> +
+
+
+
+
+
{0}
+
+
+
+
Secret
+
+
+ {numSecretsMissingPerEnv && + userAvailableEnvs?.map((env) => { + return ( +
+
+ {env.name} + {numSecretsMissingPerEnv[env.slug] > 0 && ( +
+ + + {numSecretsMissingPerEnv[env.slug]} + +
-
- ); - })} -
-
- {!isDashboardSecretEmpty && ( - - - - {Object.keys(secrets?.secrets || {})?.filter((secret: any) => secret.toUpperCase().includes(searchFilter.toUpperCase())).map((key, index) => ( - - ))} - -
-
- )} - {isDashboardSecretEmpty && ( -
-
- - No secrets found. - To add more secrets you can explore any environment. + )}
- )} - {/* In future, we should add an option to add environments here -
- -
*/} -
-
-
-
0
+ ); + })} +
+
+ {!isDashboardEmpty && ( + + + + {Object.keys(foldersGroupedByEnv || {}).map((folderName, index) => ( + + ))} + {Object.keys(secrets?.secrets || {}) + ?.filter((secret: any) => + secret.toUpperCase().includes(searchFilter.toUpperCase()) + ) + .map((key) => ( + + ))} + +
+
+ )} + {isDashboardEmpty && ( +
+
+ + No secrets/folders found. + To add more secrets you can explore any environment.
-
- 0 - -
- {userAvailableEnvs?.map((env) => { - return ( -
- -
- ); - })}
+ )} +
+
+
+
0
- - +
+ 0 + +
+ {userAvailableEnvs?.map((env) => { + return ( +
+ +
+ ); + })} +
+
); }; diff --git a/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx b/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx index cb12a3d9ad..6ef1eb45a0 100644 --- a/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx +++ b/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx @@ -1,11 +1,10 @@ /* eslint-disable react/jsx-no-useless-fragment */ import { useCallback, useState } from 'react'; -import { faCircle, faEye, faEyeSlash, faMinus } from '@fortawesome/free-solid-svg-icons'; +import { faCircle, faEye, faEyeSlash, faKey, faMinus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { twMerge } from 'tailwind-merge'; type Props = { - index: number; secrets: any[] | undefined; // permission and external state's that decided to hide or show isReadOnly?: boolean; @@ -30,7 +29,7 @@ const DashboardInput = ({ if (val === undefined) return ( - + ); if (val?.length === 0) @@ -110,7 +109,6 @@ const DashboardInput = ({ }; export const EnvComparisonRow = ({ - index, secrets, isSecretValueHidden, isReadOnly, @@ -126,7 +124,9 @@ export const EnvComparisonRow = ({ return ( -
{index + 1}
+
+ +
diff --git a/frontend/src/views/DashboardPage/components/EnvComparisonRow/FolderComparisonRow.tsx b/frontend/src/views/DashboardPage/components/EnvComparisonRow/FolderComparisonRow.tsx new file mode 100644 index 0000000000..188106a96c --- /dev/null +++ b/frontend/src/views/DashboardPage/components/EnvComparisonRow/FolderComparisonRow.tsx @@ -0,0 +1,42 @@ +import { faCheck, faFolder, faXmark } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +type Props = { + folderInEnv: Record; + userAvailableEnvs?: Array<{ slug: string; name: string }>; + folderName: string; + onClick: (folderName: string) => void; +}; + +export const FolderComparisonRow = ({ + folderInEnv = {}, + userAvailableEnvs = [], + folderName, + onClick +}: Props) => ( + onClick(folderName)} + > + +
+ +
+ + +
{folderName}
+ + {userAvailableEnvs?.map(({ slug }) => ( + + + + ))} + +);