Merge pull request #758 from akhilmhdh/feat/secret-import

Implemented secret link/import feature
This commit is contained in:
Maidul Islam
2023-07-18 16:58:46 -04:00
committed by GitHub
38 changed files with 1572 additions and 213 deletions
+3 -1
View File
@@ -15,6 +15,7 @@ import * as userController from "./userController";
import * as workspaceController from "./workspaceController"; import * as workspaceController from "./workspaceController";
import * as secretScanningController from "./secretScanningController"; import * as secretScanningController from "./secretScanningController";
import * as webhookController from "./webhookController"; import * as webhookController from "./webhookController";
import * as secretImportController from "./secretImportController";
export { export {
authController, authController,
@@ -33,5 +34,6 @@ export {
userController, userController,
workspaceController, workspaceController,
secretScanningController, secretScanningController,
webhookController webhookController,
secretImportController
}; };
@@ -0,0 +1,117 @@
import { Request, Response } from "express";
import { validateMembership } from "../../helpers";
import SecretImport from "../../models/secretImports";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import { BadRequestError } from "../../utils/errors";
import { ADMIN, MEMBER } from "../../variables";
export const createSecretImport = async (req: Request, res: Response) => {
const { workspaceId, environment, folderId, secretImport } = req.body;
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
const doc = new SecretImport({
workspace: workspaceId,
environment,
folderId,
imports: [{ environment: secretImport.environment, secretPath: secretImport.secretPath }]
});
await doc.save();
return res.status(200).json({ message: "successfully created secret import" });
}
const doesImportExist = importSecDoc.imports.find(
(el) => el.environment === secretImport.environment && el.secretPath === secretImport.secretPath
);
if (doesImportExist) {
throw BadRequestError({ message: "Secret import already exist" });
}
importSecDoc.imports.push({
environment: secretImport.environment,
secretPath: secretImport.secretPath
});
await importSecDoc.save();
return res.status(200).json({ message: "successfully created secret import" });
};
// to keep the ordering, you must pass all the imports in here not the only updated one
// this is because the order decide which import gets overriden
export const updateSecretImport = async (req: Request, res: Response) => {
const { id } = req.params;
const { secretImports } = req.body;
const importSecDoc = await SecretImport.findById(id);
if (!importSecDoc) {
throw BadRequestError({ message: "Import not found" });
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: importSecDoc.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
importSecDoc.imports = secretImports;
await importSecDoc.save();
return res.status(200).json({ message: "successfully updated secret import" });
};
export const deleteSecretImport = async (req: Request, res: Response) => {
const { id } = req.params;
const { secretImportEnv, secretImportPath } = req.body;
const importSecDoc = await SecretImport.findById(id);
if (!importSecDoc) {
throw BadRequestError({ message: "Import not found" });
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: importSecDoc.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
importSecDoc.imports = importSecDoc.imports.filter(
({ environment, secretPath }) =>
!(environment === secretImportEnv && secretPath === secretImportPath)
);
await importSecDoc.save();
return res.status(200).json({ message: "successfully delete secret import" });
};
export const getSecretImports = async (req: Request, res: Response) => {
const { workspaceId, environment, folderId } = req.query;
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
return res.status(200).json({ secretImport: {} });
}
return res.status(200).json({ secretImport: importSecDoc });
};
export const getAllSecretsFromImport = async (req: Request, res: Response) => {
const { workspaceId, environment, folderId } = req.query as {
workspaceId: string;
environment: string;
folderId: string;
};
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
return res.status(200).json({ secrets: {} });
}
const secrets = await getAllImportedSecrets(workspaceId, environment, folderId);
return res.status(200).json({ secrets });
};
@@ -35,6 +35,7 @@ import {
} from "../../services/FolderService"; } from "../../services/FolderService";
import { isValidScope } from "../../helpers/secrets"; import { isValidScope } from "../../helpers/secrets";
import path from "path"; import path from "path";
import { getAllImportedSecrets } from "../../services/SecretImportService";
/** /**
* Peform a batch of any specified CUD secret operations * Peform a batch of any specified CUD secret operations
@@ -690,7 +691,7 @@ export const getSecrets = async (req: Request, res: Response) => {
} }
*/ */
const { tagSlugs, secretPath } = req.query; const { tagSlugs, secretPath, include_imports } = req.query;
let { folderId } = req.query; let { folderId } = req.query;
const workspaceId = req.query.workspaceId as string; const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string; const environment = req.query.environment as string;
@@ -827,6 +828,12 @@ export const getSecrets = async (req: Request, res: Response) => {
secrets = await Secret.find(secretQuery).populate("tags"); secrets = await Secret.find(secretQuery).populate("tags");
} }
// TODO(akhilmhdh) - secret-imp change this to org type
let importedSecrets: any[] = [];
if (include_imports) {
importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId as string);
}
const channel = getChannelFromUserAgent(req.headers["user-agent"]); const channel = getChannelFromUserAgent(req.headers["user-agent"]);
const readAction = await EELogService.createAction({ const readAction = await EELogService.createAction({
@@ -868,7 +875,8 @@ export const getSecrets = async (req: Request, res: Response) => {
} }
return res.status(200).send({ return res.status(200).send({
secrets secrets,
...(include_imports && { imports: importedSecrets })
}); });
}; };
@@ -5,6 +5,10 @@ import { eventPushSecrets } from "../../events";
import { BotService } from "../../services"; import { BotService } from "../../services";
import { repackageSecretToRaw } from "../../helpers/secrets"; import { repackageSecretToRaw } from "../../helpers/secrets";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto"; import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import Folder from "../../models/folder";
import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
/** /**
* Return secrets for workspace with id [workspaceId] and environment * Return secrets for workspace with id [workspaceId] and environment
@@ -16,6 +20,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string; const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string; const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string; const secretPath = req.query.secretPath as string;
const includeImports = req.query.include_imports as string;
const secrets = await SecretService.getSecrets({ const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
@@ -28,13 +33,38 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)
}); });
if (includeImports) {
const folders = await Folder.findOne({ workspace: workspaceId, environment });
let folderId = "root";
// if folder exist get it and replace folderid with new one
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath as string);
if (!folder) {
throw BadRequestError({ message: "Folder not found" });
}
folderId = folder.id;
}
const importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId);
return res.status(200).send({
secrets: secrets.map((secret) =>
repackageSecretToRaw({
secret,
key
})
),
imports: importedSecrets.map((el) => ({
...el,
secrets: el.secrets.map((secret) => repackageSecretToRaw({ secret, key }))
}))
});
}
return res.status(200).send({ return res.status(200).send({
secrets: secrets.map((secret) => { secrets: secrets.map((secret) => {
const rep = repackageSecretToRaw({ const rep = repackageSecretToRaw({
secret, secret,
key key
}); });
return rep; return rep;
}) })
}); });
@@ -232,6 +262,7 @@ export const getSecrets = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string; const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string; const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string; const secretPath = req.query.secretPath as string;
const includeImports = req.query.include_imports as string;
const secrets = await SecretService.getSecrets({ const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
@@ -240,6 +271,24 @@ export const getSecrets = async (req: Request, res: Response) => {
authData: req.authData authData: req.authData
}); });
if (includeImports) {
const folders = await Folder.findOne({ workspace: workspaceId, environment });
let folderId = "root";
// if folder exist get it and replace folderid with new one
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath as string);
if (!folder) {
throw BadRequestError({ message: "Folder not found" });
}
folderId = folder.id;
}
const importedSecrets = await getAllImportedSecrets(workspaceId, environment, folderId);
return res.status(200).send({
secrets,
imports: importedSecrets
});
}
return res.status(200).send({ return res.status(200).send({
secrets secrets
}); });
+3 -1
View File
@@ -41,8 +41,9 @@ import {
signup as v1SignupRouter, signup as v1SignupRouter,
userAction as v1UserActionRouter, userAction as v1UserActionRouter,
user as v1UserRouter, user as v1UserRouter,
workspace as v1WorkspaceRouter,
webhooks as v1WebhooksRouter, webhooks as v1WebhooksRouter,
workspace as v1WorkspaceRouter secretImport as v1SecretImportRouter
} from "./routes/v1"; } from "./routes/v1";
import { import {
auth as v2AuthRouter, auth as v2AuthRouter,
@@ -151,6 +152,7 @@ const main = async () => {
app.use("/api/v1/folders", v1SecretsFolder); app.use("/api/v1/folders", v1SecretsFolder);
app.use("/api/v1/secret-scanning", v1SecretScanningRouter); app.use("/api/v1/secret-scanning", v1SecretScanningRouter);
app.use("/api/v1/webhooks", v1WebhooksRouter); app.use("/api/v1/webhooks", v1WebhooksRouter);
app.use("/api/v1/secret-imports", v1SecretImportRouter);
// v2 routes (improvements) // v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter); app.use("/api/v2/signup", v2SignupRouter);
+52
View File
@@ -0,0 +1,52 @@
import { Schema, Types, model } from "mongoose";
export interface ISecretImports {
_id: Types.ObjectId;
workspace: Types.ObjectId;
environment: string;
folderId: string;
imports: Array<{
environment: string;
secretPath: string;
}>;
}
const secretImportSchema = new Schema<ISecretImports>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true
},
environment: {
type: String,
required: true
},
folderId: {
type: String,
required: true,
default: "root"
},
imports: {
type: [
{
environment: {
type: String,
required: true
},
secretPath: {
type: String,
required: true
}
}
],
default: []
}
},
{
timestamps: true
}
);
const SecretImport = model<ISecretImports>("SecretImports", secretImportSchema);
export default SecretImport;
+3 -1
View File
@@ -17,6 +17,7 @@ import integrationAuth from "./integrationAuth";
import secretsFolder from "./secretsFolder"; import secretsFolder from "./secretsFolder";
import secretScanning from "./secretScanning"; import secretScanning from "./secretScanning";
import webhooks from "./webhook"; import webhooks from "./webhook";
import secretImport from "./secretImport";
export { export {
signup, signup,
@@ -37,5 +38,6 @@ export {
integrationAuth, integrationAuth,
secretsFolder, secretsFolder,
secretScanning, secretScanning,
webhooks webhooks,
secretImport
}; };
+84
View File
@@ -0,0 +1,84 @@
import express from "express";
const router = express.Router();
import { body, param, query } from "express-validator";
import { secretImportController } from "../../controllers/v1";
import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
import { ADMIN, AUTH_MODE_JWT, MEMBER } from "../../variables";
router.post(
"/",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "body"
}),
body("workspaceId").exists().isString().trim().notEmpty(),
body("environment").exists().isString().trim().notEmpty(),
body("folderId").default("root").isString().trim(),
body("secretImport").exists().isObject(),
body("secretImport.environment").isString().exists().trim(),
body("secretImport.secretPath").isString().exists().trim(),
validateRequest,
secretImportController.createSecretImport
);
router.put(
"/:id",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param("id").exists().isString().trim(),
body("secretImports").exists().isArray(),
body("secretImports.*.environment").isString().exists().trim(),
body("secretImports.*.secretPath").isString().exists().trim(),
validateRequest,
secretImportController.updateSecretImport
);
router.delete(
"/:id",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param("id").exists().isString().trim(),
body("secretImportPath").isString().exists().trim(),
body("secretImportEnv").isString().exists().trim(),
validateRequest,
secretImportController.updateSecretImport
);
router.get(
"/",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query"
}),
query("workspaceId").exists().isString().trim().notEmpty(),
query("environment").exists().isString().trim().notEmpty(),
query("folderId").default("root").isString().trim(),
validateRequest,
secretImportController.getSecretImports
);
router.get(
"/secrets",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query"
}),
query("workspaceId").exists().isString().trim().notEmpty(),
query("environment").exists().isString().trim().notEmpty(),
query("folderId").default("root").isString().trim(),
validateRequest,
secretImportController.getAllSecretsFromImport
);
export default router;
+1
View File
@@ -127,6 +127,7 @@ router.get(
query("tagSlugs"), query("tagSlugs"),
query("folderId").default("root").isString().trim(), query("folderId").default("root").isString().trim(),
query("secretPath").optional().isString().trim(), query("secretPath").optional().isString().trim(),
query("include_imports").optional().default(false).isBoolean(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [
+33 -36
View File
@@ -1,10 +1,6 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
requireAuth,
requireWorkspaceAuth,
validateRequest,
} from "../../middleware";
import { body, param, query } from "express-validator"; import { body, param, query } from "express-validator";
import { secretsController } from "../../controllers/v3"; import { secretsController } from "../../controllers/v3";
import { import {
@@ -17,7 +13,7 @@ import {
PERMISSION_READ_SECRETS, PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS, PERMISSION_WRITE_SECRETS,
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED, SECRET_SHARED
} from "../../variables"; } from "../../variables";
router.get( router.get(
@@ -25,14 +21,15 @@ router.get(
query("workspaceId").exists().isString().trim(), query("workspaceId").exists().isString().trim(),
query("environment").exists().isString().trim(), query("environment").exists().isString().trim(),
query("secretPath").default("/").isString().trim(), query("secretPath").default("/").isString().trim(),
query("include_imports").isBoolean().default(false),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -40,7 +37,7 @@ router.get(
locationEnvironment: "query", locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: true, requireE2EEOff: true
}), }),
secretsController.getSecretsRaw secretsController.getSecretsRaw
); );
@@ -58,8 +55,8 @@ router.get(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -67,7 +64,7 @@ router.get(
locationEnvironment: "query", locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: true, requireE2EEOff: true
}), }),
secretsController.getSecretByNameRaw secretsController.getSecretByNameRaw
); );
@@ -86,8 +83,8 @@ router.post(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -95,7 +92,7 @@ router.post(
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: true, requireE2EEOff: true
}), }),
secretsController.createSecretRaw secretsController.createSecretRaw
); );
@@ -114,8 +111,8 @@ router.patch(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -123,7 +120,7 @@ router.patch(
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: true, requireE2EEOff: true
}), }),
secretsController.updateSecretByNameRaw secretsController.updateSecretByNameRaw
); );
@@ -141,8 +138,8 @@ router.delete(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -150,7 +147,7 @@ router.delete(
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: true, requireE2EEOff: true
}), }),
secretsController.deleteSecretByNameRaw secretsController.deleteSecretByNameRaw
); );
@@ -166,8 +163,8 @@ router.get(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -175,7 +172,7 @@ router.get(
locationEnvironment: "query", locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: false, requireE2EEOff: false
}), }),
secretsController.getSecrets secretsController.getSecrets
); );
@@ -201,8 +198,8 @@ router.post(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -210,7 +207,7 @@ router.post(
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: false, requireE2EEOff: false
}), }),
secretsController.createSecret secretsController.createSecret
); );
@@ -228,15 +225,15 @@ router.get(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query", locationWorkspaceId: "query",
locationEnvironment: "query", locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true
}), }),
secretsController.getSecretByName secretsController.getSecretByName
); );
@@ -257,8 +254,8 @@ router.patch(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -266,7 +263,7 @@ router.patch(
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: false, requireE2EEOff: false
}), }),
secretsController.updateSecretByName secretsController.updateSecretByName
); );
@@ -284,8 +281,8 @@ router.delete(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
@@ -293,7 +290,7 @@ router.delete(
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true, requireBlindIndicesEnabled: true,
requireE2EEOff: false, requireE2EEOff: false
}), }),
secretsController.deleteSecretByName secretsController.deleteSecretByName
); );
@@ -0,0 +1,82 @@
import { Types } from "mongoose";
import Folder from "../models/folder";
import Secret, { ISecret } from "../models/secret";
import SecretImport from "../models/secretImports";
import { getFolderByPath } from "./FolderService";
type TSecretImportFid = { environment: string; folderId: string; secretPath: string };
export const getAllImportedSecrets = async (
workspaceId: string,
environment: string,
folderId = "root"
) => {
const secImports = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!secImports) return [];
if (secImports.imports.length === 0) return [];
const importedEnv: Record<string, boolean> = {}; // to get folders from all environment
secImports.imports.forEach((el) => (importedEnv[el.environment] = true));
const folders = await Folder.find({
workspace: workspaceId,
environment: { $in: Object.keys(importedEnv) }
});
const importedSecByFid: TSecretImportFid[] = [];
secImports.imports.forEach((el) => {
const folder = folders.find((fl) => fl.environment === el.environment);
if (folder) {
const secPathFolder = getFolderByPath(folder.nodes, el.secretPath);
if (secPathFolder)
importedSecByFid.push({
environment: el.environment,
folderId: secPathFolder.id,
secretPath: el.secretPath
});
}
});
if (importedSecByFid.length === 0) return [];
const secsGroupedByRef = await Secret.aggregate([
{
$match: {
workspace: new Types.ObjectId(workspaceId),
type: "shared"
}
},
{
$group: {
_id: {
environment: "$environment",
folderId: "$folder"
},
secrets: { $push: "$$ROOT" }
}
},
{
$match: {
$or: importedSecByFid.map(({ environment, folderId: fid }) => ({
"_id.environment": environment,
"_id.folderId": fid
}))
}
}
]);
// now let stitch together secrets.
const importedSecrets: Array<TSecretImportFid & { secrets: ISecret[] }> = [];
importedSecByFid.forEach(({ environment, folderId, secretPath }) => {
const secretsGrouped = secsGroupedByRef.find(
(el) => el._id.environment === environment && el._id.folderId === folderId
);
if (secretsGrouped) {
importedSecrets.push({ secretPath, folderId, environment, secrets: secretsGrouped.secrets });
}
});
return importedSecrets;
};
+9 -9
View File
@@ -9,12 +9,12 @@ import SecretService from "./SecretService";
import GithubSecretScanningService from "./GithubSecretScanningService" import GithubSecretScanningService from "./GithubSecretScanningService"
export { export {
TelemetryService, TelemetryService,
DatabaseService, DatabaseService,
BotService, BotService,
EventService, EventService,
IntegrationService, IntegrationService,
TokenService, TokenService,
SecretService, SecretService,
GithubSecretScanningService GithubSecretScanningService
} };
+4
View File
@@ -235,6 +235,10 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req
SetQueryParam("environment", request.Environment). SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId) SetQueryParam("workspaceId", request.WorkspaceId)
if request.IncludeImport {
httpRequest.SetQueryParam("include_imports", "true")
}
if request.SecretPath != "" { if request.SecretPath != "" {
httpRequest.SetQueryParam("secretPath", request.SecretPath) httpRequest.SetQueryParam("secretPath", request.SecretPath)
} }
+42 -31
View File
@@ -272,40 +272,51 @@ type GetNewAccessTokenWithRefreshTokenResponse struct {
} }
type GetEncryptedSecretsV3Request struct { type GetEncryptedSecretsV3Request struct {
Environment string `json:"environment"` Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"` WorkspaceId string `json:"workspaceId"`
SecretPath string `json:"secretPath"` SecretPath string `json:"secretPath"`
IncludeImport bool `json:"include_imports"`
}
type EncryptedSecretV3 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"`
}
type ImportedSecretV3 struct {
Environment string `json:"environment"`
FolderId string `json:"folderId"`
SecretPath string `json:"secretPath"`
Secrets []EncryptedSecretV3 `json:"secrets"`
} }
type GetEncryptedSecretsV3Response struct { type GetEncryptedSecretsV3Response struct {
Secrets []struct { Secrets []EncryptedSecretV3 `json:"secrets"`
ID string `json:"_id"` ImportedSecrets []ImportedSecretV3 `json:"imports,omitempty"`
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 { type CreateSecretV3Request struct {
+7 -1
View File
@@ -87,7 +87,12 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}) includeImports, err := cmd.Flags().GetBool("include-imports")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports})
if err != nil { 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") util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
@@ -186,6 +191,7 @@ func init() {
runCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") runCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from") runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
runCmd.Flags().Bool("include-imports", true, "Import linked secrets ")
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") 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("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().StringP("tags", "t", "", "filter secrets by tag slugs ")
+7 -1
View File
@@ -54,12 +54,17 @@ var secretsCmd = &cobra.Command{
util.HandleError(err) util.HandleError(err)
} }
includeImports, err := cmd.Flags().GetBool("include-imports")
if err != nil {
util.HandleError(err)
}
tagSlugs, err := cmd.Flags().GetString("tags") tagSlugs, err := cmd.Flags().GetString("tags")
if err != nil { if err != nil {
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}) secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports})
if err != nil { if err != nil {
util.HandleError(err) util.HandleError(err)
} }
@@ -647,6 +652,7 @@ func init() {
secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token")
secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on") 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.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
secretsCmd.Flags().Bool("include-imports", true, "Imported linked secrets ")
secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs") secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs")
secretsCmd.Flags().String("path", "/", "get secrets within a folder path") secretsCmd.Flags().String("path", "/", "get secrets within a folder path")
rootCmd.AddCommand(secretsCmd) rootCmd.AddCommand(secretsCmd)
+1
View File
@@ -65,4 +65,5 @@ type GetAllSecretsParameters struct {
TagSlugs string TagSlugs string
WorkspaceId string WorkspaceId string
SecretsPath string SecretsPath string
IncludeImport bool
} }
+58 -14
View File
@@ -17,7 +17,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) { func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) {
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4) serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 { if len(serviceTokenParts) < 4 {
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again") return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
@@ -45,9 +45,10 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
} }
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{ encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
WorkspaceId: serviceTokenDetails.Workspace, WorkspaceId: serviceTokenDetails.Workspace,
Environment: environment, Environment: environment,
SecretPath: secretPath, SecretPath: secretPath,
IncludeImport: includeImports,
}) })
if err != nil { if err != nil {
@@ -64,15 +65,22 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to decrypt the required workspace key") return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to decrypt the required workspace key")
} }
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets) plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets.Secrets)
if err != nil { if err != nil {
return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err) return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
} }
if includeImports {
plainTextSecrets, err = InjectImportedSecret(plainTextWorkspaceKey, plainTextSecrets, encryptedSecrets.ImportedSecrets)
if err != nil {
return nil, api.GetServiceTokenDetailsResponse{}, err
}
}
return plainTextSecrets, serviceTokenDetails, nil return plainTextSecrets, serviceTokenDetails, nil
} }
func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string) ([]models.SingleEnvironmentVariable, error) { func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string, includeImports bool) ([]models.SingleEnvironmentVariable, error) {
httpClient := resty.New() httpClient := resty.New()
httpClient.SetAuthToken(JTWToken). httpClient.SetAuthToken(JTWToken).
SetHeader("Accept", "application/json") SetHeader("Accept", "application/json")
@@ -114,8 +122,9 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
getSecretsRequest := api.GetEncryptedSecretsV3Request{ getSecretsRequest := api.GetEncryptedSecretsV3Request{
WorkspaceId: workspaceId, WorkspaceId: workspaceId,
Environment: environmentName, Environment: environmentName,
IncludeImport: includeImports,
// TagSlugs: tagSlugs, // TagSlugs: tagSlugs,
} }
@@ -124,19 +133,53 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
} }
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, getSecretsRequest) encryptedSecrets, err := api.CallGetSecretsV3(httpClient, getSecretsRequest)
if err != nil { if err != nil {
return nil, err return nil, err
} }
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets) plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets.Secrets)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to decrypt your secrets [err=%v]", err) return nil, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
} }
if includeImports {
plainTextSecrets, err = InjectImportedSecret(plainTextWorkspaceKey, plainTextSecrets, encryptedSecrets.ImportedSecrets)
if err != nil {
return nil, err
}
}
return plainTextSecrets, nil return plainTextSecrets, nil
} }
func InjectImportedSecret(plainTextWorkspaceKey []byte, secrets []models.SingleEnvironmentVariable, importedSecrets []api.ImportedSecretV3) ([]models.SingleEnvironmentVariable, error) {
if importedSecrets == nil {
return secrets, nil
}
hasOverriden := make(map[string]bool)
for _, sec := range secrets {
hasOverriden[sec.Key] = true
}
for i := len(importedSecrets) - 1; i >= 0; i-- {
importSec := importedSecrets[i]
plainTextImportedSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, importSec.Secrets)
if err != nil {
return nil, fmt.Errorf("unable to decrypt your imported secrets [err=%v]", err)
}
for _, sec := range plainTextImportedSecrets {
if _, ok := hasOverriden[sec.Key]; !ok {
secrets = append(secrets, sec)
hasOverriden[sec.Key] = true
}
}
}
return secrets, nil
}
func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models.SingleEnvironmentVariable, error) { func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models.SingleEnvironmentVariable, error) {
var infisicalToken string var infisicalToken string
if params.InfisicalToken == "" { if params.InfisicalToken == "" {
@@ -179,7 +222,8 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err) 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, params.SecretsPath) secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId,
params.Environment, params.TagSlugs, params.SecretsPath, params.IncludeImport)
log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn) log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn)
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32] backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
@@ -199,7 +243,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
} else { } else {
log.Debug().Msg("Trying to fetch secrets using service token") log.Debug().Msg("Trying to fetch secrets using service token")
secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(infisicalToken, params.Environment, params.SecretsPath) secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(infisicalToken, params.Environment, params.SecretsPath, params.IncludeImport)
} }
return secretsToReturn, errorToReturn return secretsToReturn, errorToReturn
@@ -427,9 +471,9 @@ func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType stri
return secretsToReturn return secretsToReturn
} }
func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV3Response) ([]models.SingleEnvironmentVariable, error) { func GetPlainTextSecrets(key []byte, encryptedSecrets []api.EncryptedSecretV3) ([]models.SingleEnvironmentVariable, error) {
plainTextSecrets := []models.SingleEnvironmentVariable{} plainTextSecrets := []models.SingleEnvironmentVariable{}
for _, secret := range encryptedSecrets.Secrets { for _, secret := range encryptedSecrets {
// Decrypt key // Decrypt key
key_iv, err := base64.StdEncoding.DecodeString(secret.SecretKeyIV) key_iv, err := base64.StdEncoding.DecodeString(secret.SecretKeyIV)
if err != nil { if err != nil {
@@ -1,5 +1,5 @@
--- ---
title: "Reference Secrets" title: "Reference/Import Secrets"
description: "How to use reference secrets in Infisical" description: "How to use reference secrets in Infisical"
--- ---
@@ -34,4 +34,16 @@ For instance, to access a secret 'A' composed of secrets 'B' and 'C' from differ
When using [service tokens](./token) to fetch referenced secrets, ensure the service token has read access to all referenced environments and folders. When using [service tokens](./token) to fetch referenced secrets, ensure the service token has read access to all referenced environments and folders.
Without proper permissions, the final secret value may be incomplete. Without proper permissions, the final secret value may be incomplete.
## Import multiple secrets from a environment secret path
You can import or link another environment's folder to your dashboard to inherit its secrets. This is useful when you need to share secrets across multiple environments.
To add an import/link, just click on `Secret Link` button and provide an `environment` and `secret path` to which the secrets must be pulled.
![secret import change order](../../images/secret-import-add.png)
The import hierarchy is "last one wins." This means that the order in which you import matters. The last folder you import will override the secrets from any previous folders. Additionally, any secrets you define in your dashboard will override the secrets from any imported folders.
You can change the order by dragging and positioning according using the `Change Order` drag handle.
![secret import change order](../../images/secret-import-change-order.png
Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

+109
View File
@@ -5,6 +5,9 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^6.0.1",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/css": "^11.10.0", "@emotion/css": "^11.10.0",
"@emotion/server": "^11.10.0", "@emotion/server": "^11.10.0",
"@fontsource/inter": "^4.5.15", "@fontsource/inter": "^4.5.15",
@@ -2468,6 +2471,68 @@
"node": ">=10.0.0" "node": ">=10.0.0"
} }
}, },
"node_modules/@dnd-kit/accessibility": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz",
"integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz",
"integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==",
"dependencies": {
"@dnd-kit/accessibility": "^3.0.0",
"@dnd-kit/utilities": "^3.2.1",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/modifiers": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz",
"integrity": "sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==",
"dependencies": {
"@dnd-kit/utilities": "^3.2.1",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.0.6",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz",
"integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==",
"dependencies": {
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.0.7",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz",
"integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/babel-plugin": { "node_modules/@emotion/babel-plugin": {
"version": "11.11.0", "version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
@@ -24491,6 +24556,50 @@
"integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
"dev": true "dev": true
}, },
"@dnd-kit/accessibility": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz",
"integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==",
"requires": {
"tslib": "^2.0.0"
}
},
"@dnd-kit/core": {
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz",
"integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==",
"requires": {
"@dnd-kit/accessibility": "^3.0.0",
"@dnd-kit/utilities": "^3.2.1",
"tslib": "^2.0.0"
}
},
"@dnd-kit/modifiers": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz",
"integrity": "sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==",
"requires": {
"@dnd-kit/utilities": "^3.2.1",
"tslib": "^2.0.0"
}
},
"@dnd-kit/sortable": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz",
"integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==",
"requires": {
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
}
},
"@dnd-kit/utilities": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz",
"integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==",
"requires": {
"tslib": "^2.0.0"
}
},
"@emotion/babel-plugin": { "@emotion/babel-plugin": {
"version": "11.11.0", "version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
+3
View File
@@ -13,6 +13,9 @@
"build-storybook": "storybook build" "build-storybook": "storybook build"
}, },
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^6.0.1",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/css": "^11.10.0", "@emotion/css": "^11.10.0",
"@emotion/server": "^11.10.0", "@emotion/server": "^11.10.0",
"@fontsource/inter": "^4.5.15", "@fontsource/inter": "^4.5.15",
+1
View File
@@ -6,6 +6,7 @@ export * from "./integrations";
export * from "./keys"; export * from "./keys";
export * from "./organization"; export * from "./organization";
export * from "./secretFolders"; export * from "./secretFolders";
export * from "./secretImports";
export * from "./secrets"; export * from "./secrets";
export * from "./secretSnapshots"; export * from "./secretSnapshots";
export * from "./serviceAccounts"; export * from "./serviceAccounts";
@@ -0,0 +1,2 @@
export { useCreateSecretImport, useDeleteSecretImport, useUpdateSecretImport } from "./mutation";
export { useGetImportedSecrets, useGetSecretImports } from "./queries";
@@ -0,0 +1,78 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { secretImportKeys } from "./queries";
import { TCreateSecretImportDTO, TDeleteSecretImportDTO, TUpdateSecretImportDTO } from "./types";
export const useCreateSecretImport = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TCreateSecretImportDTO>({
mutationFn: async ({ secretImport, environment, workspaceId, folderId }) => {
const { data } = await apiRequest.post("/api/v1/secret-imports", {
secretImport,
environment,
workspaceId,
folderId
});
return data;
},
onSuccess: (_, { workspaceId, environment, folderId }) => {
queryClient.invalidateQueries(
secretImportKeys.getProjectSecretImports(workspaceId, environment, folderId)
);
queryClient.invalidateQueries(
secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId)
);
}
});
};
export const useUpdateSecretImport = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TUpdateSecretImportDTO>({
mutationFn: async ({ environment, workspaceId, folderId, secretImports, id }) => {
const { data } = await apiRequest.put(`/api/v1/secret-imports/${id}`, {
secretImports,
environment,
workspaceId,
folderId
});
return data;
},
onSuccess: (_, { workspaceId, environment, folderId }) => {
queryClient.invalidateQueries(
secretImportKeys.getProjectSecretImports(workspaceId, environment, folderId)
);
queryClient.invalidateQueries(
secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId)
);
}
});
};
export const useDeleteSecretImport = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TDeleteSecretImportDTO>({
mutationFn: async ({ id, secretImportEnv, secretImportPath }) => {
const { data } = await apiRequest.delete(`/api/v1/secret-imports/${id}`, {
data: {
secretImportPath,
secretImportEnv
}
});
return data;
},
onSuccess: (_, { workspaceId, environment, folderId }) => {
queryClient.invalidateQueries(
secretImportKeys.getProjectSecretImports(workspaceId, environment, folderId)
);
queryClient.invalidateQueries(
secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId)
);
}
});
};
@@ -0,0 +1,124 @@
import { useCallback } from "react";
import { useQuery } from "@tanstack/react-query";
import {
decryptAssymmetric,
decryptSymmetric
} from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request";
import { TGetImportedSecrets, TImportedSecrets, TSecretImports } from "./types";
export const secretImportKeys = {
getProjectSecretImports: (workspaceId: string, env: string | string[], folderId?: string) => [
{ workspaceId, env, folderId },
"secrets-imports"
],
getSecretImportSecrets: (workspaceId: string, env: string | string[], folderId?: string) => [
{ workspaceId, env, folderId },
"secrets-import-sec"
]
};
const fetchSecretImport = async (workspaceId: string, environment: string, folderId?: string) => {
const { data } = await apiRequest.get<{ secretImport: TSecretImports }>(
"/api/v1/secret-imports",
{
params: {
workspaceId,
environment,
folderId
}
}
);
return data.secretImport;
};
export const useGetSecretImports = (workspaceId: string, env: string, folderId?: string) =>
useQuery({
enabled: Boolean(workspaceId) && Boolean(env),
queryKey: secretImportKeys.getProjectSecretImports(workspaceId, env, folderId),
queryFn: () => fetchSecretImport(workspaceId, env, folderId)
});
const fetchImportedSecrets = async (
workspaceId: string,
environment: string,
folderId?: string
) => {
const { data } = await apiRequest.get<{ secrets: TImportedSecrets }>(
"/api/v1/secret-imports/secrets",
{
params: {
workspaceId,
environment,
folderId
}
}
);
return data.secrets;
};
export const useGetImportedSecrets = ({
workspaceId,
environment,
folderId,
decryptFileKey
}: TGetImportedSecrets) =>
useQuery({
enabled: Boolean(workspaceId) && Boolean(environment) && Boolean(decryptFileKey),
queryKey: secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId),
queryFn: () => fetchImportedSecrets(workspaceId, environment, folderId),
select: useCallback(
(data: TImportedSecrets) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = decryptFileKey;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
return data.map((el) => ({
environment: el.environment,
secretPath: el.secretPath,
folderId: el.folderId,
secrets: el.secrets.map((encSecret) => {
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key
});
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key
});
const secretComment = decryptSymmetric({
ciphertext: encSecret.secretCommentCiphertext,
iv: encSecret.secretCommentIV,
tag: encSecret.secretCommentTag,
key
});
return {
_id: encSecret._id,
env: encSecret.environment,
key: secretKey,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt
};
})
}));
},
[decryptFileKey]
)
});
@@ -0,0 +1,56 @@
import { EncryptedSecret } from "../secrets/types";
import { UserWsKeyPair } from "../types";
export type TSecretImports = {
_id: string;
workspaceId: string;
environment: string;
folderId: string;
imports: Array<{ environment: string; secretPath: string }>;
createdAt: string;
updatedAt: string;
};
export type TImportedSecrets = {
environment: string;
secretPath: string;
folderId: string;
secrets: EncryptedSecret[];
}[];
export type TGetImportedSecrets = {
workspaceId: string;
environment: string;
folderId?: string;
decryptFileKey: UserWsKeyPair;
};
export type TCreateSecretImportDTO = {
workspaceId: string;
environment: string;
folderId?: string;
secretImport: {
environment: string;
secretPath: string;
};
};
export type TUpdateSecretImportDTO = {
id: string;
workspaceId: string;
environment: string;
folderId?: string;
secretImports: Array<{
environment: string;
secretPath: string;
}>;
};
export type TDeleteSecretImportDTO = {
id: string;
workspaceId: string;
environment: string;
folderId?: string;
secretImportPath: string;
secretImportEnv: string;
};
@@ -24,6 +24,10 @@ export const secretKeys = {
{ workspaceId, env, folderId }, { workspaceId, env, folderId },
"secrets" "secrets"
], ],
getProjectSecretImports: (workspaceId: string, env: string | string[], folderId?: string) => [
{ workspaceId, env, folderId },
"secrets-imports"
],
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"]
}; };
+2
View File
@@ -34,6 +34,7 @@ export type DecryptedSecret = {
valueOverride?: string; valueOverride?: string;
idOverride?: string; idOverride?: string;
overrideAction?: string; overrideAction?: string;
folderId?: string;
}; };
export type EncryptedSecretVersion = { export type EncryptedSecretVersion = {
@@ -98,6 +99,7 @@ export type GetProjectSecretsDTO = {
folderId?: string; folderId?: string;
secretPath?: string; secretPath?: string;
isPaused?: boolean; isPaused?: boolean;
include_imports?: boolean;
onSuccess?: (data: DecryptedSecret[]) => void; onSuccess?: (data: DecryptedSecret[]) => void;
}; };
+287 -112
View File
@@ -3,6 +3,19 @@ import { FormProvider, useFieldArray, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { import {
closestCenter,
DndContext,
DragEndEvent,
KeyboardSensor,
MouseSensor,
TouchSensor,
useSensor,
useSensors
} from "@dnd-kit/core";
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
import { arrayMove } from "@dnd-kit/sortable";
import {
faAngleDown,
faArrowLeft, faArrowLeft,
faCheck, faCheck,
faClockRotateLeft, faClockRotateLeft,
@@ -10,12 +23,14 @@ import {
faDownload, faDownload,
faEye, faEye,
faEyeSlash, faEyeSlash,
faFileImport,
faFolderPlus, faFolderPlus,
faMagnifyingGlass, faMagnifyingGlass,
faPlus faPlus
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
@@ -41,10 +56,14 @@ import { useLeaveConfirm, usePopUp, useToggle } from "@app/hooks";
import { import {
useBatchSecretsOp, useBatchSecretsOp,
useCreateFolder, useCreateFolder,
useCreateSecretImport,
useCreateWsTag, useCreateWsTag,
useDeleteFolder, useDeleteFolder,
useDeleteSecretImport,
useGetImportedSecrets,
useGetProjectFolders, useGetProjectFolders,
useGetProjectSecrets, useGetProjectSecrets,
useGetSecretImports,
useGetSecretVersion, useGetSecretVersion,
useGetSnapshotSecrets, useGetSnapshotSecrets,
useGetUserAction, useGetUserAction,
@@ -55,7 +74,8 @@ import {
useGetWsTags, useGetWsTags,
usePerformSecretRollback, usePerformSecretRollback,
useRegisterUserAction, useRegisterUserAction,
useUpdateFolder useUpdateFolder,
useUpdateSecretImport
} from "@app/hooks/api"; } from "@app/hooks/api";
import { secretKeys } from "@app/hooks/api/secrets/queries"; import { secretKeys } from "@app/hooks/api/secrets/queries";
import { WorkspaceEnv } from "@app/hooks/api/types"; import { WorkspaceEnv } from "@app/hooks/api/types";
@@ -71,6 +91,8 @@ import {
import { PitDrawer } from "./components/PitDrawer"; import { PitDrawer } from "./components/PitDrawer";
import { SecretDetailDrawer } from "./components/SecretDetailDrawer"; import { SecretDetailDrawer } from "./components/SecretDetailDrawer";
import { SecretDropzone } from "./components/SecretDropzone"; import { SecretDropzone } from "./components/SecretDropzone";
import { SecretImportForm } from "./components/SecretImportForm";
import { SecretImportSection } from "./components/SecretImportSection";
import { SecretInputRow } from "./components/SecretInputRow"; import { SecretInputRow } from "./components/SecretInputRow";
import { SecretTableHeader } from "./components/SecretTableHeader"; import { SecretTableHeader } from "./components/SecretTableHeader";
import { import {
@@ -84,7 +106,7 @@ import {
} from "./DashboardPage.utils"; } from "./DashboardPage.utils";
const USER_ACTION_PUSH = "first_time_secrets_pushed"; const USER_ACTION_PUSH = "first_time_secrets_pushed";
type TDeleteSecretImport = { environment: string; secretPath: string };
/* /*
* Some imp aspects to consider. Here there are multiple stats changing * Some imp aspects to consider. Here there are multiple stats changing
* Thus ideally we need to use a context. But instead we rely on react hook form * Thus ideally we need to use a context. But instead we rely on react hook form
@@ -113,7 +135,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
"compareSecrets", "compareSecrets",
"folderForm", "folderForm",
"deleteFolder", "deleteFolder",
"upgradePlan" "upgradePlan",
"addSecretImport",
"deleteSecretImport"
] as const); ] as const);
const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true); const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true);
const [searchFilter, setSearchFilter] = useState(""); const [searchFilter, setSearchFilter] = useState("");
@@ -129,6 +153,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const { currentWorkspace, isLoading } = useWorkspace(); const { currentWorkspace, isLoading } = useWorkspace();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const workspaceId = currentWorkspace?._id as string; const workspaceId = currentWorkspace?._id as string;
const selectedEnvSlug = selectedEnv?.slug || "";
const { data: latestFileKey } = useGetUserWsKey(workspaceId); const { data: latestFileKey } = useGetUserWsKey(workspaceId);
useEffect(() => { useEffect(() => {
@@ -161,7 +187,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({ const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
workspaceId, workspaceId,
env: selectedEnv?.slug || "", env: selectedEnvSlug,
decryptFileKey: latestFileKey!, decryptFileKey: latestFileKey!,
isPaused: Boolean(snapshotId), isPaused: Boolean(snapshotId),
folderId folderId
@@ -169,7 +195,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const { data: folderData, isLoading: isFoldersLoading } = useGetProjectFolders({ const { data: folderData, isLoading: isFoldersLoading } = useGetProjectFolders({
workspaceId: workspaceId || "", workspaceId: workspaceId || "",
environment: selectedEnv?.slug || "", environment: selectedEnvSlug,
parentFolderId: folderId, parentFolderId: folderId,
isPaused: isRollbackMode, isPaused: isRollbackMode,
sortDir sortDir
@@ -182,7 +208,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
isFetchingNextPage isFetchingNextPage
} = useGetWorkspaceSecretSnapshots({ } = useGetWorkspaceSecretSnapshots({
workspaceId, workspaceId,
environment: selectedEnv?.slug || "", environment: selectedEnvSlug,
folder: folderId, folder: folderId,
limit: 10 limit: 10
}); });
@@ -193,17 +219,18 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
isFetching: isSnapshotChanging isFetching: isSnapshotChanging
} = useGetSnapshotSecrets({ } = useGetSnapshotSecrets({
snapshotId: snapshotId || "", snapshotId: snapshotId || "",
env: selectedEnv?.slug || "", env: selectedEnvSlug,
decryptFileKey: latestFileKey! decryptFileKey: latestFileKey!
}); });
const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = useGetWsSnapshotCount( const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = useGetWsSnapshotCount(
workspaceId, workspaceId,
selectedEnv?.slug || "", selectedEnvSlug,
folderId folderId
); );
const { data: wsTags } = useGetWsTags(workspaceId); const { data: wsTags } = useGetWsTags(workspaceId);
// mutation calls // mutation calls
const { mutateAsync: batchSecretOp } = useBatchSecretsOp(); const { mutateAsync: batchSecretOp } = useBatchSecretsOp();
const { mutateAsync: performSecretRollback } = usePerformSecretRollback(); const { mutateAsync: performSecretRollback } = usePerformSecretRollback();
@@ -213,6 +240,50 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const { mutateAsync: updateFolder } = useUpdateFolder(folderId); const { mutateAsync: updateFolder } = useUpdateFolder(folderId);
const { mutateAsync: deleteFolder } = useDeleteFolder(folderId); const { mutateAsync: deleteFolder } = useDeleteFolder(folderId);
const { data: secretImportCfg, isFetching: isSecretImportCfgFetching } = useGetSecretImports(
workspaceId,
selectedEnvSlug,
folderId
);
const { data: importedSecrets } = useGetImportedSecrets({
workspaceId,
decryptFileKey: latestFileKey!,
environment: selectedEnvSlug,
folderId
});
// This is for dnd-kit. As react-query state mutation async
// This will act as a placeholder to avoid a glitching animation on dropping items
const [items, setItems] = useState<
Array<{ environment: string; secretPath: string; id: string }>
>([]);
useEffect(() => {
if (
!isSecretImportCfgFetching ||
// case in which u go to a folder and come back to fill in with cache data
(items.length === 0 && secretImportCfg?.imports?.length !== 0 && isSecretImportCfgFetching)
) {
setItems(
secretImportCfg?.imports?.map((el) => ({
...el,
id: `${el.environment}-${el.secretPath}`
})) || []
);
}
}, [isSecretImportCfgFetching]);
const { mutateAsync: createSecretImport } = useCreateSecretImport();
const { mutate: updateSecretImportSync } = useUpdateSecretImport();
const { mutateAsync: deleteSecretImport } = useDeleteSecretImport();
const sensors = useSensors(
useSensor(MouseSensor, {}),
useSensor(TouchSensor, {}),
useSensor(KeyboardSensor, {})
);
const method = useForm<FormData>({ const method = useForm<FormData>({
// why any: well yup inferred ts expects other keys to defined as undefined // why any: well yup inferred ts expects other keys to defined as undefined
defaultValues: secrets as any, defaultValues: secrets as any,
@@ -319,14 +390,12 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
await performSecretRollback({ await performSecretRollback({
workspaceId, workspaceId,
version: snapshotSecret.version, version: snapshotSecret.version,
environment: selectedEnv?.slug || "", environment: selectedEnvSlug,
folderId folderId
}); });
setValue("isSnapshotMode", false); setValue("isSnapshotMode", false);
setSnaphotId(null); setSnaphotId(null);
queryClient.invalidateQueries( queryClient.invalidateQueries(secretKeys.getProjectSecret(workspaceId, selectedEnvSlug));
secretKeys.getProjectSecret(workspaceId, selectedEnv?.slug || "")
);
createNotification({ createNotification({
text: "Successfully rollback secrets", text: "Successfully rollback secrets",
type: "success" type: "success"
@@ -522,6 +591,79 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
} }
}; };
const handleSecretImportCreate = async (env: string, secretPath: string) => {
try {
await createSecretImport({
workspaceId,
environment: selectedEnv?.slug || "",
folderId,
secretImport: {
environment: env,
secretPath
}
});
createNotification({
type: "success",
text: "Successfully create secret link"
});
handlePopUpClose("addSecretImport");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create secret link",
type: "error"
});
}
};
const handleSecretImportDelete = async () => {
const { environment: importEnv, secretPath: impSecPath } = popUp.deleteSecretImport
?.data as TDeleteSecretImport;
try {
if (secretImportCfg?._id) {
await deleteSecretImport({
workspaceId,
environment: selectedEnvSlug,
folderId,
id: secretImportCfg?._id,
secretImportEnv: importEnv,
secretImportPath: impSecPath
});
handlePopUpClose("deleteSecretImport");
createNotification({
type: "success",
text: "Successfully removed secret link"
});
}
} catch (err) {
console.error(err);
createNotification({
text: "Failed to remove secret link",
type: "error"
});
}
};
const handleDragEnd = (evt: DragEndEvent) => {
const { active, over } = evt;
if (over?.id && active.id !== over.id) {
const oldIndex = items.findIndex(({ id }) => id === active.id);
const newIndex = items.findIndex(({ id }) => id === over.id);
const newImportOrder = arrayMove(items, oldIndex, newIndex);
setItems(newImportOrder);
updateSecretImportSync({
workspaceId,
environment: selectedEnvSlug,
folderId,
id: secretImportCfg?._id || "",
secretImports: newImportOrder.map((el) => ({
environment: el.environment,
secretPath: el.secretPath
}))
});
}
};
// when secrets is not loading and secrets list is empty // when secrets is not loading and secrets list is empty
const isDashboardSecretEmpty = !isSecretsLoading && !fields?.length; const isDashboardSecretEmpty = !isSecretsLoading && !fields?.length;
@@ -534,7 +676,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const isSnapshotSecretEmtpy = const isSnapshotSecretEmtpy =
isRollbackMode && !isSnapshotSecretsLoading && !snapshotSecret?.secrets?.length; isRollbackMode && !isSnapshotSecretsLoading && !snapshotSecret?.secrets?.length;
const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty) || isSnapshotSecretEmtpy; const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty) || isSnapshotSecretEmtpy;
const isEmptyPage = isFoldersEmpty && isSecretEmpty; const isSecretImportEmpty = !importedSecrets?.length;
const isEmptyPage = isFoldersEmpty && isSecretEmpty && isSecretImportEmpty;
if (isSecretsLoading || isEnvListLoading) { if (isSecretsLoading || isEnvListLoading) {
return ( return (
@@ -652,54 +795,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
</Button> </Button>
</div> </div>
{!isReadOnly && !isRollbackMode && ( {!isReadOnly && !isRollbackMode && (
<> <div className="flex flex-row items-center justify-center">
<div className="block lg:hidden"> <button
<Tooltip content="Add Folder"> type="button"
<IconButton onClick={() => {
ariaLabel="recovery" if (!(isReadOnly || isRollbackMode)) {
variant="outline_bg"
onClick={() => handlePopUpOpen("folderForm")}
>
<FontAwesomeIcon icon={faFolderPlus} />
</IconButton>
</Tooltip>
</div>
<div className="block lg:hidden">
<Tooltip content="Point-in-time Recovery">
<IconButton
ariaLabel="recovery"
variant="outline_bg"
onClick={() => {
if (secretContainer.current) {
secretContainer.current.scroll({
top: 0,
behavior: "smooth"
});
}
prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false });
}}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</Tooltip>
</div>
<div className="hidden lg:block">
<Button
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
onClick={() => handlePopUpOpen("folderForm")}
isDisabled={isReadOnly || isRollbackMode}
variant="outline_bg"
className="h-10"
>
Add Folder
</Button>
</div>
<div className="hidden lg:block">
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (secretContainer.current) { if (secretContainer.current) {
secretContainer.current.scroll({ secretContainer.current.scroll({
top: 0, top: 0,
@@ -708,15 +808,48 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
} }
prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false }); prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false });
setSearchFilter(""); setSearchFilter("");
}} }
isDisabled={isReadOnly || isRollbackMode} }}
variant="outline_bg" className="font-semibold bg-mineshaft-600 border border-mineshaft-500 p-2 rounded-l-md text-sm text-mineshaft-300 cursor-pointer hover:bg-primary/[0.1] hover:border-primary/40 pr-4 duration-200"
className="h-10" >
> <FontAwesomeIcon icon={faPlus} className="px-2"/>Add Secret
Add Secret </button>
</Button> <DropdownMenu>
</div> <DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600">
</> <div className="bg-mineshaft-600 border border-mineshaft-500 p-2 rounded-r-md text-sm text-mineshaft-300 cursor-pointer hover:bg-primary/[0.1] hover:border-primary/40 duration-200">
<FontAwesomeIcon icon={faAngleDown} className="pr-2 pl-1.5"/>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="mt-1 z-[60] left-20 w-[10.8rem]">
<div className="bg-mineshaft-800 p-1 border border-mineshaft-600 rounded-md">
<div className="w-full pb-1">
<Button
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
onClick={() => handlePopUpOpen("folderForm")}
isDisabled={isReadOnly || isRollbackMode}
variant="outline_bg"
className="h-10"
isFullWidth
>
Add Folder
</Button>
</div>
<div className="w-full">
<Button
leftIcon={<FontAwesomeIcon icon={faFileImport} />}
onClick={() => handlePopUpOpen("addSecretImport")}
isDisabled={isReadOnly || isRollbackMode}
variant="outline_bg"
className="h-10"
isFullWidth
>
Add Import
</Button>
</div>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
)} )}
{isRollbackMode && ( {isRollbackMode && (
<Button <Button
@@ -751,49 +884,67 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
ref={secretContainer} ref={secretContainer}
> >
{!isEmptyPage && ( {!isEmptyPage && (
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar"> <DndContext
<table className="secret-table relative"> onDragEnd={handleDragEnd}
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} /> sensors={sensors}
<tbody className="max-h-96 overflow-y-auto"> collisionDetection={closestCenter}
<FolderSection modifiers={[restrictToVerticalAxis]}
onFolderOpen={handleFolderOpen} >
onFolderUpdate={(id, name) => handlePopUpOpen("folderForm", { id, name })} <TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar">
onFolderDelete={(id, name) => handlePopUpOpen("deleteFolder", { id, name })} <table className="secret-table relative">
folders={folderList} <SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
search={searchFilter} <tbody className="max-h-96 overflow-y-auto">
/> <SecretImportSection
{fields.map(({ id, _id }, index) => ( onSecretImportDelete={(impSecEnv, impSecPath) =>
<SecretInputRow handlePopUpOpen("deleteSecretImport", {
key={id} environment: impSecEnv,
isReadOnly={isReadOnly} secretPath: impSecPath
isRollbackMode={isRollbackMode} })
isAddOnly={isAddOnly} }
index={index} secrets={secrets?.secrets}
searchTerm={searchFilter} importedSecrets={importedSecrets}
onSecretDelete={onSecretDelete} items={items}
onRowExpand={() => onDrawerOpen({ id: _id as string, index })}
isSecretValueHidden={isSecretValueHidden}
wsTags={wsTags}
onCreateTagOpen={() => handlePopUpOpen("addTag")}
/> />
))} <FolderSection
{!isReadOnly && !isRollbackMode && ( onFolderOpen={handleFolderOpen}
<tr> onFolderUpdate={(id, name) => handlePopUpOpen("folderForm", { id, name })}
<td colSpan={3} className="hover:bg-mineshaft-700"> onFolderDelete={(id, name) => handlePopUpOpen("deleteFolder", { id, name })}
<button folders={folderList}
type="button" search={searchFilter}
className="flex h-8 w-full cursor-default items-center justify-start pl-12 font-normal text-bunker-300" />
onClick={onAppendSecret} {fields.map(({ id, _id }, index) => (
> <SecretInputRow
<FontAwesomeIcon icon={faPlus} /> key={id}
<span className="ml-2 w-20">Add Secret</span> isReadOnly={isReadOnly}
</button> isRollbackMode={isRollbackMode}
</td> isAddOnly={isAddOnly}
</tr> index={index}
)} searchTerm={searchFilter}
</tbody> onSecretDelete={onSecretDelete}
</table> onRowExpand={() => onDrawerOpen({ id: _id as string, index })}
</TableContainer> isSecretValueHidden={isSecretValueHidden}
wsTags={wsTags}
onCreateTagOpen={() => handlePopUpOpen("addTag")}
/>
))}
{!isReadOnly && !isRollbackMode && (
<tr>
<td colSpan={3} className="hover:bg-mineshaft-700">
<button
type="button"
className="flex h-8 w-full cursor-default items-center justify-start pl-12 font-normal text-bunker-300"
onClick={onAppendSecret}
>
<FontAwesomeIcon icon={faPlus} />
<span className="ml-2 w-20">Add Secret</span>
</button>
</td>
</tr>
)}
</tbody>
</table>
</TableContainer>
</DndContext>
)} )}
<PitDrawer <PitDrawer
isDrawerOpen={popUp?.secretSnapshots?.isOpen} isDrawerOpen={popUp?.secretSnapshots?.isOpen}
@@ -881,6 +1032,20 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
/> />
</ModalContent> </ModalContent>
</Modal> </Modal>
<Modal
isOpen={popUp?.addSecretImport?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addSecretImport", isOpen)}
>
<ModalContent
title="Add Secret Link"
subTitle="To inherit secrets from another environment or folder"
>
<SecretImportForm
environments={currentWorkspace?.environments}
onCreate={handleSecretImportCreate}
/>
</ModalContent>
</Modal>
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteFolder.isOpen} isOpen={popUp.deleteFolder.isOpen}
deleteKey={(popUp.deleteFolder?.data as TDeleteFolderForm)?.name} deleteKey={(popUp.deleteFolder?.data as TDeleteFolderForm)?.name}
@@ -888,6 +1053,16 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
onChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)} onChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)}
onDeleteApproved={handleFolderDelete} onDeleteApproved={handleFolderDelete}
/> />
<DeleteActionModal
isOpen={popUp.deleteSecretImport.isOpen}
deleteKey="unlink"
title="Do you want to remove this secret import?"
subTitle={`This will unlink secrets from environment ${
(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.environment
} of path ${(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.secretPath}?`}
onChange={(isOpen) => handlePopUpToggle("deleteSecretImport", isOpen)}
onDeleteApproved={handleSecretImportDelete}
/>
<Modal <Modal
isOpen={popUp?.compareSecrets?.isOpen} isOpen={popUp?.compareSecrets?.isOpen}
onOpenChange={(open) => handlePopUpToggle("compareSecrets", open)} onOpenChange={(open) => handlePopUpToggle("compareSecrets", open)}
@@ -67,7 +67,7 @@ export const FolderSection = ({
ariaLabel="delete" ariaLabel="delete"
onClick={() => handleFolderDelete(id, name)} onClick={() => handleFolderDelete(id, name)}
> >
<FontAwesomeIcon icon={faXmark} /> <FontAwesomeIcon icon={faXmark} size="lg" />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</div> </div>
@@ -0,0 +1,86 @@
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
type Props = {
onCreate: (environment: string, secretPath: string) => Promise<void>;
environments?: Array<{ slug: string; name: string }>;
};
const formSchema = yup.object({
environment: yup.string().required().label("Environment").trim(),
secretPath: yup
.string()
.required()
.label("Secret Path")
.trim()
.transform((val) =>
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
)
});
type TFormData = yup.InferType<typeof formSchema>;
export const SecretImportForm = ({ onCreate, environments = [] }: Props): JSX.Element => {
const {
control,
reset,
formState: { isSubmitting },
handleSubmit
} = useForm<TFormData>({
resolver: yupResolver(formSchema)
});
const onSubmit = async ({ environment, secretPath }: TFormData) => {
await onCreate(environment, secretPath);
reset();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="environment"
defaultValue={environments?.[0]?.slug}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Environment" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{environments.map(({ name, slug }) => (
<SelectItem value={slug} key={slug}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="secretPath"
defaultValue="/"
render={({ field, fieldState: { error } }) => (
<FormControl label="Secret Path" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} />
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button className="mr-4" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
Create
</Button>
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</ModalClose>
</div>
</form>
);
};
@@ -0,0 +1 @@
export { SecretImportForm } from "./SecretImportForm";
@@ -0,0 +1,144 @@
import { useEffect } from "react";
import { useSortable } from "@dnd-kit/sortable";
import { faFileImport, faFolder, faUpDown, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconButton, TableContainer, Tooltip } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks/useToggle";
type Props = {
onDelete: (environment: string, secretPath: string) => void;
importedEnv: string;
importedSecPath: string;
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
};
// to show the environment and folder icon
export const EnvFolderIcon = ({ env, secretPath }: { env: string; secretPath: string }) => (
<div className="inline-flex items-center space-x-2">
<div style={{ minWidth: "96px" }}>{env || "-"}</div>
{secretPath && (
<div className="inline-flex items-center space-x-2 border-l border-mineshaft-600 pl-2">
<FontAwesomeIcon icon={faFolder} size="lg" className="text-primary-700" />
<span>{secretPath}</span>
</div>
)}
</div>
);
export const SecretImportItem = ({
importedEnv,
importedSecPath,
onDelete,
importedSecrets = []
}: Props) => {
const [isExpanded, setIsExpanded] = useToggle();
const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({
id: `${importedEnv}-${importedSecPath}`
});
const { currentWorkspace } = useWorkspace();
const rowEnv = currentWorkspace?.environments?.find(({ slug }) => slug === importedEnv);
useEffect(() => {
if (isDragging) {
setIsExpanded.off();
}
}, [isDragging]);
const style = {
transform: transform ? `translateY(${transform.y ? Math.round(transform.y) : 0}px)` : "",
transition
};
return (
<>
<tr
ref={setNodeRef}
style={style}
className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700"
onClick={() => setIsExpanded.toggle()}
>
<td className="ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4">
<FontAwesomeIcon icon={faFileImport} className="text-primary-700" />
</td>
<td
colSpan={2}
className="relative flex w-full min-w-[220px] items-center justify-between overflow-hidden text-ellipsis lg:min-w-[240px] xl:min-w-[280px]"
style={{ paddingTop: "0", paddingBottom: "0" }}
>
<div className="flex-grow p-2">
<EnvFolderIcon env={rowEnv?.name || ""} secretPath={importedSecPath} />
</div>
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Change Order" className="capitalize">
<IconButton
size="md"
colorSchema="primary"
variant="plain"
ariaLabel="expand"
{...attributes}
{...listeners}
>
<FontAwesomeIcon icon={faUpDown} size="lg" />
</IconButton>
</Tooltip>
</div>
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Delete" className="capitalize">
<IconButton
size="md"
variant="plain"
colorSchema="danger"
ariaLabel="delete"
onClick={(evt) => {
evt.stopPropagation();
onDelete(importedEnv, importedSecPath);
}}
>
<FontAwesomeIcon icon={faXmark} size="lg" />
</IconButton>
</Tooltip>
</div>
</div>
</td>
</tr>
<tr>
{isExpanded && !isDragging && (
<td colSpan={3}>
<div className="rounded-md bg-bunker-700 p-4 pb-6">
<div className="mb-2 text-lg font-medium">Secrets Imported</div>
<TableContainer>
<table className="secret-table">
<thead>
<tr>
<td style={{ padding: "0.25rem 1rem" }}>Key</td>
<td style={{ padding: "0.25rem 1rem" }}>Value</td>
<td style={{ padding: "0.25rem 1rem" }}>Override</td>
</tr>
</thead>
<tbody>
{importedSecrets.map(({ key, value, overriden }, index) => (
<tr key={`${importedEnv}-${importedSecPath}-${key}-${index + 1}`}>
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
{key}
</td>
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
{value}
</td>
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
<EnvFolderIcon env={overriden?.env} secretPath={overriden?.secretPath} />
</td>
</tr>
))}
</tbody>
</table>
</TableContainer>
</div>
</td>
)}
</tr>
</>
);
};
@@ -0,0 +1,93 @@
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { useWorkspace } from "@app/context";
import { DecryptedSecret } from "@app/hooks/api/secrets/types";
import { SecretImportItem } from "./SecretImportItem";
type TImportedSecrets = Array<{
environment: string;
secretPath: string;
folderId: string;
secrets: DecryptedSecret[];
}>;
const SECRET_IN_DASHBOARD = "Present In Dashboard";
export const computeImportedSecretRows = (
importedSecEnv: string,
importedSecPath: string,
importSecrets: TImportedSecrets = [],
secrets: DecryptedSecret[] = [],
environments: { name: string; slug: string }[] = []
) => {
const importedSecIndex = importSecrets.findIndex(
({ secretPath, environment }) =>
secretPath === importedSecPath && importedSecEnv === environment
);
if (importedSecIndex === -1) return [];
const importedSec = importSecrets[importedSecIndex];
const overridenSec: Record<string, { env: string; secretPath: string }> = {};
const envSlug2Name: Record<string, string> = {};
environments.forEach((el) => {
envSlug2Name[el.slug] = el.name;
});
for (let i = importedSecIndex + 1; i < importSecrets.length; i += 1) {
importSecrets[i].secrets.forEach((el) => {
overridenSec[el.key] = {
env: envSlug2Name?.[importSecrets[i].environment] || "unknown",
secretPath: importSecrets[i].secretPath
};
});
}
secrets.forEach((el) => {
overridenSec[el.key] = { env: SECRET_IN_DASHBOARD, secretPath: "" };
});
return importedSec.secrets.map(({ key, value }) => ({
key,
value,
overriden: overridenSec?.[key]
}));
};
type Props = {
secrets?: DecryptedSecret[];
importedSecrets?: TImportedSecrets;
onSecretImportDelete: (env: string, secPath: string) => void;
items: { id: string; environment: string; secretPath: string }[];
};
export const SecretImportSection = ({
secrets = [],
importedSecrets = [],
onSecretImportDelete,
items = []
}: Props) => {
const { currentWorkspace } = useWorkspace();
const environments = currentWorkspace?.environments || [];
return (
<SortableContext items={items} strategy={verticalListSortingStrategy}>
{items.map(({ secretPath: impSecPath, environment: importSecEnv, id }) => (
<SecretImportItem
key={id}
importedEnv={importSecEnv}
importedSecrets={computeImportedSecretRows(
importSecEnv,
impSecPath,
importedSecrets,
secrets,
environments
)}
onDelete={onSecretImportDelete}
importedSecPath={impSecPath}
/>
))}
</SortableContext>
);
};
@@ -0,0 +1 @@
export { SecretImportSection } from "./SecretImportSection";
@@ -407,7 +407,7 @@ export const SecretInputRow = memo(
<div className="opacity-0 group-hover:opacity-100"> <div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Delete"> <Tooltip content="Delete">
<IconButton <IconButton
size="md" size="lg"
variant="plain" variant="plain"
colorSchema="danger" colorSchema="danger"
ariaLabel="delete" ariaLabel="delete"