mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 19:28:58 +00:00
Merge branch 'main' into ENG-2661
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
import { Knex } from "knex";
|
||||
import { Tables } from "knex/types/tables";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
import { DatabaseError } from "../errors";
|
||||
import { buildDynamicKnexQuery, TKnexDynamicOperator } from "./dynamic";
|
||||
|
||||
@@ -25,28 +27,41 @@ export type TFindFilter<R extends object = object> = Partial<R> & {
|
||||
$search?: Partial<{ [k in keyof R]: R[k] }>;
|
||||
$complex?: TKnexDynamicOperator<R>;
|
||||
};
|
||||
|
||||
export const buildFindFilter =
|
||||
<R extends object = object>({ $in, $notNull, $search, $complex, ...filter }: TFindFilter<R>) =>
|
||||
<R extends object = object>(
|
||||
{ $in, $notNull, $search, $complex, ...filter }: TFindFilter<R>,
|
||||
tableName?: TableName,
|
||||
excludeKeys?: Array<keyof R>
|
||||
) =>
|
||||
(bd: Knex.QueryBuilder<R, R>) => {
|
||||
void bd.where(filter);
|
||||
const processedFilter = tableName
|
||||
? Object.fromEntries(
|
||||
Object.entries(filter)
|
||||
.filter(([key]) => !excludeKeys || !excludeKeys.includes(key as keyof R))
|
||||
.map(([key, value]) => [`${tableName}.${key}`, value])
|
||||
)
|
||||
: filter;
|
||||
|
||||
void bd.where(processedFilter);
|
||||
if ($in) {
|
||||
Object.entries($in).forEach(([key, val]) => {
|
||||
if (val) {
|
||||
void bd.whereIn(key as never, val as never);
|
||||
void bd.whereIn([`${tableName ? `${tableName}.` : ""}${key}`] as never, val as never);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ($notNull?.length) {
|
||||
$notNull.forEach((key) => {
|
||||
void bd.whereNotNull(key as never);
|
||||
void bd.whereNotNull([`${tableName ? `${tableName}.` : ""}${key as string}`] as never);
|
||||
});
|
||||
}
|
||||
|
||||
if ($search) {
|
||||
Object.entries($search).forEach(([key, val]) => {
|
||||
if (val) {
|
||||
void bd.whereILike(key as never, val as never);
|
||||
void bd.whereILike([`${tableName ? `${tableName}.` : ""}${key}`] as never, val as never);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,13 +15,13 @@ export const blockLocalAndPrivateIpAddresses = async (url: string) => {
|
||||
|
||||
const validUrl = new URL(url);
|
||||
const inputHostIps: string[] = [];
|
||||
if (isIPv4(validUrl.host)) {
|
||||
inputHostIps.push(validUrl.host);
|
||||
if (isIPv4(validUrl.hostname)) {
|
||||
inputHostIps.push(validUrl.hostname);
|
||||
} else {
|
||||
if (validUrl.host === "localhost" || validUrl.host === "host.docker.internal") {
|
||||
if (validUrl.hostname === "localhost" || validUrl.hostname === "host.docker.internal") {
|
||||
throw new BadRequestError({ message: "Local IPs not allowed as URL" });
|
||||
}
|
||||
const resolvedIps = await dns.resolve4(validUrl.host);
|
||||
const resolvedIps = await dns.resolve4(validUrl.hostname);
|
||||
inputHostIps.push(...resolvedIps);
|
||||
}
|
||||
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
||||
|
||||
@@ -1541,6 +1541,7 @@ export const registerRoutes = async (
|
||||
|
||||
const secretSyncService = secretSyncServiceFactory({
|
||||
secretSyncDAL,
|
||||
secretImportDAL,
|
||||
permissionService,
|
||||
appConnectionService,
|
||||
folderDAL,
|
||||
|
||||
@@ -154,7 +154,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
secrets: z
|
||||
.object({
|
||||
secretId: z.string(),
|
||||
referencedSecretKey: z.string()
|
||||
referencedSecretKey: z.string(),
|
||||
referencedSecretEnv: z.string()
|
||||
})
|
||||
.array()
|
||||
.optional()
|
||||
@@ -166,6 +167,16 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
usedBySecretSyncs: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
destination: z.string(),
|
||||
environment: z.string(),
|
||||
id: z.string(),
|
||||
path: z.string()
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
totalFolderCount: z.number().optional(),
|
||||
totalDynamicSecretCount: z.number().optional(),
|
||||
totalSecretCount: z.number().optional(),
|
||||
@@ -500,6 +511,24 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
}
|
||||
|
||||
const usedBySecretSyncs: { name: string; destination: string; environment: string; id: string; path: string }[] =
|
||||
[];
|
||||
for await (const environment of environments) {
|
||||
const secretSyncs = await server.services.secretSync.listSecretSyncsBySecretPath(
|
||||
{ projectId, secretPath, environment },
|
||||
req.permission
|
||||
);
|
||||
secretSyncs.forEach((sync) => {
|
||||
usedBySecretSyncs.push({
|
||||
name: sync.name,
|
||||
destination: sync.destination,
|
||||
environment,
|
||||
id: sync.id,
|
||||
path: sync.folder?.path || "/"
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
folders,
|
||||
dynamicSecrets,
|
||||
@@ -512,6 +541,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
totalSecretCount,
|
||||
totalSecretRotationCount,
|
||||
importedByEnvs,
|
||||
usedBySecretSyncs,
|
||||
totalCount:
|
||||
(totalFolderCount ?? 0) +
|
||||
(totalDynamicSecretCount ?? 0) +
|
||||
@@ -611,6 +641,16 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
totalFolderCount: z.number().optional(),
|
||||
totalDynamicSecretCount: z.number().optional(),
|
||||
totalSecretCount: z.number().optional(),
|
||||
usedBySecretSyncs: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
destination: z.string(),
|
||||
environment: z.string(),
|
||||
id: z.string(),
|
||||
path: z.string()
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
importedBy: z
|
||||
.object({
|
||||
environment: z.object({
|
||||
@@ -624,7 +664,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
secrets: z
|
||||
.object({
|
||||
secretId: z.string(),
|
||||
referencedSecretKey: z.string()
|
||||
referencedSecretKey: z.string(),
|
||||
referencedSecretEnv: z.string()
|
||||
})
|
||||
.array()
|
||||
.optional()
|
||||
@@ -904,6 +945,18 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
secrets
|
||||
});
|
||||
|
||||
const secretSyncs = await server.services.secretSync.listSecretSyncsBySecretPath(
|
||||
{ projectId, secretPath, environment },
|
||||
req.permission
|
||||
);
|
||||
const usedBySecretSyncs = secretSyncs.map((sync) => ({
|
||||
name: sync.name,
|
||||
destination: sync.destination,
|
||||
environment: sync.environment?.name || environment,
|
||||
id: sync.id,
|
||||
path: sync.folder?.path || "/"
|
||||
}));
|
||||
|
||||
if (secrets?.length || secretRotations?.length) {
|
||||
const secretCount =
|
||||
(secrets?.length ?? 0) +
|
||||
@@ -950,6 +1003,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
totalSecretCount,
|
||||
totalSecretRotationCount,
|
||||
importedBy,
|
||||
usedBySecretSyncs,
|
||||
totalCount:
|
||||
(totalImportCount ?? 0) +
|
||||
(totalFolderCount ?? 0) +
|
||||
|
||||
@@ -171,6 +171,19 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getFolderImports = async (secretPath: string, environmentId: string, tx?: Knex) => {
|
||||
try {
|
||||
const folderImports = await (tx || db.replicaNode())(TableName.SecretImport)
|
||||
.where({ importPath: secretPath, importEnv: environmentId })
|
||||
.join(TableName.SecretFolder, `${TableName.SecretImport}.folderId`, `${TableName.SecretFolder}.id`)
|
||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||
.select(db.ref("id").withSchema(TableName.SecretFolder).as("folderId"));
|
||||
return folderImports;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "get secret imports" });
|
||||
}
|
||||
};
|
||||
|
||||
const getFolderIsImportedBy = async (
|
||||
secretPath: string,
|
||||
environmentId: string,
|
||||
@@ -203,7 +216,8 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
||||
db.ref("name").withSchema(TableName.Environment).as("envName"),
|
||||
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||
db.ref("id").withSchema(TableName.SecretFolder).as("folderId"),
|
||||
db.ref("secretKey").withSchema(TableName.SecretReferenceV2).as("referencedSecretKey")
|
||||
db.ref("secretKey").withSchema(TableName.SecretReferenceV2).as("referencedSecretKey"),
|
||||
db.ref("environment").withSchema(TableName.SecretReferenceV2).as("referencedSecretEnv")
|
||||
);
|
||||
|
||||
const folderResults = folderImports.map(({ envName, envSlug, folderName, folderId }) => ({
|
||||
@@ -214,13 +228,14 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
||||
}));
|
||||
|
||||
const secretResults = secretReferences.map(
|
||||
({ envName, envSlug, secretId, folderName, folderId, referencedSecretKey }) => ({
|
||||
({ envName, envSlug, secretId, folderName, folderId, referencedSecretKey, referencedSecretEnv }) => ({
|
||||
envName,
|
||||
envSlug,
|
||||
secretId,
|
||||
folderName,
|
||||
folderId,
|
||||
referencedSecretKey
|
||||
referencedSecretKey,
|
||||
referencedSecretEnv
|
||||
})
|
||||
);
|
||||
|
||||
@@ -235,6 +250,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
||||
secrets: {
|
||||
secretId: string;
|
||||
referencedSecretKey: string;
|
||||
referencedSecretEnv: string;
|
||||
}[];
|
||||
folderId: string;
|
||||
folderImported: boolean;
|
||||
@@ -264,7 +280,11 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
||||
if ("secretId" in item && item.secretId) {
|
||||
updatedAcc[env].folders[folder].secrets = [
|
||||
...updatedAcc[env].folders[folder].secrets,
|
||||
{ secretId: item.secretId, referencedSecretKey: item.referencedSecretKey }
|
||||
{
|
||||
secretId: item.secretId,
|
||||
referencedSecretKey: item.referencedSecretKey,
|
||||
referencedSecretEnv: item.referencedSecretEnv
|
||||
}
|
||||
];
|
||||
} else {
|
||||
updatedAcc[env].folders[folder].folderImported = true;
|
||||
@@ -309,6 +329,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
||||
findLastImportPosition,
|
||||
updateAllPosition,
|
||||
getProjectImportCount,
|
||||
getFolderIsImportedBy
|
||||
getFolderIsImportedBy,
|
||||
getFolderImports
|
||||
};
|
||||
};
|
||||
|
||||
@@ -808,7 +808,7 @@ export const secretImportServiceFactory = ({
|
||||
actorOrgId,
|
||||
secrets
|
||||
}: TGetSecretImportsDTO & {
|
||||
secrets: { secretKey: string; secretValue: string }[] | undefined;
|
||||
secrets: { secretKey: string; secretValue: string; id: string }[] | undefined;
|
||||
}) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
@@ -877,7 +877,8 @@ export const secretImportServiceFactory = ({
|
||||
)
|
||||
.map((otherSecret) => ({
|
||||
secretId: secret.secretKey,
|
||||
referencedSecretKey: otherSecret.secretKey
|
||||
referencedSecretKey: otherSecret.secretKey,
|
||||
referencedSecretEnv: environment
|
||||
}));
|
||||
}) || [];
|
||||
if (locallyReferenced.length > 0) {
|
||||
|
||||
@@ -56,11 +56,12 @@ export type FolderResult = {
|
||||
export type SecretResult = {
|
||||
secretId: string;
|
||||
referencedSecretKey: string;
|
||||
referencedSecretEnv: string;
|
||||
} & FolderResult;
|
||||
|
||||
export type FolderInfo = {
|
||||
folderName: string;
|
||||
secrets?: { secretId: string; referencedSecretKey: string }[];
|
||||
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
|
||||
folderId: string;
|
||||
folderImported: boolean;
|
||||
envSlug?: string;
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
TDeleteSecretSyncDTO,
|
||||
TFindSecretSyncByIdDTO,
|
||||
TFindSecretSyncByNameDTO,
|
||||
TListSecretSyncsByFolderId,
|
||||
TListSecretSyncsByProjectId,
|
||||
TSecretSync,
|
||||
TTriggerSecretSyncImportSecretsByIdDTO,
|
||||
@@ -31,12 +32,14 @@ import {
|
||||
TUpdateSecretSyncDTO
|
||||
} from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
||||
import { TSecretSyncDALFactory } from "./secret-sync-dal";
|
||||
import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "./secret-sync-maps";
|
||||
import { TSecretSyncQueueFactory } from "./secret-sync-queue";
|
||||
|
||||
type TSecretSyncServiceFactoryDep = {
|
||||
secretSyncDAL: TSecretSyncDALFactory;
|
||||
secretImportDAL: TSecretImportDALFactory;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||
@@ -53,6 +56,7 @@ export type TSecretSyncServiceFactory = ReturnType<typeof secretSyncServiceFacto
|
||||
export const secretSyncServiceFactory = ({
|
||||
secretSyncDAL,
|
||||
folderDAL,
|
||||
secretImportDAL,
|
||||
permissionService,
|
||||
appConnectionService,
|
||||
projectBotService,
|
||||
@@ -85,6 +89,37 @@ export const secretSyncServiceFactory = ({
|
||||
return secretSyncs as TSecretSync[];
|
||||
};
|
||||
|
||||
const listSecretSyncsBySecretPath = async (
|
||||
{ projectId, secretPath, environment }: TListSecretSyncsByFolderId,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (permission.cannot(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder) return [];
|
||||
|
||||
const folderImports = await secretImportDAL.getFolderImports(secretPath, folder.envId);
|
||||
|
||||
const secretSyncs = await secretSyncDAL.find({
|
||||
$in: {
|
||||
folderId: folderImports.map((folderImport) => folderImport.folderId).concat(folder.id)
|
||||
}
|
||||
});
|
||||
|
||||
return secretSyncs as TSecretSync[];
|
||||
};
|
||||
|
||||
const findSecretSyncById = async ({ destination, syncId }: TFindSecretSyncByIdDTO, actor: OrgServiceActor) => {
|
||||
const secretSync = await secretSyncDAL.findById(syncId);
|
||||
|
||||
@@ -518,6 +553,7 @@ export const secretSyncServiceFactory = ({
|
||||
return {
|
||||
listSecretSyncOptions,
|
||||
listSecretSyncsByProjectId,
|
||||
listSecretSyncsBySecretPath,
|
||||
findSecretSyncById,
|
||||
findSecretSyncByName,
|
||||
createSecretSync,
|
||||
|
||||
@@ -144,6 +144,13 @@ export type TListSecretSyncsByProjectId = {
|
||||
destination?: SecretSync;
|
||||
};
|
||||
|
||||
export type TListSecretSyncsByFolderId = {
|
||||
projectId: string;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
destination?: SecretSync;
|
||||
};
|
||||
|
||||
export type TFindSecretSyncByIdDTO = {
|
||||
syncId: string;
|
||||
destination: SecretSync;
|
||||
|
||||
@@ -64,7 +64,8 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
||||
const findOne = async (filter: Partial<TSecretsV2>, tx?: Knex) => {
|
||||
try {
|
||||
const docs = await (tx || db)(TableName.SecretV2)
|
||||
.where(filter)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.where(buildFindFilter(filter, TableName.SecretV2))
|
||||
.leftJoin(
|
||||
TableName.SecretV2JnTag,
|
||||
`${TableName.SecretV2}.id`,
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from "node:path";
|
||||
|
||||
import RE2 from "re2";
|
||||
|
||||
import { TableName, TSecretFolders, TSecretsV2 } from "@app/db/schemas";
|
||||
import { SecretType, TableName, TSecretFolders, TSecretsV2 } from "@app/db/schemas";
|
||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
@@ -720,7 +720,7 @@ export const reshapeBridgeSecret = (
|
||||
secretReminderRecipients: secret.secretReminderRecipients || [],
|
||||
...(secretValueHidden
|
||||
? {
|
||||
secretValue: INFISICAL_SECRET_VALUE_HIDDEN_MASK,
|
||||
secretValue: secret.type === SecretType.Personal ? secret.value : INFISICAL_SECRET_VALUE_HIDDEN_MASK,
|
||||
secretValueHidden: true
|
||||
}
|
||||
: {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: "Assume Privileges"
|
||||
description: "Learn how to temporarily assume the privileges of a user or machine identity within a project."
|
||||
---
|
||||
|
||||
This feature allows authorized users to temporarily take on the permissions of another user or identity. It helps administrators and access managers test and verify permissions before granting access, ensuring everything is set up correctly.
|
||||
It also reduces back-and-forth with end users when troubleshooting permission-related issues.
|
||||
|
||||
## How It Works
|
||||
|
||||
When an authorized user activates assume privileges mode, they temporarily inherit the target user or identity’s permissions for up to one hour.
|
||||
During this time, they can perform actions within the system with the same level of access as the target user.
|
||||
|
||||
- **Permission-based**: Only permissions are inherited, not the full identity
|
||||
- **Time-limited**: Access automatically expires after one hour
|
||||
- **Audited**: All actions are logged under the original user's account. This means any action taken during the session will be recorded under the entity assuming the privileges, not the target entity.
|
||||
- **Authorization required**: Only users with the specific **assume privilege** permission can use this feature
|
||||
- **Scoped to a single project**: You can only assume privileges for one project at a time
|
||||
|
||||
## How to Assume Privileges
|
||||
|
||||
<Steps>
|
||||
<Step title="Go to Project Access">
|
||||
Click on the user or identity you want to assume.
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Click Assume Privilege">
|
||||
Click **Assume Privilege**, then type `assume` to confirm and start your session.
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Session is Active">
|
||||
You will see a yellow banner indicating that your assume privilege session is active. You can exit at any time by clicking **Exit**.
|
||||
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -9,6 +9,9 @@ This guide will walk you through the steps needed to configure external KMS supp
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An AWS KMS Key configured as a `Symmetric` key and with `Encrypt and Decrypt` key usage.
|
||||

|
||||
|
||||
Before you begin, you'll first need to choose a method of authentication with AWS from below.
|
||||
|
||||
<Tabs>
|
||||
|
||||
@@ -268,11 +268,11 @@ For organizations that work with US government agencies, FIPS compliance is almo
|
||||
|
||||
<Steps>
|
||||
<Step title="Create HSM client folder">
|
||||
When using Kubernetes, you need to mount the path containing the HSM client files. This section covers how to configure your Infisical instance to use an HSM with Kubernetes.
|
||||
When using Kubernetes, you need to mount the path containing the HSM client files. This section covers how to configure your Infisical instance to use an HSM with Kubernetes. In this example, we are going to be using `/etc/luna-docker`.
|
||||
|
||||
|
||||
```bash
|
||||
mkdir /etc/hsm-client
|
||||
mkdir /etc/luna-docker
|
||||
```
|
||||
|
||||
After [setting up your Luna Cloud HSM client](https://thalesdocs.com/gphsm/luna/7/docs/network/Content/install/client_install/add_dpod.htm), you should have a set of files, referred to as the HSM client. You don't need all the files, but for simplicity we recommend copying all the files from the client.
|
||||
@@ -306,20 +306,60 @@ For organizations that work with US government agencies, FIPS compliance is almo
|
||||
The most important parts of the client folder is the `Chrystoki.conf` file, and the `libs`, `plugins`, and `jsp` folders. You need to copy these files to the folder you created in the first step.
|
||||
|
||||
```bash
|
||||
cp -r /<path-to-where-your-hsm-client-is-located> /etc/hsm-client
|
||||
cp -r /<path-to-where-your-luna-client-is-located>/* /etc/luna-docker
|
||||
```
|
||||
|
||||
<Note>
|
||||
The `/*` wildcard will copy all files and folders within the HSM client. The wildcard is important to ensure that the file structure is inline with the rest of this guide.
|
||||
</Note>
|
||||
|
||||
After copying the files, the `/etc/luna-docker` directory should have the following file structure:
|
||||
```bash
|
||||
$ ls -R /etc/luna-docker
|
||||
Chrystoki.conf etc lock server-certificate.pem
|
||||
Chrystoki.conf.tmp2E jsp partition-ca-certificate.pem setenv
|
||||
lch-support-linux-64bit partition-certificate.pem
|
||||
bin libs plugins
|
||||
|
||||
/etc/luna-docker/bin:
|
||||
64
|
||||
|
||||
/etc/luna-docker/bin/64:
|
||||
ckdemo cmu lunacm multitoken vtl
|
||||
|
||||
/etc/luna-docker/etc:
|
||||
openssl.cnf
|
||||
|
||||
/etc/luna-docker/jsp:
|
||||
64 LunaProvider.jar
|
||||
|
||||
/etc/luna-docker/jsp/64:
|
||||
libLunaAPI.so
|
||||
|
||||
/etc/luna-docker/libs:
|
||||
64
|
||||
|
||||
/etc/luna-docker/libs/64:
|
||||
libCryptoki2.so
|
||||
|
||||
/etc/luna-docker/lock:
|
||||
|
||||
/etc/luna-docker/plugins:
|
||||
libcloud.plugin
|
||||
```
|
||||
|
||||
</Step>
|
||||
<Step title="Update Chrystoki.conf">
|
||||
The `Chrystoki.conf` file is used to configure the HSM client. You need to update the `Chrystoki.conf` file to point to the correct file paths.
|
||||
|
||||
In this example, we will be mounting the `/etc/hsm-client` folder from the host to containers in our deployment's pods at the path `/hsm-client`. This means the contents of `/etc/hsm-client` on the host will be accessible at `/hsm-client` within the containers.
|
||||
In this example, we will be mounting the `/etc/luna-docker` folder from the host to containers in our deployment's pods at the path `/usr/safenet/lunaclient`. This means the contents of `/etc/luna-docker` on the host will be accessible at `/usr/safenet/lunaclient` within the containers.
|
||||
|
||||
An example config file will look like this:
|
||||
|
||||
```Chrystoki.conf
|
||||
Chrystoki2 = {
|
||||
# This path points to the mounted path, /hsm-client
|
||||
LibUNIX64 = /hsm-client/libs/64/libCryptoki2.so;
|
||||
# This path points to the mounted path, /usr/safenet/lunaclient
|
||||
LibUNIX64 = /usr/safenet/lunaclient/libs/64/libCryptoki2.so;
|
||||
}
|
||||
|
||||
Luna = {
|
||||
@@ -339,8 +379,8 @@ For organizations that work with US government agencies, FIPS compliance is almo
|
||||
|
||||
Misc = {
|
||||
# Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step.
|
||||
PluginModuleDir = /hsm-client/plugins;
|
||||
MutexFolder = /hsm-client/lock;
|
||||
PluginModuleDir = /usr/safenet/lunaclient/plugins;
|
||||
MutexFolder = /usr/safenet/lunaclient/lock;
|
||||
PE1746Enabled = 1;
|
||||
ToolsDir = /usr/bin;
|
||||
|
||||
@@ -353,7 +393,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
|
||||
LunaSA Client = {
|
||||
ReceiveTimeout = 20000;
|
||||
# Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step.
|
||||
SSLConfigFile = /hsm-client/etc/openssl.cnf;
|
||||
SSLConfigFile = /usr/safenet/lunaclient/etc/openssl.cnf;
|
||||
ClientPrivKeyFile = ./etc/ClientNameKey.pem;
|
||||
ClientCertFile = ./etc/ClientNameCert.pem;
|
||||
ServerCAFile = ./etc/CAFile.pem;
|
||||
@@ -441,7 +481,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
|
||||
|
||||
```bash
|
||||
kubectl exec hsm-setup-pod -- mkdir -p /data/ # Create the data directory
|
||||
kubectl cp ./hsm-client/ hsm-setup-pod:/data/ # Copy the HSM client files into the PVC
|
||||
kubectl cp /etc/luna-docker/. hsm-setup-pod:/data/ # Copy the HSM client files into the PVC
|
||||
kubectl exec hsm-setup-pod -- chmod -R 755 /data/ # Set the correct permissions for the HSM client files
|
||||
```
|
||||
|
||||
@@ -456,7 +496,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
|
||||
Next we need to update the environment variables used for the deployment. If you followed the [setup instructions for Kubernetes deployments](/self-hosting/deployment-options/kubernetes-helm), you should have a Kubernetes secret called `infisical-secrets`.
|
||||
We need to update the secret with the following environment variables:
|
||||
|
||||
- `HSM_LIB_PATH` - The path to the HSM client library _(mapped to `/hsm-client/libs/64/libCryptoki2.so`)_
|
||||
- `HSM_LIB_PATH` - The path to the HSM client library _(mapped to `/usr/safenet/lunaclient/libs/64/libCryptoki2.so`)_
|
||||
- `HSM_PIN` - The PIN for the HSM device that you created when setting up your Luna Cloud HSM client
|
||||
- `HSM_SLOT` - The slot number for the HSM device that you selected when setting up your Luna Cloud HSM client
|
||||
- `HSM_KEY_LABEL` - The label for the HSM key. If no key is found with the provided key label, the HSM will create a new key with the provided label.
|
||||
@@ -471,7 +511,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
|
||||
type: Opaque
|
||||
stringData:
|
||||
# ... Other environment variables ...
|
||||
HSM_LIB_PATH: "/hsm-client/libs/64/libCryptoki2.so" # If you followed this guide, this will be the path of the Luna Cloud HSM client
|
||||
HSM_LIB_PATH: "/usr/safenet/lunaclient/libs/64/libCryptoki2.so" # If you followed this guide, this will be the path of the Luna Cloud HSM client
|
||||
HSM_PIN: "<your-hsm-device-pin>"
|
||||
HSM_SLOT: "<hsm-device-slot>"
|
||||
HSM_KEY_LABEL: "<your-key-label>"
|
||||
@@ -487,7 +527,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
|
||||
<Step title="Updating the Deployment">
|
||||
After we've successfully configured the PVC and updated our environment variables, we are ready to update the deployment configuration so that the pods it creates can access the HSM client files.
|
||||
|
||||
We need to update the Docker image of the deployment to use `infisical/infisical-fips`. The `infisical/infisical-fips` image is a functionally identical image to the `infisical/infisical` image, but it is built with support for HSM encryption.
|
||||
We need to update the Docker image of the deployment to use `infisical/infisical-fips`. The `infisical/infisical-fips` image is a functionally identical image to the `infisical/infisical` image, but it is built with HSM support.
|
||||
|
||||
```yaml
|
||||
# ... The rest of the values.yaml file ...
|
||||
@@ -499,8 +539,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
|
||||
|
||||
extraVolumeMounts:
|
||||
- name: hsm-data
|
||||
mountPath: /hsm-client # The path we will mount the HSM client files to
|
||||
subPath: ./hsm-client
|
||||
mountPath: /usr/safenet/lunaclient # The path we will mount the HSM client files to
|
||||
|
||||
extraVolumes:
|
||||
- name: hsm-data
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 335 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 360 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 439 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
@@ -34,7 +34,7 @@ description: "Learn how to configure a TeamCity Sync for Infisical."
|
||||
- **Build Configuration**: The build configuration to sync secrets to.
|
||||
|
||||
<Note>
|
||||
Not including a Build Configuration will sync secrets to the entire project.
|
||||
Not including a Build Configuration will sync secrets to the project.
|
||||
</Note>
|
||||
|
||||
5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
|
||||
@@ -44,6 +44,11 @@ description: "Learn how to configure a TeamCity Sync for Infisical."
|
||||
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
|
||||
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over TeamCity when keys conflict.
|
||||
- **Import Secrets (Prioritize TeamCity)**: Imports secrets from the destination endpoint before syncing, prioritizing values from TeamCity over Infisical when keys conflict.
|
||||
|
||||
<Note>
|
||||
Infisical only syncs secrets from within the target scope; inherited secrets will not be imported.
|
||||
</Note>
|
||||
|
||||
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
|
||||
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
|
||||
|
||||
|
||||
+3
-2
@@ -160,6 +160,7 @@
|
||||
},
|
||||
"documentation/platform/access-controls/additional-privileges",
|
||||
"documentation/platform/access-controls/temporary-access",
|
||||
"documentation/platform/access-controls/assume-privilege",
|
||||
"documentation/platform/access-controls/access-requests",
|
||||
"documentation/platform/access-controls/project-access-requests",
|
||||
"documentation/platform/pr-workflows",
|
||||
@@ -887,8 +888,8 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "LDAP Password",
|
||||
"pages": [
|
||||
"group": "LDAP Password",
|
||||
"pages": [
|
||||
"api-reference/endpoints/secret-rotations/ldap-password/create",
|
||||
"api-reference/endpoints/secret-rotations/ldap-password/delete",
|
||||
"api-reference/endpoints/secret-rotations/ldap-password/get-by-id",
|
||||
|
||||
@@ -19,6 +19,7 @@ type Props = {
|
||||
formContent?: ReactNode;
|
||||
children?: ReactNode;
|
||||
deletionMessage?: ReactNode;
|
||||
buttonColorSchema?: "danger" | "primary" | "secondary" | "gray" | null;
|
||||
};
|
||||
|
||||
export const DeleteActionModal = ({
|
||||
@@ -32,6 +33,7 @@ export const DeleteActionModal = ({
|
||||
buttonText = "Delete",
|
||||
formContent,
|
||||
deletionMessage,
|
||||
buttonColorSchema = "danger",
|
||||
children
|
||||
}: Props): JSX.Element => {
|
||||
const [inputData, setInputData] = useState("");
|
||||
@@ -67,7 +69,7 @@ export const DeleteActionModal = ({
|
||||
<div className="mx-2 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
colorSchema="danger"
|
||||
colorSchema={buttonColorSchema}
|
||||
isDisabled={!(deleteKey === inputData) || isLoading}
|
||||
onClick={onDelete}
|
||||
isLoading={isLoading}
|
||||
|
||||
@@ -93,6 +93,7 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
onFocus={(evt) => {
|
||||
onFocus?.(evt);
|
||||
setIsSecretFocused.on();
|
||||
evt.currentTarget.select();
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
spellCheck={false}
|
||||
|
||||
@@ -25,9 +25,18 @@ export type DashboardProjectSecretsOverviewResponse = {
|
||||
totalUniqueFoldersInPage: number;
|
||||
totalUniqueSecretImportsInPage: number;
|
||||
importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[];
|
||||
usedBySecretSyncs?: UsedBySecretSyncs[];
|
||||
totalUniqueSecretRotationsInPage: number;
|
||||
};
|
||||
|
||||
export type UsedBySecretSyncs = {
|
||||
name: string;
|
||||
destination: string;
|
||||
environment: string;
|
||||
id: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
export type DashboardProjectSecretsDetailsResponse = {
|
||||
imports?: TSecretImport[];
|
||||
folders?: TSecretFolder[];
|
||||
@@ -43,13 +52,14 @@ export type DashboardProjectSecretsDetailsResponse = {
|
||||
totalSecretRotationCount?: number;
|
||||
totalCount: number;
|
||||
importedBy?: ProjectSecretsImportedBy[];
|
||||
usedBySecretSyncs?: UsedBySecretSyncs[];
|
||||
};
|
||||
|
||||
export type ProjectSecretsImportedBy = {
|
||||
environment: { name: string; slug: string };
|
||||
folders: {
|
||||
name: string;
|
||||
secrets?: { secretId: string; referencedSecretKey: string }[];
|
||||
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
|
||||
isImported: boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
@@ -74,7 +74,7 @@ import {
|
||||
useUpdateSecretV3
|
||||
} from "@app/hooks/api";
|
||||
import { useGetProjectSecretsOverview } from "@app/hooks/api/dashboard/queries";
|
||||
import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
|
||||
import { DashboardSecretsOrderBy, ProjectSecretsImportedBy } from "@app/hooks/api/dashboard/types";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
|
||||
import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types";
|
||||
@@ -274,7 +274,8 @@ export const OverviewPage = () => {
|
||||
totalUniqueSecretImportsInPage,
|
||||
totalUniqueDynamicSecretsInPage,
|
||||
totalUniqueSecretRotationsInPage,
|
||||
importedByEnvs
|
||||
importedByEnvs,
|
||||
usedBySecretSyncs
|
||||
} = overview ?? {};
|
||||
|
||||
const secretImportsShaped = secretImports
|
||||
@@ -726,6 +727,98 @@ export const OverviewPage = () => {
|
||||
}
|
||||
}, [routerSearch.search]);
|
||||
|
||||
const selectedKeysCount = Object.keys(selectedEntries.secret).length;
|
||||
|
||||
const secretsToDeleteKeys = useMemo(() => {
|
||||
return Object.values(selectedEntries.secret).flatMap((entries) =>
|
||||
Object.values(entries).map((secret) => secret.key)
|
||||
);
|
||||
}, [selectedEntries]);
|
||||
|
||||
const filterAndMergeEnvironments = (
|
||||
envNames: string[],
|
||||
envs: { environment: string; importedBy: ProjectSecretsImportedBy[] }[]
|
||||
): ProjectSecretsImportedBy[] => {
|
||||
const filteredEnvs = envs.filter((env) => envNames.includes(env.environment));
|
||||
|
||||
if (filteredEnvs.length === 0) return [];
|
||||
|
||||
const allImportedBy = filteredEnvs.flatMap((env) => env.importedBy);
|
||||
const groupedBySlug: Record<string, ProjectSecretsImportedBy[]> = {};
|
||||
|
||||
allImportedBy.forEach((item) => {
|
||||
const { slug } = item.environment;
|
||||
if (!groupedBySlug[slug]) groupedBySlug[slug] = [];
|
||||
groupedBySlug[slug].push(item);
|
||||
});
|
||||
|
||||
const mergedImportedBy = Object.values(groupedBySlug).map((group) => {
|
||||
const { environment } = group[0];
|
||||
const allFolders = group.flatMap((item) => item.folders);
|
||||
|
||||
const foldersByName: Record<string, (typeof allFolders)[number][]> = {};
|
||||
allFolders.forEach((folder) => {
|
||||
if (!foldersByName[folder.name]) foldersByName[folder.name] = [];
|
||||
foldersByName[folder.name].push(folder);
|
||||
});
|
||||
|
||||
const mergedFolders = Object.entries(foldersByName).map(([name, foldersData]) => {
|
||||
const isImported = foldersData.some((folder) => folder.isImported);
|
||||
const allSecrets = foldersData.flatMap((folder) => folder.secrets || []);
|
||||
|
||||
const uniqueSecrets: {
|
||||
secretId: string;
|
||||
referencedSecretKey: string;
|
||||
referencedSecretEnv: string;
|
||||
}[] = [];
|
||||
const secretIds = new Set<string>();
|
||||
|
||||
allSecrets
|
||||
.filter(
|
||||
(secret) =>
|
||||
!secretsToDeleteKeys ||
|
||||
secretsToDeleteKeys.length === 0 ||
|
||||
secretsToDeleteKeys.includes(secret.referencedSecretKey)
|
||||
)
|
||||
.forEach((secret) => {
|
||||
if (!secretIds.has(secret.secretId)) {
|
||||
secretIds.add(secret.secretId);
|
||||
uniqueSecrets.push(secret);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
name,
|
||||
isImported,
|
||||
...(uniqueSecrets.length > 0 ? { secrets: uniqueSecrets } : {})
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
environment,
|
||||
folders: mergedFolders.filter(
|
||||
(folder) => folder.isImported || (folder.secrets && folder.secrets.length > 0)
|
||||
)
|
||||
};
|
||||
});
|
||||
|
||||
return mergedImportedBy;
|
||||
};
|
||||
|
||||
const importedBy = useMemo(() => {
|
||||
if (!importedByEnvs) return [];
|
||||
if (selectedKeysCount === 0) {
|
||||
return filterAndMergeEnvironments(
|
||||
visibleEnvs.map(({ slug }) => slug),
|
||||
importedByEnvs
|
||||
);
|
||||
}
|
||||
return filterAndMergeEnvironments(
|
||||
Object.values(selectedEntries.secret).flatMap((entries) => Object.keys(entries)),
|
||||
importedByEnvs
|
||||
);
|
||||
}, [importedByEnvs, selectedEntries, selectedKeysCount]);
|
||||
|
||||
if (isProjectV3 && visibleEnvs.length > 0 && isOverviewLoading) {
|
||||
return (
|
||||
<div className="container mx-auto flex h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||
@@ -1044,7 +1137,9 @@ export const OverviewPage = () => {
|
||||
secretPath={secretPath}
|
||||
selectedEntries={selectedEntries}
|
||||
resetSelectedEntries={resetSelectedEntries}
|
||||
importedByEnvs={importedByEnvs}
|
||||
importedBy={importedBy}
|
||||
secretsToDeleteKeys={secretsToDeleteKeys}
|
||||
usedBySecretSyncs={usedBySecretSyncs}
|
||||
/>
|
||||
<div className="thin-scrollbar mt-4">
|
||||
<TableContainer
|
||||
@@ -1261,6 +1356,7 @@ export const OverviewPage = () => {
|
||||
secretKey={key}
|
||||
getSecretByKey={getSecretByKey}
|
||||
scrollOffset={debouncedScrollOffset}
|
||||
importedBy={importedBy}
|
||||
/>
|
||||
))}
|
||||
<SecretNoAccessOverviewTableRow
|
||||
|
||||
+105
-26
@@ -4,6 +4,7 @@ import { subject } from "@casl/ability";
|
||||
import {
|
||||
faCheck,
|
||||
faCopy,
|
||||
faEyeSlash,
|
||||
faProjectDiagram,
|
||||
faTrash,
|
||||
faXmark
|
||||
@@ -25,13 +26,13 @@ import {
|
||||
ModalTrigger,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { Blur } from "@app/components/v2/Blur";
|
||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import { SecretType } from "@app/hooks/api/types";
|
||||
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||
import { CollapsibleSecretImports } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports";
|
||||
|
||||
type Props = {
|
||||
defaultValue?: string | null;
|
||||
@@ -54,6 +55,14 @@ type Props = {
|
||||
) => Promise<void>;
|
||||
onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>;
|
||||
isRotatedSecret?: boolean;
|
||||
importedBy?: {
|
||||
environment: { name: string; slug: string };
|
||||
folders: {
|
||||
name: string;
|
||||
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
|
||||
isImported: boolean;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
|
||||
export const SecretEditRow = ({
|
||||
@@ -70,8 +79,13 @@ export const SecretEditRow = ({
|
||||
secretPath,
|
||||
isVisible,
|
||||
secretId,
|
||||
isRotatedSecret
|
||||
isRotatedSecret,
|
||||
importedBy
|
||||
}: Props) => {
|
||||
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
||||
"editSecret"
|
||||
] as const);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
@@ -115,6 +129,20 @@ export const SecretEditRow = ({
|
||||
if (isCreatable) {
|
||||
await onSecretCreate(environment, secretName, value);
|
||||
} else {
|
||||
if (
|
||||
importedBy &&
|
||||
importedBy.some(({ folders }) =>
|
||||
folders?.some(({ secrets }) =>
|
||||
secrets?.some(
|
||||
({ referencedSecretKey, referencedSecretEnv }) =>
|
||||
referencedSecretKey === secretName && referencedSecretEnv === environment
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
handlePopUpOpen("editSecret", { secretValue: value });
|
||||
return;
|
||||
}
|
||||
await onSecretUpdate(
|
||||
environment,
|
||||
secretName,
|
||||
@@ -124,7 +152,25 @@ export const SecretEditRow = ({
|
||||
);
|
||||
}
|
||||
}
|
||||
reset({ value });
|
||||
if (secretValueHidden && !isOverride) {
|
||||
setTimeout(() => {
|
||||
reset({ value: defaultValue || null });
|
||||
}, 50);
|
||||
} else {
|
||||
reset({ value });
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditSecret = async ({ secretValue }: { secretValue: string }) => {
|
||||
await onSecretUpdate(
|
||||
environment,
|
||||
secretName,
|
||||
secretValue,
|
||||
isOverride ? SecretType.Personal : SecretType.Shared,
|
||||
secretId
|
||||
);
|
||||
reset({ value: secretValue });
|
||||
handlePopUpClose("editSecret");
|
||||
};
|
||||
|
||||
const canReadSecretValue = hasSecretReadValueOrDescribePermission(
|
||||
@@ -132,6 +178,16 @@ export const SecretEditRow = ({
|
||||
ProjectPermissionSecretActions.ReadValue
|
||||
);
|
||||
|
||||
const canEditSecretValue = permission.can(
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName,
|
||||
secretTags: ["*"]
|
||||
})
|
||||
);
|
||||
|
||||
const handleDeleteSecret = useCallback(async () => {
|
||||
setIsDeleting.on();
|
||||
setIsModalOpen(false);
|
||||
@@ -153,29 +209,32 @@ export const SecretEditRow = ({
|
||||
deleteKey={secretName}
|
||||
onDeleteApproved={handleDeleteSecret}
|
||||
/>
|
||||
|
||||
{secretValueHidden && !isOverride && (
|
||||
<Tooltip
|
||||
content={`You do not have access to view the current value${canEditSecretValue && !isRotatedSecret ? ", but you can set a new one" : "."}`}
|
||||
>
|
||||
<FontAwesomeIcon className="pl-2" size="sm" icon={faEyeSlash} />
|
||||
</Tooltip>
|
||||
)}
|
||||
<div className="flex-grow border-r border-r-mineshaft-600 pl-1 pr-2">
|
||||
{secretValueHidden ? (
|
||||
<Blur tooltipText="You do not have permission to read the value of this secret." />
|
||||
) : (
|
||||
<Controller
|
||||
disabled={isImportedSecret && !defaultValue}
|
||||
control={control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<InfisicalSecretInput
|
||||
{...field}
|
||||
isReadOnly={isImportedSecret || isRotatedSecret}
|
||||
value={field.value as string}
|
||||
key="secret-input"
|
||||
isVisible={isVisible}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
isImport={isImportedSecret}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
disabled={isImportedSecret && !defaultValue}
|
||||
control={control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<InfisicalSecretInput
|
||||
{...field}
|
||||
isReadOnly={isImportedSecret || (isRotatedSecret && !isOverride)}
|
||||
value={field.value as string}
|
||||
key="secret-input"
|
||||
isVisible={isVisible && !secretValueHidden}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
isImport={isImportedSecret}
|
||||
defaultValue={secretValueHidden ? "" : undefined}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -306,6 +365,26 @@ export const SecretEditRow = ({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.editSecret.isOpen}
|
||||
deleteKey="confirm"
|
||||
buttonColorSchema="secondary"
|
||||
buttonText="Save"
|
||||
subTitle=""
|
||||
title="Do you want to edit this secret?"
|
||||
onChange={(isOpen) => handlePopUpToggle("editSecret", isOpen)}
|
||||
onDeleteApproved={() => handleEditSecret(popUp?.editSecret?.data)}
|
||||
formContent={
|
||||
importedBy &&
|
||||
importedBy.length > 0 && (
|
||||
<CollapsibleSecretImports
|
||||
importedBy={importedBy}
|
||||
secretsToDelete={[secretName]}
|
||||
onlyReferences
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+40
-8
@@ -1,3 +1,4 @@
|
||||
import { subject } from "@casl/ability";
|
||||
import { faCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faAngleDown,
|
||||
@@ -14,6 +15,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Button, Checkbox, TableContainer, Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import { useProjectPermission } from "@app/context";
|
||||
import {
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
@@ -44,6 +50,14 @@ type Props = {
|
||||
secretName: string
|
||||
) => { secret?: SecretV3RawSanitized; environmentInfo?: WorkspaceEnv } | undefined;
|
||||
scrollOffset: number;
|
||||
importedBy?: {
|
||||
environment: { name: string; slug: string };
|
||||
folders: {
|
||||
name: string;
|
||||
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
|
||||
isImported: boolean;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
|
||||
export const SecretOverviewTableRow = ({
|
||||
@@ -58,12 +72,35 @@ export const SecretOverviewTableRow = ({
|
||||
getImportedSecretByKey,
|
||||
scrollOffset,
|
||||
onToggleSecretSelect,
|
||||
isSelected
|
||||
isSelected,
|
||||
importedBy
|
||||
}: Props) => {
|
||||
const [isFormExpanded, setIsFormExpanded] = useToggle();
|
||||
const totalCols = environments.length + 1; // secret key row
|
||||
const [isSecretVisible, setIsSecretVisible] = useToggle();
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const getDefaultValue = (
|
||||
secret: SecretV3RawSanitized | undefined,
|
||||
importedSecret: { secret?: SecretV3RawSanitized } | undefined
|
||||
) => {
|
||||
const canEditSecretValue = permission.can(
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: secret?.env || "",
|
||||
secretPath: secret?.path || "",
|
||||
secretName: secret?.key || "",
|
||||
secretTags: ["*"]
|
||||
})
|
||||
);
|
||||
|
||||
if (secret?.secretValueHidden && !secret?.valueOverride) {
|
||||
return canEditSecretValue ? "******" : "";
|
||||
}
|
||||
return secret?.valueOverride || secret?.value || importedSecret?.secret?.value || "";
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tr isHoverable isSelectable onClick={() => setIsFormExpanded.toggle()} className="group">
|
||||
@@ -228,13 +265,7 @@ export const SecretOverviewTableRow = ({
|
||||
isVisible={isSecretVisible}
|
||||
secretName={secretKey}
|
||||
secretValueHidden={secret?.secretValueHidden || false}
|
||||
defaultValue={
|
||||
secret?.secretValueHidden
|
||||
? ""
|
||||
: secret?.valueOverride ||
|
||||
secret?.value ||
|
||||
importedSecret?.secret?.value
|
||||
}
|
||||
defaultValue={getDefaultValue(secret, importedSecret)}
|
||||
secretId={secret?.id}
|
||||
isOverride={Boolean(secret?.valueOverride)}
|
||||
isImportedSecret={isImportedSecret}
|
||||
@@ -244,6 +275,7 @@ export const SecretOverviewTableRow = ({
|
||||
onSecretUpdate={onSecretUpdate}
|
||||
environment={slug}
|
||||
isRotatedSecret={secret?.isRotatedSecret}
|
||||
importedBy={importedBy}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
+16
-80
@@ -15,7 +15,7 @@ import {
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api";
|
||||
import { ProjectSecretsImportedBy } from "@app/hooks/api/dashboard/types";
|
||||
import { ProjectSecretsImportedBy, UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
|
||||
import {
|
||||
SecretType,
|
||||
SecretV3RawSanitized,
|
||||
@@ -37,14 +37,18 @@ type Props = {
|
||||
[EntryType.FOLDER]: Record<string, Record<string, TSecretFolder>>;
|
||||
[EntryType.SECRET]: Record<string, Record<string, SecretV3RawSanitized>>;
|
||||
};
|
||||
importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[];
|
||||
importedBy?: ProjectSecretsImportedBy[] | null;
|
||||
usedBySecretSyncs?: UsedBySecretSyncs[];
|
||||
secretsToDeleteKeys: string[];
|
||||
};
|
||||
|
||||
export const SelectionPanel = ({
|
||||
secretPath,
|
||||
resetSelectedEntries,
|
||||
selectedEntries,
|
||||
importedByEnvs
|
||||
importedBy,
|
||||
secretsToDeleteKeys,
|
||||
usedBySecretSyncs = []
|
||||
}: Props) => {
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
@@ -81,80 +85,11 @@ export const SelectionPanel = ({
|
||||
)
|
||||
);
|
||||
|
||||
const secretsToDeleteKeys = useMemo(() => {
|
||||
return Object.values(selectedEntries.secret).flatMap((entries) =>
|
||||
Object.values(entries).map((secret) => secret.key)
|
||||
);
|
||||
}, [selectedEntries]);
|
||||
|
||||
const filterAndMergeEnvironments = (
|
||||
envNames: string[],
|
||||
envs: { environment: string; importedBy: ProjectSecretsImportedBy[] }[]
|
||||
): ProjectSecretsImportedBy[] => {
|
||||
const filteredEnvs = envs.filter((env) => envNames.includes(env.environment));
|
||||
|
||||
if (filteredEnvs.length === 0) return [];
|
||||
|
||||
const allImportedBy = filteredEnvs.flatMap((env) => env.importedBy);
|
||||
const groupedBySlug: Record<string, ProjectSecretsImportedBy[]> = {};
|
||||
|
||||
allImportedBy.forEach((item) => {
|
||||
const { slug } = item.environment;
|
||||
if (!groupedBySlug[slug]) groupedBySlug[slug] = [];
|
||||
groupedBySlug[slug].push(item);
|
||||
});
|
||||
|
||||
const mergedImportedBy = Object.values(groupedBySlug).map((group) => {
|
||||
const { environment } = group[0];
|
||||
const allFolders = group.flatMap((item) => item.folders);
|
||||
|
||||
const foldersByName: Record<string, (typeof allFolders)[number][]> = {};
|
||||
allFolders.forEach((folder) => {
|
||||
if (!foldersByName[folder.name]) foldersByName[folder.name] = [];
|
||||
foldersByName[folder.name].push(folder);
|
||||
});
|
||||
|
||||
const mergedFolders = Object.entries(foldersByName).map(([name, folders]) => {
|
||||
const isImported = folders.some((folder) => folder.isImported);
|
||||
const allSecrets = folders.flatMap((folder) => folder.secrets || []);
|
||||
|
||||
const uniqueSecrets: { secretId: string; referencedSecretKey: string }[] = [];
|
||||
const secretIds = new Set<string>();
|
||||
|
||||
allSecrets
|
||||
.filter((secret) => secretsToDeleteKeys.includes(secret.referencedSecretKey))
|
||||
.forEach((secret) => {
|
||||
if (!secretIds.has(secret.secretId)) {
|
||||
secretIds.add(secret.secretId);
|
||||
uniqueSecrets.push(secret);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
name,
|
||||
isImported,
|
||||
...(uniqueSecrets.length > 0 ? { secrets: uniqueSecrets } : {})
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
environment,
|
||||
folders: mergedFolders.filter(
|
||||
(folder) => folder.isImported || (folder.secrets && folder.secrets.length > 0)
|
||||
)
|
||||
};
|
||||
});
|
||||
|
||||
return mergedImportedBy;
|
||||
};
|
||||
|
||||
const importedBy = useMemo(() => {
|
||||
if (selectedKeysCount === 0 || !importedByEnvs) return null;
|
||||
return filterAndMergeEnvironments(
|
||||
Object.values(selectedEntries.secret).flatMap((entries) => Object.keys(entries)),
|
||||
importedByEnvs
|
||||
);
|
||||
}, [importedByEnvs, selectedEntries, selectedKeysCount]);
|
||||
const usedBySecretSyncsFiltered = useMemo(() => {
|
||||
if (selectedKeysCount === 0 || usedBySecretSyncs.length === 0) return null;
|
||||
const envs = Object.values(selectedEntries.secret).flatMap((entries) => Object.keys(entries));
|
||||
return usedBySecretSyncs.filter((syncItem) => envs.includes(syncItem.environment));
|
||||
}, [selectedEntries, usedBySecretSyncs, selectedKeysCount]);
|
||||
|
||||
const getDeleteModalTitle = () => {
|
||||
if (selectedFolderCount > 0 && selectedKeysCount > 0) {
|
||||
@@ -326,11 +261,12 @@ export const SelectionPanel = ({
|
||||
onChange={(isOpen) => handlePopUpToggle("bulkDeleteEntries", isOpen)}
|
||||
onDeleteApproved={handleBulkDelete}
|
||||
formContent={
|
||||
importedBy &&
|
||||
importedBy.some((element) => element.folders.length > 0) && (
|
||||
((usedBySecretSyncsFiltered && usedBySecretSyncsFiltered.length > 0) ||
|
||||
(importedBy && importedBy.some((element) => element.folders.length > 0))) && (
|
||||
<CollapsibleSecretImports
|
||||
importedBy={importedBy}
|
||||
importedBy={importedBy || []}
|
||||
secretsToDelete={secretsToDeleteKeys}
|
||||
usedBySecretSyncs={usedBySecretSyncsFiltered}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -220,6 +220,7 @@ const Page = () => {
|
||||
totalSecretCount = 0,
|
||||
totalCount = 0,
|
||||
importedBy,
|
||||
usedBySecretSyncs,
|
||||
totalSecretRotationCount = 0
|
||||
} = data ?? {};
|
||||
|
||||
@@ -441,6 +442,7 @@ const Page = () => {
|
||||
onClickRollbackMode={() => handlePopUpToggle("snapshots", true)}
|
||||
protectedBranchPolicyName={boardPolicy?.name}
|
||||
importedBy={importedBy}
|
||||
usedBySecretSyncs={usedBySecretSyncs}
|
||||
/>
|
||||
<div className="thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md rounded-b-none bg-mineshaft-800 text-left text-sm text-bunker-300">
|
||||
<div className="flex flex-col" id="dashboard">
|
||||
@@ -530,6 +532,7 @@ const Page = () => {
|
||||
secretPath={secretPath}
|
||||
isProtectedBranch={isProtectedBranch}
|
||||
importedBy={importedBy}
|
||||
usedBySecretSyncs={usedBySecretSyncs}
|
||||
/>
|
||||
)}
|
||||
{noAccessSecretCount > 0 && <SecretNoAccessListView count={noAccessSecretCount} />}
|
||||
|
||||
+8
-4
@@ -69,6 +69,7 @@ import {
|
||||
dashboardKeys,
|
||||
fetchDashboardProjectSecretsByKeys
|
||||
} from "@app/hooks/api/dashboard/queries";
|
||||
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
|
||||
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
|
||||
import { fetchProjectSecrets, secretKeys } from "@app/hooks/api/secrets/queries";
|
||||
import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types";
|
||||
@@ -113,11 +114,12 @@ type Props = {
|
||||
onVisibilityToggle: () => void;
|
||||
onToggleRowType: (rowType: RowType) => void;
|
||||
onClickRollbackMode: () => void;
|
||||
usedBySecretSyncs?: UsedBySecretSyncs[];
|
||||
importedBy?: {
|
||||
environment: { name: string; slug: string };
|
||||
folders: {
|
||||
name: string;
|
||||
secrets?: { secretId: string; referencedSecretKey: string }[];
|
||||
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
|
||||
isImported: boolean;
|
||||
}[];
|
||||
}[];
|
||||
@@ -139,7 +141,8 @@ export const ActionBar = ({
|
||||
onClickRollbackMode,
|
||||
onToggleRowType,
|
||||
protectedBranchPolicyName,
|
||||
importedBy
|
||||
importedBy,
|
||||
usedBySecretSyncs
|
||||
}: Props) => {
|
||||
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
||||
"addFolder",
|
||||
@@ -1071,11 +1074,12 @@ export const ActionBar = ({
|
||||
onChange={(isOpen) => handlePopUpToggle("bulkDeleteSecrets", isOpen)}
|
||||
onDeleteApproved={handleSecretBulkDelete}
|
||||
formContent={
|
||||
importedBy &&
|
||||
importedBy.length > 0 && (
|
||||
((importedBy && importedBy.length > 0) ||
|
||||
(usedBySecretSyncs && usedBySecretSyncs?.length > 0)) && (
|
||||
<CollapsibleSecretImports
|
||||
importedBy={importedBy}
|
||||
secretsToDelete={Object.values(selectedSecrets).map((s) => s.key)}
|
||||
usedBySecretSyncs={usedBySecretSyncs}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
+90
-17
@@ -1,13 +1,16 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import React, { useMemo } from "react";
|
||||
import { faFileImport, faKey, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faFileImport, faKey, faSync, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Table, TBody, Td, Th, THead, Tr } from "@app/components/v2";
|
||||
import { Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
|
||||
|
||||
enum ItemType {
|
||||
Folder = "Folder",
|
||||
Secret = "Secret"
|
||||
Secret = "Secret",
|
||||
SecretSync = "SecretSync"
|
||||
}
|
||||
|
||||
interface FlatItem {
|
||||
@@ -17,6 +20,8 @@ interface FlatItem {
|
||||
reference: string;
|
||||
id: string;
|
||||
environment: { name: string; slug: string };
|
||||
tooltipText?: string;
|
||||
destination?: string;
|
||||
}
|
||||
|
||||
interface CollapsibleSecretImportsProps {
|
||||
@@ -24,16 +29,20 @@ interface CollapsibleSecretImportsProps {
|
||||
environment: { name: string; slug: string };
|
||||
folders: {
|
||||
name: string;
|
||||
secrets?: { secretId: string; referencedSecretKey: string }[];
|
||||
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
|
||||
isImported: boolean;
|
||||
}[];
|
||||
}[];
|
||||
usedBySecretSyncs?: UsedBySecretSyncs[] | null;
|
||||
secretsToDelete: string[];
|
||||
onlyReferences?: boolean;
|
||||
}
|
||||
|
||||
export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> = ({
|
||||
importedBy = [],
|
||||
secretsToDelete
|
||||
usedBySecretSyncs = [],
|
||||
secretsToDelete,
|
||||
onlyReferences
|
||||
}) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
@@ -51,6 +60,15 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
|
||||
};
|
||||
|
||||
const handlePathClick = (item: FlatItem) => {
|
||||
if (item.type === ItemType.SecretSync) {
|
||||
window.open(
|
||||
`/secret-manager/${currentWorkspace.id}/integrations/secret-syncs/${item.destination}/${item.id}`,
|
||||
"_blank",
|
||||
"noopener,noreferrer"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let pathToNavigate;
|
||||
if (item.type === ItemType.Folder) {
|
||||
pathToNavigate = item.path;
|
||||
@@ -70,7 +88,7 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
|
||||
|
||||
importedBy.forEach((env) => {
|
||||
env.folders.forEach((folder) => {
|
||||
if (folder.isImported) {
|
||||
if (folder.isImported && !onlyReferences) {
|
||||
items.push({
|
||||
type: ItemType.Folder,
|
||||
path: folder.name,
|
||||
@@ -103,7 +121,26 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
|
||||
});
|
||||
});
|
||||
|
||||
// Add secret sync items
|
||||
usedBySecretSyncs?.forEach((syncItem) => {
|
||||
items.push({
|
||||
type: ItemType.SecretSync,
|
||||
destination: syncItem.destination,
|
||||
path: syncItem.path,
|
||||
id: syncItem.id,
|
||||
reference: "Secret Sync",
|
||||
environment: { name: syncItem.environment, slug: "" },
|
||||
tooltipText: `Currently used by Secret Sync: ${syncItem.name}`
|
||||
});
|
||||
});
|
||||
|
||||
return items.sort((a, b) => {
|
||||
if (a.type === ItemType.SecretSync && b.type !== ItemType.SecretSync) return 1;
|
||||
if (a.type !== ItemType.SecretSync && b.type === ItemType.SecretSync) return -1;
|
||||
|
||||
if (a.type === ItemType.SecretSync && b.type === ItemType.SecretSync) {
|
||||
return a.path.localeCompare(b.path);
|
||||
}
|
||||
const envCompare = a.environment.name.localeCompare(b.environment.name);
|
||||
if (envCompare !== 0) return envCompare;
|
||||
|
||||
@@ -119,7 +156,7 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
|
||||
|
||||
return aPath.localeCompare(bPath);
|
||||
});
|
||||
}, [importedBy]);
|
||||
}, [importedBy, usedBySecretSyncs, secretsToDelete, onlyReferences]);
|
||||
|
||||
const hasImportedItems = importedBy.some((element) => {
|
||||
if (element.folders && element.folders.length > 0) {
|
||||
@@ -135,19 +172,33 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!hasImportedItems) {
|
||||
const hasSecretSyncItems = usedBySecretSyncs && usedBySecretSyncs.length > 0;
|
||||
|
||||
if (!hasImportedItems && !hasSecretSyncItems) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const alertColors = onlyReferences
|
||||
? {
|
||||
border: "border-yellow-700/30",
|
||||
bg: "bg-yellow-900/20",
|
||||
text: "text-yellow-500"
|
||||
}
|
||||
: {
|
||||
border: "border-red-700/30",
|
||||
bg: "bg-red-900/20",
|
||||
text: "text-red-500"
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4 w-full">
|
||||
<div className="mb-4 rounded-md border border-red-700/30 bg-red-900/20">
|
||||
<div className={`mb-4 rounded-md border ${alertColors.border} ${alertColors.bg}`}>
|
||||
<div className="flex items-start gap-3 p-4">
|
||||
<div className="mt-0.5 flex-shrink-0 text-red-500">
|
||||
<div className={`mt-0.5 flex-shrink-0 ${alertColors.text}`}>
|
||||
<FontAwesomeIcon icon={faWarning} className="h-5 w-5" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<p className="text-sm font-semibold text-red-500">
|
||||
<p className={`text-sm font-semibold ${alertColors.text}`}>
|
||||
The following resources will be affected by this change
|
||||
</p>
|
||||
</div>
|
||||
@@ -168,14 +219,36 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
|
||||
key={item.id}
|
||||
onClick={() => handlePathClick(item)}
|
||||
className="cursor-pointer hover:bg-mineshaft-700"
|
||||
title={`Navigate to ${item.path}`}
|
||||
title={
|
||||
item.type === ItemType.SecretSync
|
||||
? "Navigate to Secret Sync"
|
||||
: `Navigate to ${item.path}`
|
||||
}
|
||||
>
|
||||
<Td>
|
||||
<FontAwesomeIcon
|
||||
icon={item.type === ItemType.Secret ? faKey : faFileImport}
|
||||
className={`h-4 w-4 ${item.type === ItemType.Secret ? "text-gray-400" : "text-green-700"}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={item.tooltipText}
|
||||
isDisabled={!item.tooltipText}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
item.type === ItemType.Secret
|
||||
? faKey
|
||||
: item.type === ItemType.Folder
|
||||
? faFileImport
|
||||
: faSync
|
||||
}
|
||||
className={`h-4 w-4 ${
|
||||
item.type === ItemType.Secret
|
||||
? "text-gray-400"
|
||||
: item.type === ItemType.Folder
|
||||
? "text-green-700"
|
||||
: "text-mineshaft-300"
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
<Td className="px-4">{item.environment.name}</Td>
|
||||
<Td className="truncate px-4">{truncatePath(item.path)}</Td>
|
||||
|
||||
+83
-10
@@ -4,6 +4,7 @@ import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -31,7 +32,7 @@ import {
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||
import { WsTag } from "@app/hooks/api/types";
|
||||
import { subject } from "@casl/ability";
|
||||
@@ -46,16 +47,18 @@ import {
|
||||
} from "@app/components/secrets/SecretReferenceDetails";
|
||||
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { Blur } from "@app/components/v2/Blur";
|
||||
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faKey, faRotate } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faEyeSlash, faKey, faRotate } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
FontAwesomeSpriteName,
|
||||
formSchema,
|
||||
SecretActionType,
|
||||
TFormSchema
|
||||
} from "./SecretListView.utils";
|
||||
import { CollapsibleSecretImports } from "./CollapsibleSecretImports";
|
||||
|
||||
const hiddenValue = "******";
|
||||
|
||||
type Props = {
|
||||
secret: SecretV3RawSanitized;
|
||||
@@ -74,6 +77,14 @@ type Props = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
handleSecretShare: () => void;
|
||||
importedBy?: {
|
||||
environment: { name: string; slug: string };
|
||||
folders: {
|
||||
name: string;
|
||||
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
|
||||
isImported: boolean;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
|
||||
export const SecretItem = memo(
|
||||
@@ -89,12 +100,33 @@ export const SecretItem = memo(
|
||||
onToggleSecretSelect,
|
||||
environment,
|
||||
secretPath,
|
||||
handleSecretShare
|
||||
handleSecretShare,
|
||||
importedBy
|
||||
}: Props) => {
|
||||
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
||||
"editSecret"
|
||||
] as const);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { permission } = useProjectPermission();
|
||||
const { isRotatedSecret } = secret;
|
||||
|
||||
const canEditSecretValue = permission.can(
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: secret.key,
|
||||
secretTags: ["*"]
|
||||
})
|
||||
);
|
||||
|
||||
const getDefaultValue = () => {
|
||||
if (secret.secretValueHidden) {
|
||||
return canEditSecretValue ? hiddenValue : "";
|
||||
}
|
||||
return secret.valueOverride || secret.value || "";
|
||||
};
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
@@ -108,11 +140,11 @@ export const SecretItem = memo(
|
||||
} = useForm<TFormSchema>({
|
||||
defaultValues: {
|
||||
...secret,
|
||||
value: secret.secretValueHidden ? "" : secret.value
|
||||
value: getDefaultValue()
|
||||
},
|
||||
values: {
|
||||
...secret,
|
||||
value: secret.secretValueHidden ? "" : secret.value
|
||||
value: getDefaultValue()
|
||||
},
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
@@ -154,6 +186,7 @@ export const SecretItem = memo(
|
||||
secretTags: selectedTagSlugs
|
||||
})
|
||||
);
|
||||
|
||||
const { secretValueHidden } = secret;
|
||||
|
||||
const [isSecValueCopied, setIsSecValueCopied] = useToggle(false);
|
||||
@@ -194,9 +227,24 @@ export const SecretItem = memo(
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (data: TFormSchema) => {
|
||||
const hasDirectReferences = importedBy?.some(({ folders }) =>
|
||||
folders?.some(({ secrets }) =>
|
||||
secrets?.some(({ referencedSecretKey }) => referencedSecretKey === secret.key)
|
||||
)
|
||||
);
|
||||
|
||||
if (hasDirectReferences) {
|
||||
handlePopUpOpen("editSecret", data);
|
||||
return;
|
||||
}
|
||||
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
|
||||
};
|
||||
|
||||
const handleEditSecret = async (data: TFormSchema) => {
|
||||
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
|
||||
handlePopUpClose("editSecret");
|
||||
};
|
||||
|
||||
const handleTagSelect = (tag: WsTag) => {
|
||||
if (selectedTagsGroupById?.[tag.id]) {
|
||||
const tagPos = selectedTags.findIndex(({ id }) => id === tag.id);
|
||||
@@ -286,6 +334,13 @@ export const SecretItem = memo(
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
{secretValueHidden && !isOverriden && (
|
||||
<Tooltip
|
||||
content={`You do not have access to view the current value${canEditSecretValue && !isRotatedSecret ? ", but you can set a new one" : "."}`}
|
||||
>
|
||||
<FontAwesomeIcon className="pr-2" size="sm" icon={faEyeSlash} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{isOverriden ? (
|
||||
<Controller
|
||||
name="valueOverride"
|
||||
@@ -301,8 +356,6 @@ export const SecretItem = memo(
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : secretValueHidden ? (
|
||||
<Blur tooltipText="You do not have permission to read the value of this secret." />
|
||||
) : (
|
||||
<Controller
|
||||
name="value"
|
||||
@@ -312,11 +365,11 @@ export const SecretItem = memo(
|
||||
<InfisicalSecretInput
|
||||
isReadOnly={isReadOnly || isRotatedSecret}
|
||||
key="secret-value"
|
||||
isVisible={isVisible}
|
||||
isVisible={isVisible && !secretValueHidden}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
{...field}
|
||||
defaultValue={secretValueHidden ? "" : undefined}
|
||||
defaultValue={secretValueHidden ? hiddenValue : undefined}
|
||||
containerClassName="py-1.5 rounded-md transition-all"
|
||||
/>
|
||||
)}
|
||||
@@ -682,6 +735,26 @@ export const SecretItem = memo(
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.editSecret.isOpen}
|
||||
deleteKey="confirm"
|
||||
buttonColorSchema="secondary"
|
||||
buttonText="Save"
|
||||
subTitle=""
|
||||
title="Do you want to edit this secret?"
|
||||
onChange={(isOpen) => handlePopUpToggle("editSecret", isOpen)}
|
||||
onDeleteApproved={() => handleEditSecret(popUp?.editSecret?.data)}
|
||||
formContent={
|
||||
importedBy &&
|
||||
importedBy.length > 0 && (
|
||||
<CollapsibleSecretImports
|
||||
importedBy={importedBy}
|
||||
secretsToDelete={[secret.key]}
|
||||
onlyReferences
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
+8
-5
@@ -8,6 +8,7 @@ import { DeleteActionModal } from "@app/components/v2";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useCreateSecretV3, useDeleteSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
|
||||
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
|
||||
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
|
||||
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
|
||||
import { secretKeys } from "@app/hooks/api/secrets/queries";
|
||||
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||
@@ -29,11 +30,12 @@ type Props = {
|
||||
tags?: WsTag[];
|
||||
isVisible?: boolean;
|
||||
isProtectedBranch?: boolean;
|
||||
usedBySecretSyncs?: UsedBySecretSyncs[];
|
||||
importedBy?: {
|
||||
environment: { name: string; slug: string };
|
||||
folders: {
|
||||
name: string;
|
||||
secrets?: { secretId: string; referencedSecretKey: string }[];
|
||||
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
|
||||
isImported: boolean;
|
||||
}[];
|
||||
}[];
|
||||
@@ -47,10 +49,9 @@ export const SecretListView = ({
|
||||
tags: wsTags = [],
|
||||
isVisible,
|
||||
isProtectedBranch = false,
|
||||
usedBySecretSyncs,
|
||||
importedBy
|
||||
}: Props) => {
|
||||
console.log("secretssssss", secrets);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"deleteSecret",
|
||||
@@ -368,6 +369,7 @@ export const SecretListView = ({
|
||||
onSaveSecret={handleSaveSecret}
|
||||
onDeleteSecret={onDeleteSecret}
|
||||
onDetailViewSecret={onDetailViewSecret}
|
||||
importedBy={importedBy}
|
||||
onCreateTag={onCreateTag}
|
||||
handleSecretShare={() =>
|
||||
handlePopUpOpen("createSharedSecret", {
|
||||
@@ -384,10 +386,11 @@ export const SecretListView = ({
|
||||
onDeleteApproved={handleSecretDelete}
|
||||
buttonText="Delete Secret"
|
||||
formContent={
|
||||
importedBy &&
|
||||
importedBy.length > 0 && (
|
||||
((importedBy && importedBy.length > 0) ||
|
||||
(usedBySecretSyncs && usedBySecretSyncs?.length > 0)) && (
|
||||
<CollapsibleSecretImports
|
||||
importedBy={importedBy}
|
||||
usedBySecretSyncs={usedBySecretSyncs}
|
||||
secretsToDelete={[(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key || ""]}
|
||||
/>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user