Merge origin/main to feat/pki-sync-v2

This commit is contained in:
Carlos Monastyrski
2025-10-29 13:24:37 -03:00
276 changed files with 2996 additions and 2313 deletions
@@ -2,7 +2,7 @@ import { Knex } from "knex";
import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists";
import { AccessScope, TableName } from "../schemas";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasParentOrgId = await knex.schema.hasColumn(TableName.Organization, "parentOrgId");
@@ -18,8 +18,6 @@ export async function up(knex: Knex): Promise<void> {
await dropConstraintIfExists(TableName.Organization, "organizations_slug_unique", knex);
t.unique(["rootOrgId", "parentOrgId", "slug"]);
});
// had to switch to raw for null not distinct
}
const hasIdentityOrgCol = await knex.schema.hasColumn(TableName.Identity, "orgId");
@@ -28,24 +26,6 @@ export async function up(knex: Knex): Promise<void> {
t.uuid("orgId");
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
});
await knex.raw(
`
UPDATE ?? AS identity
SET "orgId" = membership."scopeOrgId"
FROM ?? AS membership
WHERE
membership."actorIdentityId" = identity."id"
AND membership."scope" = ?
`,
[TableName.Identity, TableName.Membership, AccessScope.Organization]
);
await knex.raw(`DELETE FROM ?? WHERE "orgId" IS NULL`, [TableName.Identity]);
await knex.schema.alterTable(TableName.Identity, (t) => {
t.uuid("orgId").notNullable().alter();
});
}
}
@@ -0,0 +1,48 @@
import { Knex } from "knex";
import { chunkArray } from "@app/lib/fn";
import { AccessScope, TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
await knex.transaction(async (tx) => {
const hasIdentityOrgCol = await tx.schema.hasColumn(TableName.Identity, "orgId");
if (hasIdentityOrgCol) {
const identityMemberships = await tx(TableName.Membership)
.where({
scope: AccessScope.Organization
})
.whereNotNull("actorIdentityId")
.select("actorIdentityId", "scopeOrgId");
const identityToOrgMapping: Record<string, string> = {};
identityMemberships.forEach((el) => {
if (el.actorIdentityId) {
identityToOrgMapping[el.actorIdentityId] = el.scopeOrgId;
}
});
const batchMemberships = chunkArray(identityMemberships, 500);
for await (const membership of batchMemberships) {
const identityIds = membership.map((el) => el.actorIdentityId).filter(Boolean) as string[];
if (identityIds.length) {
const identities = await tx(TableName.Identity).whereIn("id", identityIds).select("*");
await tx(TableName.Identity)
.insert(
identities.map((el) => ({
...el,
orgId: identityToOrgMapping[el.id]
}))
)
.onConflict("id")
.merge();
}
}
}
});
}
export async function down(): Promise<void> {}
const config = { transaction: false };
export { config };
@@ -0,0 +1,27 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasOrgBlockDuplicateColumn = await knex.schema.hasColumn(
TableName.Organization,
"blockDuplicateSecretSyncDestinations"
);
if (!hasOrgBlockDuplicateColumn) {
await knex.schema.table(TableName.Organization, (table) => {
table.boolean("blockDuplicateSecretSyncDestinations").notNullable().defaultTo(false);
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasOrgBlockDuplicateColumn = await knex.schema.hasColumn(
TableName.Organization,
"blockDuplicateSecretSyncDestinations"
);
if (hasOrgBlockDuplicateColumn) {
await knex.schema.table(TableName.Organization, (table) => {
table.dropColumn("blockDuplicateSecretSyncDestinations");
});
}
}
@@ -0,0 +1,22 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
await knex.transaction(async (tx) => {
await tx.schema.alterTable(TableName.IdentityAccessToken, (table) => {
table.dropForeign("identityId");
});
});
}
export async function down(knex: Knex): Promise<void> {
await knex.transaction(async (tx) => {
await tx.schema.alterTable(TableName.IdentityAccessToken, (table) => {
table.foreign("identityId").references("id").inTable(TableName.Identity);
});
});
}
const config = { transaction: false };
export { config };
@@ -0,0 +1,30 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
const MIGRATION_TIMEOUT = 30 * 60 * 1000; // 30 minutes
export async function up(knex: Knex): Promise<void> {
const result = await knex.raw("SHOW statement_timeout");
const originalTimeout = result.rows[0].statement_timeout;
await knex.transaction(async (tx) => {
try {
await tx.raw(`SET statement_timeout = ${MIGRATION_TIMEOUT}`);
const hasIdentityOrgCol = await tx.schema.hasColumn(TableName.Identity, "orgId");
if (hasIdentityOrgCol) {
await tx(TableName.Identity).whereNull("orgId").delete();
await tx.schema.alterTable(TableName.Identity, (t) => {
t.uuid("orgId").notNullable().alter();
});
}
} finally {
await tx.raw(`SET statement_timeout = '${originalTimeout}'`);
}
});
}
export async function down(): Promise<void> {}
const config = { transaction: false };
export { config };
+2 -1
View File
@@ -40,7 +40,8 @@ export const OrganizationsSchema = z.object({
googleSsoAuthEnforced: z.boolean().default(false),
googleSsoAuthLastUsed: z.date().nullable().optional(),
parentOrgId: z.string().uuid().nullable().optional(),
rootOrgId: z.string().uuid().nullable().optional()
rootOrgId: z.string().uuid().nullable().optional(),
blockDuplicateSecretSyncDestinations: z.boolean().default(false)
});
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
@@ -2,7 +2,6 @@ import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -32,8 +31,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
}),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.environmentSlug),
@@ -127,8 +126,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
}),
projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.projectSlug),
path: z
@@ -2,7 +2,6 @@ import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { writeLimit } from "@app/server/config/rateLimiter";
@@ -32,8 +31,8 @@ export const registerKubernetesDynamicSecretLeaseRouter = async (server: Fastify
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
}),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.environmentSlug),
@@ -3,7 +3,6 @@ import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/providers/models";
import { ApiDocsTags, DYNAMIC_SECRETS } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { isValidHandleBarTemplate } from "@app/lib/template/validate-handlebars";
@@ -60,8 +59,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
}),
maxTTL: z
.string()
@@ -72,8 +71,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
})
.nullable(),
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
@@ -130,8 +129,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
}),
maxTTL: z
.string()
@@ -142,8 +141,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
})
.nullable(),
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional(),
+4 -2
View File
@@ -182,7 +182,8 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
algorithm: z.string(),
isActive: z.boolean(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
kmipMetadata: z.record(z.any()).nullish()
})
}
},
@@ -384,7 +385,8 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
isActive: z.boolean(),
algorithm: z.string(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
kmipMetadata: z.record(z.any()).nullish()
})
.array()
})
@@ -112,7 +112,7 @@ export const dynamicSecretServiceFactory = ({
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
if (existingDynamicSecret)
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
throw new BadRequestError({ message: "Provided dynamic secret already exists under the folder" });
const selectedProvider = dynamicSecretProviders[provider.type];
const inputs = await selectedProvider.validateProviderInputs(provider.inputs, { projectId });
@@ -265,7 +265,7 @@ export const dynamicSecretServiceFactory = ({
if (newName) {
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
if (existingDynamicSecret)
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
throw new BadRequestError({ message: "Provided dynamic secret already exists under the folder" });
}
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
await kmsService.createCipherPairWithDataKey({
@@ -341,7 +341,8 @@ export const kmipOperationServiceFactory = ({
algorithm: completeKeyDetails.internalKms.encryptionAlgorithm,
isActive: !key.isDisabled,
createdAt: key.createdAt,
updatedAt: key.updatedAt
updatedAt: key.updatedAt,
kmipMetadata: key.kmipMetadata as Record<string, unknown>
};
};
@@ -1517,7 +1517,7 @@ export const secretApprovalRequestServiceFactory = ({
}))
);
if (secrets.length)
throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` });
throw new BadRequestError({ message: `Secret already exists: ${secrets.map((el) => el.key).join(",")}` });
commits.push(
...createdSecrets.map((createdSecret) => ({
+2
View File
@@ -1960,6 +1960,8 @@ export const registerRoutes = async (
secretImportDAL,
permissionService,
appConnectionService,
projectDAL,
orgDAL,
folderDAL,
secretSyncQueue,
projectBotService,
@@ -323,7 +323,11 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
.min(1, "Max Shared Secret view count cannot be lower than 1")
.max(1000, "Max Shared Secret view count cannot exceed 1000")
.nullable()
.optional(),
blockDuplicateSecretSyncDestinations: z
.boolean()
.optional()
.describe("Block duplicate secret sync destinations across the organization")
}),
response: {
200: z.object({
@@ -393,6 +393,56 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
}
});
server.route({
method: "GET",
url: "/vault/kubernetes-roles",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
namespace: z.string(),
mountPath: z.string()
}),
response: {
200: z.object({
roles: z.array(
z.object({
name: z.string(),
mountPath: z.string(),
allowed_kubernetes_namespaces: z.array(z.string()).nullish(),
allowed_kubernetes_namespace_selector: z.string().nullish(),
token_max_ttl: z.number().nullish(),
token_default_ttl: z.number().nullish(),
token_default_audiences: z.array(z.string()).nullish(),
service_account_name: z.string().nullish(),
kubernetes_role_name: z.string().nullish(),
kubernetes_role_type: z.string().nullish(),
generated_role_rules: z.string().nullish(),
name_template: z.string().nullish(),
extra_annotations: z.record(z.string()).nullish(),
extra_labels: z.record(z.string()).nullish(),
config: z.object({
kubernetes_host: z.string(),
kubernetes_ca_cert: z.string().nullish()
})
})
)
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const roles = await server.services.migration.getVaultKubernetesRoles({
actor: req.permission,
namespace: req.query.namespace,
mountPath: req.query.mountPath
});
return { roles };
}
});
server.route({
method: "GET",
url: "/vault/secret-paths",
@@ -5,5 +5,6 @@ export enum GitLabConnectionMethod {
export enum GitLabAccessTokenType {
Project = "project",
Personal = "personal"
Personal = "personal",
Group = "group"
}
@@ -21,6 +21,8 @@ import {
THCVaultKubernetesAuthConfig,
THCVaultKubernetesAuthRole,
THCVaultKubernetesAuthRoleWithConfig,
THCVaultKubernetesRole,
THCVaultKubernetesSecretsConfig,
THCVaultMount,
THCVaultMountResponse
} from "./hc-vault-connection-types";
@@ -816,3 +818,122 @@ export const getHCVaultKubernetesAuthRoles = async (
});
}
};
export const getHCVaultKubernetesRoles = async (
namespace: string,
mountPath: string,
connection: THCVaultConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
): Promise<THCVaultKubernetesRole[]> => {
// Remove trailing slash from mount path
const cleanMountPath = mountPath.endsWith("/") ? mountPath.slice(0, -1) : mountPath;
try {
const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
// 1. Get the Kubernetes secrets engine configuration for this mount
const { data: configResponse } = await requestWithHCVaultGateway<{ data: THCVaultKubernetesSecretsConfig }>(
connection,
gatewayService,
{
url: `${instanceUrl}/v1/${cleanMountPath}/config`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
}
);
const kubernetesConfig = configResponse.data;
// 2. List all roles in this mount
let roleNames: string[] = [];
try {
const { data: roleListResponse } = await requestWithHCVaultGateway<{ data: { keys: string[] } }>(
connection,
gatewayService,
{
url: `${instanceUrl}/v1/${cleanMountPath}/roles?list=true`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
}
);
roleNames = roleListResponse.data.keys || [];
} catch (error) {
// Vault returns 404 when no roles are configured yet
if (error && typeof error === "object" && "response" in error) {
const axiosError = error as { response?: { status?: number } };
if (axiosError.response?.status === 404) {
return [];
}
}
throw error;
}
if (!roleNames || roleNames.length === 0) {
return [];
}
// 3. Fetch details for each role with concurrency control
const limiter = createConcurrencyLimiter(HC_VAULT_CONCURRENCY_LIMIT);
const roleDetailsPromises = roleNames.map((roleName) =>
limiter(async () => {
const { data: roleResponse } = await requestWithHCVaultGateway<{
data: {
allowed_kubernetes_namespaces?: string[];
allowed_kubernetes_namespace_selector?: string;
token_max_ttl?: number;
token_default_ttl?: number;
token_default_audiences?: string[];
service_account_name?: string;
kubernetes_role_name?: string;
kubernetes_role_type?: string;
generated_role_rules?: string;
name_template?: string;
extra_annotations?: Record<string, string>;
extra_labels?: Record<string, string>;
};
}>(connection, gatewayService, {
url: `${instanceUrl}/v1/${cleanMountPath}/roles/${roleName}`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
});
// 4. Merge the role with the config
return {
...roleResponse.data,
name: roleName,
config: kubernetesConfig,
mountPath: cleanMountPath
} as THCVaultKubernetesRole;
})
);
const roles = await Promise.all(roleDetailsPromises);
return roles;
} catch (error: unknown) {
logger.error(error, "Unable to list HC Vault Kubernetes secrets engine roles");
if (error instanceof AxiosError) {
const errorMessage =
(error.response?.data as { errors?: string[] })?.errors?.[0] || error.message || "Unknown error";
throw new BadRequestError({
message: `Failed to list Kubernetes secrets engine roles: ${errorMessage}`
});
}
throw new BadRequestError({
message: "Unable to list Kubernetes secrets engine roles from HashiCorp Vault"
});
}
};
@@ -95,3 +95,26 @@ export type THCVaultKubernetesAuthRoleWithConfig = THCVaultKubernetesAuthRole &
config: THCVaultKubernetesAuthConfig;
mountPath: string;
};
export type THCVaultKubernetesSecretsConfig = {
kubernetes_host: string;
kubernetes_ca_cert?: string;
};
export type THCVaultKubernetesRole = {
name: string;
allowed_kubernetes_namespaces?: string[];
allowed_kubernetes_namespace_selector?: string;
token_max_ttl?: number;
token_default_ttl?: number;
token_default_audiences?: string[];
service_account_name?: string;
kubernetes_role_name?: string;
kubernetes_role_type?: string;
generated_role_rules?: string;
name_template?: string;
extra_annotations?: Record<string, string>;
extra_labels?: Record<string, string>;
config: THCVaultKubernetesSecretsConfig;
mountPath: string;
};
@@ -82,7 +82,7 @@ export const importDataIntoInfisicalFn = async ({
if (existingEnv) {
throw new BadRequestError({
message: `Environment with slug '${slug}' already exist`,
message: `Environment with slug '${slug}' already exists`,
name: "CreateEnvironment"
});
}
@@ -312,7 +312,7 @@ export const importDataIntoInfisicalFn = async ({
);
if (secretsByKeys.length) {
throw new BadRequestError({
message: `Secret already exist: ${secretsByKeys.map((el) => el.key).join(",")}`
message: `Secret already exists: ${secretsByKeys.map((el) => el.key).join(",")}`
});
}
await fnSecretBulkInsert({
@@ -19,6 +19,7 @@ import {
convertVaultValueToString,
getHCVaultAuthMounts,
getHCVaultKubernetesAuthRoles,
getHCVaultKubernetesRoles,
getHCVaultSecretsForPath,
HCVaultAuthType,
listHCVaultMounts,
@@ -762,6 +763,56 @@ export const externalMigrationServiceFactory = ({
return roles;
};
const getVaultKubernetesRoles = async ({
actor,
namespace,
mountPath
}: {
actor: OrgServiceActor;
namespace: string;
mountPath: string;
}) => {
const { hasRole } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor: actor.type,
actorId: actor.id,
orgId: actor.orgId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId
});
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can get Kubernetes roles" });
}
const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({
orgId: actor.orgId,
namespace
});
if (!vaultConfig) {
throw new NotFoundError({ message: "Vault migration config not found for this namespace" });
}
if (!vaultConfig.connection) {
throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" });
}
const credentials = await decryptAppConnectionCredentials({
orgId: vaultConfig.orgId,
encryptedCredentials: vaultConfig.connection.encryptedCredentials,
kmsService,
projectId: null
});
const connection = {
...vaultConfig.connection,
credentials
} as THCVaultConnection;
return getHCVaultKubernetesRoles(namespace, mountPath, connection, gatewayService);
};
return {
importEnvKeyData,
importVaultData,
@@ -776,6 +827,7 @@ export const externalMigrationServiceFactory = ({
getVaultAuthMounts,
getVaultSecretPaths,
importVaultSecrets,
getVaultKubernetesAuthRoles
getVaultKubernetesAuthRoles,
getVaultKubernetesRoles
};
};
+2 -1
View File
@@ -112,7 +112,8 @@ export const kmskeyDALFactory = (db: TDbClient) => {
...KmsKeysSchema.parse(entry),
isActive: !entry.isDisabled,
algorithm: entry.internalKmsEncryptionAlgorithm,
version: entry.internalKmsVersion
version: entry.internalKmsVersion,
kmipMetadata: entry.kmipMetadata as Record<string, unknown>
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find project cmeks" });
+2 -1
View File
@@ -27,5 +27,6 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
scannerProductEnabled: true,
shareSecretsProductEnabled: true,
maxSharedSecretLifetime: true,
maxSharedSecretViewLimit: true
maxSharedSecretViewLimit: true,
blockDuplicateSecretSyncDestinations: true
});
+6 -4
View File
@@ -405,7 +405,8 @@ export const orgServiceFactory = ({
scannerProductEnabled,
shareSecretsProductEnabled,
maxSharedSecretLifetime,
maxSharedSecretViewLimit
maxSharedSecretViewLimit,
blockDuplicateSecretSyncDestinations
}
}: TUpdateOrgDTO) => {
const appCfg = getConfig();
@@ -516,7 +517,7 @@ export const orgServiceFactory = ({
if (slug) {
const existingOrg = await orgDAL.findOne({ slug, rootOrgId: null });
if (existingOrg && existingOrg?.id !== orgId)
throw new BadRequestError({ message: `Organization with slug ${slug} already exist` });
throw new BadRequestError({ message: `Organization with slug ${slug} already exists` });
}
if (googleSsoAuthEnforced) {
@@ -589,7 +590,8 @@ export const orgServiceFactory = ({
scannerProductEnabled,
shareSecretsProductEnabled,
maxSharedSecretLifetime,
maxSharedSecretViewLimit
maxSharedSecretViewLimit,
blockDuplicateSecretSyncDestinations
});
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
return org;
@@ -1147,7 +1149,7 @@ export const orgServiceFactory = ({
const doesIncidentContactExist = await incidentContactDAL.findOne(orgId, { email });
if (doesIncidentContactExist) {
throw new BadRequestError({
message: "Incident contact already exist",
message: "Incident contact already exists",
name: "Incident contact exist"
});
}
+1
View File
@@ -90,6 +90,7 @@ export type TUpdateOrgDTO = {
shareSecretsProductEnabled: boolean;
maxSharedSecretLifetime: number;
maxSharedSecretViewLimit: number | null;
blockDuplicateSecretSyncDestinations: boolean;
}>;
} & TOrgPermission;
@@ -76,7 +76,7 @@ export const projectEnvServiceFactory = ({
const existingEnv = envs.find(({ slug: envSlug }) => envSlug === slug);
if (existingEnv)
throw new BadRequestError({
message: "Environment with slug already exist",
message: "Environment with slug already exists",
name: "CreateEnvironment"
});
@@ -171,7 +171,7 @@ export const projectEnvServiceFactory = ({
const existingEnv = await projectEnvDAL.findOne({ slug, projectId });
if (existingEnv && existingEnv.id !== id) {
throw new BadRequestError({
message: "Environment with slug already exist",
message: "Environment with slug already exists",
name: "UpdateEnvironment"
});
}
+55 -41
View File
@@ -10,7 +10,8 @@ import {
ProjectType,
ProjectVersion,
TableName,
TProjectEnvironments
TProjectEnvironments,
TProjects
} from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
@@ -40,7 +41,8 @@ import { TSshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-hos
import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TProjectPermission } from "@app/lib/types";
@@ -310,19 +312,34 @@ export const projectServiceFactory = ({
});
}
const project = await projectDAL.create(
{
name: workspaceName,
type,
description: workspaceDescription,
orgId: organization.id,
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
kmsSecretManagerKeyId: kmsKeyId,
version: ProjectVersion.V3,
pitVersionLimit: 10
},
tx
);
const slug = projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`);
let project: TProjects;
try {
project = await projectDAL.create(
{
name: workspaceName,
type,
description: workspaceDescription,
orgId: organization.id,
slug,
kmsSecretManagerKeyId: kmsKeyId,
version: ProjectVersion.V3,
pitVersionLimit: 10
},
tx
);
} catch (err) {
if (
err instanceof DatabaseError &&
(err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation
) {
throw new BadRequestError({
message: `A project with the slug "${slug}" already exists in your organization. Please choose a different name or slug.`
});
}
throw err;
}
if (type === ProjectType.SSH) {
await bootstrapSshProject({
@@ -586,39 +603,36 @@ export const projectServiceFactory = ({
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
if (update.slug) {
const existingProject = await projectDAL.findOne({
slug: update.slug,
orgId: actorOrgId
});
if (existingProject && existingProject.id !== project.id) {
throw new BadRequestError({
message: `Failed to update project slug. The project "${existingProject.name}" with the slug "${existingProject.slug}" already exists in your organization. Please choose a unique slug for your project.`
});
}
}
if (update.secretDetectionIgnoreValues && !hasRole(ProjectMembershipRole.Admin)) {
throw new ForbiddenRequestError({
message: "Only admins can update secret detection ignore values"
});
}
const updatedProject = await projectDAL.updateById(project.id, {
name: update.name,
description: update.description,
autoCapitalization: update.autoCapitalization,
enforceCapitalization: update.autoCapitalization,
hasDeleteProtection: update.hasDeleteProtection,
slug: update.slug,
secretSharing: update.secretSharing,
defaultProduct: update.defaultProduct,
showSnapshotsLegacy: update.showSnapshotsLegacy,
secretDetectionIgnoreValues: update.secretDetectionIgnoreValues,
pitVersionLimit: update.pitVersionLimit
});
try {
const updatedProject = await projectDAL.updateById(project.id, {
name: update.name,
description: update.description,
autoCapitalization: update.autoCapitalization,
enforceCapitalization: update.autoCapitalization,
hasDeleteProtection: update.hasDeleteProtection,
slug: update.slug,
secretSharing: update.secretSharing,
defaultProduct: update.defaultProduct,
showSnapshotsLegacy: update.showSnapshotsLegacy,
secretDetectionIgnoreValues: update.secretDetectionIgnoreValues,
pitVersionLimit: update.pitVersionLimit
});
return updatedProject;
return updatedProject;
} catch (err) {
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
throw new BadRequestError({
message: `Failed to update project. A project with the slug "${update.slug}" already exists in your organization. Please choose a different slug.`
});
}
throw err;
}
};
const toggleAutoCapitalization = async ({
@@ -15,6 +15,8 @@ import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { deepEqualSkipFields } from "@app/lib/fn/object";
import { OrgServiceActor } from "@app/lib/types";
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
@@ -50,6 +52,8 @@ type TSecretSyncServiceFactoryDep = {
secretImportDAL: TSecretImportDALFactory;
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
projectDAL: Pick<TProjectDALFactory, "findById">;
orgDAL: Pick<TOrgDALFactory, "findById">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId" | "findById" | "findBySecretPath">;
keyStore: Pick<TKeyStoreFactory, "getItem">;
@@ -68,6 +72,8 @@ export const secretSyncServiceFactory = ({
secretImportDAL,
permissionService,
appConnectionService,
projectDAL,
orgDAL,
projectBotService,
secretSyncQueue,
keyStore,
@@ -225,6 +231,61 @@ export const secretSyncServiceFactory = ({
return secretSync as TSecretSync;
};
const checkDuplicateDestination = async (
{ destination, destinationConfig, excludeSyncId, projectId }: TCheckDuplicateDestinationDTO,
actor: OrgServiceActor
) => {
const skipFields = SECRET_SYNC_SKIP_FIELDS_MAP[destination];
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretManager,
projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Read,
ProjectPermissionSub.SecretSyncs
);
if (!destinationConfig || Object.keys(destinationConfig).length === 0) {
return { hasDuplicate: false, duplicateProjectId: undefined };
}
try {
const existingSyncs = await secretSyncDAL.findByDestinationAndOrgId(destination, actor.orgId);
const duplicates = existingSyncs.filter((sync) => {
if (sync.id === excludeSyncId) {
return false;
}
try {
const baseFieldsMatch = deepEqualSkipFields(sync.destinationConfig, destinationConfig, skipFields);
if (baseFieldsMatch) {
return DESTINATION_DUPLICATE_CHECK_MAP[destination](
sync.destinationConfig as Record<string, unknown>,
destinationConfig
);
}
return false;
} catch {
return false;
}
});
const hasDuplicate = duplicates.length > 0;
return {
hasDuplicate,
duplicateProjectId: hasDuplicate ? duplicates[0].projectId : undefined
};
} catch (error) {
return { hasDuplicate: false, duplicateProjectId: undefined };
}
};
const createSecretSync = async (
{ projectId, secretPath, environment, ...params }: TCreateSecretSyncDTO,
actor: OrgServiceActor
@@ -271,6 +332,30 @@ export const secretSyncServiceFactory = ({
message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"`
});
const project = await projectDAL.findById(projectId);
if (!project) {
throw new NotFoundError({ message: "Project not found" });
}
const organization = await orgDAL.findById(project.orgId);
if (organization?.blockDuplicateSecretSyncDestinations) {
const duplicateCheck = await checkDuplicateDestination(
{
destination: params.destination,
destinationConfig: params.destinationConfig,
projectId
},
actor
);
if (duplicateCheck.hasDuplicate) {
throw new BadRequestError({
message: `A secret sync with this destination already exists${
duplicateCheck.duplicateProjectId ? ` in project ${duplicateCheck.duplicateProjectId}` : ""
}.`
});
}
}
const destinationApp = SECRET_SYNC_CONNECTION_MAP[params.destination];
// validates permission to connect and app is valid for sync destination
@@ -369,6 +454,33 @@ export const secretSyncServiceFactory = ({
let { folderId } = secretSync;
if (params.destinationConfig) {
const project = await projectDAL.findById(secretSync.projectId);
if (!project) {
throw new NotFoundError({ message: "Project not found" });
}
const organization = await orgDAL.findById(project.orgId);
if (organization?.blockDuplicateSecretSyncDestinations) {
const duplicateCheck = await checkDuplicateDestination(
{
destination,
destinationConfig: params.destinationConfig,
projectId: secretSync.projectId,
excludeSyncId: secretSync.id
},
actor
);
if (duplicateCheck.hasDuplicate) {
throw new BadRequestError({
message: `A secret sync with this destination already exists${
duplicateCheck.duplicateProjectId ? ` in project ${duplicateCheck.duplicateProjectId}` : ""
}.`
});
}
}
}
if (params.connectionId) {
const destinationApp = SECRET_SYNC_CONNECTION_MAP[secretSync.destination as SecretSync];
@@ -703,61 +815,6 @@ export const secretSyncServiceFactory = ({
return updatedSecretSync as TSecretSync;
};
const checkDuplicateDestination = async (
{ destination, destinationConfig, excludeSyncId, projectId }: TCheckDuplicateDestinationDTO,
actor: OrgServiceActor
) => {
const skipFields = SECRET_SYNC_SKIP_FIELDS_MAP[destination];
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretManager,
projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Read,
ProjectPermissionSub.SecretSyncs
);
if (!destinationConfig || Object.keys(destinationConfig).length === 0) {
return { hasDuplicate: false, duplicateProjectId: undefined };
}
try {
const existingSyncs = await secretSyncDAL.findByDestinationAndOrgId(destination, actor.orgId);
const duplicates = existingSyncs.filter((sync) => {
if (sync.id === excludeSyncId) {
return false;
}
try {
const baseFieldsMatch = deepEqualSkipFields(sync.destinationConfig, destinationConfig, skipFields);
if (baseFieldsMatch) {
return DESTINATION_DUPLICATE_CHECK_MAP[destination](
sync.destinationConfig as Record<string, unknown>,
destinationConfig
);
}
return false;
} catch {
return false;
}
});
const hasDuplicate = duplicates.length > 0;
return {
hasDuplicate,
duplicateProjectId: hasDuplicate ? duplicates[0].projectId : undefined
};
} catch (error) {
return { hasDuplicate: false, duplicateProjectId: undefined };
}
};
return {
listSecretSyncOptions,
listSecretSyncsByProjectId,
@@ -35,7 +35,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
const existingTag = await secretTagDAL.findOne({ slug, projectId });
if (existingTag) throw new BadRequestError({ message: "Tag already exist" });
if (existingTag) throw new BadRequestError({ message: "Tag already exists" });
const newTag = await secretTagDAL.create({
projectId,
@@ -53,7 +53,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
if (slug) {
const existingTag = await secretTagDAL.findOne({ slug, projectId: tag.projectId });
if (existingTag && existingTag.id !== tag.id) throw new BadRequestError({ message: "Tag already exist" });
if (existingTag && existingTag.id !== tag.id) throw new BadRequestError({ message: "Tag already exists" });
}
const { permission } = await permissionService.getProjectPermission({
@@ -282,7 +282,7 @@ export const secretV2BridgeServiceFactory = ({
folderId
});
if (inputSecret.type === SecretType.Shared && doesSecretExist)
throw new BadRequestError({ message: "Secret already exist" });
throw new BadRequestError({ message: "Secret already exists" });
// if user creating personal check its shared also exist
if (inputSecret.type === SecretType.Personal && !doesSecretExist) {
@@ -527,7 +527,7 @@ export const secretV2BridgeServiceFactory = ({
type: SecretType.Shared,
folderId
});
if (doesNewNameSecretExist) throw new BadRequestError({ message: "Secret with the new name already exist" });
if (doesNewNameSecretExist) throw new BadRequestError({ message: "Secret with the new name already exists" });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
@@ -1674,7 +1674,7 @@ export const secretV2BridgeServiceFactory = ({
}
});
if (secrets.length)
throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` });
throw new BadRequestError({ message: `Secret already exists: ${secrets.map((el) => el.key).join(",")}` });
const project = await projectDAL.findById(projectId);
await scanSecretPolicyViolations(projectId, secretPath, inputSecrets, project.secretDetectionIgnoreValues || []);
+2 -2
View File
@@ -525,7 +525,7 @@ export const fnSecretBlindIndexCheck = async ({
);
if (isNew) {
if (secrets.length) throw new BadRequestError({ message: "Secret already exist" });
if (secrets.length) throw new BadRequestError({ message: "Secret already exists" });
} else {
const secretKeysInDB = unique(secrets, (el) => el.secretBlindIndex as string).map(
(el) => blindIndex2KeyName[el.secretBlindIndex as string]
@@ -819,7 +819,7 @@ export const createManySecretsRawFnFactory = ({
);
if (secretsStoredInDB.length)
throw new BadRequestError({
message: `Secret already exist: ${secretsStoredInDB.map((el) => el.key).join(",")}`
message: `Secret already exists: ${secretsStoredInDB.map((el) => el.key).join(",")}`
});
const inputSecrets = secrets.map((secret) => {
@@ -2751,7 +2751,7 @@ export const secretServiceFactory = ({
const existingSecretTags = await secretDAL.getSecretTags(secret.id);
if (existingSecretTags.some((tag) => tagSlugs.includes(tag.slug))) {
throw new BadRequestError({ message: "One or more tags already exist on the secret" });
throw new BadRequestError({ message: "One or more tags already exists on the secret" });
}
const combinedTags = new Set([...existingSecretTags.map((tag) => tag.id), ...tags.map((el) => el.id)]);
+7 -1
View File
@@ -7,4 +7,10 @@ Infisical's Public (REST) API provides users an alternative way to programmatica
secrets via HTTPS requests. This can be useful for automating tasks, such as
rotating credentials, or for integrating secret management into a larger system.
With the Public API, you can create, read, update, and delete secrets, as well as manage access control, query audit logs, and more.
With the Public API, you can create, read, update, and delete secrets, as well as manage access control, query audit logs, and more.
## API Versioning
The API is versioned on a per-resource basis. A resource's version is only incremented for breaking changes, so different endpoints may have different version numbers (e.g., `/api/v4/secrets` vs. `/api/v1/secret-syncs`).
As a best practice, always use the latest available version for each endpoint to ensure access to the most recent features and improvements.
@@ -26,6 +26,12 @@ Infisical provides two approaches for migrating from HashiCorp Vault.
This migration approach lets you set up a connection to your Vault instance once, then import specific resources as needed throughout Infisical.
<Warning>
**Organization Admin Access Required:** All in-platform migration features
(importing secrets, Kubernetes configurations, and policies from Vault) are
only accessible to organization admins.
</Warning>
### Step 1: Set Up Your Vault Connection
<Steps>
@@ -84,6 +90,19 @@ This migration approach lets you set up a connection to your Vault instance once
path "auth/+/role/*" {
capabilities = ["read"]
}
# Kubernetes secrets engine - for reading secrets engine configuration and roles
path "+/config" {
capabilities = ["read"]
}
path "+/roles" {
capabilities = ["list"]
}
path "+/roles/*" {
capabilities = ["read"]
}
```
</Accordion>
@@ -160,6 +179,34 @@ The authentication settings (service accounts, TTL, policies, etc.) will be auto
must be manually provided in the form after importing the configuration.
</Note>
#### Import Kubernetes Dynamic Secret Configurations
When creating a Kubernetes dynamic secret, you can import the configuration from a Vault Kubernetes secrets engine role:
1. Navigate to your project and select an environment
2. Click **"+ Add Secret"** dropdown and choose **"Dynamic Secret"**
3. Select **Kubernetes** as the provider
4. Click **"Load from Vault"** at the top of the form
![Load Kubernetes Dynamic Secret from Vault](/images/platform/external-migrations/vault-in-platform/import-vault-kubernetes-role-modal.png)
5. Select your Vault namespace, Kubernetes secrets engine mount, and role
6. Click **"Load Configuration"**
The form will be automatically populated with the role's configuration including:
- Cluster URL and CA certificate
- Credential type (Static or Dynamic)
- Service account name or Kubernetes role settings
- Allowed namespaces
- Token TTL values
- Token audiences
<Note>
Sensitive values like cluster tokens cannot be retrieved from Vault and must
be manually provided in the form after loading the configuration.
</Note>
#### Import and Translate Access Control Policies
When configuring project role-based access control, you can import Vault HCL policies and automatically translate them to Infisical permissions.
Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

+82 -21
View File
@@ -187,31 +187,92 @@ Infisical supports two methods for connecting to GitLab: **OAuth** and **Access
</Step>
</Steps>
</Tab>
</Tabs>
## Setup GitLab Access Token Connection in Infisical
<Tab title="Group Access Token">
Group access tokens provide access to all projects within a GitLab group, offering group-level control.
<Steps>
<Step title="Navigate to App Connections">
Navigate to the **App Connections** page in the desired project.
![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step>
<Step title="Add Connection">
Select the **GitLab Connection** option from the connection options modal.
![Select GitLab Connection](/images/app-connections/gitlab/select-gitlab-connection.png)
</Step>
<Step title="Configure Access Token">
Select the **Access Token** method, paste your GitLab access token in the provided field, and select the appropriate token type.
<Steps>
<Step title="Navigate to Group Settings">
Go to your GitLab group and navigate to Settings > Access Tokens. Click **Add new token** to create a new group access token.
![GitLab Group Access Tokens](/images/app-connections/gitlab/gitlab-group-access-token-list.png)
</Step>
<Step title="Configure Token">
Fill in the token details:
- **Token name**: A descriptive name for the token
- **Expiration date**: Set an appropriate expiration date
- **Select role and scopes**: Depending on your use case, add the required role and one or more of the following scopes:
![Configure Access Token](/images/app-connections/gitlab/create-gitlab-access-token-connection.png)
<Tabs>
<Tab title="Secret Sync">
For Secret Syncs, the required role depends on your sync destination:
- **Project variables**: Requires **Maintainer** role or higher
- **Group variables**: Requires **Owner** role
Click **Connect** to establish the connection.
</Step>
<Step title="Connection Created">
Your **GitLab Connection** is now available for use.
![GitLab Access Token Connection](/images/app-connections/gitlab/gitlab-access-token-connection.png)
</Step>
</Steps>
Your token will require the `api` scope.
![GitLab Create Group Token](/images/app-connections/gitlab/gitlab-group-access-token-form-secret-sync.png)
Click **Create group access token** to create the token.
<Note>
Use the **Owner** role if you need to sync to group-level variables. The **Maintainer** role is sufficient only for project-level variables.
</Note>
</Tab>
<Tab title="Secret Scanning">
To set up Secret Scanning, the required permissions depend on the data source level:
- **Project-level data source:** Requires **Maintainer** role or higher
- **Group-level data source:** Requires **Owner** role
Your token will require the `api` scope.
![GitLab Create Group Token](/images/app-connections/gitlab/gitlab-group-access-token-form-secret-sync.png)
Click **Create group access token** to create the token.
</Tab>
</Tabs>
<Info>
Group Access Token connections require manual token rotation when your GitLab access token expires or is regenerated. Monitor your connection status and update the token as needed.
</Info>
</Step>
<Step title="Copy Token">
Copy the generated token immediately as it won't be shown again.
![GitLab Group Token Created](/images/app-connections/gitlab/gitlab-group-access-token-created.png)
<Warning>
Keep your access token secure and do not share it. Anyone with access to this token can access all projects within your GitLab group.
</Warning>
</Step>
</Steps>
</Tab>
</Tabs>
## Setup GitLab Access Token Connection in Infisical
<Steps>
<Step title="Navigate to App Connections">
Navigate to the **App Connections** page in the desired project.
![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step>
<Step title="Add Connection">
Select the **GitLab Connection** option from the connection options modal.
![Select GitLab Connection](/images/app-connections/gitlab/select-gitlab-connection.png)
</Step>
<Step title="Configure Access Token">
Select the **Access Token** method, paste your GitLab access token in the provided field, and select the appropriate token type.
![Configure Access Token](/images/app-connections/gitlab/create-gitlab-access-token-connection.png)
Click **Connect** to establish the connection.
</Step>
<Step title="Connection Created">
Your **GitLab Connection** is now available for use.
![GitLab Access Token Connection](/images/app-connections/gitlab/gitlab-access-token-connection.png)
</Step>
</Steps>
</Tab>
</Tabs>
@@ -3,7 +3,7 @@ import type { Decorator } from "@storybook/react-vite";
export const DocumentDecorator: Decorator = (Story) => {
useEffect(() => {
const root = document.getElementsByTagName("html")[0];
const root = document.documentElement;
root.setAttribute("class", "overflow-visible");
}, []);
@@ -1,9 +1,11 @@
import { components, OptionProps } from "react-select";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import { faBuilding, faPlus } from "@fortawesome/free-solid-svg-icons";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge, Tooltip } from "@app/components/v2";
import { Tooltip } from "@app/components/v2";
import { Badge, OrgIcon, SubOrgIcon } from "@app/components/v3";
import { useOrganization } from "@app/context";
import { TAvailableAppConnection } from "@app/hooks/api/appConnections";
export const AppConnectionOption = ({
@@ -13,6 +15,8 @@ export const AppConnectionOption = ({
}: OptionProps<TAvailableAppConnection>) => {
const isCreateOption = props.data.id === "_create";
const { isSubOrganization } = useOrganization();
return (
<components.Option isSelected={isSelected} {...props}>
<div className="flex flex-row items-center justify-between">
@@ -23,15 +27,22 @@ export const AppConnectionOption = ({
</div>
) : (
<>
<p className="truncate">{children}</p>
<p className="mr-auto truncate">{children}</p>
{!props.data.projectId && (
<Tooltip content="This connection belongs to your organization.">
<div className="mr-auto ml-2">
<Badge className="flex h-5 w-min items-center gap-1 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300 hover:text-bunker-300">
<FontAwesomeIcon icon={faBuilding} size="sm" />
<Tooltip
content={`This connection belongs to your ${isSubOrganization ? "sub-" : ""}organization.`}
>
{isSubOrganization ? (
<Badge variant="sub-org">
<SubOrgIcon />
Sub-Organization
</Badge>
) : (
<Badge variant="org">
<OrgIcon />
Organization
</Badge>
</div>
)}
</Tooltip>
)}
{isSelected && (
@@ -4,7 +4,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import {
Badge,
Button,
Checkbox,
EmptyState,
@@ -20,6 +19,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useProject } from "@app/context";
import {
CertStatus,
@@ -1,27 +1,20 @@
import { ReactNode, useEffect, useMemo, useState } from "react";
import {
faCheck,
faDownload,
faTriangleExclamation,
faXmark,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { differenceInSeconds } from "date-fns";
import { twMerge } from "tailwind-merge";
import { CheckIcon, DownloadIcon, LucideIcon, TriangleAlertIcon } from "lucide-react";
import { Badge, Tooltip } from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge";
import { Tooltip } from "@app/components/v2";
import { Badge, TBadgeProps } from "@app/components/v3";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs";
type Props = {
pkiSync: TPkiSync;
className?: string;
mini?: boolean;
};
export const PkiSyncImportStatusBadge = ({ pkiSync, className, mini }: Props) => {
export const PkiSyncImportStatusBadge = ({ pkiSync, mini }: Props) => {
const { importStatus, lastImportMessage, lastImportedAt, destination } = pkiSync;
const [hide, setHide] = useState(importStatus === PkiSyncStatus.Succeeded);
const destinationName = PKI_SYNC_MAP[destination].name;
@@ -50,24 +43,24 @@ export const PkiSyncImportStatusBadge = ({ pkiSync, className, mini }: Props) =>
if (!importStatus || hide) return null;
let variant: BadgeProps["variant"];
let variant: TBadgeProps["variant"];
let label: string;
let icon: IconDefinition;
let Icon: LucideIcon;
let tooltipContent: ReactNode;
switch (importStatus) {
case PkiSyncStatus.Pending:
case PkiSyncStatus.Running:
variant = "primary";
variant = "warning";
label = "Importing Certificates...";
tooltipContent = `Importing certificates from ${destinationName}. This may take a moment.`;
icon = faDownload;
Icon = DownloadIcon;
break;
case PkiSyncStatus.Failed:
variant = "danger";
label = "Failed to Import Certificates";
icon = faTriangleExclamation;
Icon = TriangleAlertIcon;
tooltipContent = (
<div className="flex flex-col gap-2 py-1 whitespace-normal">
{failureMessage && (
@@ -93,20 +86,15 @@ export const PkiSyncImportStatusBadge = ({ pkiSync, className, mini }: Props) =>
tooltipContent = "Successfully imported certificates.";
variant = "success";
label = "Certificates Imported";
icon = faCheck;
Icon = CheckIcon;
}
return (
<Tooltip position="bottom" className="max-w-sm" content={tooltipContent}>
<div>
<Badge
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
variant={variant}
>
<FontAwesomeIcon icon={icon} />
{!mini && <span>{label}</span>}
</Badge>
</div>
<Badge isSquare={mini} variant={variant}>
<Icon />
{!mini && label}
</Badge>
</Tooltip>
);
};
@@ -1,6 +1,4 @@
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DocumentationLinkBadge } from "@app/components/v3";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSync } from "@app/hooks/api/pkiSyncs";
@@ -20,23 +18,11 @@ export const PkiSyncModalHeader = ({ destination, isConfigured }: Props) => {
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
/>
<div>
<div className="flex items-center text-mineshaft-300">
<div className="flex items-center gap-x-2 text-mineshaft-300">
{destinationDetails.name} Certificate Sync
<a
target="_blank"
href={`https://infisical.com/docs/integrations/pki-syncs/${destination}`}
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
<DocumentationLinkBadge
href={`https://infisical.com/docs/documentation/platform/pki/certificate-syncs/${destination}`}
/>
</div>
<p className="text-sm leading-4 text-mineshaft-400">
{isConfigured
@@ -1,27 +1,20 @@
import { ReactNode, useEffect, useMemo, useState } from "react";
import {
faCheck,
faEraser,
faTriangleExclamation,
faXmark,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { differenceInSeconds } from "date-fns";
import { twMerge } from "tailwind-merge";
import { AlertTriangleIcon, CheckIcon, EraserIcon, LucideIcon } from "lucide-react";
import { Badge, Tooltip } from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge";
import { Tooltip } from "@app/components/v2";
import { Badge, TBadgeProps } from "@app/components/v3";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs";
type Props = {
pkiSync: TPkiSync;
className?: string;
mini?: boolean;
};
export const PkiSyncRemoveStatusBadge = ({ pkiSync, className, mini }: Props) => {
export const PkiSyncRemoveStatusBadge = ({ pkiSync, mini }: Props) => {
const { removeStatus, lastRemoveMessage, lastRemovedAt, destination } = pkiSync;
const [hide, setHide] = useState(removeStatus === PkiSyncStatus.Succeeded);
const destinationName = PKI_SYNC_MAP[destination].name;
@@ -50,24 +43,24 @@ export const PkiSyncRemoveStatusBadge = ({ pkiSync, className, mini }: Props) =>
if (!removeStatus || hide) return null;
let variant: BadgeProps["variant"];
let variant: TBadgeProps["variant"];
let label: string;
let icon: IconDefinition;
let Icon: LucideIcon;
let tooltipContent: ReactNode;
switch (removeStatus) {
case PkiSyncStatus.Pending:
case PkiSyncStatus.Running:
variant = "primary";
variant = "warning";
label = "Removing Certificates...";
tooltipContent = `Removing certificates from ${destinationName}. This may take a moment.`;
icon = faEraser;
Icon = EraserIcon;
break;
case PkiSyncStatus.Failed:
variant = "danger";
label = "Failed to Remove Certificates";
icon = faTriangleExclamation;
Icon = AlertTriangleIcon;
tooltipContent = (
<div className="flex flex-col gap-2 py-1 whitespace-normal">
{failureMessage && (
@@ -93,20 +86,15 @@ export const PkiSyncRemoveStatusBadge = ({ pkiSync, className, mini }: Props) =>
tooltipContent = "Successfully removed certificates.";
variant = "success";
label = "Certificates Removed";
icon = faCheck;
Icon = CheckIcon;
}
return (
<Tooltip position="bottom" className="max-w-sm" content={tooltipContent}>
<div>
<Badge
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
variant={variant}
>
<FontAwesomeIcon icon={icon} />
{!mini && <span>{label}</span>}
</Badge>
</div>
<Badge isSquare={mini} variant={variant}>
<Icon />
{!mini && label}
</Badge>
</Tooltip>
);
};
@@ -69,7 +69,10 @@ export const PkiSyncSelect = ({ onSelect }: Props) => {
type="button"
onClick={() =>
enterprise && !subscription.enterpriseCertificateSyncs
? handlePopUpOpen("upgradePlan")
? handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true,
text: "You can use every Certificate Sync if you switch to Infisical's Enterprise plan."
})
: onSelect(destination)
}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
@@ -148,6 +151,7 @@ export const PkiSyncSelect = ({ onSelect }: Props) => {
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
text="You can use every Certificate Sync if you switch to Infisical's Enterprise plan."
/>
</div>
@@ -1,55 +1,51 @@
import {
faCheck,
faExclamationTriangle,
faHourglass,
faRotate,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
AlertTriangleIcon,
CheckIcon,
HourglassIcon,
LucideIcon,
RefreshCwIcon
} from "lucide-react";
import { Badge, BadgeProps } from "@app/components/v2/Badge/Badge";
import { Badge, TBadgeProps } from "@app/components/v3";
import { PkiSyncStatus } from "@app/hooks/api/pkiSyncs";
type Props = {
status: PkiSyncStatus;
} & Omit<BadgeProps, "children" | "variant">;
} & Omit<TBadgeProps, "children" | "variant">;
export const PkiSyncStatusBadge = ({ status }: Props) => {
let variant: BadgeProps["variant"];
let variant: TBadgeProps["variant"];
let text: string;
let icon: IconDefinition;
let Icon: LucideIcon;
switch (status) {
case PkiSyncStatus.Failed:
variant = "danger";
text = "Failed to Sync";
icon = faExclamationTriangle;
Icon = AlertTriangleIcon;
break;
case PkiSyncStatus.Succeeded:
variant = "success";
text = "Synced";
icon = faCheck;
Icon = CheckIcon;
break;
case PkiSyncStatus.Pending:
variant = "primary";
variant = "info";
text = "Queued";
icon = faHourglass;
Icon = HourglassIcon;
break;
case PkiSyncStatus.Running:
default:
variant = "primary";
variant = "info";
text = "Syncing";
icon = faRotate;
Icon = RefreshCwIcon;
break;
}
return (
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap" variant={variant}>
<FontAwesomeIcon
icon={icon}
className={status === PkiSyncStatus.Running ? "animate-spin" : ""}
/>
<span>{text}</span>
<Badge variant={variant}>
<Icon className={status === PkiSyncStatus.Running ? "animate-spin" : ""} />
{text}
</Badge>
);
};
@@ -2,7 +2,6 @@ import { faPlug, faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Badge,
EmptyState,
IconButton,
Table,
@@ -13,6 +12,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
type Props = {
@@ -29,10 +29,10 @@ const getSyncStatusBadge = (status?: string) => {
case "FAILED":
return <Badge variant="danger">Failed</Badge>;
case "RUNNING":
return <Badge variant="primary">Running</Badge>;
return <Badge variant="warning">Running</Badge>;
case "PENDING":
default:
return <Badge variant="primary">Pending</Badge>;
return <Badge variant="warning">Pending</Badge>;
}
};
@@ -71,7 +71,7 @@ export const PkiSyncTable = ({ pkiSyncs, onEdit, onDelete, onTrigger }: Props) =
<Td>{pkiSync.name}</Td>
<Td>
<div className="flex items-center space-x-2">
<Badge variant="primary">{pkiSync.destination}</Badge>
<Badge variant="warning">{pkiSync.destination}</Badge>
</div>
</Td>
<Td>{pkiSync.appConnectionName || "Unknown"}</Td>
@@ -1,6 +1,7 @@
import { useFormContext } from "react-hook-form";
import { Badge, GenericFieldLabel } from "@app/components/v2";
import { GenericFieldLabel } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useProject } from "@app/context";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { useListWorkspaceCertificates } from "@app/hooks/api/projects";
@@ -1,6 +1,4 @@
import { useEffect, useState } from "react";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useRouterState } from "@tanstack/react-router";
import { SecretRotationV2Form } from "@app/components/secret-rotations-v2/forms";
@@ -8,6 +6,7 @@ import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms
import { SecretRotationV2ModalHeader } from "@app/components/secret-rotations-v2/SecretRotationV2ModalHeader";
import { SecretRotationV2Select } from "@app/components/secret-rotations-v2/SecretRotationV2Select";
import { Modal, ModalContent } from "@app/components/v2";
import { DocumentationLinkBadge } from "@app/components/v3";
import { ProjectEnv } from "@app/hooks/api/projects/types";
import { SecretRotation, TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
@@ -115,23 +114,9 @@ export const CreateSecretRotationV2Modal = ({ onOpenChange, isOpen, ...props }:
selectedRotation ? (
<SecretRotationV2ModalHeader isConfigured={false} type={selectedRotation} />
) : (
<div className="flex items-center text-mineshaft-300">
<div className="flex items-center gap-x-2 text-mineshaft-300">
Add Secret Rotation
<a
target="_blank"
href="https://infisical.com/docs/documentation/platform/secret-rotation/overview"
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/secret-rotation/overview" />
</div>
)
}
@@ -1,6 +1,4 @@
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DocumentationLinkBadge } from "@app/components/v3";
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
@@ -20,23 +18,11 @@ export const SecretRotationV2ModalHeader = ({ type, isConfigured }: Props) => {
className="h-12 w-12 rounded-md bg-bunker-500 p-2"
/>
<div>
<div className="flex items-center text-mineshaft-300">
<div className="flex items-center gap-x-2 text-mineshaft-300">
{destinationDetails.name} Rotation
<a
target="_blank"
<DocumentationLinkBadge
href={`https://infisical.com/docs/documentation/platform/secret-rotation/${type}`}
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
/>
</div>
<p className="text-sm leading-4 text-mineshaft-400">
{isConfigured
@@ -1,18 +1,17 @@
import { faBan, faRotate, faXmark } from "@fortawesome/free-solid-svg-icons";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format, formatDistanceToNow } from "date-fns";
import { twMerge } from "tailwind-merge";
import { BanIcon, RefreshCwIcon, XIcon } from "lucide-react";
import { Tooltip } from "@app/components/v2";
import { Badge } from "@app/components/v2/Badge/Badge";
import { Badge } from "@app/components/v3";
import { SecretRotationStatus, TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
type Props = {
secretRotation: TSecretRotationV2;
className?: string;
};
export const SecretRotationV2StatusBadge = ({ secretRotation, className }: Props) => {
export const SecretRotationV2StatusBadge = ({ secretRotation }: Props) => {
const { isAutoRotationEnabled, rotationStatus, nextRotationAt, lastRotationMessage } =
secretRotation;
@@ -50,28 +49,18 @@ export const SecretRotationV2StatusBadge = ({ secretRotation, className }: Props
</div>
}
>
<div>
<Badge
variant="danger"
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
>
<FontAwesomeIcon icon={faXmark} />
Rotation Failed
</Badge>
</div>
<Badge variant="danger">
<XIcon />
Rotation Failed
</Badge>
</Tooltip>
);
}
if (!isAutoRotationEnabled) {
return (
<Badge
className={twMerge(
"flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300",
className
)}
>
<FontAwesomeIcon icon={faBan} />
<Badge variant="neutral">
<BanIcon />
Auto-Rotation Disabled
</Badge>
);
@@ -92,20 +81,12 @@ export const SecretRotationV2StatusBadge = ({ secretRotation, className }: Props
</>
}
>
<div>
<Badge
variant={daysToRotation >= 7 ? "success" : "primary"}
className={twMerge(
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap capitalize",
className
)}
>
<FontAwesomeIcon icon={faRotate} />
{daysToRotation < 0
? "Rotating"
: `Rotates ${formatDistanceToNow(nextRotationAt, { addSuffix: true })}`}
</Badge>
</div>
<Badge variant={daysToRotation >= 7 ? "info" : "warning"} className="capitalize">
<RefreshCwIcon />
{daysToRotation < 0
? "Rotating"
: `Rotates ${formatDistanceToNow(nextRotationAt, { addSuffix: true })}`}
</Badge>
</Tooltip>
);
};
@@ -1,8 +1,10 @@
import { ReactNode } from "react";
import { faArrowRight, faKey } from "@fortawesome/free-solid-svg-icons";
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { KeyIcon } from "lucide-react";
import { Badge, FormLabel } from "@app/components/v2";
import { FormLabel } from "@app/components/v2";
import { Badge } from "@app/components/v3";
type Props = {
items: { name: string; input: ReactNode }[];
@@ -32,9 +34,10 @@ export const SecretsMappingTable = ({ items }: Props) => {
<tr key={name}>
<td className="whitespace-nowrap">
<div className="mb-4 flex h-full items-start justify-center">
<Badge className="pointer-events-none flex h-[36px] w-full items-center justify-center gap-1.5 border border-mineshaft-600 bg-mineshaft-600 whitespace-nowrap text-bunker-200">
<FontAwesomeIcon icon={faKey} />
<span>{name}</span>
{/* TODO(scott): probably shouldn't be a badge */}
<Badge variant="neutral" className="h-[36px] w-full justify-center text-xs">
<KeyIcon />
{name}
</Badge>
</div>
</td>
@@ -1,10 +1,9 @@
import { useEffect, useState } from "react";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useRouterState } from "@tanstack/react-router";
import { TSecretScanningDataSourceForm } from "@app/components/secret-scanning/forms/schemas";
import { Modal, ModalContent } from "@app/components/v2";
import { DocumentationLinkBadge } from "@app/components/v3";
import {
SecretScanningDataSource,
TSecretScanningDataSource
@@ -108,23 +107,9 @@ export const CreateSecretScanningDataSourceModal = ({ onOpenChange, isOpen, ...p
selectedDataSource ? (
<SecretScanningDataSourceModalHeader isConfigured={false} type={selectedDataSource} />
) : (
<div className="flex items-center text-mineshaft-300">
<div className="flex items-center gap-x-2 text-mineshaft-300">
Add Data Source
<a
target="_blank"
href="https://infisical.com/docs/documentation/platform/secret-scanning/overview"
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/secret-scanning/overview" />
</div>
)
}
@@ -1,6 +1,4 @@
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DocumentationLinkBadge } from "@app/components/v3";
import { SECRET_SCANNING_DATA_SOURCE_MAP } from "@app/helpers/secretScanningV2";
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
@@ -20,23 +18,11 @@ export const SecretScanningDataSourceModalHeader = ({ type, isConfigured }: Prop
className="h-12 rounded-md bg-bunker-500 p-2"
/>
<div>
<div className="flex items-center text-mineshaft-300">
<div className="flex items-center gap-x-2 text-mineshaft-300">
{dataSourceDetails.name} Data Source
<a
target="_blank"
<DocumentationLinkBadge
href={`https://infisical.com/docs/documentation/platform/secret-scanning/${type}`}
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
/>
</div>
<p className="text-sm leading-4 text-mineshaft-400">
{isConfigured ? "Edit" : "Connect a"} {dataSourceDetails.name} Data Source
@@ -1,22 +1,23 @@
import { faArrowRotateForward, faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { formatDistance } from "date-fns";
import { twMerge } from "tailwind-merge";
import { CheckIcon, RotateCwIcon, XIcon } from "lucide-react";
import { Badge, Tooltip } from "@app/components/v2";
import { Tooltip } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { SecretScanningScanStatus } from "@app/hooks/api/secretScanningV2";
type Props = {
status: SecretScanningScanStatus;
statusMessage?: string | null;
className?: string;
scannedAt?: string | null;
};
export const SecretScanningScanStatusBadge = ({
status,
statusMessage,
className,
scannedAt
}: Props) => {
if (status === SecretScanningScanStatus.Failed) {
@@ -53,11 +54,8 @@ export const SecretScanningScanStatusBadge = ({
}
>
<div>
<Badge
variant="danger"
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
>
<FontAwesomeIcon icon={faXmark} />
<Badge variant="danger">
<XIcon />
Scan Error
</Badge>
</div>
@@ -67,26 +65,17 @@ export const SecretScanningScanStatusBadge = ({
if (status === SecretScanningScanStatus.Queued || status === SecretScanningScanStatus.Scanning) {
return (
<Badge
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
variant="primary"
>
<FontAwesomeIcon icon={faArrowRotateForward} className="animate-spin" />
<span>Scanning</span>
<Badge variant="info">
<RotateCwIcon className="animate-spin" />
Scanning
</Badge>
);
}
return (
<Badge
variant="success"
className={twMerge(
"flex h-5 w-min items-center gap-1.5 whitespace-nowrap capitalize",
className
)}
>
<FontAwesomeIcon icon={faCheck} />
<span>Complete</span>
<Badge variant="success">
<CheckIcon />
Complete
</Badge>
);
};
@@ -1,27 +1,20 @@
import { ReactNode, useEffect, useMemo, useState } from "react";
import {
faCheck,
faDownload,
faTriangleExclamation,
faXmark,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { differenceInSeconds } from "date-fns";
import { twMerge } from "tailwind-merge";
import { AlertTriangleIcon, CheckIcon, DownloadIcon, LucideIcon } from "lucide-react";
import { Badge, Tooltip } from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge";
import { Tooltip } from "@app/components/v2";
import { Badge, TBadgeProps } from "@app/components/v3";
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { SecretSyncStatus, TSecretSync } from "@app/hooks/api/secretSyncs";
type Props = {
secretSync: TSecretSync;
className?: string;
mini?: boolean;
};
export const SecretSyncImportStatusBadge = ({ secretSync, className, mini }: Props) => {
export const SecretSyncImportStatusBadge = ({ secretSync, mini }: Props) => {
const { importStatus, lastImportMessage, lastImportedAt, destination } = secretSync;
const [hide, setHide] = useState(importStatus === SecretSyncStatus.Succeeded);
const destinationName = SECRET_SYNC_MAP[destination].name;
@@ -50,24 +43,24 @@ export const SecretSyncImportStatusBadge = ({ secretSync, className, mini }: Pro
if (!importStatus || hide) return null;
let variant: BadgeProps["variant"];
let variant: TBadgeProps["variant"];
let label: string;
let icon: IconDefinition;
let Icon: LucideIcon;
let tooltipContent: ReactNode;
switch (importStatus) {
case SecretSyncStatus.Pending:
case SecretSyncStatus.Running:
variant = "primary";
variant = "warning";
label = "Importing Secrets...";
tooltipContent = `Importing secrets from ${destinationName}. This may take a moment.`;
icon = faDownload;
Icon = DownloadIcon;
break;
case SecretSyncStatus.Failed:
variant = "danger";
label = "Failed to Import Secrets";
icon = faTriangleExclamation;
Icon = AlertTriangleIcon;
tooltipContent = (
<div className="flex flex-col gap-2 py-1 whitespace-normal">
{failureMessage && (
@@ -93,20 +86,15 @@ export const SecretSyncImportStatusBadge = ({ secretSync, className, mini }: Pro
tooltipContent = "Successfully imported secrets.";
variant = "success";
label = "Secrets Imported";
icon = faCheck;
Icon = CheckIcon;
}
return (
<Tooltip position="bottom" className="max-w-sm" content={tooltipContent}>
<div>
<Badge
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
variant={variant}
>
<FontAwesomeIcon icon={icon} />
{!mini && <span>{label}</span>}
</Badge>
</div>
<Badge isSquare={mini} variant={variant}>
<Icon />
{!mini && label}
</Badge>
</Tooltip>
);
};
@@ -1,6 +1,4 @@
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DocumentationLinkBadge } from "@app/components/v3";
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { SecretSync } from "@app/hooks/api/secretSyncs";
@@ -20,23 +18,11 @@ export const SecretSyncModalHeader = ({ destination, isConfigured }: Props) => {
className="h-12 w-12 rounded-md bg-bunker-500 object-contain p-2"
/>
<div>
<div className="flex items-center text-mineshaft-300">
<div className="flex items-center gap-x-2 text-mineshaft-300">
{destinationDetails.name} Sync
<a
target="_blank"
<DocumentationLinkBadge
href={`https://infisical.com/docs/integrations/secret-syncs/${destination}`}
className="mb-1 ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1 text-[10px]"
/>
</div>
</a>
/>
</div>
<p className="text-sm leading-4 text-mineshaft-400">
{isConfigured
@@ -1,27 +1,20 @@
import { ReactNode, useEffect, useMemo, useState } from "react";
import {
faCheck,
faEraser,
faTriangleExclamation,
faXmark,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { differenceInSeconds } from "date-fns";
import { twMerge } from "tailwind-merge";
import { AlertTriangleIcon, CheckIcon, EraserIcon, LucideIcon } from "lucide-react";
import { Badge, Tooltip } from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge";
import { Tooltip } from "@app/components/v2";
import { Badge, TBadgeProps } from "@app/components/v3";
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { SecretSyncStatus, TSecretSync } from "@app/hooks/api/secretSyncs";
type Props = {
secretSync: TSecretSync;
className?: string;
mini?: boolean;
};
export const SecretSyncRemoveStatusBadge = ({ secretSync, className, mini }: Props) => {
export const SecretSyncRemoveStatusBadge = ({ secretSync, mini }: Props) => {
const { removeStatus, lastRemoveMessage, lastRemovedAt, destination } = secretSync;
const [hide, setHide] = useState(removeStatus === SecretSyncStatus.Succeeded);
const destinationName = SECRET_SYNC_MAP[destination].name;
@@ -50,24 +43,24 @@ export const SecretSyncRemoveStatusBadge = ({ secretSync, className, mini }: Pro
if (!removeStatus || hide) return null;
let variant: BadgeProps["variant"];
let variant: TBadgeProps["variant"];
let label: string;
let icon: IconDefinition;
let Icon: LucideIcon;
let tooltipContent: ReactNode;
switch (removeStatus) {
case SecretSyncStatus.Pending:
case SecretSyncStatus.Running:
variant = "primary";
variant = "warning";
label = "Removing Secrets...";
tooltipContent = `Removing secrets from ${destinationName}. This may take a moment.`;
icon = faEraser;
Icon = EraserIcon;
break;
case SecretSyncStatus.Failed:
variant = "danger";
label = "Failed to Remove Secrets";
icon = faTriangleExclamation;
Icon = AlertTriangleIcon;
tooltipContent = (
<div className="flex flex-col gap-2 py-1 whitespace-normal">
{failureMessage && (
@@ -93,20 +86,15 @@ export const SecretSyncRemoveStatusBadge = ({ secretSync, className, mini }: Pro
tooltipContent = "Successfully removed secrets.";
variant = "success";
label = "Secrets Removed";
icon = faCheck;
Icon = CheckIcon;
}
return (
<Tooltip position="bottom" className="max-w-sm" content={tooltipContent}>
<div>
<Badge
className={twMerge("flex h-5 w-min items-center gap-1.5 whitespace-nowrap", className)}
variant={variant}
>
<FontAwesomeIcon icon={icon} />
{!mini && <span>{label}</span>}
</Badge>
</div>
<Badge isSquare={mini} variant={variant}>
<Icon />
{!mini && label}
</Badge>
</Tooltip>
);
};
@@ -67,7 +67,9 @@ export const SecretSyncSelect = ({ onSelect }: Props) => {
type="button"
onClick={() =>
enterprise && !subscription.enterpriseSecretSyncs
? handlePopUpOpen("upgradePlan")
? handlePopUpOpen("upgradePlan", {
isEnterpriseFeature: true
})
: onSelect(destination)
}
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
@@ -145,6 +147,7 @@ export const SecretSyncSelect = ({ onSelect }: Props) => {
)}
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use every Secret Sync if you switch to Infisical's Enterprise plan."
/>
@@ -1,55 +1,51 @@
import {
faCheck,
faExclamationTriangle,
faHourglass,
faRotate,
IconDefinition
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
AlertTriangleIcon,
CheckIcon,
HourglassIcon,
LucideIcon,
RefreshCwIcon
} from "lucide-react";
import { Badge, BadgeProps } from "@app/components/v2/Badge/Badge";
import { Badge, TBadgeProps } from "@app/components/v3";
import { SecretSyncStatus } from "@app/hooks/api/secretSyncs";
type Props = {
status: SecretSyncStatus;
} & Omit<BadgeProps, "children" | "variant">;
} & Omit<TBadgeProps, "children" | "variant">;
export const SecretSyncStatusBadge = ({ status }: Props) => {
let variant: BadgeProps["variant"];
let variant: TBadgeProps["variant"];
let text: string;
let icon: IconDefinition;
let Icon: LucideIcon;
switch (status) {
case SecretSyncStatus.Failed:
variant = "danger";
text = "Failed to Sync";
icon = faExclamationTriangle;
Icon = AlertTriangleIcon;
break;
case SecretSyncStatus.Succeeded:
variant = "success";
text = "Synced";
icon = faCheck;
Icon = CheckIcon;
break;
case SecretSyncStatus.Pending:
variant = "primary";
variant = "info";
text = "Queued";
icon = faHourglass;
Icon = HourglassIcon;
break;
case SecretSyncStatus.Running:
default:
variant = "primary";
variant = "info";
text = "Syncing";
icon = faRotate;
Icon = RefreshCwIcon;
break;
}
return (
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap" variant={variant}>
<FontAwesomeIcon
icon={icon}
className={[SecretSyncStatus.Running].includes(status) ? "animate-spin" : ""}
/>
<span>{text}</span>
<Badge variant={variant}>
<Icon className={[SecretSyncStatus.Running].includes(status) ? "animate-spin" : ""} />
{text}
</Badge>
);
};
@@ -8,13 +8,14 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Switch } from "@app/components/v2";
import { useProject } from "@app/context";
import { useOrganization, useProject } from "@app/context";
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import {
SecretSync,
SecretSyncInitialSyncBehavior,
TSecretSync,
useCreateSecretSync,
useDuplicateDestinationCheck,
useSecretSyncOption
} from "@app/hooks/api/secretSyncs";
@@ -48,6 +49,7 @@ export const CreateSecretSyncForm = ({
}: Props) => {
const createSecretSync = useCreateSecretSync();
const { currentProject } = useProject();
const { currentOrg } = useOrganization();
const { name: destinationName } = SECRET_SYNC_MAP[destination];
const [showConfirmation, setShowConfirmation] = useState(false);
@@ -106,11 +108,20 @@ export const CreateSecretSyncForm = ({
setSelectedTabIndex((prev) => prev - 1);
};
const { handleSubmit, trigger, control } = formMethods;
const { handleSubmit, trigger, control, watch } = formMethods;
const { hasDuplicate } = useDuplicateDestinationCheck({
destination,
projectId: currentProject?.id || "",
enabled: true,
destinationConfig: watch("destinationConfig")
});
const isStepValid = async (index: number) => trigger(FORM_TABS[index].fields);
const isFinalStep = selectedTabIndex === FORM_TABS.length - 1;
const isCreateButtonDisabled =
isFinalStep && hasDuplicate && currentOrg?.blockDuplicateSecretSyncDestinations;
const handleNext = async () => {
if (isFinalStep) {
@@ -245,7 +256,7 @@ export const CreateSecretSyncForm = ({
</FormProvider>
<div className="flex w-full flex-row-reverse justify-between gap-4 pt-4">
<Button onClick={handleNext} colorSchema="secondary">
<Button onClick={handleNext} colorSchema="secondary" isDisabled={isCreateButtonDisabled}>
{isFinalStep ? "Create Sync" : "Next"}
</Button>
{selectedTabIndex > 0 && (
@@ -6,6 +6,7 @@ type Props = {
onConfirm: () => void;
isLoading?: boolean;
duplicateProjectId?: string;
isDisabled?: boolean;
};
export const DuplicateDestinationConfirmationModal = ({
@@ -13,7 +14,8 @@ export const DuplicateDestinationConfirmationModal = ({
onOpenChange,
onConfirm,
isLoading,
duplicateProjectId
duplicateProjectId,
isDisabled
}: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
@@ -21,7 +23,12 @@ export const DuplicateDestinationConfirmationModal = ({
<div className="mb-4 text-sm">
<p>
Another secret sync in your organization is already configured with the same
destination. Proceeding may cause conflicts or overwrite existing data.
destination.{" "}
<span className={isDisabled ? "text-red-400" : ""}>
{isDisabled
? "Your organization does not allow duplicate destination configurations."
: "Proceeding may cause conflicts or overwrite existing data."}
</span>
</p>
{duplicateProjectId && (
<p className="mt-2 text-xs text-mineshaft-400">
@@ -31,26 +38,28 @@ export const DuplicateDestinationConfirmationModal = ({
</code>
</p>
)}
<p className="mt-2">Are you sure you want to continue?</p>
{!isDisabled && <p className="mt-2">Are you sure you want to continue?</p>}
</div>
<div className="flex items-center gap-4 pt-4">
<ModalClose asChild>
<Button
onClick={onConfirm}
colorSchema="danger"
isLoading={isLoading}
isDisabled={isLoading}
>
Continue
</Button>
</ModalClose>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain" isDisabled={isLoading}>
Cancel
</Button>
</ModalClose>
</div>
{!isDisabled && (
<div className="flex items-center gap-4 pt-4">
<ModalClose asChild>
<Button
onClick={onConfirm}
colorSchema="danger"
isLoading={isLoading}
isDisabled={isLoading}
>
Continue
</Button>
</ModalClose>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain" isDisabled={isLoading}>
Cancel
</Button>
</ModalClose>
</div>
)}
</ModalContent>
</Modal>
);
@@ -5,6 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { createNotification } from "@app/components/notifications";
import { SecretSyncEditFields } from "@app/components/secret-syncs/types";
import { Button, ModalClose } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import {
TSecretSync,
@@ -30,6 +31,7 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) =>
const { name: destinationName } = SECRET_SYNC_MAP[secretSync.destination];
const [showDuplicateConfirmation, setShowDuplicateConfirmation] = useState(false);
const [pendingFormData, setPendingFormData] = useState<TSecretSyncForm | null>(null);
const { currentOrg } = useOrganization();
const formMethods = useForm<TSecretSyncForm>({
resolver: zodResolver(UpdateSecretSyncFormSchema),
@@ -209,6 +211,7 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) =>
onConfirm={handleConfirmDuplicate}
isLoading={updateSecretSync.isPending}
duplicateProjectId={storedDuplicateProjectId}
isDisabled={currentOrg?.blockDuplicateSecretSyncDestinations}
/>
</>
);
@@ -5,14 +5,8 @@ import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import {
Badge,
FilterableSelect,
FormControl,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { GCP_SYNC_SCOPES } from "@app/helpers/secretSyncs";
import {
useGcpConnectionListProjectLocations,
@@ -26,10 +20,7 @@ import { TSecretSyncForm } from "../schemas";
const formatOptionLabel = ({ displayName, locationId }: TGcpLocation) => (
<div className="flex w-full flex-row items-center gap-1">
<span>{displayName}</span>{" "}
<Badge className="h-5 leading-5" variant="success">
{locationId}
</Badge>
<span>{displayName}</span> <Badge variant="info">{locationId}</Badge>
</div>
);
@@ -2,7 +2,8 @@ import { components, OptionProps, SingleValue } from "react-select";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge, FilterableSelect } from "@app/components/v2";
import { FilterableSelect } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { AWS_REGIONS } from "@app/helpers/appConnections";
const Option = ({ isSelected, children, ...props }: OptionProps<(typeof AWS_REGIONS)[number]>) => {
@@ -10,7 +11,7 @@ const Option = ({ isSelected, children, ...props }: OptionProps<(typeof AWS_REGI
<components.Option isSelected={isSelected} {...props}>
<div className="flex flex-row items-center justify-between">
<p className="truncate">{children}</p>
<Badge variant="success" className="mr-auto ml-1 cursor-pointer">
<Badge variant="neutral" className="mr-auto ml-1">
{props.data.slug}
</Badge>
{isSelected && (
@@ -1,10 +1,10 @@
import { useFormContext } from "react-hook-form";
import { faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { EyeIcon } from "lucide-react";
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { Badge, Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
import { Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { AWS_REGIONS } from "@app/helpers/appConnections";
import { SecretSync } from "@app/hooks/api/secretSyncs";
@@ -41,11 +41,9 @@ export const AwsParameterStoreSyncOptionsReviewFields = () => {
}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>
{tags.length} Tag{tags.length > 1 ? "s" : ""}
</span>
<Badge variant="neutral">
<EyeIcon />
{tags.length} Tag{tags.length > 1 ? "s" : ""}
</Badge>
</div>
</Tooltip>
@@ -1,10 +1,10 @@
import { useFormContext } from "react-hook-form";
import { faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { EyeIcon } from "lucide-react";
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { Badge, Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
import { Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { AWS_REGIONS } from "@app/helpers/appConnections";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs/types/aws-secrets-manager-sync";
@@ -26,7 +26,7 @@ export const AwsSecretsManagerSyncReviewFields = () => {
<>
<GenericFieldLabel label="Region">
{awsRegion?.name}
<Badge className="ml-1" variant="success">
<Badge className="ml-1" variant="info">
{awsRegion?.slug}{" "}
</Badge>
</GenericFieldLabel>
@@ -73,11 +73,9 @@ export const AwsSecretsManagerSyncOptionsReviewFields = () => {
}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 bg-mineshaft-400/50 whitespace-nowrap text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>
{tags.length} Tag{tags.length > 1 ? "s" : ""}
</span>
<Badge variant="neutral">
<EyeIcon />
{tags.length} Tag{tags.length > 1 ? "s" : ""}
</Badge>
</div>
</Tooltip>
@@ -2,7 +2,7 @@ import { useFormContext } from "react-hook-form";
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { Badge } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { RenderSyncScope } from "@app/hooks/api/secretSyncs/types/render-sync";
@@ -5,8 +5,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { Badge } from "@app/components/v2";
import { useProject } from "@app/context";
import { Badge } from "@app/components/v3";
import { useOrganization, useProject } from "@app/context";
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
import { SecretSync, useDuplicateDestinationCheck } from "@app/hooks/api/secretSyncs";
@@ -51,6 +51,7 @@ import { ZabbixSyncReviewFields } from "./ZabbixSyncReviewFields";
export const SecretSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm>();
const { currentProject } = useProject();
const { currentOrg } = useOrganization();
let DestinationFieldsComponent: ReactNode;
let AdditionalSyncOptionsFieldsComponent: ReactNode;
@@ -193,18 +194,50 @@ export const SecretSyncReviewFields = () => {
{isChecking && <span className="text-xs text-mineshaft-400">Checking...</span>}
</div>
{hasDuplicate && (
<div className="mb-2 flex items-start rounded-md border border-yellow-600 bg-yellow-900/20 px-3 py-2">
<div className="flex text-sm text-yellow-100">
<FontAwesomeIcon icon={faWarning} className="mt-1 mr-2 text-yellow-600" />
<div
className={`mb-2 flex items-start rounded-md border px-3 py-2 ${
currentOrg?.blockDuplicateSecretSyncDestinations
? "border-red-600 bg-red-900/20"
: "border-yellow-600 bg-yellow-900/20"
}`}
>
<div
className={`flex text-sm ${
currentOrg?.blockDuplicateSecretSyncDestinations
? "text-red-100"
: "text-yellow-100"
}`}
>
<FontAwesomeIcon
icon={faWarning}
className={`mt-1 mr-2 ${
currentOrg?.blockDuplicateSecretSyncDestinations
? "text-red-600"
: "text-yellow-600"
}`}
/>
<div>
<p>
Another secret sync in your organization is already configured with the same
destination. This may lead to conflicts or unexpected behavior.
{currentOrg?.blockDuplicateSecretSyncDestinations
? "Another secret sync in your organization is already configured with the same destination. Your organization does not allow duplicate destination configurations."
: "Another secret sync in your organization is already configured with the same destination. This may lead to conflicts or unexpected behavior."}
</p>
{duplicateProjectId && (
<p className="mt-1 text-xs text-yellow-200">
<p
className={`mt-1 text-xs ${
currentOrg?.blockDuplicateSecretSyncDestinations
? "text-red-200"
: "text-yellow-200"
}`}
>
Duplicate found in project ID:{" "}
<code className="rounded-sm bg-yellow-800/50 px-1 py-0.5">
<code
className={`rounded-sm px-1 py-0.5 ${
currentOrg?.blockDuplicateSecretSyncDestinations
? "bg-red-800/50"
: "bg-yellow-800/50"
}`}
>
{duplicateProjectId}
</code>
</p>
@@ -224,7 +257,7 @@ export const SecretSyncReviewFields = () => {
</div>
<div className="flex flex-wrap gap-x-8 gap-y-2">
<GenericFieldLabel label="Auto-Sync">
<Badge variant={isAutoSyncEnabled ? "success" : "danger"}>
<Badge variant={isAutoSyncEnabled ? "success" : "neutral"}>
{isAutoSyncEnabled ? "Enabled" : "Disabled"}
</Badge>
</GenericFieldLabel>
@@ -235,7 +268,7 @@ export const SecretSyncReviewFields = () => {
{AdditionalSyncOptionsFieldsComponent}
{disableSecretDeletion && (
<GenericFieldLabel label="Secret Deletion">
<Badge variant="primary">Disabled</Badge>
<Badge variant="warning">Disabled</Badge>
</GenericFieldLabel>
)}
</div>
@@ -1,47 +0,0 @@
import { forwardRef } from "react";
import { cva, VariantProps } from "cva";
import { twMerge } from "tailwind-merge";
interface IProps {
children: React.ReactNode;
className?: string;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
}
const badgeVariants = cva(
[
"inline-block cursor-default rounded-md bg-yellow/20 px-1.5 py-0.5 text-xs text-yellow opacity-80 hover:opacity-100"
],
{
variants: {
variant: {
primary: "bg-yellow/20 text-yellow",
danger: "bg-red/20 text-red",
success: "bg-green/20 text-green",
org: "bg-org-v1/20 text-org-v1 [&_svg]:text-org-v1 flex items-center opacity-100 hover:bg-org-v1/10 [&_svg]:size-3 gap-x-1 w-min whitespace-nowrap",
namespace:
"bg-namespace-v1/20 text-namespace-v1 [&_svg]:text-namespace-v1 flex opacity-100 hover:bg-namespace-v1/10 items-center [&_svg]:size-3.5 gap-x-1.5 w-min whitespace-nowrap",
project:
"bg-primary/10 text-primary [&_svg]:text-primary opacity-100 hover:bg-primary/10 flex items-center [&_svg]:size-3 w-min gap-x-1.5 whitespace-nowrap",
instance:
"bg-mineshaft-200/20 text-mineshaft-200 [&_svg]:text-mineshaft-200 opacity-100 hover:bg-mineshaft-200/20 flex items-center [&_svg]:size-3 gap-x-1.5 w-min whitespace-nowrap"
}
}
}
);
export type BadgeProps = VariantProps<typeof badgeVariants> & IProps;
export const Badge = forwardRef<HTMLDivElement, BadgeProps>(
({ children, className, variant, ...props }, ref) => {
return (
<div
className={twMerge(badgeVariants({ variant: variant || "primary" }), className)}
{...props}
ref={ref}
>
{children}
</div>
);
}
);
@@ -1 +0,0 @@
export { Badge } from "./Badge";
@@ -1,11 +1,16 @@
import { IconDefinition } from "@fortawesome/free-brands-svg-icons";
import { faCube, faCubes, faGlobe, faServer } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createElement } from "react";
import { ReactNode } from "@tanstack/react-router";
import { LucideIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { Badge } from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge";
import {
Badge,
InstanceIcon,
OrgIcon,
ProjectIcon,
SubOrgIcon,
TBadgeProps
} from "@app/components/v3";
import { ProjectType } from "@app/hooks/api/projects/types";
type Props = {
@@ -16,19 +21,19 @@ type Props = {
scope: "org" | "namespace" | "instance" | ProjectType | null;
};
const SCOPE_NAME: Record<NonNullable<Props["scope"]>, { label: string; icon: IconDefinition }> = {
org: { label: "Organization", icon: faGlobe },
[ProjectType.SecretManager]: { label: "Project", icon: faCube },
[ProjectType.CertificateManager]: { label: "Project", icon: faCube },
[ProjectType.SSH]: { label: "Project", icon: faCube },
[ProjectType.KMS]: { label: "Project", icon: faCube },
[ProjectType.PAM]: { label: "Project", icon: faCube },
[ProjectType.SecretScanning]: { label: "Project", icon: faCube },
namespace: { label: "Sub-Organization", icon: faCubes },
instance: { label: "Server", icon: faServer }
const SCOPE_NAME: Record<NonNullable<Props["scope"]>, { label: string; icon: LucideIcon }> = {
org: { label: "Organization", icon: OrgIcon },
[ProjectType.SecretManager]: { label: "Project", icon: ProjectIcon },
[ProjectType.CertificateManager]: { label: "Project", icon: ProjectIcon },
[ProjectType.SSH]: { label: "Project", icon: ProjectIcon },
[ProjectType.KMS]: { label: "Project", icon: ProjectIcon },
[ProjectType.PAM]: { label: "Project", icon: ProjectIcon },
[ProjectType.SecretScanning]: { label: "Project", icon: ProjectIcon },
namespace: { label: "Sub-Organization", icon: SubOrgIcon },
instance: { label: "Server", icon: InstanceIcon }
};
const SCOPE_VARIANT: Record<NonNullable<Props["scope"]>, BadgeProps["variant"]> = {
const SCOPE_VARIANT: Record<NonNullable<Props["scope"]>, TBadgeProps["variant"]> = {
org: "org",
[ProjectType.SecretManager]: "project",
[ProjectType.CertificateManager]: "project",
@@ -36,8 +41,8 @@ const SCOPE_VARIANT: Record<NonNullable<Props["scope"]>, BadgeProps["variant"]>
[ProjectType.KMS]: "project",
[ProjectType.PAM]: "project",
[ProjectType.SecretScanning]: "project",
namespace: "namespace",
instance: "instance"
namespace: "sub-org",
instance: "neutral"
};
export const PageHeader = ({ title, description, children, className, scope }: Props) => (
@@ -47,7 +52,7 @@ export const PageHeader = ({ title, description, children, className, scope }: P
<h1 className="text-3xl font-medium text-white capitalize">{title}</h1>
{scope && (
<Badge variant={SCOPE_VARIANT[scope]} className="mt-1 ml-2.5">
<FontAwesomeIcon icon={SCOPE_NAME[scope].icon} />
{createElement(SCOPE_NAME[scope].icon)}
{SCOPE_NAME[scope].label}
</Badge>
)}
@@ -3,6 +3,7 @@ import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { faCaretDown, faCaretUp, faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as SelectPrimitive from "@radix-ui/react-select";
import { LucideIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { Spinner } from "../Spinner";
@@ -19,6 +20,7 @@ type Props = {
icon?: IconProp;
isMulti?: boolean;
iconClassName?: string;
LucideIcon?: LucideIcon;
dropdownContainerStyle?: React.CSSProperties;
side?: SelectPrimitive.SelectContentProps["side"];
};
@@ -39,6 +41,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
iconClassName,
dropdownContainerStyle,
side,
LucideIcon: Lucide,
...props
},
ref
@@ -65,6 +68,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
>
<div className="flex items-center space-x-2 overflow-hidden text-ellipsis whitespace-nowrap">
{props.icon && <FontAwesomeIcon icon={props.icon} className={iconClassName} />}
{Lucide && <Lucide className={twMerge("size-3.5", iconClassName)} />}
<div className="flex-1 truncate">
<SelectPrimitive.Value placeholder={placeholder} />
</div>
-1
View File
@@ -2,7 +2,6 @@
export * from "./AccessRestrictedBanner";
export * from "./Accordion";
export * from "./Alert";
export * from "./Badge";
export * from "./Breadcrumb";
export * from "./Button";
export * from "./Card";
@@ -1,9 +1,9 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Link } from "@tanstack/react-router";
import {
AlertTriangleIcon,
AsteriskIcon,
BanIcon,
BoxesIcon,
BoxIcon,
CheckIcon,
ChevronsUpDownIcon,
CircleXIcon,
@@ -11,9 +11,11 @@ import {
GlobeIcon,
InfoIcon,
RadarIcon,
TriangleAlertIcon
TriangleAlertIcon,
UserIcon
} from "lucide-react";
import { OrgIcon, ProjectIcon, SubOrgIcon } from "../../platform";
import { Badge } from "./Badge";
/**
@@ -69,7 +71,29 @@ export const Neutral: Story = {
parameters: {
docs: {
description: {
story: "Use this variant when indicating neutral or disabled states."
story:
"Use this variant when indicating neutral or disabled states or when linking to external documents."
}
}
}
};
export const Ghost: Story = {
name: "Variant: Ghost",
args: {
variant: "ghost",
children: (
<>
<UserIcon />
User
</>
)
},
parameters: {
docs: {
description: {
story:
"Use this variant when indicating a configuration or property value. Avoid using this variant as an interactive element as it is not intuitive to interact with."
}
}
}
@@ -109,8 +133,7 @@ export const Info: Story = {
parameters: {
docs: {
description: {
story:
"Use this variant when indicating informational states or linking to external references."
story: "Use this variant when indicating informational states."
}
}
}
@@ -162,7 +185,7 @@ export const Organization: Story = {
variant: "org",
children: (
<>
<GlobeIcon />
<OrgIcon />
Organization
</>
)
@@ -182,7 +205,7 @@ export const SubOrganization: Story = {
variant: "sub-org",
children: (
<>
<BoxesIcon />
<SubOrgIcon />
Sub-Organization
</>
)
@@ -202,7 +225,7 @@ export const Project: Story = {
variant: "project",
children: (
<>
<BoxIcon />
<ProjectIcon />
Project
</>
)
@@ -283,10 +306,11 @@ export const AsButton: Story = {
export const IsTruncatable: Story = {
name: "Example: isTruncatable",
args: {
variant: "org",
isTruncatable: true,
children: (
<>
<GlobeIcon />
<OrgIcon />
<span>Infisical Infrastructure</span>
</>
)
@@ -295,7 +319,7 @@ export const IsTruncatable: Story = {
docs: {
description: {
story:
"Use the `isTruncatable` prop with a `span` tag wrapping the text content to support truncation."
"Use the `isTruncatable` prop with a `span` tag wrapping the text content to support truncation. Parent `div` should have a fixed width and `flex` class."
}
}
},
@@ -305,3 +329,48 @@ export const IsTruncatable: Story = {
</div>
)
};
export const IsSquare: Story = {
name: "Example: isSquare",
args: {
variant: "danger",
isSquare: true,
children: <AlertTriangleIcon />
},
parameters: {
docs: {
description: {
story:
"Use the `isSquare` prop when displaying a squared badge with 1-2 character text or only an icon."
}
}
}
};
export const IsFullWidth: Story = {
name: "Example: isFullWidth",
args: {
variant: "neutral",
isFullWidth: true,
children: (
<>
<AsteriskIcon />
Secret Value
</>
)
},
parameters: {
docs: {
description: {
story:
"Use the `isFullWidth` prop to expand the badges width to fill it's parent container."
}
}
},
decorators: (Story) => (
<div className="w-32">
<Story />
</div>
)
};
@@ -6,30 +6,37 @@ import { cn } from "@app/components/v3/utils";
const badgeVariants = cva(
[
"select-none items-center rounded-sm px-1.5 py-0.5 text-xs",
"gap-x-1 [a&,button&]:cursor-pointer inline-flex",
"[&>svg]:pointer-events-none [&>svg]:shrink-0 [&>svg]:stroke-[2.25] [&>svg]:size-3",
"select-none items-center align-middle rounded-sm h-4.5 px-1.5 text-xs",
"gap-x-1 [a&,button&]:cursor-pointer inline-flex font-normal",
"[&>svg]:pointer-events-none [&>svg]:shrink-0 [&>svg]:stroke-[2.25] [&_svg:not([class*='size-'])]:size-3",
"transition duration-200 ease-in-out"
],
{
variants: {
isTruncatable: {
true: "[&>span,&>p]:truncate min-w-0",
false: "w-fit shrink-0 whitespace-nowrap overflow-hidden"
false: "w-fit shrink-0 min-w-fit whitespace-nowrap overflow-hidden"
},
isFullWidth: {
true: "w-full justify-center"
},
isSquare: {
true: "w-4.5 justify-center px-0.5"
},
variant: {
neutral: "bg-neutral/30 text-neutral [a&,button&]:hover:bg-neutral/40",
success: "bg-success/30 text-success [a&,button&]:hover:bg-success/40",
info: "bg-info/30 text-info [a&,button&]:hover:bg-info/40",
warning: "bg-warning/30 text-warning [a&,button&]:hover:bg-warning/40",
danger: "bg-danger/30 text-danger [a&,button&]:hover:bg-danger/40",
project: "bg-project/30 text-project [a&,button&]:hover:bg-project/40",
org: "bg-org/30 text-org [a&,button&]:hover:bg-org/40",
"sub-org": "bg-sub-org/30 text-sub-org [a&,button&]:hover:bg-sub-org/40"
ghost: "text-mineshaft-200 gap-x-2",
neutral: "bg-neutral/25 text-neutral [a&,button&]:hover:bg-neutral/35",
success: "bg-success/25 text-success [a&,button&]:hover:bg-success/35",
info: "bg-info/25 text-info [a&,button&]:hover:bg-info/35",
warning: "bg-warning/25 text-warning [a&,button&]:hover:bg-warning/35",
danger: "bg-danger/25 text-danger [a&,button&]:hover:bg-danger/35",
project: "bg-project/25 text-project [a&,button&]:hover:bg-project/35",
org: "bg-org/25 text-org [a&,button&]:hover:bg-org/35",
"sub-org": "bg-sub-org/25 text-sub-org [a&,button&]:hover:bg-sub-org/35"
}
},
defaultVariants: {
variant: "success"
variant: "neutral"
}
}
);
@@ -37,16 +44,28 @@ const badgeVariants = cva(
type TBadgeProps = VariantProps<typeof badgeVariants> &
React.ComponentProps<"span"> & {
asChild?: boolean;
variant: NonNullable<VariantProps<typeof badgeVariants>["variant"]>; // TODO: REMOVE
};
const Badge = forwardRef<HTMLSpanElement, TBadgeProps>(
({ className, variant, asChild = false, isTruncatable = false, ...props }, ref): JSX.Element => {
(
{
className,
variant,
asChild = false,
isTruncatable = false,
isFullWidth = false,
isSquare = false,
...props
},
ref
): JSX.Element => {
const Comp = asChild ? Slot : "span";
return (
<Comp
ref={ref}
data-slot="badge"
className={cn(badgeVariants({ variant, isTruncatable }), className)}
className={cn(badgeVariants({ variant, isTruncatable, isFullWidth, isSquare }), className)}
{...props}
/>
);
+2
View File
@@ -0,0 +1,2 @@
export * from "./generic";
export * from "./platform";
@@ -0,0 +1,24 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { DocumentationLinkBadge } from "./DocumentationLinkBadge";
/**
* The documentation link badge is a re-usable component to link to Infisical documentation references.
*/
const meta = {
title: "Platform/Documentation Link Badge",
component: DocumentationLinkBadge,
parameters: {
layout: "centered"
},
tags: ["autodocs"],
argTypes: {},
args: { href: "https://infisical.com/docs/documentation/getting-started/introduction" }
} satisfies Meta<typeof DocumentationLinkBadge>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
name: "Example: Default"
};
@@ -0,0 +1,18 @@
import { BookOpenIcon } from "lucide-react";
import { Badge } from "@app/components/v3";
type TDocumentationLinkBadgeProps = {
href: string;
};
export function DocumentationLinkBadge({ href }: TDocumentationLinkBadgeProps) {
return (
<Badge variant="neutral" asChild>
<a href={href} target="_blank" rel="noopener noreferrer">
<BookOpenIcon />
Documentation
</a>
</Badge>
);
}
@@ -0,0 +1 @@
export * from "./DocumentationLinkBadge";
@@ -0,0 +1,8 @@
import { BoxesIcon, BoxIcon, Building2Icon, ServerIcon } from "lucide-react";
const InstanceIcon = ServerIcon;
const OrgIcon = Building2Icon;
const SubOrgIcon = BoxesIcon;
const ProjectIcon = BoxIcon;
export { InstanceIcon, OrgIcon, ProjectIcon, SubOrgIcon };
@@ -0,0 +1,2 @@
export * from "./DocumentationLinkBadge";
export * from "./ScopeIcons";
-4
View File
@@ -301,10 +301,6 @@ export const ROUTE_PATHS = Object.freeze({
"/projects/cert-management/$projectId/subscribers",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers"
),
CertificatesPage: setRoute(
"/projects/cert-management/$projectId/certificates",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates"
),
CertificateAuthoritiesPage: setRoute(
"/projects/cert-management/$projectId/certificate-authorities",
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities"
+7 -7
View File
@@ -1,20 +1,20 @@
import { IconDefinition } from "@fortawesome/free-brands-svg-icons";
import { faArrowRightToBracket, faEdit } from "@fortawesome/free-solid-svg-icons";
import { LucideIcon, UserCheckIcon, UserPenIcon } from "lucide-react";
import { TBadgeProps } from "@app/components/v3";
import { PolicyType } from "@app/hooks/api/policies/enums";
export const policyDetails: Record<
PolicyType,
{ name: string; className: string; icon: IconDefinition }
{ name: string; variant: TBadgeProps["variant"]; Icon: LucideIcon }
> = {
[PolicyType.AccessPolicy]: {
className: "bg-green/20 text-green",
variant: "ghost",
name: "Access Policy",
icon: faArrowRightToBracket
Icon: UserCheckIcon
},
[PolicyType.ChangePolicy]: {
className: "bg-yellow/20 text-yellow",
variant: "ghost",
name: "Change Policy",
icon: faEdit
Icon: UserPenIcon
}
};
+24 -12
View File
@@ -1,10 +1,6 @@
import {
faBan,
faCheck,
faMagnifyingGlassMinus,
faWarning
} from "@fortawesome/free-solid-svg-icons";
import { AlertTriangleIcon, BanIcon, CheckIcon, LucideIcon, SearchSlashIcon } from "lucide-react";
import { TBadgeProps } from "@app/components/v3";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
SecretScanningDataSource,
@@ -74,12 +70,28 @@ export const RESOURCE_DESCRIPTION_HELPER: Record<
}
};
export const SECRET_SCANNING_FINDING_STATUS_ICON_MAP = {
[SecretScanningFindingStatus.Resolved]: { icon: faCheck, className: "text-green" },
[SecretScanningFindingStatus.Unresolved]: { icon: faWarning, className: "text-yellow" },
[SecretScanningFindingStatus.Ignore]: { icon: faBan, className: "text-mineshaft-400" },
export const SECRET_SCANNING_FINDING_STATUS_MAP: Record<
SecretScanningFindingStatus,
{ Icon: LucideIcon; variant: TBadgeProps["variant"]; className: string }
> = {
[SecretScanningFindingStatus.Resolved]: {
Icon: CheckIcon,
variant: "success",
className: "text-success"
},
[SecretScanningFindingStatus.Unresolved]: {
Icon: AlertTriangleIcon,
variant: "warning",
className: "text-warning"
},
[SecretScanningFindingStatus.Ignore]: {
Icon: BanIcon,
variant: "neutral",
className: "text-neutral"
},
[SecretScanningFindingStatus.FalsePositive]: {
icon: faMagnifyingGlassMinus,
className: "text-mineshaft-400"
Icon: SearchSlashIcon,
variant: "neutral",
className: "text-neutral"
}
};
@@ -10,5 +10,6 @@ export type TGitLabGroup = {
export enum GitLabAccessTokenType {
Personal = "personal",
Project = "project"
Project = "project",
Group = "group"
}
+1 -1
View File
@@ -53,6 +53,6 @@ export const getCaStatusBadgeVariant = (status: CaStatus | SshCaStatus | SshCert
case CaStatus.DISABLED:
return "danger";
default:
return "primary";
return "warning";
}
};
+34 -1
View File
@@ -5,7 +5,8 @@ import { apiRequest } from "@app/config/request";
import {
ExternalMigrationProviders,
TVaultExternalMigrationConfig,
VaultKubernetesAuthRole
VaultKubernetesAuthRole,
VaultKubernetesRole
} from "./types";
export const externalMigrationQueryKeys = {
@@ -31,6 +32,11 @@ export const externalMigrationQueryKeys = {
"vault-kubernetes-auth-roles",
namespace,
mountPath
],
vaultKubernetesRoles: (namespace?: string, mountPath?: string) => [
"vault-kubernetes-roles",
namespace,
mountPath
]
};
@@ -172,3 +178,30 @@ export const useGetVaultKubernetesAuthRoles = (
enabled: enabled && !!namespace && !!mountPath
});
};
export const useGetVaultKubernetesRoles = (
enabled = true,
namespace?: string,
mountPath?: string
) => {
return useQuery({
queryKey: externalMigrationQueryKeys.vaultKubernetesRoles(namespace, mountPath),
queryFn: async () => {
if (!namespace || !mountPath) {
throw new Error("Both namespace and mountPath are required");
}
const { data } = await apiRequest.get<{
roles: VaultKubernetesRole[];
}>("/api/v3/external-migration/vault/kubernetes-roles", {
params: {
namespace,
mountPath
}
});
return data.roles;
},
enabled: enabled && !!namespace && !!mountPath
});
};
+21
View File
@@ -49,3 +49,24 @@ export type VaultKubernetesAuthRole = {
disable_local_ca_jwt?: boolean;
};
};
export type VaultKubernetesRole = {
name: string;
mountPath: string;
allowed_kubernetes_namespaces?: string[];
allowed_kubernetes_namespace_selector?: string;
token_max_ttl?: number;
token_default_ttl?: number;
token_default_audiences?: string[];
service_account_name?: string;
kubernetes_role_name?: string;
kubernetes_role_type?: string;
generated_role_rules?: string;
name_template?: string;
extra_annotations?: Record<string, string>;
extra_labels?: Record<string, string>;
config: {
kubernetes_host: string;
kubernetes_ca_cert?: string;
};
};
@@ -125,7 +125,8 @@ export const useUpdateOrg = () => {
scannerProductEnabled,
shareSecretsProductEnabled,
maxSharedSecretLifetime,
maxSharedSecretViewLimit
maxSharedSecretViewLimit,
blockDuplicateSecretSyncDestinations
}) => {
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
name,
@@ -146,7 +147,8 @@ export const useUpdateOrg = () => {
scannerProductEnabled,
shareSecretsProductEnabled,
maxSharedSecretLifetime,
maxSharedSecretViewLimit
maxSharedSecretViewLimit,
blockDuplicateSecretSyncDestinations
});
},
onSuccess: () => {
@@ -29,6 +29,7 @@ export type Organization = {
shareSecretsProductEnabled: boolean;
maxSharedSecretLifetime: number;
maxSharedSecretViewLimit: number | null;
blockDuplicateSecretSyncDestinations: boolean;
};
export type UpdateOrgDTO = {
@@ -52,6 +53,7 @@ export type UpdateOrgDTO = {
shareSecretsProductEnabled?: boolean;
maxSharedSecretViewLimit?: number | null;
maxSharedSecretLifetime?: number;
blockDuplicateSecretSyncDestinations?: boolean;
};
export type BillingDetails = {
@@ -15,6 +15,6 @@ export const getPkiSubscriberStatusBadgeVariant = (status: PkiSubscriberStatus)
case PkiSubscriberStatus.DISABLED:
return "danger";
default:
return "primary";
return "warning";
}
};
@@ -7,15 +7,12 @@ import {
faCaretDown,
faCheck,
faChevronRight,
faCubes,
faEnvelope,
faExclamationTriangle,
faGlobe,
faInfinity,
faInfo,
faInfoCircle,
faPlus,
faServer,
faSignOut,
faToolbox,
faUser,
@@ -30,7 +27,6 @@ import { Mfa } from "@app/components/auth/Mfa";
import { createNotification } from "@app/components/notifications";
import SecurityClient from "@app/components/utilities/SecurityClient";
import {
Badge,
BreadcrumbContainer,
Button,
DropdownMenu,
@@ -46,6 +42,7 @@ import {
TBreadcrumbFormat,
Tooltip
} from "@app/components/v2";
import { Badge, InstanceIcon, OrgIcon, SubOrgIcon } from "@app/components/v3";
import { envConfig } from "@app/config/env";
import { useOrganization, useSubscription, useUser } from "@app/context";
import { isInfisicalCloud } from "@app/helpers/platform";
@@ -271,9 +268,7 @@ export const Navbar = () => {
to="/admin"
className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary"
>
<div>
<FontAwesomeIcon icon={faServer} className="text-xs text-bunker-300" />
</div>
<InstanceIcon className="size-3.5 text-xs text-bunker-300" />
<div className="whitespace-nowrap">Server Console</div>
</Link>
<p className="pr-3 pl-3 text-lg text-mineshaft-400/70">/</p>
@@ -288,24 +283,31 @@ export const Navbar = () => {
<DropdownMenu modal={false} open={isOrgSelectOpen} onOpenChange={setIsOrgSelectOpen}>
<div className="group flex cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100 hover:text-primary">
<Badge
onClick={async () => {
navigate({
to: "/organization/projects",
search: (search) => ({ ...search, subOrganization: undefined })
});
if (isSubOrganization) {
await router.invalidate({ sync: true }).catch(() => null);
}
}}
asChild
variant="org"
isTruncatable
// TODO(scott): either add badge size/style variant or create designated component for namespace/org nav bar
className={twMerge(
"max-w-full min-w-0 cursor-pointer text-sm",
"gap-x-1.5 text-sm",
(!isOrgScope || isSubOrganization) &&
"bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline"
"bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-org"
)}
>
<FontAwesomeIcon icon={faGlobe} />
<p className="truncate">{currentOrg?.name}</p>
<button
type="button"
onClick={async () => {
navigate({
to: "/organization/projects",
search: (search) => ({ ...search, subOrganization: undefined })
});
if (isSubOrganization) {
await router.invalidate({ sync: true }).catch(() => null);
}
}}
>
<OrgIcon className="size-[12px]" />
<span>{currentOrg?.name}</span>
</button>
</Badge>
<div className="mr-1 rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline!">
{getPlan(subscription)}
@@ -443,19 +445,22 @@ export const Navbar = () => {
<>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
<DropdownMenu modal={false}>
<Link className="overflow-hidden" to="/organization/projects">
<Badge
variant="namespace"
className={twMerge(
"flex max-w-full min-w-0 cursor-pointer text-sm",
!isOrgScope &&
"bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline"
)}
>
<FontAwesomeIcon icon={faCubes} />
<p className="truncate">{currentOrg.subOrganization.name}</p>
</Badge>
</Link>
<Badge
asChild
isTruncatable
variant="sub-org"
// TODO(scott): either add badge size/style variant or create designated component for namespace/org nav bar
className={twMerge(
"gap-x-1.5 text-sm",
!isOrgScope &&
"bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-sub-org"
)}
>
<Link to="/organization/projects">
<SubOrgIcon className="size-[12px]" />
<span>{currentOrg.subOrganization.name}</span>
</Link>
</Badge>
<DropdownMenuTrigger asChild>
<div>
<IconButton
@@ -549,10 +554,10 @@ export const Navbar = () => {
)}
{user.superAdmin && !location.pathname.startsWith("/admin") && (
<Link
className="mr-2 rounded-md border border-mineshaft-500 px-2.5 py-1.5 text-sm whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
className="mr-2 flex items-center rounded-md border border-mineshaft-500 px-2.5 py-1.5 text-sm whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
to="/admin"
>
<FontAwesomeIcon icon={faServer} className="mr-2" />
<InstanceIcon className="mr-2 inline-block size-3.5" />
Server Console
</Link>
)}
+6 -2
View File
@@ -18,7 +18,10 @@ export const PamLayout = () => {
useEffect(() => {
if (subscription && !subscription.pam) {
handlePopUpOpen("upgradePlan");
handlePopUpOpen("upgradePlan", {
description: "You can use PAM if you switch to Infisical's Enterprise plan.",
isEnterpriseFeature: true
});
}
}, [subscription]);
@@ -111,7 +114,8 @@ export const PamLayout = () => {
onOpenChange={(isOpen) => {
handlePopUpToggle("upgradePlan", isOpen);
}}
text="You can use PAM if you switch to a paid Infisical plan."
text={popUp.upgradePlan.data?.description}
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
/>
</>
);
@@ -3,7 +3,6 @@ import { faStar } from "@fortawesome/free-regular-svg-icons";
import {
faCaretDown,
faCheck,
faCube,
faMagnifyingGlass,
faPlus,
faStar as faSolidStar
@@ -16,7 +15,6 @@ import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { NewProjectModal } from "@app/components/projects";
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -25,6 +23,7 @@ import {
Input,
Tooltip
} from "@app/components/v2";
import { Badge, ProjectIcon } from "@app/components/v3";
import {
OrgPermissionActions,
OrgPermissionSubjects,
@@ -120,11 +119,9 @@ export const ProjectSelect = () => {
<p className="inline-block truncate text-mineshaft-200 group-hover:underline">
{currentWorkspace?.name}
</p>
<Badge variant="project" className="cursor-pointer">
<FontAwesomeIcon icon={faCube} />
<span>
{currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"}
</span>
<Badge variant="project">
<ProjectIcon />
{currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"}
</Badge>
</Link>
<DropdownMenuTrigger asChild>
@@ -4,7 +4,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Badge, Tab, TabList, Tabs } from "@app/components/v2";
import { Tab, TabList, Tabs } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useProject, useProjectPermission } from "@app/context";
import {
useGetAccessRequestsCount,
@@ -88,7 +89,7 @@ export const SecretManagerLayout = () => {
{Boolean(
secretApprovalReqCount?.open || accessApprovalRequestCount?.pendingCount
) && (
<Badge variant="primary" className="ml-1.5">
<Badge variant="warning" isSquare className="ml-1.5">
{pendingRequestsCount}
</Badge>
)}
@@ -1,7 +1,8 @@
import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Badge, Tab, TabList, Tabs } from "@app/components/v2";
import { Tab, TabList, Tabs } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import {
ProjectPermissionSub,
useProject,
@@ -66,7 +67,7 @@ export const SecretScanningLayout = () => {
<Tab value={isActive ? "selected" : ""}>
Findings
{Boolean(unresolvedFindings) && (
<Badge variant="primary" className="ml-2 h-min">
<Badge isSquare variant="warning" className="ml-2">
{unresolvedFindings}
</Badge>
)}
@@ -7,16 +7,15 @@ import {
faTrash,
faUsers,
faUserXmark,
faWarning,
faXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AlertTriangleIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import {
Badge,
Button,
Checkbox,
DeleteActionModal,
@@ -38,6 +37,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useSubscription, useUser } from "@app/context";
import {
getUserTablePreference,
@@ -451,7 +451,7 @@ export const ServerAdminsTable = () => {
const email = user.email ?? user.username;
return (
<li key={user.id}>
<div className="flex items-center">
<div className="flex items-center gap-x-1">
<p>
{user.firstName || user.lastName ? (
<>
@@ -464,15 +464,10 @@ export const ServerAdminsTable = () => {
</p>
{userId === user.id && (
<Tooltip content="Are you sure you want to remove yourself from this instance?">
<div className="inline-block">
<Badge
variant="primary"
className="mt-[0.05rem] ml-1 inline-flex w-min items-center gap-1.5 whitespace-nowrap"
>
<FontAwesomeIcon icon={faWarning} />
<span>Deleting Yourself</span>
</Badge>
</div>
<Badge variant="danger">
<AlertTriangleIcon />
Deleting Yourself
</Badge>
</Tooltip>
)}
</div>
@@ -1,9 +1,9 @@
import { useEffect, useState } from "react";
import { faRotate } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { RefreshCwIcon } from "lucide-react";
import { createNotification } from "@app/components/notifications";
import { Badge, Button, DeleteActionModal } from "@app/components/v2";
import { Button, DeleteActionModal } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useUser } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useInvalidateCache } from "@app/hooks/api";
@@ -62,11 +62,8 @@ export const CachingPageForm = () => {
<div className="mb-2 flex items-center gap-3">
<span className="text-xl font-medium text-mineshaft-100">Secrets Cache</span>
{isInvalidating && (
<Badge
variant="danger"
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
>
<FontAwesomeIcon icon={faRotate} className="animate-spin" />
<Badge variant="danger">
<RefreshCwIcon className="animate-spin" />
Invalidating Cache
</Badge>
)}

Some files were not shown because too many files have changed in this diff Show More