Compare commits

...

24 Commits

Author SHA1 Message Date
1f3f0375b9 add secret import to k8 operator 2023-07-18 23:59:03 -04:00
8ad851d4b0 added the ability to change user name 2023-07-18 18:36:34 -07:00
3b5bc151ba Merge pull request #758 from akhilmhdh/feat/secret-import
Implemented secret link/import feature
2023-07-18 16:58:46 -04:00
678cdd3308 Merge branch 'main' into feat/secret-import 2023-07-18 16:52:25 -04:00
33554f4057 patch bug when imports don't show with no secrets 2023-07-18 15:32:14 -04:00
c539d4d243 remove print 2023-07-18 15:31:31 -04:00
124e6dd998 feat(secret-import): added workspace validation for get imports and imported secret api 2023-07-18 19:40:35 +05:30
cef29f5dd7 minor style update 2023-07-17 21:39:05 -07:00
95c914631a patch notify user on risk found 2023-07-17 21:52:24 -04:00
49ae61da08 remove border from risk selection 2023-07-17 21:49:58 -04:00
993abd0921 add secret scanning status to api 2023-07-17 21:28:47 -04:00
f37b497e48 Update overview.mdx 2023-07-17 21:11:27 -04:00
0d2e55a06f add telemetry for cloud secret scanning 2023-07-17 20:29:20 -04:00
040243d4f7 add telemetry for cloud secret scanning 2023-07-17 20:29:07 -04:00
c450b01763 update email for secret leak 2023-07-17 20:20:11 -04:00
4cd203c194 add ss-webhook to values file k8-infisical 2023-07-17 19:56:07 -04:00
178d444deb add web hook under api temporarily 2023-07-17 18:58:39 -04:00
139ca9022e Update build-staging-img.yml 2023-07-17 17:36:57 -04:00
34d3e80d17 Merge pull request #743 from Infisical/git-scanning-app
bring back secret engine for dev
2023-07-17 17:21:34 -04:00
f681f0a98d fix(secret-import): resolved build failure in frontend 2023-07-18 00:29:42 +05:30
23cd6fd861 doc(secret-imports): updated docs for secret import 2023-07-17 23:10:42 +05:30
cf45c3dc8b feat(secret-import): updated cli to support secret import 2023-07-17 23:10:14 +05:30
45584e0c1a feat(secret-import): implemented ui for secret import 2023-07-17 23:08:57 +05:30
202900a7a3 feat(secret-import): implemented api for secret import 2023-07-17 23:08:42 +05:30
61 changed files with 1830 additions and 365 deletions

View File

@ -105,36 +105,6 @@ jobs:
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
secret-scanning-git-app:
name: Build secret scanning git app
runs-on: ubuntu-latest
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 🏗️ Build secret scanning git app and push
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
push: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: secret-engine
tags: |
infisical/staging_deployment_secret-scanning-git-app:${{ steps.commit.outputs.short }}
infisical/staging_deployment_secret-scanning-git-app:latest
platforms: linux/amd64,linux/arm64
gamma-deployment:
name: Deploy to gamma
runs-on: ubuntu-latest

View File

@ -15,6 +15,7 @@ import * as userController from "./userController";
import * as workspaceController from "./workspaceController";
import * as secretScanningController from "./secretScanningController";
import * as webhookController from "./webhookController";
import * as secretImportController from "./secretImportController";
export {
authController,
@ -33,5 +34,6 @@ export {
userController,
workspaceController,
secretScanningController,
webhookController
webhookController,
secretImportController
};

View File

@ -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 });
};

View File

