mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-22 13:29:55 +00:00
Compare commits
24 Commits
daniel/k8s
...
add-missin
Author | SHA1 | Date | |
---|---|---|---|
fec55bc9f8 | |||
47bb3c10fa | |||
5d44d58ff4 | |||
ff294dab8d | |||
c99440ba81 | |||
6d5a6f42e0 | |||
0c027fdc43 | |||
727a6a7701 | |||
7f1f9e7fd0 | |||
98f742a807 | |||
03ad5c5db0 | |||
e6c4c27a87 | |||
d4ac4f8d8f | |||
f0229c5ecf | |||
8d711af23b | |||
7bd61d88fc | |||
c47d76a6c7 | |||
e959ed7fab | |||
4e4b1b689b | |||
024ed0c0d8 | |||
e99e360339 | |||
f345801bd6 | |||
4160009913 | |||
d5065af7e9 |
@ -0,0 +1,27 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const appCfg = getConfig();
|
||||
const tokenDuration = appCfg?.JWT_REFRESH_LIFETIME;
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.Organization, "userTokenExpiration"))) {
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
t.string("userTokenExpiration");
|
||||
});
|
||||
if (tokenDuration) {
|
||||
await knex(TableName.Organization).update({ userTokenExpiration: tokenDuration });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.Organization, "userTokenExpiration")) {
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
t.dropColumn("userTokenExpiration");
|
||||
});
|
||||
}
|
||||
}
|
@ -28,7 +28,8 @@ export const OrganizationsSchema = z.object({
|
||||
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
||||
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
||||
privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
|
||||
bypassOrgAuthEnabled: z.boolean().default(false)
|
||||
bypassOrgAuthEnabled: z.boolean().default(false),
|
||||
userTokenExpiration: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||
|
@ -6,4 +6,5 @@ export * from "./array";
|
||||
export * from "./dates";
|
||||
export * from "./object";
|
||||
export * from "./string";
|
||||
export * from "./time";
|
||||
export * from "./undefined";
|
||||
|
21
backend/src/lib/fn/time.ts
Normal file
21
backend/src/lib/fn/time.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import ms, { StringValue } from "ms";
|
||||
|
||||
const convertToMilliseconds = (exp: string | number): number => {
|
||||
if (typeof exp === "number") {
|
||||
return exp * 1000;
|
||||
}
|
||||
|
||||
const result = ms(exp as StringValue);
|
||||
if (typeof result !== "number") {
|
||||
throw new Error(`Invalid expiration format: ${exp}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getMinExpiresIn = (exp1: string | number, exp2: string | number): string | number => {
|
||||
const ms1 = convertToMilliseconds(exp1);
|
||||
const ms2 = convertToMilliseconds(exp2);
|
||||
|
||||
return ms1 <= ms2 ? exp1 : exp2;
|
||||
};
|
@ -47,21 +47,21 @@ export const buildFindFilter =
|
||||
if ($in) {
|
||||
Object.entries($in).forEach(([key, val]) => {
|
||||
if (val) {
|
||||
void bd.whereIn(`${tableName ? `${tableName}.` : ""}${key}`, val as never);
|
||||
void bd.whereIn([`${tableName ? `${tableName}.` : ""}${key}`] as never, val as never);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ($notNull?.length) {
|
||||
$notNull.forEach((key) => {
|
||||
void bd.whereNotNull(`${tableName ? `${tableName}.` : ""}${key as string}`);
|
||||
void bd.whereNotNull([`${tableName ? `${tableName}.` : ""}${key as string}`] as never);
|
||||
});
|
||||
}
|
||||
|
||||
if ($search) {
|
||||
Object.entries($search).forEach(([key, val]) => {
|
||||
if (val) {
|
||||
void bd.whereILike(`${tableName ? `${tableName}.` : ""}${key}`, val as never);
|
||||
void bd.whereILike([`${tableName ? `${tableName}.` : ""}${key}`] as never, val as never);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1541,6 +1541,7 @@ export const registerRoutes = async (
|
||||
|
||||
const secretSyncService = secretSyncServiceFactory({
|
||||
secretSyncDAL,
|
||||
secretImportDAL,
|
||||
permissionService,
|
||||
appConnectionService,
|
||||
folderDAL,
|
||||
|
@ -2,6 +2,7 @@ import jwt from "jsonwebtoken";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { getMinExpiresIn } from "@app/lib/fn";
|
||||
import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode, AuthTokenType } from "@app/services/auth/auth-type";
|
||||
@ -79,6 +80,18 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
||||
handler: async (req) => {
|
||||
const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
|
||||
const appCfg = getConfig();
|
||||
let expiresIn: string | number = appCfg.JWT_AUTH_LIFETIME;
|
||||
if (decodedToken.organizationId) {
|
||||
const org = await server.services.org.findOrganizationById(
|
||||
decodedToken.userId,
|
||||
decodedToken.organizationId,
|
||||
decodedToken.authMethod,
|
||||
decodedToken.organizationId
|
||||
);
|
||||
if (org && org.userTokenExpiration) {
|
||||
expiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
|
||||
}
|
||||
}
|
||||
|
||||
const token = jwt.sign(
|
||||
{
|
||||
@ -92,7 +105,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
||||
mfaMethod: decodedToken.mfaMethod
|
||||
},
|
||||
appCfg.AUTH_SECRET,
|
||||
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }
|
||||
{ expiresIn }
|
||||
);
|
||||
|
||||
return { token, organizationId: decodedToken.organizationId };
|
||||
|
@ -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) +
|
||||
|
@ -1,3 +1,4 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
@ -263,7 +264,18 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
enforceMfa: z.boolean().optional(),
|
||||
selectedMfaMethod: z.nativeEnum(MfaMethod).optional(),
|
||||
allowSecretSharingOutsideOrganization: z.boolean().optional(),
|
||||
bypassOrgAuthEnabled: z.boolean().optional()
|
||||
bypassOrgAuthEnabled: z.boolean().optional(),
|
||||
userTokenExpiration: z
|
||||
.string()
|
||||
.refine((val) => new RE2(/^\d+[mhdw]$/).test(val), "Must be a number followed by m, h, d, or w")
|
||||
.refine(
|
||||
(val) => {
|
||||
const numericPart = val.slice(0, -1);
|
||||
return parseInt(numericPart, 10) >= 1;
|
||||
},
|
||||
{ message: "Duration value must be at least 1" }
|
||||
)
|
||||
.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@ -69,6 +69,5 @@ export const listTeamCityProjects = async (appConnection: TTeamCityConnection) =
|
||||
}
|
||||
);
|
||||
|
||||
// Filter out the root project. Should not be seen by users.
|
||||
return resp.data.project.filter((proj) => proj.id !== "_Root");
|
||||
return resp.data.project;
|
||||
};
|
||||
|
@ -12,7 +12,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { getMinExpiresIn, removeTrailingSlash } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
@ -143,6 +143,17 @@ export const authLoginServiceFactory = ({
|
||||
);
|
||||
if (!tokenSession) throw new Error("Failed to create token");
|
||||
|
||||
let tokenSessionExpiresIn: string | number = cfg.JWT_AUTH_LIFETIME;
|
||||
let refreshTokenExpiresIn: string | number = cfg.JWT_REFRESH_LIFETIME;
|
||||
|
||||
if (organizationId) {
|
||||
const org = await orgDAL.findById(organizationId);
|
||||
if (org && org.userTokenExpiration) {
|
||||
tokenSessionExpiresIn = getMinExpiresIn(cfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
|
||||
refreshTokenExpiresIn = org.userTokenExpiration;
|
||||
}
|
||||
}
|
||||
|
||||
const accessToken = jwt.sign(
|
||||
{
|
||||
authMethod,
|
||||
@ -155,7 +166,7 @@ export const authLoginServiceFactory = ({
|
||||
mfaMethod
|
||||
},
|
||||
cfg.AUTH_SECRET,
|
||||
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
|
||||
{ expiresIn: tokenSessionExpiresIn }
|
||||
);
|
||||
|
||||
const refreshToken = jwt.sign(
|
||||
@ -170,7 +181,7 @@ export const authLoginServiceFactory = ({
|
||||
mfaMethod
|
||||
},
|
||||
cfg.AUTH_SECRET,
|
||||
{ expiresIn: cfg.JWT_REFRESH_LIFETIME }
|
||||
{ expiresIn: refreshTokenExpiresIn }
|
||||
);
|
||||
|
||||
return { access: accessToken, refresh: refreshToken };
|
||||
|
@ -10,6 +10,7 @@ import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { getMinExpiresIn } from "@app/lib/fn";
|
||||
import { isDisposableEmail } from "@app/lib/validator";
|
||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
@ -46,7 +47,7 @@ type TAuthSignupDep = {
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
||||
orgService: Pick<TOrgServiceFactory, "createOrganization" | "findOrganizationById">;
|
||||
orgDAL: TOrgDALFactory;
|
||||
tokenService: TAuthTokenServiceFactory;
|
||||
smtpService: TSmtpService;
|
||||
@ -320,6 +321,17 @@ export const authSignupServiceFactory = ({
|
||||
projectBotDAL
|
||||
});
|
||||
|
||||
let tokenSessionExpiresIn: string | number = appCfg.JWT_AUTH_LIFETIME;
|
||||
let refreshTokenExpiresIn: string | number = appCfg.JWT_REFRESH_LIFETIME;
|
||||
|
||||
if (organizationId) {
|
||||
const org = await orgService.findOrganizationById(user.id, organizationId, authMethod, organizationId);
|
||||
if (org && org.userTokenExpiration) {
|
||||
tokenSessionExpiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
|
||||
refreshTokenExpiresIn = org.userTokenExpiration;
|
||||
}
|
||||
}
|
||||
|
||||
const tokenSession = await tokenService.getUserTokenSession({
|
||||
userAgent,
|
||||
ip,
|
||||
@ -337,7 +349,7 @@ export const authSignupServiceFactory = ({
|
||||
organizationId
|
||||
},
|
||||
appCfg.AUTH_SECRET,
|
||||
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }
|
||||
{ expiresIn: tokenSessionExpiresIn }
|
||||
);
|
||||
|
||||
const refreshToken = jwt.sign(
|
||||
@ -350,7 +362,7 @@ export const authSignupServiceFactory = ({
|
||||
organizationId
|
||||
},
|
||||
appCfg.AUTH_SECRET,
|
||||
{ expiresIn: appCfg.JWT_REFRESH_LIFETIME }
|
||||
{ expiresIn: refreshTokenExpiresIn }
|
||||
);
|
||||
|
||||
return { user: updateduser.info, accessToken, refreshToken, organizationId };
|
||||
|
@ -17,5 +17,6 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
|
||||
shouldUseNewPrivilegeSystem: true,
|
||||
privilegeUpgradeInitiatedByUsername: true,
|
||||
privilegeUpgradeInitiatedAt: true,
|
||||
bypassOrgAuthEnabled: true
|
||||
bypassOrgAuthEnabled: true,
|
||||
userTokenExpiration: true
|
||||
});
|
||||
|
@ -170,8 +170,12 @@ export const orgServiceFactory = ({
|
||||
actorOrgId: string | undefined
|
||||
) => {
|
||||
await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
||||
const appCfg = getConfig();
|
||||
const org = await orgDAL.findOrgById(orgId);
|
||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||
if (!org.userTokenExpiration) {
|
||||
return { ...org, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME };
|
||||
}
|
||||
return org;
|
||||
};
|
||||
/*
|
||||
@ -350,7 +354,8 @@ export const orgServiceFactory = ({
|
||||
enforceMfa,
|
||||
selectedMfaMethod,
|
||||
allowSecretSharingOutsideOrganization,
|
||||
bypassOrgAuthEnabled
|
||||
bypassOrgAuthEnabled,
|
||||
userTokenExpiration
|
||||
}
|
||||
}: TUpdateOrgDTO) => {
|
||||
const appCfg = getConfig();
|
||||
@ -451,7 +456,8 @@ export const orgServiceFactory = ({
|
||||
enforceMfa,
|
||||
selectedMfaMethod,
|
||||
allowSecretSharingOutsideOrganization,
|
||||
bypassOrgAuthEnabled
|
||||
bypassOrgAuthEnabled,
|
||||
userTokenExpiration
|
||||
});
|
||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||
return org;
|
||||
|
@ -74,6 +74,7 @@ export type TUpdateOrgDTO = {
|
||||
selectedMfaMethod: MfaMethod;
|
||||
allowSecretSharingOutsideOrganization: boolean;
|
||||
bypassOrgAuthEnabled: boolean;
|
||||
userTokenExpiration: string;
|
||||
}>;
|
||||
} & TOrgPermission;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -1,4 +1,4 @@
|
||||
---
|
||||
title: "Find By Privilege Slug"
|
||||
title: "Find By Slug"
|
||||
openapi: "GET /api/v1/additional-privilege/identity/{privilegeSlug}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v2/identity-project-additional-privilege"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v2/identity-project-additional-privilege/{id}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Find By ID"
|
||||
openapi: "GET /api/v2/identity-project-additional-privilege/{id}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Find By Slug"
|
||||
openapi: "GET /api/v2/identity-project-additional-privilege/slug/{privilegeSlug}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v2/identity-project-additional-privilege"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v2/identity-project-additional-privilege/{id}"
|
||||
---
|
@ -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>
|
||||
|
@ -27,6 +27,10 @@ The **Settings** page lets you manage information about your organization includ
|
||||
|
||||

|
||||
|
||||
<Tip>
|
||||
You can adjust the maximum time a user token will remain valid for your organization. After this period, users will be required to re-authenticate. This helps improve security by enforcing regular sign-ins.
|
||||
</Tip>
|
||||
|
||||
## Access Control
|
||||
|
||||
The **Access Control** page is where you can manage identities (both people and machines) that are part of your organization.
|
||||
|
BIN
docs/images/platform/kms/aws/aws-kms-key-create.png
Normal file
BIN
docs/images/platform/kms/aws/aws-kms-key-create.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 170 KiB |
Binary file not shown.
Before Width: | Height: | Size: 519 KiB After Width: | Height: | Size: 352 KiB |
@ -931,12 +931,28 @@
|
||||
{
|
||||
"group": "Identity Specific Privilege",
|
||||
"pages": [
|
||||
"api-reference/endpoints/identity-specific-privilege/create-permanent",
|
||||
"api-reference/endpoints/identity-specific-privilege/create-temporary",
|
||||
"api-reference/endpoints/identity-specific-privilege/update",
|
||||
"api-reference/endpoints/identity-specific-privilege/delete",
|
||||
"api-reference/endpoints/identity-specific-privilege/find-by-slug",
|
||||
"api-reference/endpoints/identity-specific-privilege/list"
|
||||
{
|
||||
"group": "V1 (Legacy)",
|
||||
"pages": [
|
||||
"api-reference/endpoints/identity-specific-privilege/v1/create-permanent",
|
||||
"api-reference/endpoints/identity-specific-privilege/v1/create-temporary",
|
||||
"api-reference/endpoints/identity-specific-privilege/v1/update",
|
||||
"api-reference/endpoints/identity-specific-privilege/v1/delete",
|
||||
"api-reference/endpoints/identity-specific-privilege/v1/find-by-slug",
|
||||
"api-reference/endpoints/identity-specific-privilege/v1/list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "V2",
|
||||
"pages": [
|
||||
"api-reference/endpoints/identity-specific-privilege/v2/create",
|
||||
"api-reference/endpoints/identity-specific-privilege/v2/update",
|
||||
"api-reference/endpoints/identity-specific-privilege/v2/delete",
|
||||
"api-reference/endpoints/identity-specific-privilege/v2/list",
|
||||
"api-reference/endpoints/identity-specific-privilege/v2/find-by-id",
|
||||
"api-reference/endpoints/identity-specific-privilege/v2/find-by-slug"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -73,7 +73,8 @@ The platform utilizes Postgres to persist all of its data and Redis for caching
|
||||
### PostgreSQL
|
||||
|
||||
<Info>
|
||||
Please note that the database user must have **CREATE** privileges along with ability to create and modify tables. This is needed for Infisical to run schema migrations.
|
||||
Please note that the database user you create must be granted all privileges on the Infisical database.
|
||||
This includes the ability to create new schemas, create, update, delete, modify tables and indexes, etc.
|
||||
</Info>
|
||||
|
||||
<ParamField query="DB_CONNECTION_URI" type="string" default="" required>
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
}[];
|
||||
};
|
||||
|
@ -111,7 +111,8 @@ export const useUpdateOrg = () => {
|
||||
enforceMfa,
|
||||
selectedMfaMethod,
|
||||
allowSecretSharingOutsideOrganization,
|
||||
bypassOrgAuthEnabled
|
||||
bypassOrgAuthEnabled,
|
||||
userTokenExpiration
|
||||
}) => {
|
||||
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
|
||||
name,
|
||||
@ -122,7 +123,8 @@ export const useUpdateOrg = () => {
|
||||
enforceMfa,
|
||||
selectedMfaMethod,
|
||||
allowSecretSharingOutsideOrganization,
|
||||
bypassOrgAuthEnabled
|
||||
bypassOrgAuthEnabled,
|
||||
userTokenExpiration
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
@ -18,6 +18,7 @@ export type Organization = {
|
||||
selectedMfaMethod?: MfaMethod;
|
||||
shouldUseNewPrivilegeSystem: boolean;
|
||||
allowSecretSharingOutsideOrganization?: boolean;
|
||||
userTokenExpiration?: string;
|
||||
userRole: string;
|
||||
};
|
||||
|
||||
@ -32,6 +33,7 @@ export type UpdateOrgDTO = {
|
||||
selectedMfaMethod?: MfaMethod;
|
||||
allowSecretSharingOutsideOrganization?: boolean;
|
||||
bypassOrgAuthEnabled?: boolean;
|
||||
userTokenExpiration?: string;
|
||||
};
|
||||
|
||||
export type BillingDetails = {
|
||||
|
@ -23,6 +23,7 @@ import { OrgLDAPSection } from "./OrgLDAPSection";
|
||||
import { OrgOIDCSection } from "./OrgOIDCSection";
|
||||
import { OrgScimSection } from "./OrgSCIMSection";
|
||||
import { OrgSSOSection } from "./OrgSSOSection";
|
||||
import { OrgUserAccessTokenLimitSection } from "./OrgUserAccessTokenLimitSection";
|
||||
import { SSOModal } from "./SSOModal";
|
||||
|
||||
export const OrgAuthTab = withPermission(
|
||||
@ -167,6 +168,7 @@ export const OrgAuthTab = withPermission(
|
||||
return (
|
||||
<>
|
||||
<OrgGenericAuthSection />
|
||||
<OrgUserAccessTokenLimitSection />
|
||||
{shouldShowCreateIdentityProviderView ? (
|
||||
createIdentityProviderView
|
||||
) : (
|
||||
|
@ -0,0 +1,171 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useUpdateOrg } from "@app/hooks/api";
|
||||
|
||||
const formSchema = z.object({
|
||||
expirationValue: z.number().min(1, "Value must be at least 1"),
|
||||
expirationUnit: z.enum(["m", "h", "d", "w"], {
|
||||
invalid_type_error: "Please select a valid time unit"
|
||||
})
|
||||
});
|
||||
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
// Function to parse duration string like "30d" into value and unit
|
||||
const parseDuration = (duration: string): { value: number; unit: string } => {
|
||||
const match = duration.match(/^(\d+)([mhdw])$/);
|
||||
if (match) {
|
||||
return {
|
||||
value: parseInt(match[1], 10),
|
||||
unit: match[2]
|
||||
};
|
||||
}
|
||||
// Default to 30 days if invalid format
|
||||
return { value: 30, unit: "d" };
|
||||
};
|
||||
|
||||
// Function to format value and unit back to duration string
|
||||
const formatDuration = (value: number, unit: string): string => {
|
||||
return `${value}${unit}`;
|
||||
};
|
||||
|
||||
export const OrgUserAccessTokenLimitSection = () => {
|
||||
const { mutateAsync: updateUserTokenExpiration } = useUpdateOrg();
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
// Parse the current duration or use default
|
||||
const currentDuration = parseDuration(currentOrg?.userTokenExpiration || "30d");
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting, isDirty },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
expirationValue: currentDuration.value,
|
||||
expirationUnit: currentDuration.unit as "m" | "h" | "d" | "w"
|
||||
}
|
||||
});
|
||||
|
||||
if (!currentOrg) return null;
|
||||
|
||||
const handleUserTokenExpirationSubmit = async (formData: TForm) => {
|
||||
try {
|
||||
const userTokenExpiration = formatDuration(formData.expirationValue, formData.expirationUnit);
|
||||
|
||||
await updateUserTokenExpiration({
|
||||
userTokenExpiration,
|
||||
orgId: currentOrg.id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated user token expiration",
|
||||
type: "success"
|
||||
});
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "Failed updating user token expiration",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Units for the dropdown with readable labels
|
||||
const timeUnits = [
|
||||
{ value: "m", label: "Minutes" },
|
||||
{ value: "h", label: "Hours" },
|
||||
{ value: "d", label: "Days" },
|
||||
{ value: "w", label: "Weeks" }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<p className="text-xl font-semibold">User Token Expiration</p>
|
||||
</div>
|
||||
<p className="mb-4 mt-2 text-sm text-gray-400">
|
||||
This defines the maximum time a user token will be valid. After this time, the user will
|
||||
need to re-authenticate.
|
||||
</p>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
|
||||
{(isAllowed) => (
|
||||
<form onSubmit={handleSubmit(handleUserTokenExpirationSubmit)} autoComplete="off">
|
||||
<div className="flex max-w-md gap-4">
|
||||
<div className="flex-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expirationValue"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Expiration value"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={1}
|
||||
step={1}
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(parseInt(e.target.value, 10))}
|
||||
disabled={!isAllowed}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expirationUnit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Time unit"
|
||||
>
|
||||
<Select
|
||||
value={field.value}
|
||||
className="pr-2"
|
||||
onValueChange={field.onChange}
|
||||
placeholder="Select time unit"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
{timeUnits.map(({ value, label }) => (
|
||||
<SelectItem
|
||||
key={value}
|
||||
value={value}
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">{label}</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
disabled={!isDirty}
|
||||
className="mt-4"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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
|
||||
|
@ -29,9 +29,10 @@ import {
|
||||
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,
|
||||
@ -133,6 +161,18 @@ export const SecretEditRow = ({
|
||||
}
|
||||
};
|
||||
|
||||
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(
|
||||
permission,
|
||||
ProjectPermissionSecretActions.ReadValue
|
||||
@ -325,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>
|
||||
);
|
||||
};
|
||||
|
@ -50,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 = ({
|
||||
@ -64,7 +72,8 @@ export const SecretOverviewTableRow = ({
|
||||
getImportedSecretByKey,
|
||||
scrollOffset,
|
||||
onToggleSecretSelect,
|
||||
isSelected
|
||||
isSelected,
|
||||
importedBy
|
||||
}: Props) => {
|
||||
const [isFormExpanded, setIsFormExpanded] = useToggle();
|
||||
const totalCols = environments.length + 1; // secret key row
|
||||
@ -266,6 +275,7 @@ export const SecretOverviewTableRow = ({
|
||||
onSecretUpdate={onSecretUpdate}
|
||||
environment={slug}
|
||||
isRotatedSecret={secret?.isRotatedSecret}
|
||||
importedBy={importedBy}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -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} />}
|
||||
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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";
|
||||
@ -55,6 +56,7 @@ import {
|
||||
SecretActionType,
|
||||
TFormSchema
|
||||
} from "./SecretListView.utils";
|
||||
import { CollapsibleSecretImports } from "./CollapsibleSecretImports";
|
||||
|
||||
const hiddenValue = "******";
|
||||
|
||||
@ -75,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(
|
||||
@ -90,8 +100,12 @@ 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;
|
||||
@ -213,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);
|
||||
@ -706,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,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,6 +49,7 @@ export const SecretListView = ({
|
||||
tags: wsTags = [],
|
||||
isVisible,
|
||||
isProtectedBranch = false,
|
||||
usedBySecretSyncs,
|
||||
importedBy
|
||||
}: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
@ -366,6 +369,7 @@ export const SecretListView = ({
|
||||
onSaveSecret={handleSaveSecret}
|
||||
onDeleteSecret={onDeleteSecret}
|
||||
onDetailViewSecret={onDetailViewSecret}
|
||||
importedBy={importedBy}
|
||||
onCreateTag={onCreateTag}
|
||||
handleSecretShare={() =>
|
||||
handlePopUpOpen("createSharedSecret", {
|
||||
@ -382,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