@ -35,6 +35,7 @@ import {
} from "../../services/FolderService";
import { isValidScope } from "../../helpers/secrets";
import path from "path";
import { getAllImportedSecrets } from "../../services/SecretImportService";
/**
* 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;
const workspaceId = req.query.workspaceId 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");
}
// 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 readAction = await EELogService.createAction({
@ -868,7 +875,8 @@ export const getSecrets = async (req: Request, res: Response) => {
}
return res.status(200).send({
secrets
secrets,
...(include_imports && { imports: importedSecrets })
});
};

View File

@ -3,10 +3,10 @@ import { Types } from "mongoose";
import crypto from "crypto";
import bcrypt from "bcrypt";
import {
MembershipOrg,
User,
APIKeyData,
TokenVersion
MembershipOrg,
TokenVersion,
User
} from "../../models";
import { getSaltRounds } from "../../config";
@ -80,6 +80,26 @@ export const updateMyMfaEnabled = async (req: Request, res: Response) => {
});
}
/**
* Update the current user's name [firstName, lastName].
* @param req
* @param res
* @returns
*/
export const updateName = async (req: Request, res: Response) => {
const { firstName, lastName }: { firstName: string; lastName: string; } = req.body;
req.user.firstName = firstName;
req.user.lastName = lastName || "";
await req.user.save();
const user = req.user;
return res.status(200).send({
user,
});
}
/**
* Return organizations that the current user is part of.
* @param req

View File

@ -5,6 +5,10 @@ import { eventPushSecrets } from "../../events";
import { BotService } from "../../services";
import { repackageSecretToRaw } from "../../helpers/secrets";
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
@ -16,6 +20,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
const includeImports = req.query.include_imports as string;
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
@ -28,13 +33,38 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
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({
secrets: secrets.map((secret) => {
const rep = repackageSecretToRaw({
secret,
key
});
return rep;
})
});
@ -232,6 +262,7 @@ export const getSecrets = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
const includeImports = req.query.include_imports as string;
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
@ -240,6 +271,24 @@ export const getSecrets = async (req: Request, res: Response) => {
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({
secrets
});

View File

@ -42,7 +42,8 @@ import {
userAction as v1UserActionRouter,
user as v1UserRouter,
workspace as v1WorkspaceRouter,
webhooks as v1WebhooksRouter
webhooks as v1WebhooksRouter,
secretImport as v1SecretImportRouter
} from "./routes/v1";
import {
auth as v2AuthRouter,
@ -88,7 +89,7 @@ const main = async () => {
})
);
if (await getSecretScanningGitAppId()) {
if (await getSecretScanningGitAppId() && await getSecretScanningWebhookSecret() && await getSecretScanningPrivateKey()) {
const probot = new Probot({
appId: await getSecretScanningGitAppId(),
privateKey: await getSecretScanningPrivateKey(),
@ -151,6 +152,7 @@ const main = async () => {
app.use("/api/v1/folders", v1SecretsFolder);
app.use("/api/v1/secret-scanning", v1SecretScanningRouter);
app.use("/api/v1/webhooks", v1WebhooksRouter);
app.use("/api/v1/secret-imports", v1SecretImportRouter);
// v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter);

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;

View File

@ -1,5 +1,5 @@
import express, { Request, Response } from "express";
import { getSmtpConfigured } from "../../config";
import { getSecretScanningGitAppId, getSecretScanningPrivateKey, getSecretScanningWebhookSecret, getSmtpConfigured } from "../../config";
const router = express.Router();
@ -10,6 +10,7 @@ router.get(
date: new Date(),
message: "Ok",
emailConfigured: await getSmtpConfigured(),
secretScanningConfigured: await getSecretScanningGitAppId() && await getSecretScanningWebhookSecret() && await getSecretScanningPrivateKey(),
})
}
);

View File

@ -17,6 +17,7 @@ import integrationAuth from "./integrationAuth";
import secretsFolder from "./secretsFolder";
import secretScanning from "./secretScanning";
import webhooks from "./webhook";
import secretImport from "./secretImport";
export {
signup,
@ -37,5 +38,6 @@ export {
integrationAuth,
secretsFolder,
secretScanning,
webhooks
webhooks,
secretImport
};

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;

View File

@ -127,6 +127,7 @@ router.get(
query("tagSlugs"),
query("folderId").default("root").isString().trim(),
query("secretPath").optional().isString().trim(),
query("include_imports").optional().default(false).isBoolean(),
validateRequest,
requireAuth({
acceptedAuthModes: [

View File

@ -29,6 +29,16 @@ router.patch(
usersController.updateMyMfaEnabled
);
router.patch(
"/me/name",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
body("firstName").exists(),
validateRequest,
usersController.updateName
);
router.get(
"/me/organizations",
requireAuth({

View File

@ -1,10 +1,6 @@
import express from "express";
const router = express.Router();
import {
requireAuth,
requireWorkspaceAuth,
validateRequest,
} from "../../middleware";
import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
import { body, param, query } from "express-validator";
import { secretsController } from "../../controllers/v3";
import {
@ -17,7 +13,7 @@ import {
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS,
SECRET_PERSONAL,
SECRET_SHARED,
SECRET_SHARED
} from "../../variables";
router.get(
@ -25,14 +21,15 @@ router.get(
query("workspaceId").exists().isString().trim(),
query("environment").exists().isString().trim(),
query("secretPath").default("/").isString().trim(),
query("include_imports").isBoolean().default(false),
validateRequest,
requireAuth({
acceptedAuthModes: [
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -40,7 +37,7 @@ router.get(
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true,
requireE2EEOff: true
}),
secretsController.getSecretsRaw
);
@ -58,8 +55,8 @@ router.get(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -67,7 +64,7 @@ router.get(
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true,
requireE2EEOff: true
}),
secretsController.getSecretByNameRaw
);
@ -86,8 +83,8 @@ router.post(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -95,7 +92,7 @@ router.post(
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true,
requireE2EEOff: true
}),
secretsController.createSecretRaw
);
@ -114,8 +111,8 @@ router.patch(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -123,7 +120,7 @@ router.patch(
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true,
requireE2EEOff: true
}),
secretsController.updateSecretByNameRaw
);
@ -141,8 +138,8 @@ router.delete(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -150,7 +147,7 @@ router.delete(
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: true,
requireE2EEOff: true
}),
secretsController.deleteSecretByNameRaw
);
@ -166,8 +163,8 @@ router.get(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -175,7 +172,7 @@ router.get(
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: false,
requireE2EEOff: false
}),
secretsController.getSecrets
);
@ -201,8 +198,8 @@ router.post(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -210,7 +207,7 @@ router.post(
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: false,
requireE2EEOff: false
}),
secretsController.createSecret
);
@ -228,15 +225,15 @@ router.get(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query",
locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS],
requireBlindIndicesEnabled: true,
requireBlindIndicesEnabled: true
}),
secretsController.getSecretByName
);
@ -257,8 +254,8 @@ router.patch(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -266,7 +263,7 @@ router.patch(
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: false,
requireE2EEOff: false
}),
secretsController.updateSecretByName
);
@ -284,8 +281,8 @@ router.delete(
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT,
],
AUTH_MODE_SERVICE_ACCOUNT
]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
@ -293,7 +290,7 @@ router.delete(
locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS],
requireBlindIndicesEnabled: true,
requireE2EEOff: false,
requireE2EEOff: false
}),
secretsController.deleteSecretByName
);

View File

@ -9,6 +9,7 @@ import MembershipOrg from "../models/membershipOrg";
import { ADMIN, OWNER } from "../variables";
import User from "../models/user";
import { sendMail } from "../helpers";
import TelemetryService from "./TelemetryService";
type SecretMatch = {
Description: string;
@ -127,25 +128,29 @@ export default async (app: Probot) => {
const adminOrOwnerEmails = userEmails.map(userObject => userObject.email)
// TODO
// don't notify if the risk is marked as false positive
// loop through each finding and check if the finger print without commit has a status of false positive, if so don't add it to the list of risks that need to be notified
const usersToNotify = pusher?.email ? [pusher.email, ...adminOrOwnerEmails] : [...adminOrOwnerEmails]
await sendMail({
template: "secretLeakIncident.handlebars",
subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.full_name}`,
recipients: ["pusher.email", ...adminOrOwnerEmails],
recipients: usersToNotify,
substitutions: {
numberOfSecrets: Object.keys(allFindingsByFingerprint).length,
pusher_email: pusher.email,
pusher_name: pusher.name
}
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "cloud secret scan",
distinctId: pusher.email,
properties: {
numberOfCommitsScanned: commits.length,
numberOfRisksFound: Object.keys(allFindingsByFingerprint).length,
}
});
}
});
};

View File

@ -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;
};

View File

@ -9,12 +9,12 @@ import SecretService from "./SecretService";
import GithubSecretScanningService from "./GithubSecretScanningService"
export {
TelemetryService,
DatabaseService,
BotService,
EventService,
IntegrationService,
TokenService,
SecretService,
GithubSecretScanningService
}
TelemetryService,
DatabaseService,
BotService,
EventService,
IntegrationService,
TokenService,
SecretService,
GithubSecretScanningService
};

View File

@ -10,13 +10,14 @@
<body>
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push</h3>
<p><a href="https://app.infisical.com/secret-scanning"><strong>View leaked secrets</strong></a></p>
<p>One or more secret leaks have been detected in a recent commit pushed by {{pusher_name}} ({{pusher_email}}). If
<p>You are receiving this notification because one or more secret leaks have been detected in a recent commit pushed
by {{pusher_name}} ({{pusher_email}}). If
these are test secrets, please add `infisical-scan:ignore` at the end of the line containing the secret as comment
in the given programming. This will prevent future notifications from being sent out for the given secret(s).</p>
in the given programming. This will prevent future notifications from being sent out for those secret(s).</p>
<p>If these are production secrets, please rotate them immediately.</p>
<p>Once you have taken action, be sure to update the status of the risk in your<a
<p>Once you have taken action, be sure to update the status of the risk in your <a
href="https://app.infisical.com/">Infisical
dashboard</a>.</p>
</body>

View File

@ -235,6 +235,10 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req
SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId)
if request.IncludeImport {
httpRequest.SetQueryParam("include_imports", "true")
}
if request.SecretPath != "" {
httpRequest.SetQueryParam("secretPath", request.SecretPath)
}

View File

@ -272,40 +272,51 @@ type GetNewAccessTokenWithRefreshTokenResponse struct {
}
type GetEncryptedSecretsV3Request struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
SecretPath string `json:"secretPath"`
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
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 {
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"`
Secrets []EncryptedSecretV3 `json:"secrets"`
ImportedSecrets []ImportedSecretV3 `json:"imports,omitempty"`
}
type CreateSecretV3Request struct {

View File

@ -87,7 +87,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: 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 {
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().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("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().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 ")

View File

@ -54,12 +54,17 @@ var secretsCmd = &cobra.Command{
util.HandleError(err)
}
includeImports, err := cmd.Flags().GetBool("include-imports")
if err != nil {
util.HandleError(err)
}
tagSlugs, err := cmd.Flags().GetString("tags")
if err != nil {
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 {
util.HandleError(err)
}
@ -647,6 +652,7 @@ func init() {
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.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.Flags().String("path", "/", "get secrets within a folder path")
rootCmd.AddCommand(secretsCmd)

View File

@ -65,4 +65,5 @@ type GetAllSecretsParameters struct {
TagSlugs string
WorkspaceId string
SecretsPath string
IncludeImport bool
}

View File

@ -17,7 +17,7 @@ import (
"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)
if len(serviceTokenParts) < 4 {
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{
WorkspaceId: serviceTokenDetails.Workspace,
Environment: environment,
SecretPath: secretPath,
WorkspaceId: serviceTokenDetails.Workspace,
Environment: environment,
SecretPath: secretPath,
IncludeImport: includeImports,
})
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")
}
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets)
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets.Secrets)
if err != nil {
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
}
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.SetAuthToken(JTWToken).
SetHeader("Accept", "application/json")
@ -114,8 +122,9 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey)
getSecretsRequest := api.GetEncryptedSecretsV3Request{
WorkspaceId: workspaceId,
Environment: environmentName,
WorkspaceId: workspaceId,
Environment: environmentName,
IncludeImport: includeImports,
// TagSlugs: tagSlugs,
}
@ -124,19 +133,53 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work
}
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, getSecretsRequest)
if err != nil {
return nil, err
}
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets)
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets.Secrets)
if err != nil {
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
}
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) {
var infisicalToken string
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)
}
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)
backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32]
@ -199,7 +243,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
} else {
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
@ -427,9 +471,9 @@ func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType stri
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{}
for _, secret := range encryptedSecrets.Secrets {
for _, secret := range encryptedSecrets {
// Decrypt key
key_iv, err := base64.StdEncoding.DecodeString(secret.SecretKeyIV)
if err != nil {

View File

@ -1,5 +1,5 @@
---
title: "Reference Secrets"
title: "Reference/Import Secrets"
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.
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

View File

@ -8,7 +8,7 @@ Self-hosted Infisical allows you to maintain your sensitive information within y
Choose from a variety of deployment options listed below to get started.
<Card
title="Kubernetes"
title="Kubernetes (recommended)"
color="#ea5a0c"
href="deployment-options/kubernetes-helm"
>

View File

@ -5,6 +5,9 @@
"packages": {
"": {
"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/server": "^11.10.0",
"@fontsource/inter": "^4.5.15",
@ -2468,6 +2471,68 @@
"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": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
@ -24491,6 +24556,50 @@
"integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
"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": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",

View File

@ -13,6 +13,9 @@
"build-storybook": "storybook build"
},
"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/server": "^11.10.0",
"@fontsource/inter": "^4.5.15",

View File

@ -6,6 +6,7 @@ export * from "./integrations";
export * from "./keys";
export * from "./organization";
export * from "./secretFolders";
export * from "./secretImports";
export * from "./secrets";
export * from "./secretSnapshots";
export * from "./serviceAccounts";

View File

@ -0,0 +1,2 @@
export { useCreateSecretImport, useDeleteSecretImport, useUpdateSecretImport } from "./mutation";
export { useGetImportedSecrets, useGetSecretImports } from "./queries";

View File

@ -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)
);
}
});
};

View File

@ -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]
)
});

View File

@ -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;
};

View File

@ -24,6 +24,10 @@ export const secretKeys = {
{ workspaceId, env, folderId },
"secrets"
],
getProjectSecretImports: (workspaceId: string, env: string | string[], folderId?: string) => [
{ workspaceId, env, folderId },
"secrets-imports"
],
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"]
};

View File

@ -34,6 +34,7 @@ export type DecryptedSecret = {
valueOverride?: string;
idOverride?: string;
overrideAction?: string;
folderId?: string;
};
export type EncryptedSecretVersion = {
@ -98,6 +99,7 @@ export type GetProjectSecretsDTO = {
folderId?: string;
secretPath?: string;
isPaused?: boolean;
include_imports?: boolean;
onSuccess?: (data: DecryptedSecret[]) => void;
};

View File

@ -16,6 +16,7 @@ import {
CreateAPIKeyRes,
DeletOrgMembershipDTO,
OrgUser,
RenameUserDTO,
TokenVersion,
UpdateOrgUserRoleDTO,
User} from "./types";
@ -45,6 +46,18 @@ const fetchUserAction = async (action: string) => {
return data.userAction;
};
export const useRenameUser = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, RenameUserDTO>({
mutationFn: ({ newName }) =>
apiRequest.patch("/api/v2/users/me/name", { firstName: newName?.split(" ")[0], lastName: newName?.split(" ").slice(1).join(" ") }),
onSuccess: () => {
queryClient.invalidateQueries(userKeys.getUser);
}
});
};
export const useGetUserAction = (action: string) =>
useQuery({
queryKey: userKeys.userAction,

View File

@ -67,6 +67,10 @@ export type CreateAPIKeyRes = {
apiKeyData: APIKeyData;
}
export type RenameUserDTO = {
newName: string;
};
export type APIKeyData = {
_id: string;
name: string;

View File

@ -3,6 +3,19 @@ import { FormProvider, useFieldArray, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
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,
faCheck,
faClockRotateLeft,
@ -10,12 +23,14 @@ import {
faDownload,
faEye,
faEyeSlash,
faFileImport,
faFolderPlus,
faMagnifyingGlass,
faPlus
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
import { useQueryClient } from "@tanstack/react-query";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
@ -41,10 +56,14 @@ import { useLeaveConfirm, usePopUp, useToggle } from "@app/hooks";
import {
useBatchSecretsOp,
useCreateFolder,
useCreateSecretImport,
useCreateWsTag,
useDeleteFolder,
useDeleteSecretImport,
useGetImportedSecrets,
useGetProjectFolders,
useGetProjectSecrets,
useGetSecretImports,
useGetSecretVersion,
useGetSnapshotSecrets,
useGetUserAction,
@ -55,7 +74,8 @@ import {
useGetWsTags,
usePerformSecretRollback,
useRegisterUserAction,
useUpdateFolder
useUpdateFolder,
useUpdateSecretImport
} from "@app/hooks/api";
import { secretKeys } from "@app/hooks/api/secrets/queries";
import { WorkspaceEnv } from "@app/hooks/api/types";
@ -71,6 +91,8 @@ import {
import { PitDrawer } from "./components/PitDrawer";
import { SecretDetailDrawer } from "./components/SecretDetailDrawer";
import { SecretDropzone } from "./components/SecretDropzone";
import { SecretImportForm } from "./components/SecretImportForm";
import { SecretImportSection } from "./components/SecretImportSection";
import { SecretInputRow } from "./components/SecretInputRow";
import { SecretTableHeader } from "./components/SecretTableHeader";
import {
@ -84,7 +106,7 @@ import {
} from "./DashboardPage.utils";
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
* 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",
"folderForm",
"deleteFolder",
"upgradePlan"
"upgradePlan",
"addSecretImport",
"deleteSecretImport"
] as const);
const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true);
const [searchFilter, setSearchFilter] = useState("");
@ -129,6 +153,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const { currentWorkspace, isLoading } = useWorkspace();
const { currentOrg } = useOrganization();
const workspaceId = currentWorkspace?._id as string;
const selectedEnvSlug = selectedEnv?.slug || "";
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
useEffect(() => {
@ -161,7 +187,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
workspaceId,
env: selectedEnv?.slug || "",
env: selectedEnvSlug,
decryptFileKey: latestFileKey!,
isPaused: Boolean(snapshotId),
folderId
@ -169,7 +195,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const { data: folderData, isLoading: isFoldersLoading } = useGetProjectFolders({
workspaceId: workspaceId || "",
environment: selectedEnv?.slug || "",
environment: selectedEnvSlug,
parentFolderId: folderId,
isPaused: isRollbackMode,
sortDir
@ -182,7 +208,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
isFetchingNextPage
} = useGetWorkspaceSecretSnapshots({
workspaceId,
environment: selectedEnv?.slug || "",
environment: selectedEnvSlug,
folder: folderId,
limit: 10
});
@ -193,17 +219,18 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
isFetching: isSnapshotChanging
} = useGetSnapshotSecrets({
snapshotId: snapshotId || "",
env: selectedEnv?.slug || "",
env: selectedEnvSlug,
decryptFileKey: latestFileKey!
});
const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = useGetWsSnapshotCount(
workspaceId,
selectedEnv?.slug || "",
selectedEnvSlug,
folderId
);
const { data: wsTags } = useGetWsTags(workspaceId);
// mutation calls
const { mutateAsync: batchSecretOp } = useBatchSecretsOp();
const { mutateAsync: performSecretRollback } = usePerformSecretRollback();
@ -213,6 +240,50 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const { mutateAsync: updateFolder } = useUpdateFolder(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>({
// why any: well yup inferred ts expects other keys to defined as undefined
defaultValues: secrets as any,
@ -319,14 +390,12 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
await performSecretRollback({
workspaceId,
version: snapshotSecret.version,
environment: selectedEnv?.slug || "",
environment: selectedEnvSlug,
folderId
});
setValue("isSnapshotMode", false);
setSnaphotId(null);
queryClient.invalidateQueries(
secretKeys.getProjectSecret(workspaceId, selectedEnv?.slug || "")
);
queryClient.invalidateQueries(secretKeys.getProjectSecret(workspaceId, selectedEnvSlug));
createNotification({
text: "Successfully rollback secrets",
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
const isDashboardSecretEmpty = !isSecretsLoading && !fields?.length;
@ -534,7 +676,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
const isSnapshotSecretEmtpy =
isRollbackMode && !isSnapshotSecretsLoading && !snapshotSecret?.secrets?.length;
const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty) || isSnapshotSecretEmtpy;
const isEmptyPage = isFoldersEmpty && isSecretEmpty;
const isSecretImportEmpty = !importedSecrets?.length;
const isEmptyPage = isFoldersEmpty && isSecretEmpty && isSecretImportEmpty;
if (isSecretsLoading || isEnvListLoading) {
return (
@ -652,54 +795,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
</Button>
</div>
{!isReadOnly && !isRollbackMode && (
<>
<div className="block lg:hidden">
<Tooltip content="Add Folder">
<IconButton
ariaLabel="recovery"
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={() => {
<div className="flex flex-row items-center justify-center">
<button
type="button"
onClick={() => {
if (!(isReadOnly || isRollbackMode)) {
if (secretContainer.current) {
secretContainer.current.scroll({
top: 0,
@ -708,15 +808,48 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
}
prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false });
setSearchFilter("");
}}
isDisabled={isReadOnly || isRollbackMode}
variant="outline_bg"
className="h-10"
>
Add Secret
</Button>
</div>
</>
}
}}
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"
>
<FontAwesomeIcon icon={faPlus} className="px-2"/>Add Secret
</button>
<DropdownMenu>
<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 && (
<Button
@ -751,49 +884,67 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
ref={secretContainer}
>
{!isEmptyPage && (
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar">
<table className="secret-table relative">
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
<tbody className="max-h-96 overflow-y-auto">
<FolderSection
onFolderOpen={handleFolderOpen}
onFolderUpdate={(id, name) => handlePopUpOpen("folderForm", { id, name })}
onFolderDelete={(id, name) => handlePopUpOpen("deleteFolder", { id, name })}
folders={folderList}
search={searchFilter}
/>
{fields.map(({ id, _id }, index) => (
<SecretInputRow
key={id}
isReadOnly={isReadOnly}
isRollbackMode={isRollbackMode}
isAddOnly={isAddOnly}
index={index}
searchTerm={searchFilter}
onSecretDelete={onSecretDelete}
onRowExpand={() => onDrawerOpen({ id: _id as string, index })}
isSecretValueHidden={isSecretValueHidden}
wsTags={wsTags}
onCreateTagOpen={() => handlePopUpOpen("addTag")}
<DndContext
onDragEnd={handleDragEnd}
sensors={sensors}
collisionDetection={closestCenter}
modifiers={[restrictToVerticalAxis]}
>
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar">
<table className="secret-table relative">
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
<tbody className="max-h-96 overflow-y-auto">
<SecretImportSection
onSecretImportDelete={(impSecEnv, impSecPath) =>
handlePopUpOpen("deleteSecretImport", {
environment: impSecEnv,
secretPath: impSecPath
})
}
secrets={secrets?.secrets}
importedSecrets={importedSecrets}
items={items}
/>
))}
{!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>
<FolderSection
onFolderOpen={handleFolderOpen}
onFolderUpdate={(id, name) => handlePopUpOpen("folderForm", { id, name })}
onFolderDelete={(id, name) => handlePopUpOpen("deleteFolder", { id, name })}
folders={folderList}
search={searchFilter}
/>
{fields.map(({ id, _id }, index) => (
<SecretInputRow
key={id}
isReadOnly={isReadOnly}
isRollbackMode={isRollbackMode}
isAddOnly={isAddOnly}
index={index}
searchTerm={searchFilter}
onSecretDelete={onSecretDelete}
onRowExpand={() => onDrawerOpen({ id: _id as string, index })}
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
isDrawerOpen={popUp?.secretSnapshots?.isOpen}
@ -881,6 +1032,20 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
/>
</ModalContent>
</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
isOpen={popUp.deleteFolder.isOpen}
deleteKey={(popUp.deleteFolder?.data as TDeleteFolderForm)?.name}
@ -888,6 +1053,16 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
onChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)}
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
isOpen={popUp?.compareSecrets?.isOpen}
onOpenChange={(open) => handlePopUpToggle("compareSecrets", open)}

View File

@ -67,7 +67,7 @@ export const FolderSection = ({
ariaLabel="delete"
onClick={() => handleFolderDelete(id, name)}
>
<FontAwesomeIcon icon={faXmark} />
<FontAwesomeIcon icon={faXmark} size="lg" />
</IconButton>
</Tooltip>
</div>

View File

@ -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>
);
};

View File

@ -0,0 +1 @@
export { SecretImportForm } from "./SecretImportForm";

View File

@ -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>
</>
);
};

View File

@ -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>
);
};

View File

@ -0,0 +1 @@
export { SecretImportSection } from "./SecretImportSection";

View File

@ -407,7 +407,7 @@ export const SecretInputRow = memo(
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Delete">
<IconButton
size="md"
size="lg"
variant="plain"
colorSchema="danger"
ariaLabel="delete"

View File

@ -17,7 +17,7 @@ export const RiskStatusSelection = ({riskId, currentSelection}: {riskId: any, cu
<select
value={selectedRiskStatus}
onChange={(e) => setSelectedRiskStatus(e.target.value)}
className="block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
className="block w-full py-2 px-3 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
>
<option>Unresolved</option>
<option value={RiskStatus.RESOLVED_FALSE_POSITIVE}>This is a false positive, resolved</option>

View File

@ -15,7 +15,7 @@ export const ChangeLanguageSection = () => {
};
return (
<div className="p-4 bg-mineshaft-900 mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600">
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<p className="text-xl font-semibold text-mineshaft-100 mb-8">
{t("settings.personal.change-language")}
</p>

View File

@ -3,10 +3,12 @@ import { ChangePasswordSection } from "../ChangePasswordSection";
import { EmergencyKitSection } from "../EmergencyKitSection";
import { SecuritySection } from "../SecuritySection";
import { SessionsSection } from "../SessionsSection";
import { UserNameSection } from "../UserNameSection";
export const PersonalSecurityTab = () => {
return (
<div>
<UserNameSection />
<ChangeLanguageSection />
<SecuritySection />
<SessionsSection />

View File

@ -12,7 +12,7 @@ const tabs = [
export const PersonalTabGroup = () => {
return (
<Tab.Group>
<Tab.List className="mb-6 border-b-2 border-mineshaft-800 w-full">
<Tab.List className="mb-4 border-b-2 border-mineshaft-800 w-full">
{tabs.map((tab) => (
<Tab as={Fragment} key={tab.key}>
{({ selected }) => (

View File

@ -0,0 +1,82 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { Button, FormControl, Input } from "@app/components/v2";
import { useUser } from "@app/context";
import { useRenameUser } from "@app/hooks/api/users/queries";
const formSchema = yup.object({
name: yup.string().required().label("User Name"),
});
type FormData = yup.InferType<typeof formSchema>;
export const UserNameSection = (): JSX.Element => {
const { user } = useUser();
const { createNotification } = useNotificationContext();
const {
handleSubmit,
control,
reset
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
const { mutateAsync, isLoading } = useRenameUser();
useEffect(() => {
if (user) {
reset({ name: `${user?.firstName}${user?.lastName && " "}${user?.lastName}` });
}
}, [user]);
const onFormSubmit = async ({ name }: FormData) => {
try {
if (!user?._id) return;
if (name === "") return;
await mutateAsync({ newName: name});
createNotification({
text: "Successfully renamed user",
type: "success"
});
} catch (error) {
console.error(error);
createNotification({
text: "Failed to rename user",
type: "error"
});
}
};
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
>
<p className="text-xl font-semibold text-mineshaft-100 mb-4">
User name
</p>
<div className="mb-2 max-w-md">
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input placeholder={`${user?.firstName} ${user?.lastName}`} {...field} />
</FormControl>
)}
control={control}
name="name"
/>
</div>
<Button
isLoading={isLoading}
colorSchema="primary"
variant="outline_bg"
type="submit"
>
Save
</Button>
</form>
);
};

View File

@ -0,0 +1 @@
export { UserNameSection } from "./UserNameSection";

View File

@ -26,15 +26,13 @@ export const ProjectSettingsPage = () => {
<p className="text-3xl font-semibold text-gray-200">{t("settings.project.title")}</p>
</div>
<Tab.Group>
<Tab.List className="mb-6 w-full border-b-2 border-mineshaft-800">
<Tab.List className="mb-4 w-full border-b-2 border-mineshaft-800">
{tabs.map((tab) => (
<Tab as={Fragment} key={tab.key}>
{({ selected }) => (
<button
type="button"
className={`w-30 p-4 font-semibold outline-none ${
selected ? "border-b-2 border-white text-white" : "text-mineshaft-400"
}`}
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"}`}
>
{tab.name}
</button>

View File

@ -1,6 +1,5 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
@ -27,7 +26,6 @@ export const ProjectNameChangeSection = () => {
control,
reset
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
const { t } = useTranslation();
useEffect(() => {
if (currentWorkspace) {
@ -67,7 +65,7 @@ export const ProjectNameChangeSection = () => {
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
>
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">
{t("common.display-name")}
Project Name
</h2>
<div className="max-w-md">
<Controller

View File

@ -7,7 +7,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.2.0
version: 0.2.1
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to

View File

@ -44,6 +44,13 @@ spec:
name: {{ include "infisical.backend.fullname" . }}
port:
number: 4000
- path: /ss-webhook
pathType: Exact
backend:
service:
name: {{ include "infisical.backend.fullname" . }}
port:
number: 4000
{{- if $ingress.hostName }}
host: {{ $ingress.hostName }}
{{- end }}

View File

@ -169,53 +169,6 @@ backendEnvironmentVariables:
##
MONGO_URL: ""
secretScanningGitApp:
## @param backend.enabled Enable git scanning app
##
enabled: false
## @param backend.name Backend name
##
name: secret-scanning-git-app
## @param backend.fullnameOverride Backend fullnameOverride
##
fullnameOverride: ""
## @param backend.podAnnotations Backend pod annotations
##
podAnnotations: {}
## @param backend.deploymentAnnotations Backend deployment annotations
##
deploymentAnnotations: {}
## @param backend.replicaCount Backend replica count
##
replicaCount: 2
## Backend image parameters
##
image:
## @param backend.image.repository Backend image repository
##
repository: infisical/staging_deployment_secret-scanning-git-app
## @param backend.image.tag Backend image tag
##
tag: "latest"
## @param backend.image.pullPolicy Backend image pullPolicy
##
pullPolicy: IfNotPresent
## @param backend.kubeSecretRef Backend secret resource reference name (containing required [backend configuration variables](https://infisical.com/docs/self-hosting/configuration/envars))
##
kubeSecretRef: ""
## Backend service
##
service:
## @param backend.service.annotations Backend service annotations
##
annotations: {}
## @param backend.service.type Backend service type
##
type: ClusterIP
## @param backend.service.nodePort Backend service nodePort (used if above type is `NodePort`)
##
nodePort: ""
## @section MongoDB(&reg;) parameters
## Documentation : https://github.com/bitnami/charts/blob/main/bitnami/mongodb/values.yaml
##
@ -374,11 +327,6 @@ ingress:
backend:
path: /api
pathType: Prefix
## @skip ingress.backend
##
secretScanningGitApp:
path: /git-app-api
pathType: Prefix
## @param ingress.tls Ingress TLS hosts (matching above hostName)
## Replace with your own domain
##

View File

@ -56,6 +56,7 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req
SetHeader("User-Agent", USER_AGENT_NAME).
SetHeader("If-None-Match", request.ETag).
SetQueryParam("environment", request.Environment).
SetQueryParam("include_imports", "true"). // TODO needs to be set as a option
SetQueryParam("workspaceId", request.WorkspaceId)
if request.SecretPath != "" {

View File

@ -29,43 +29,54 @@ type GetEncryptedWorkspaceKeyResponse struct {
}
type GetEncryptedSecretsV3Request struct {
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
SecretPath string `json:"secretPath"`
ETag string `json:"etag,omitempty"`
Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"`
SecretPath string `json:"secretPath"`
IncludeImport bool `json:"include_imports"`
ETag string `json:"etag,omitempty"`
}
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 {
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"`
Modified bool `json:"modified,omitempty"`
ETag string `json:"ETag,omitempty"`
Secrets []EncryptedSecretV3 `json:"secrets"`
ImportedSecrets []ImportedSecretV3 `json:"imports,omitempty"`
Modified bool `json:"modified,omitempty"`
ETag string `json:"ETag,omitempty"`
}
type GetServiceTokenDetailsResponse struct {

View File

@ -89,13 +89,18 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, en
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt the required workspace key")
}
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse)
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse.Secrets)
if err != nil {
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
}
plainTextSecretsMergedWithImports, err := InjectImportedSecret(plainTextWorkspaceKey, plainTextSecrets, encryptedSecretsResponse.ImportedSecrets)
if err != nil {
return nil, api.GetEncryptedSecretsV3Response{}, err
}
// expand secrets that are referenced
expandedSecrets := ExpandSecrets(plainTextSecrets, fullServiceToken)
expandedSecrets := ExpandSecrets(plainTextSecretsMergedWithImports, fullServiceToken)
return expandedSecrets, encryptedSecretsResponse, nil
}
@ -163,7 +168,7 @@ func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccou
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to fetch secrets because [err=%v]", err)
}
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse)
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse.Secrets)
if err != nil {
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get plain text secrets because [err=%v]", err)
}
@ -200,9 +205,9 @@ func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV st
}, nil
}
func GetPlainTextSecrets(key []byte, encryptedSecretsResponse api.GetEncryptedSecretsV3Response) ([]model.SingleEnvironmentVariable, error) {
func GetPlainTextSecrets(key []byte, encryptedSecrets []api.EncryptedSecretV3) ([]model.SingleEnvironmentVariable, error) {
plainTextSecrets := []model.SingleEnvironmentVariable{}
for _, secret := range encryptedSecretsResponse.Secrets {
for _, secret := range encryptedSecrets {
// Decrypt key
key_iv, err := base64.StdEncoding.DecodeString(secret.SecretKeyIV)
if err != nil {
@ -332,7 +337,7 @@ func ExpandSecrets(secrets []model.SingleEnvironmentVariable, infisicalToken str
// if not in cross reference cache, fetch it from server
refSecs, _, err := GetPlainTextSecretsViaServiceToken(infisicalToken, "", env, secPath)
if err != nil {
fmt.Println("HELLO===>", "MOO", err)
fmt.Printf("Could not fetch secrets in environment: %s secret-path: %s", env, secPath)
// HandleError(err, fmt.Sprintf("Could not fetch secrets in environment: %s secret-path: %s", env, secPath), "If you are using a service token to fetch secrets, please ensure it is valid")
}
refSecsByKey := getSecretsByKeys(refSecs)
@ -358,3 +363,32 @@ func getSecretsByKeys(secrets []model.SingleEnvironmentVariable) map[string]mode
return secretMapByName
}
func InjectImportedSecret(plainTextWorkspaceKey []byte, secrets []model.SingleEnvironmentVariable, importedSecrets []api.ImportedSecretV3) ([]model.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
}