mirror of
https://github.com/Infisical/infisical.git
synced 2025-04-14 17:22:51 +00:00
Merge branch 'main' into feat/vercelSecretSyncIntegration
This commit is contained in:
@ -4,6 +4,10 @@ on:
|
||||
tags:
|
||||
- "infisical-k8-operator/v*.*.*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release-image:
|
||||
name: Generate Helm Chart PR
|
||||
|
@ -12,3 +12,5 @@ docs/cli/commands/bootstrap.mdx:jwt:86
|
||||
docs/documentation/platform/audit-log-streams/audit-log-streams.mdx:generic-api-key:102
|
||||
docs/self-hosting/guides/automated-bootstrapping.mdx:jwt:74
|
||||
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx:generic-api-key:72
|
||||
k8-operator/config/samples/crd/pushsecret/source-secret-with-templating.yaml:private-key:11
|
||||
k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml:private-key:52
|
||||
|
@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasCol = await knex.schema.hasColumn(TableName.SecretFolder, "lastSecretModified");
|
||||
if (!hasCol) {
|
||||
await knex.schema.alterTable(TableName.SecretFolder, (t) => {
|
||||
t.datetime("lastSecretModified");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasCol = await knex.schema.hasColumn(TableName.SecretFolder, "lastSecretModified");
|
||||
if (hasCol) {
|
||||
await knex.schema.alterTable(TableName.SecretFolder, (t) => {
|
||||
t.dropColumn("lastSecretModified");
|
||||
});
|
||||
}
|
||||
}
|
@ -16,7 +16,8 @@ export const SecretFoldersSchema = z.object({
|
||||
envId: z.string().uuid(),
|
||||
parentId: z.string().uuid().nullable().optional(),
|
||||
isReserved: z.boolean().default(false).nullable().optional(),
|
||||
description: z.string().nullable().optional()
|
||||
description: z.string().nullable().optional(),
|
||||
lastSecretModified: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretFolders = z.infer<typeof SecretFoldersSchema>;
|
||||
|
@ -9,13 +9,14 @@ import { logger } from "@app/lib/logger";
|
||||
import { QueueName } from "@app/queue";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
|
||||
import { EventType } from "./audit-log-types";
|
||||
import { EventType, filterableSecretEvents } from "./audit-log-types";
|
||||
|
||||
export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>;
|
||||
|
||||
type TFindQuery = {
|
||||
actor?: string;
|
||||
projectId?: string;
|
||||
environment?: string;
|
||||
orgId?: string;
|
||||
eventType?: string;
|
||||
startDate?: string;
|
||||
@ -32,6 +33,7 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
orgId,
|
||||
projectId,
|
||||
environment,
|
||||
userAgentType,
|
||||
startDate,
|
||||
endDate,
|
||||
@ -40,12 +42,14 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
actorId,
|
||||
actorType,
|
||||
secretPath,
|
||||
secretKey,
|
||||
eventType,
|
||||
eventMetadata
|
||||
}: Omit<TFindQuery, "actor" | "eventType"> & {
|
||||
actorId?: string;
|
||||
actorType?: ActorType;
|
||||
secretPath?: string;
|
||||
secretKey?: string;
|
||||
eventType?: EventType[];
|
||||
eventMetadata?: Record<string, string>;
|
||||
},
|
||||
@ -90,8 +94,29 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (projectId && secretPath) {
|
||||
void sqlQuery.whereRaw(`"eventMetadata" @> jsonb_build_object('secretPath', ?::text)`, [secretPath]);
|
||||
const eventIsSecretType = !eventType?.length || eventType.some((event) => filterableSecretEvents.includes(event));
|
||||
// We only want to filter for environment/secretPath/secretKey if the user is either checking for all event types
|
||||
|
||||
// ? Note(daniel): use the `eventMetadata" @> ?::jsonb` approach to properly use our GIN index
|
||||
if (projectId && eventIsSecretType) {
|
||||
if (environment || secretPath) {
|
||||
// Handle both environment and secret path together to only use the GIN index once
|
||||
void sqlQuery.whereRaw(`"eventMetadata" @> ?::jsonb`, [
|
||||
JSON.stringify({
|
||||
...(environment && { environment }),
|
||||
...(secretPath && { secretPath })
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
// Handle secret key separately to include the OR condition
|
||||
if (secretKey) {
|
||||
void sqlQuery.whereRaw(
|
||||
`("eventMetadata" @> ?::jsonb
|
||||
OR "eventMetadata"->'secrets' @> ?::jsonb)`,
|
||||
[JSON.stringify({ secretKey }), JSON.stringify([{ secretKey }])]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by actor type
|
||||
|
@ -63,6 +63,8 @@ export const auditLogServiceFactory = ({
|
||||
actorType: filter.actorType,
|
||||
eventMetadata: filter.eventMetadata,
|
||||
secretPath: filter.secretPath,
|
||||
secretKey: filter.secretKey,
|
||||
environment: filter.environment,
|
||||
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId })
|
||||
});
|
||||
|
||||
|
@ -33,9 +33,11 @@ export type TListProjectAuditLogDTO = {
|
||||
endDate?: string;
|
||||
startDate?: string;
|
||||
projectId?: string;
|
||||
environment?: string;
|
||||
auditLogActorId?: string;
|
||||
actorType?: ActorType;
|
||||
secretPath?: string;
|
||||
secretKey?: string;
|
||||
eventMetadata?: Record<string, string>;
|
||||
};
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
@ -286,6 +288,16 @@ export enum EventType {
|
||||
KMIP_OPERATION_REGISTER = "kmip-operation-register"
|
||||
}
|
||||
|
||||
export const filterableSecretEvents: EventType[] = [
|
||||
EventType.GET_SECRET,
|
||||
EventType.DELETE_SECRETS,
|
||||
EventType.CREATE_SECRETS,
|
||||
EventType.UPDATE_SECRETS,
|
||||
EventType.CREATE_SECRET,
|
||||
EventType.UPDATE_SECRET,
|
||||
EventType.DELETE_SECRET
|
||||
];
|
||||
|
||||
interface UserActorMetadata {
|
||||
userId: string;
|
||||
email?: string | null;
|
||||
|
@ -632,7 +632,8 @@ export const FOLDERS = {
|
||||
environment: "The slug of the environment to list folders from.",
|
||||
path: "The path to list folders from.",
|
||||
directory: "The directory to list folders from. (Deprecated in favor of path)",
|
||||
recursive: "Whether or not to fetch all folders from the specified base path, and all of its subdirectories."
|
||||
recursive: "Whether or not to fetch all folders from the specified base path, and all of its subdirectories.",
|
||||
lastSecretModified: "The timestamp used to filter folders with secrets modified after the specified date. The format for this timestamp is ISO 8601 (e.g. 2025-04-01T09:41:45-04:00)"
|
||||
},
|
||||
GET_BY_ID: {
|
||||
folderId: "The ID of the folder to get details."
|
||||
@ -840,9 +841,13 @@ export const AUDIT_LOGS = {
|
||||
EXPORT: {
|
||||
projectId:
|
||||
"Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.",
|
||||
environment:
|
||||
"The environment to filter logs by. If not provided, logs from all environments will be returned. Note that the projectId parameter must also be provided.",
|
||||
eventType: "The type of the event to export.",
|
||||
secretPath:
|
||||
"The path of the secret to query audit logs for. Note that the projectId parameter must also be provided.",
|
||||
secretKey:
|
||||
"The key of the secret to query audit logs for. Note that the projectId parameter must also be provided.",
|
||||
userAgentType: "Choose which consuming application to export audit logs for.",
|
||||
eventMetadata:
|
||||
"Filter by event metadata key-value pairs. Formatted as `key1=value1,key2=value2`, with comma-separation.",
|
||||
|
@ -6,6 +6,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
@ -14,6 +15,7 @@ import {
|
||||
validateAltNamesField,
|
||||
validateCaDateField
|
||||
} from "@app/services/certificate-authority/certificate-authority-validators";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@ -649,6 +651,16 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IssueCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
caId: ca.id,
|
||||
commonName: req.body.commonName,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
@ -707,7 +719,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca, commonName } =
|
||||
await server.services.certificateAuthority.signCertFromCa({
|
||||
isInternal: false,
|
||||
caId: req.params.caId,
|
||||
@ -731,6 +743,16 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SignCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
caId: ca.id,
|
||||
commonName,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: certificate.toString("pem"),
|
||||
certificateChain,
|
||||
|
@ -5,6 +5,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CertExtendedKeyUsage, CertKeyUsage, CrlReason } from "@app/services/certificate/certificate-types";
|
||||
@ -12,6 +13,7 @@ import {
|
||||
validateAltNamesField,
|
||||
validateCaDateField
|
||||
} from "@app/services/certificate-authority/certificate-authority-validators";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@ -150,6 +152,17 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IssueCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
caId: req.body.caId,
|
||||
certificateTemplateId: req.body.certificateTemplateId,
|
||||
commonName: req.body.commonName,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
@ -228,7 +241,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca, commonName } =
|
||||
await server.services.certificateAuthority.signCertFromCa({
|
||||
isInternal: false,
|
||||
actor: req.permission.type,
|
||||
@ -251,6 +264,17 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SignCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
caId: req.body.caId,
|
||||
certificateTemplateId: req.body.certificateTemplateId,
|
||||
commonName,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: certificate.toString("pem"),
|
||||
certificateChain,
|
||||
|
@ -897,6 +897,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
projectId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||
recursive: booleanSchema.default(false),
|
||||
filterByAction: z
|
||||
.enum([ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue])
|
||||
.default(ProjectPermissionSecretActions.ReadValue)
|
||||
@ -915,7 +916,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { projectId, environment, secretPath, filterByAction } = req.query;
|
||||
const { projectId, environment, secretPath, filterByAction, recursive } = req.query;
|
||||
|
||||
const { secrets } = await server.services.secret.getAccessibleSecrets({
|
||||
actorId: req.permission.id,
|
||||
@ -925,7 +926,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
environment,
|
||||
secretPath,
|
||||
projectId,
|
||||
filterByAction
|
||||
filterByAction,
|
||||
recursive
|
||||
});
|
||||
|
||||
return { secrets };
|
||||
|
@ -111,12 +111,14 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
description: "Get all audit logs for an organization",
|
||||
querystring: z.object({
|
||||
projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId),
|
||||
environment: z.string().optional().describe(AUDIT_LOGS.EXPORT.environment),
|
||||
actorType: z.nativeEnum(ActorType).optional(),
|
||||
secretPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => (!val ? val : removeTrailingSlash(val)))
|
||||
.describe(AUDIT_LOGS.EXPORT.secretPath),
|
||||
secretKey: z.string().optional().describe(AUDIT_LOGS.EXPORT.secretKey),
|
||||
|
||||
// eventType is split with , for multiple values, we need to transform it to array
|
||||
eventType: z
|
||||
|
@ -335,6 +335,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim().describe(FOLDERS.LIST.workspaceId),
|
||||
environment: z.string().trim().describe(FOLDERS.LIST.environment),
|
||||
lastSecretModified: z.string().datetime().trim().optional().describe(FOLDERS.LIST.lastSecretModified),
|
||||
path: z
|
||||
.string()
|
||||
.trim()
|
||||
|
@ -1818,7 +1818,8 @@ export const certificateAuthorityServiceFactory = ({
|
||||
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
|
||||
issuingCaCertificate,
|
||||
serialNumber,
|
||||
ca
|
||||
ca,
|
||||
commonName: cn
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -3595,7 +3595,10 @@ const syncSecretsTeamCity = async ({
|
||||
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
|
||||
{
|
||||
name: `env.${key}`,
|
||||
value: secrets[key].value
|
||||
value: secrets[key].value,
|
||||
type: {
|
||||
rawValue: "password display='hidden'"
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
@ -3652,7 +3655,10 @@ const syncSecretsTeamCity = async ({
|
||||
`${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`,
|
||||
{
|
||||
name: `env.${key}`,
|
||||
value: secrets[key].value
|
||||
value: secrets[key].value,
|
||||
type: {
|
||||
rawValue: "password display='hidden'"
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
|
@ -423,7 +423,7 @@ export const projectMembershipServiceFactory = ({
|
||||
const usernamesAndEmails = [...emails, ...usernames];
|
||||
|
||||
const projectMembers = await projectMembershipDAL.findMembershipsByUsername(projectId, [
|
||||
...new Set(usernamesAndEmails.map((element) => element.toLowerCase()))
|
||||
...new Set(usernamesAndEmails.map((element) => element))
|
||||
]);
|
||||
|
||||
if (projectMembers.length !== usernamesAndEmails.length) {
|
||||
|
@ -402,7 +402,8 @@ export const secretFolderServiceFactory = ({
|
||||
orderDirection,
|
||||
limit,
|
||||
offset,
|
||||
recursive
|
||||
recursive,
|
||||
lastSecretModified
|
||||
}: TGetFolderDTO) => {
|
||||
// folder list is allowed to be read by anyone
|
||||
// permission to check does user has access
|
||||
@ -425,7 +426,16 @@ export const secretFolderServiceFactory = ({
|
||||
const recursiveFolders = await folderDAL.findByEnvsDeep({ parentIds: [parentFolder.id] });
|
||||
// remove the parent folder
|
||||
return recursiveFolders
|
||||
.filter((folder) => folder.id !== parentFolder.id)
|
||||
.filter((folder) => {
|
||||
if (lastSecretModified) {
|
||||
if (!folder.lastSecretModified) return false;
|
||||
|
||||
if (folder.lastSecretModified < new Date(lastSecretModified)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return folder.id !== parentFolder.id;
|
||||
})
|
||||
.map((folder) => ({
|
||||
...folder,
|
||||
relativePath: folder.path
|
||||
@ -445,6 +455,11 @@ export const secretFolderServiceFactory = ({
|
||||
offset
|
||||
}
|
||||
);
|
||||
if (lastSecretModified) {
|
||||
return folders.filter((el) =>
|
||||
el.lastSecretModified ? el.lastSecretModified >= new Date(lastSecretModified) : false
|
||||
);
|
||||
}
|
||||
return folders;
|
||||
};
|
||||
|
||||
@ -619,10 +634,29 @@ export const secretFolderServiceFactory = ({
|
||||
const relevantFolders = folders.filter((folder) => folder.envId === env.id);
|
||||
const foldersMap = Object.fromEntries(relevantFolders.map((folder) => [folder.id, folder]));
|
||||
|
||||
const foldersWithPath = relevantFolders.map((folder) => ({
|
||||
...folder,
|
||||
path: buildFolderPath(folder, foldersMap)
|
||||
}));
|
||||
const foldersWithPath = relevantFolders
|
||||
.map((folder) => {
|
||||
try {
|
||||
return {
|
||||
...folder,
|
||||
path: buildFolderPath(folder, foldersMap)
|
||||
};
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as {
|
||||
path: string;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
name: string;
|
||||
envId: string;
|
||||
version?: number | null | undefined;
|
||||
parentId?: string | null | undefined;
|
||||
isReserved?: boolean | undefined;
|
||||
description?: string | undefined;
|
||||
}[];
|
||||
|
||||
return [env.slug, { ...env, folders: foldersWithPath }];
|
||||
})
|
||||
|
@ -46,6 +46,7 @@ export type TGetFolderDTO = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
recursive?: boolean;
|
||||
lastSecretModified?: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetFolderByIdDTO = {
|
||||
|
@ -356,7 +356,7 @@ export const fnSecretBulkDelete = async ({
|
||||
interface FolderMap {
|
||||
[parentId: string]: TSecretFolders[];
|
||||
}
|
||||
const buildHierarchy = (folders: TSecretFolders[]): FolderMap => {
|
||||
export const buildHierarchy = (folders: TSecretFolders[]): FolderMap => {
|
||||
const map: FolderMap = {};
|
||||
map.null = []; // Initialize mapping for root directory
|
||||
|
||||
@ -371,7 +371,7 @@ const buildHierarchy = (folders: TSecretFolders[]): FolderMap => {
|
||||
return map;
|
||||
};
|
||||
|
||||
const generatePaths = (
|
||||
export const generatePaths = (
|
||||
map: FolderMap,
|
||||
parentId: string = "null",
|
||||
basePath: string = "",
|
||||
|
@ -44,10 +44,12 @@ import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
|
||||
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
|
||||
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
|
||||
import {
|
||||
buildHierarchy,
|
||||
expandSecretReferencesFactory,
|
||||
fnSecretBulkDelete,
|
||||
fnSecretBulkInsert,
|
||||
fnSecretBulkUpdate,
|
||||
generatePaths,
|
||||
getAllSecretReferences,
|
||||
recursivelyGetSecretPaths,
|
||||
reshapeBridgeSecret
|
||||
@ -2620,7 +2622,8 @@ export const secretV2BridgeServiceFactory = ({
|
||||
actorId,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
recursive
|
||||
}: TGetAccessibleSecretsDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
@ -2635,10 +2638,38 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretPath
|
||||
});
|
||||
|
||||
const folders = [];
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder) return { secrets: [] };
|
||||
folders.push({ ...folder, parentId: null });
|
||||
|
||||
const secrets = await secretDAL.findByFolderIds([folder.id]);
|
||||
const env = await projectEnvDAL.findOne({
|
||||
projectId,
|
||||
slug: environment
|
||||
});
|
||||
|
||||
if (!env) {
|
||||
throw new NotFoundError({
|
||||
message: `Environment with slug '${environment}' in project with ID ${projectId} not found`
|
||||
});
|
||||
}
|
||||
|
||||
if (recursive) {
|
||||
const subFolders = await folderDAL.find({
|
||||
envId: env.id,
|
||||
isReserved: false
|
||||
});
|
||||
folders.push(...subFolders);
|
||||
}
|
||||
|
||||
if (folders.length === 0) return { secrets: [] };
|
||||
|
||||
const folderMap = buildHierarchy(folders);
|
||||
const paths = Object.fromEntries(
|
||||
generatePaths(folderMap).map(({ folderId, path }) => [folderId, path === "/" ? path : path.substring(1)])
|
||||
);
|
||||
|
||||
const secrets = await secretDAL.findByFolderIds(folders.map((f) => f.id));
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
@ -2650,7 +2681,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
if (
|
||||
!hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretPath: paths[el.folderId],
|
||||
secretName: el.key,
|
||||
secretTags: el.tags.map((i) => i.slug)
|
||||
})
|
||||
@ -2661,7 +2692,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
if (filterByAction === ProjectPermissionSecretActions.ReadValue) {
|
||||
return hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretPath: paths[el.folderId],
|
||||
secretName: el.key,
|
||||
secretTags: el.tags.map((i) => i.slug)
|
||||
});
|
||||
@ -2674,7 +2705,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
filterByAction === ProjectPermissionSecretActions.DescribeSecret &&
|
||||
!hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretPath: paths[secret.folderId],
|
||||
secretName: secret.key,
|
||||
secretTags: secret.tags.map((i) => i.slug)
|
||||
});
|
||||
@ -2682,7 +2713,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
return reshapeBridgeSecret(
|
||||
projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
paths[secret.folderId],
|
||||
{
|
||||
...secret,
|
||||
value: secret.encryptedValue
|
||||
|
@ -356,5 +356,6 @@ export type TGetAccessibleSecretsDTO = {
|
||||
environment: string;
|
||||
projectId: string;
|
||||
secretPath: string;
|
||||
recursive?: boolean;
|
||||
filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
|
||||
} & TProjectPermission;
|
||||
|
@ -646,6 +646,10 @@ export const secretQueueFactory = ({
|
||||
}
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder) return;
|
||||
await folderDAL.updateById(folder.id, { lastSecretModified: new Date() });
|
||||
|
||||
await secretSyncQueue.queueSecretSyncsSyncSecretsByPath({ projectId, environmentSlug: environment, secretPath });
|
||||
|
||||
await syncIntegrations({ secretPath, projectId, environment, deDupeQueue, isManual: false });
|
||||
|
@ -1321,7 +1321,8 @@ export const secretServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
environment,
|
||||
filterByAction
|
||||
filterByAction,
|
||||
recursive
|
||||
}: TGetAccessibleSecretsDTO) => {
|
||||
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
|
||||
@ -1340,7 +1341,8 @@ export const secretServiceFactory = ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
actorAuthMethod,
|
||||
recursive
|
||||
});
|
||||
|
||||
return secrets;
|
||||
|
@ -184,6 +184,7 @@ export enum SecretsOrderBy {
|
||||
export type TGetAccessibleSecretsDTO = {
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
recursive?: boolean;
|
||||
filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
|
||||
} & TProjectPermission;
|
||||
|
||||
|
@ -17,7 +17,9 @@ export enum PostHogEventTypes {
|
||||
SecretRequestCreated = "Secret Request Created",
|
||||
SecretRequestDeleted = "Secret Request Deleted",
|
||||
SignSshKey = "Sign SSH Key",
|
||||
IssueSshCreds = "Issue SSH Credentials"
|
||||
IssueSshCreds = "Issue SSH Credentials",
|
||||
SignCert = "Sign PKI Certificate",
|
||||
IssueCert = "Issue PKI Certificate"
|
||||
}
|
||||
|
||||
export type TSecretModifiedEvent = {
|
||||
@ -159,6 +161,26 @@ export type TIssueSshCredsEvent = {
|
||||
};
|
||||
};
|
||||
|
||||
export type TSignCertificateEvent = {
|
||||
event: PostHogEventTypes.SignCert;
|
||||
properties: {
|
||||
caId?: string;
|
||||
certificateTemplateId?: string;
|
||||
commonName: string;
|
||||
userAgent?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TIssueCertificateEvent = {
|
||||
event: PostHogEventTypes.IssueCert;
|
||||
properties: {
|
||||
caId?: string;
|
||||
certificateTemplateId?: string;
|
||||
commonName: string;
|
||||
userAgent?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TPostHogEvent = { distinctId: string } & (
|
||||
| TSecretModifiedEvent
|
||||
| TAdminInitEvent
|
||||
@ -173,4 +195,6 @@ export type TPostHogEvent = { distinctId: string } & (
|
||||
| TSecretRequestDeletedEvent
|
||||
| TSignSshKeyEvent
|
||||
| TIssueSshCredsEvent
|
||||
| TSignCertificateEvent
|
||||
| TIssueCertificateEvent
|
||||
);
|
||||
|
@ -694,7 +694,7 @@ func SetRawSecrets(secretArgs []string, secretType string, environmentName strin
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get client with custom headers [err=%v]", err)
|
||||
}
|
||||
|
||||
httpClient.SetAuthToken(tokenDetails.Token)
|
||||
httpClient.SetHeader("Accept", "application/json")
|
||||
|
||||
// pull current secrets
|
||||
|
@ -40,7 +40,7 @@ If you're using SAML/LDAP/OIDC for only one organization on your instance, you c
|
||||
|
||||
By default, users signing up through SAML/LDAP/OIDC will still need to verify their email address to prevent email spoofing. This requirement can be skipped by enabling the switch to trust logins through the respective method.
|
||||
|
||||
### Notices
|
||||
### Broadcast Messages
|
||||
|
||||
Auth consent content is displayed to users on the login page. They can be used to display important information to users, such as a maintenance message or a new feature announcement. Both HTML and Markdown formatting are supported, allowing for customized styling like below:
|
||||
|
||||
|
@ -11,10 +11,11 @@ This means that updating the value of a base secret propagates directly to other
|
||||
|
||||

|
||||
|
||||
Since secret referencing works by reconstructing values back on the client side, the client, be it a user, service token, or a machine identity, fetching back secrets
|
||||
must be permissioned access to all base and dependent secrets.
|
||||
Since secret referencing reconstructs values on the client side, any client (user, service token, or machine identity) fetching secrets must have proper permissions to access all base and dependent secrets. Without sufficient permissions, secret references will not resolve to their appropriate values.
|
||||
|
||||
For example, to access some secret `A` whose values depend on secrets `B` and `C` from different scopes, a client must have `read` access to the scopes of secrets `A`, `B`, and `C`.
|
||||
For example, if secret A references values from secrets B and C located in different scopes, the client must have read access to all three scopes containing secrets A, B, and C. If permission to any referenced secret is missing, the reference will remain unresolved, potentially causing application errors or unexpected behavior.
|
||||
|
||||
This is an important security consideration when planning your secret access strategy, especially when working with cross-environment or cross-folder references.
|
||||
|
||||
### Syntax
|
||||
|
||||
@ -28,11 +29,11 @@ Then consider the following scenarios:
|
||||
|
||||
Here are a few more helpful examples for how to reference secrets in different contexts:
|
||||
|
||||
| Reference syntax | Environment | Folder | Secret Key |
|
||||
| --------------------- | ----------- | ------------ | ---------- |
|
||||
| `${KEY1}` | same env | same folder | KEY1 |
|
||||
| `${dev.KEY2}` | `dev` | `/` (root of dev environment) | KEY2 |
|
||||
| `${prod.frontend.KEY2}` | `prod` | `/frontend` | KEY2 |
|
||||
| Reference syntax | Environment | Folder | Secret Key |
|
||||
| ----------------------- | ----------- | ----------------------------- | ---------- |
|
||||
| `${KEY1}` | same env | same folder | KEY1 |
|
||||
| `${dev.KEY2}` | `dev` | `/` (root of dev environment) | KEY2 |
|
||||
| `${prod.frontend.KEY2}` | `prod` | `/frontend` | KEY2 |
|
||||
|
||||
## Secret Imports
|
||||
|
||||
@ -59,4 +60,12 @@ To reorder a secret import, hover over it and drag the arrows handle to the posi
|
||||
|
||||

|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/o11bMU0pXRs?si=dCprt3xLWPrSOJxy" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://www.youtube.com/embed/o11bMU0pXRs?si=dCprt3xLWPrSOJxy"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
|
@ -401,6 +401,59 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y
|
||||
</Accordion>
|
||||
|
||||
|
||||
## Using templating to push secrets
|
||||
|
||||
Pushing secrets to Infisical from the operator may not always be enough.
|
||||
Templating is a useful utility of the Infisical secrets operator that allows you to use Go Templating to template the secrets you want to push to Infisical.
|
||||
Using Go templates, you can format, combine, and create new key-value pairs of secrets that you want to push to Infisical.
|
||||
|
||||
<Accordion title="push.secret.template"/>
|
||||
<Accordion title="push.secret.template.includeAllSecrets">
|
||||
This property controls what secrets are included in your push to Infisica.
|
||||
When set to `true`, all secrets included in the `push.secret.secretName` Kubernetes secret will be pushed to Infisical.
|
||||
**Use this option when you would like to push all secrets to Infisical from the secrets operator, but want to template a subset of them.**
|
||||
|
||||
When set to `false`, only secrets defined in the `push.secret.template.data` field of the template will be pushed to Infisical.
|
||||
Use this option when you would like to push **only** a subset of secrets from the Kubernetes secret to Infisical.
|
||||
</Accordion>
|
||||
<Accordion title="push.secret.template.data">
|
||||
Define secret keys and their corresponding templates.
|
||||
Each data value uses a Golang template with access to all secrets defined in the `push.secret.secretName` Kubernetes secret.
|
||||
|
||||
Secrets are structured as follows:
|
||||
|
||||
```go
|
||||
type TemplateSecret struct {
|
||||
Value string `json:"value"`
|
||||
SecretPath string `json:"secretPath"`
|
||||
}
|
||||
```
|
||||
|
||||
#### Example template configuration:
|
||||
|
||||
```yaml
|
||||
# This example assumes that the `push-secret-demo` Kubernetes secret contains the following secrets:
|
||||
# SITE_URL = "https://example.com"
|
||||
# REGION = "us-east-1"
|
||||
# OTHER_SECRET = "other-secret"
|
||||
|
||||
push:
|
||||
secret:
|
||||
secretName: push-secret-demo
|
||||
secretNamespace: default
|
||||
template:
|
||||
includeAllSecrets: true # Includes all secrets from the `push-secret-demo` Kubernetes secret
|
||||
data:
|
||||
SITE_URL: "{{ .SITE_URL.Value }}"
|
||||
API_URL: "https://api.{{.SITE_URL.Value}}.{{.REGION.Value}}.com" # Will create a new secret in Infisical with the key `API_URL` with the value of the `SITE_URL` and `REGION` secrets
|
||||
```
|
||||
|
||||
To help transform your config map data further, the operator provides a set of built-in functions that you can use in your templates.
|
||||
|
||||
### Available templating functions
|
||||
Please refer to the [templating functions documentation](/integrations/platforms/kubernetes/overview#available-helper-functions) for more information.
|
||||
</Accordion>
|
||||
|
||||
## Applying the InfisicalPushSecret CRD to your cluster
|
||||
|
||||
Once you have configured the `InfisicalPushSecret` CRD with the required fields, you can apply it to your cluster.
|
||||
|
@ -654,30 +654,7 @@ To help transform your secrets further, the operator provides a set of built-in
|
||||
|
||||
### Available templating functions
|
||||
|
||||
<Accordion title="decodeBase64ToBytes">
|
||||
**Function name**: decodeBase64ToBytes
|
||||
|
||||
**Description**:
|
||||
Given a base64 encoded string, this function will decodes the base64-encoded string.
|
||||
This function is useful when your secrets are already stored as base64 encoded value in Infisical.
|
||||
|
||||
**Returns**: The decoded base64 string as bytes.
|
||||
|
||||
**Example**:
|
||||
The example below assumes that the `BINARY_KEY_BASE64` secret is stored as a base64 encoded value in Infisical.
|
||||
The resulting managed secret will contain the decoded value of `BINARY_KEY_BASE64`.
|
||||
|
||||
```yaml
|
||||
managedKubeSecretReferences:
|
||||
secretName: managed-secret
|
||||
secretNamespace: default
|
||||
template:
|
||||
includeAllSecrets: true
|
||||
data:
|
||||
BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}"
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
Please refer to the [templating functions documentation](/integrations/platforms/kubernetes/overview#available-helper-functions) for more information.
|
||||
|
||||
</Accordion>
|
||||
|
||||
@ -783,31 +760,8 @@ Using Go templates, you can format, combine, and create new key-value pairs from
|
||||
To help transform your config map data further, the operator provides a set of built-in functions that you can use in your templates.
|
||||
|
||||
### Available templating functions
|
||||
|
||||
<Accordion title="decodeBase64ToBytes">
|
||||
**Function name**: decodeBase64ToBytes
|
||||
|
||||
**Description**:
|
||||
Given a base64 encoded string, this function will decodes the base64-encoded string.
|
||||
This function is useful when your Infisical secrets are already stored as base64 encoded value in Infisical.
|
||||
|
||||
**Returns**: The decoded base64 string as bytes.
|
||||
|
||||
**Example**:
|
||||
The example below assumes that the `BINARY_KEY_BASE64` secret is stored as a base64 encoded value in Infisical.
|
||||
The resulting managed config map will contain the decoded value of `BINARY_KEY_BASE64`.
|
||||
|
||||
```yaml
|
||||
managedKubeConfigMapReferences:
|
||||
- configMapName: managed-configmap
|
||||
configMapNamespace: default
|
||||
template:
|
||||
includeAllSecrets: true
|
||||
data:
|
||||
BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}"
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
Please refer to the [templating functions documentation](/integrations/platforms/kubernetes/overview#available-helper-functions) for more information.
|
||||
</Accordion>
|
||||
|
||||
## Applying CRD
|
||||
@ -854,39 +808,39 @@ Here, we will highlight three of the most common ways to utilize it. Learn more
|
||||
<Accordion title="envFrom">
|
||||
This will take all the secrets from your managed secret and expose them to your container
|
||||
|
||||
````yaml
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: managed-secret # managed secret name
|
||||
```
|
||||
````yaml
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: managed-secret # managed secret name
|
||||
```
|
||||
|
||||
Example usage in a deployment
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
Example usage in a deployment
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: managed-secret # <- name of managed secret
|
||||
ports:
|
||||
- containerPort: 80
|
||||
````
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: managed-secret # <- name of managed secret
|
||||
ports:
|
||||
- containerPort: 80
|
||||
````
|
||||
|
||||
</Accordion>
|
||||
|
||||
@ -902,91 +856,90 @@ spec:
|
||||
key: SOME_SECRET_KEY # The name of the key which exists in the managed secret
|
||||
```
|
||||
|
||||
Example usage in a deployment
|
||||
Example usage in a deployment
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
env:
|
||||
- name: STRIPE_API_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: managed-secret # <- name of managed secret
|
||||
key: STRIPE_API_SECRET
|
||||
ports:
|
||||
- containerPort: 80
|
||||
```
|
||||
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
env:
|
||||
- name: STRIPE_API_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: managed-secret # <- name of managed secret
|
||||
key: STRIPE_API_SECRET
|
||||
ports:
|
||||
- containerPort: 80
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="volumes">
|
||||
This will allow you to create a volume on your container which comprises of files holding the secrets in your managed kubernetes secret
|
||||
```yaml
|
||||
volumes:
|
||||
- name: secrets-volume-name # The name of the volume under which secrets will be stored
|
||||
secret:
|
||||
secretName: managed-secret # managed secret name
|
||||
````
|
||||
This will allow you to create a volume on your container which comprises of files holding the secrets in your managed kubernetes secret
|
||||
```yaml
|
||||
volumes:
|
||||
- name: secrets-volume-name # The name of the volume under which secrets will be stored
|
||||
secret:
|
||||
secretName: managed-secret # managed secret name
|
||||
````
|
||||
|
||||
You can then mount this volume to the container's filesystem so that your deployment can access the files containing the managed secrets
|
||||
You can then mount this volume to the container's filesystem so that your deployment can access the files containing the managed secrets
|
||||
|
||||
```yaml
|
||||
volumeMounts:
|
||||
- name: secrets-volume-name
|
||||
mountPath: /etc/secrets
|
||||
readOnly: true
|
||||
```
|
||||
```yaml
|
||||
volumeMounts:
|
||||
- name: secrets-volume-name
|
||||
mountPath: /etc/secrets
|
||||
readOnly: true
|
||||
```
|
||||
|
||||
Example usage in a deployment
|
||||
Example usage in a deployment
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
volumeMounts:
|
||||
- name: secrets-volume-name
|
||||
mountPath: /etc/secrets
|
||||
readOnly: true
|
||||
ports:
|
||||
- containerPort: 80
|
||||
volumes:
|
||||
- name: secrets-volume-name
|
||||
secret:
|
||||
secretName: managed-secret # <- managed secrets
|
||||
```
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
volumeMounts:
|
||||
- name: secrets-volume-name
|
||||
mountPath: /etc/secrets
|
||||
readOnly: true
|
||||
ports:
|
||||
- containerPort: 80
|
||||
volumes:
|
||||
- name: secrets-volume-name
|
||||
secret:
|
||||
secretName: managed-secret # <- managed secrets
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
@ -1021,34 +974,34 @@ secrets.infisical.com/auto-reload: "true"
|
||||
```
|
||||
|
||||
<Accordion title="Deployment example with auto redeploy enabled">
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
annotations:
|
||||
secrets.infisical.com/auto-reload: "true" # <- redeployment annotation
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
annotations:
|
||||
secrets.infisical.com/auto-reload: "true" # <- redeployment annotation
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: managed-secret
|
||||
ports:
|
||||
- containerPort: 80
|
||||
```
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: managed-secret
|
||||
ports:
|
||||
- containerPort: 80
|
||||
```
|
||||
</Accordion>
|
||||
<Info>
|
||||
#### How it works
|
||||
@ -1069,39 +1022,39 @@ Here, we will highlight three of the most common ways to utilize it. Learn more
|
||||
<Accordion title="envFrom">
|
||||
This will take all the secrets from your managed ConfigMap and expose them to your container
|
||||
|
||||
````yaml
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: managed-configmap # managed configmap name
|
||||
```
|
||||
````yaml
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: managed-configmap # managed configmap name
|
||||
```
|
||||
|
||||
Example usage in a deployment
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
Example usage in a deployment
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: managed-configmap # <- name of managed configmap
|
||||
ports:
|
||||
- containerPort: 80
|
||||
````
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: managed-configmap # <- name of managed configmap
|
||||
ports:
|
||||
- containerPort: 80
|
||||
````
|
||||
|
||||
</Accordion>
|
||||
|
||||
@ -1117,92 +1070,91 @@ spec:
|
||||
key: SOME_CONFIG_KEY # The name of the key which exists in the managed configmap
|
||||
```
|
||||
|
||||
Example usage in a deployment
|
||||
Example usage in a deployment
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
env:
|
||||
- name: STRIPE_API_SECRET
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: managed-configmap # <- name of managed configmap
|
||||
key: STRIPE_API_SECRET
|
||||
ports:
|
||||
- containerPort: 80
|
||||
```
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
env:
|
||||
- name: STRIPE_API_SECRET
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: managed-configmap # <- name of managed configmap
|
||||
key: STRIPE_API_SECRET
|
||||
ports:
|
||||
- containerPort: 80
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="volumes">
|
||||
This will allow you to create a volume on your container which comprises of files holding the secrets in your managed kubernetes secret
|
||||
```yaml
|
||||
volumes:
|
||||
- name: configmaps-volume-name # The name of the volume under which configmaps will be stored
|
||||
configMap:
|
||||
name: managed-configmap # managed configmap name
|
||||
````
|
||||
This will allow you to create a volume on your container which comprises of files holding the secrets in your managed kubernetes secret
|
||||
```yaml
|
||||
volumes:
|
||||
- name: configmaps-volume-name # The name of the volume under which configmaps will be stored
|
||||
configMap:
|
||||
name: managed-configmap # managed configmap name
|
||||
````
|
||||
|
||||
You can then mount this volume to the container's filesystem so that your deployment can access the files containing the managed secrets
|
||||
You can then mount this volume to the container's filesystem so that your deployment can access the files containing the managed secrets
|
||||
|
||||
```yaml
|
||||
volumeMounts:
|
||||
- name: configmaps-volume-name
|
||||
mountPath: /etc/config
|
||||
readOnly: true
|
||||
```
|
||||
```yaml
|
||||
volumeMounts:
|
||||
- name: configmaps-volume-name
|
||||
mountPath: /etc/config
|
||||
readOnly: true
|
||||
```
|
||||
|
||||
Example usage in a deployment
|
||||
Example usage in a deployment
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
volumeMounts:
|
||||
- name: configmaps-volume-name
|
||||
mountPath: /etc/config
|
||||
readOnly: true
|
||||
ports:
|
||||
- containerPort: 80
|
||||
volumes:
|
||||
- name: configmaps-volume-name
|
||||
configMap:
|
||||
name: managed-configmap # <- managed configmap
|
||||
```
|
||||
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
volumeMounts:
|
||||
- name: configmaps-volume-name
|
||||
mountPath: /etc/config
|
||||
readOnly: true
|
||||
ports:
|
||||
- containerPort: 80
|
||||
volumes:
|
||||
- name: configmaps-volume-name
|
||||
configMap:
|
||||
name: managed-configmap # <- managed configmap
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
The definition file of the Kubernetes secret for the CA certificate can be structured like the following:
|
||||
@ -1228,37 +1180,37 @@ The operator will transfer all labels & annotations present on the `InfisicalSec
|
||||
Thus, if a specific label is required on the resulting secret, it can be applied as demonstrated in the following example:
|
||||
|
||||
<Accordion title="Example propagation">
|
||||
```yaml
|
||||
apiVersion: secrets.infisical.com/v1alpha1
|
||||
kind: InfisicalSecret
|
||||
metadata:
|
||||
name: infisicalsecret-sample
|
||||
labels:
|
||||
label-to-be-passed-to-managed-secret: sample-value
|
||||
annotations:
|
||||
example.com/annotation-to-be-passed-to-managed-secret: "sample-value"
|
||||
spec:
|
||||
..
|
||||
authentication:
|
||||
...
|
||||
managedKubeSecretReferences:
|
||||
...
|
||||
```
|
||||
```yaml
|
||||
apiVersion: secrets.infisical.com/v1alpha1
|
||||
kind: InfisicalSecret
|
||||
metadata:
|
||||
name: infisicalsecret-sample
|
||||
labels:
|
||||
label-to-be-passed-to-managed-secret: sample-value
|
||||
annotations:
|
||||
example.com/annotation-to-be-passed-to-managed-secret: "sample-value"
|
||||
spec:
|
||||
..
|
||||
authentication:
|
||||
...
|
||||
managedKubeSecretReferences:
|
||||
...
|
||||
```
|
||||
|
||||
This would result in the following managed secret to be created:
|
||||
This would result in the following managed secret to be created:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
data: ...
|
||||
kind: Secret
|
||||
metadata:
|
||||
annotations:
|
||||
example.com/annotation-to-be-passed-to-managed-secret: sample-value
|
||||
secrets.infisical.com/version: W/"3f1-ZyOSsrCLGSkAhhCkY2USPu2ivRw"
|
||||
labels:
|
||||
label-to-be-passed-to-managed-secret: sample-value
|
||||
name: managed-token
|
||||
namespace: default
|
||||
type: Opaque
|
||||
```
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
data: ...
|
||||
kind: Secret
|
||||
metadata:
|
||||
annotations:
|
||||
example.com/annotation-to-be-passed-to-managed-secret: sample-value
|
||||
secrets.infisical.com/version: W/"3f1-ZyOSsrCLGSkAhhCkY2USPu2ivRw"
|
||||
labels:
|
||||
label-to-be-passed-to-managed-secret: sample-value
|
||||
name: managed-token
|
||||
namespace: default
|
||||
type: Opaque
|
||||
```
|
||||
</Accordion>
|
||||
|
@ -114,6 +114,48 @@ spec:
|
||||
```
|
||||
|
||||
|
||||
## Advanced Templating
|
||||
|
||||
With the Infisical Secrets Operator, you can use templating to dynamically generate secrets in Kubernetes. The templating is built on top of [Go templates](https://pkg.go.dev/text/template), which is a powerful and flexible template engine built into Go.
|
||||
|
||||
Please be aware that trying to reference non-existing keys will result in an error. Additionally, each template field is processed individually, which means one template field cannot reference another template field.
|
||||
|
||||
<Note>
|
||||
Please note that templating is currently only supported for the `InfisicalPushSecret` and `InfisicalSecret` CRDs.
|
||||
</Note>
|
||||
|
||||
### Available helper functions
|
||||
|
||||
The Infisical Secrets Operator exposes a wide range of helper functions to make it easier to work with secrets in Kubernetes.
|
||||
|
||||
| Function | Description | Signature |
|
||||
| -------- | ----------- | --------- |
|
||||
| `decodeBase64ToBytes` | Given a base64 encoded string, this function will decode the base64-encoded string. | `decodeBase64ToBytes(encodedString string) string` |
|
||||
| `encodeBase64` | Given a string, this function will encode the string to a base64 encoded string. | `encodeBase64(plainString string) string` |
|
||||
| `pkcs12key`| Extracts all private keys from a PKCS#12 archive and encodes them in PKCS#8 PEM format. | `pkcs12key(input string) string` |
|
||||
| `pkcs12keyPass`|Same as pkcs12key. Uses the provided password to decrypt the PKCS#12 archive. | `pkcs12keyPass(pass string, input string) string` |
|
||||
| `pkcs12cert` | Extracts all certificates from a PKCS#12 archive and orders them if possible. If disjunct or multiple leaf certs are provided they are returned as-is. Sort order: `leaf / intermediate(s) / root`. | `pkcs12cert(input string) string` |
|
||||
| `pkcs12certPass` | Same as `pkcs12cert`. Uses the provided password to decrypt the PKCS#12 archive. | `pkcs12certPass(pass string, input string) string` |
|
||||
| `pemToPkcs12` | Takes a PEM encoded certificate and key and creates a base64 encoded PKCS#12 archive. | `pemToPkcs12(cert string, key string) string` |
|
||||
| `pemToPkcs12Pass` | Same as `pemToPkcs12`. Uses the provided password to encrypt the PKCS#12 archive. | `pemToPkcs12Pass(cert string, key string, pass string) string` |
|
||||
| `fullPemToPkcs12` | Takes a PEM encoded certificates chain and key and creates a base64 encoded PKCS#12 archive. | `fullPemToPkcs12(cert string, key string) string` |
|
||||
| `fullPemToPkcs12Pass` | Same as `fullPemToPkcs12`. Uses the provided password to encrypt the PKCS#12 archive. | `fullPemToPkcs12Pass(cert string, key string, pass string) string` |
|
||||
| `filterPEM` | Filters PEM blocks with a specific type from a list of PEM blocks.. | `filterPEM(pemType string, input string) string` |
|
||||
| `filterCertChain` | Filters PEM block(s) with a specific certificate type (`leaf`, `intermediate` or `root`) from a certificate chain of PEM blocks (PEM blocks with type `CERTIFICATE`). | `filterCertChain(certType string, input string) string` |
|
||||
| `jwkPublicKeyPem` | Takes an json-serialized JWK and returns an PEM block of type `PUBLIC KEY` that contains the public key. [See here](https://golang.org/pkg/crypto/x509/#MarshalPKIXPublicKey) for details. | `jwkPublicKeyPem(jwkjson string) string` |
|
||||
| `jwkPrivateKeyPem` | Takes an json-serialized JWK and returns an PEM block of type `PRIVATE KEY` that contains the private key. [See here](https://pkg.go.dev/crypto/x509#MarshalPKCS8PrivateKey) for details. | `jwkPrivateKeyPem(jwkjson string) string` |
|
||||
| `toYaml` | Takes an interface, marshals it to yaml. It returns a string, even on marshal error (empty string). | `toYaml(v any) string` |
|
||||
| `fromYaml` | Function converts a YAML document into a `map[string]any`. | `fromYaml(str string) map[string]any` |
|
||||
|
||||
### Sprig functions
|
||||
|
||||
The Infisical Secrets Operator integrates with the [Sprig library](https://github.com/Masterminds/sprig) to provide additional helper functions.
|
||||
|
||||
<Note>
|
||||
We've removed `expandEnv` and `env` from the supported functions for security reasons.
|
||||
</Note>
|
||||
|
||||
|
||||
## Global configuration
|
||||
|
||||
To configure global settings that will apply to all instances of `InfisicalSecret`, you can define these configurations in a Kubernetes ConfigMap.
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { MongoAbility, MongoQuery } from "@casl/ability";
|
||||
import {
|
||||
faAnglesUp,
|
||||
faArrowUpRightFromSquare,
|
||||
faDownLeftAndUpRightToCenter,
|
||||
faUpRightAndDownLeftFromCenter,
|
||||
faWindowRestore
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
@ -10,6 +12,7 @@ import {
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
ConnectionLineType,
|
||||
ControlButton,
|
||||
Controls,
|
||||
Node,
|
||||
NodeMouseHandler,
|
||||
@ -23,7 +26,9 @@ import { twMerge } from "tailwind-merge";
|
||||
import { Button, IconButton, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
||||
|
||||
import { AccessTreeErrorBoundary, AccessTreeProvider, PermissionSimulation } from "./components";
|
||||
import { AccessTreeSecretPathInput } from "./nodes/FolderNode/components/AccessTreeSecretPathInput";
|
||||
import { ShowMoreButtonNode } from "./nodes/ShowMoreButtonNode";
|
||||
import { AccessTreeErrorBoundary, AccessTreeProvider } from "./components";
|
||||
import { BasePermissionEdge } from "./edges";
|
||||
import { useAccessTree } from "./hooks";
|
||||
import { FolderNode, RoleNode } from "./nodes";
|
||||
@ -35,13 +40,30 @@ export type AccessTreeProps = {
|
||||
|
||||
const EdgeTypes = { base: BasePermissionEdge };
|
||||
|
||||
const NodeTypes = { role: RoleNode, folder: FolderNode };
|
||||
const NodeTypes = { role: RoleNode, folder: FolderNode, showMoreButton: ShowMoreButtonNode };
|
||||
|
||||
const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
const accessTreeData = useAccessTree(permissions);
|
||||
const { edges, nodes, isLoading, viewMode, setViewMode } = accessTreeData;
|
||||
const [selectedPath, setSelectedPath] = useState<string>("/");
|
||||
const accessTreeData = useAccessTree(permissions, selectedPath);
|
||||
const { edges, nodes, isLoading, viewMode, setViewMode, environment } = accessTreeData;
|
||||
const [initialRender, setInitialRender] = useState(true);
|
||||
|
||||
const { fitView, getViewport, setCenter } = useReactFlow();
|
||||
useEffect(() => {
|
||||
setSelectedPath("/");
|
||||
}, [environment]);
|
||||
|
||||
const { getViewport, setCenter, fitView } = useReactFlow();
|
||||
|
||||
const goToRootNode = useCallback(() => {
|
||||
const roleNode = nodes.find((node) => node.type === "role");
|
||||
if (roleNode) {
|
||||
setCenter(
|
||||
roleNode.position.x + (roleNode.width ? roleNode.width / 2 : 0),
|
||||
roleNode.position.y + (roleNode.height ? roleNode.height / 2 : 0),
|
||||
{ duration: 800, zoom: 1 }
|
||||
);
|
||||
}
|
||||
}, [nodes, setCenter]);
|
||||
|
||||
const onNodeClick: NodeMouseHandler<Node> = useCallback(
|
||||
(_, node) => {
|
||||
@ -55,14 +77,19 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
fitView({
|
||||
padding: 0.2,
|
||||
duration: 1000,
|
||||
maxZoom: 1
|
||||
});
|
||||
}, 1);
|
||||
}, [fitView, nodes, edges, getViewport()]);
|
||||
setInitialRender(true);
|
||||
}, [selectedPath, environment]);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (initialRender) {
|
||||
timer = setTimeout(() => {
|
||||
goToRootNode();
|
||||
setInitialRender(false);
|
||||
}, 500);
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, [nodes, edges, getViewport(), initialRender, goToRootNode]);
|
||||
|
||||
const handleToggleModalView = () =>
|
||||
setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Docked : ViewMode.Modal));
|
||||
@ -133,13 +160,13 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
edges={edges}
|
||||
edgeTypes={EdgeTypes}
|
||||
nodeTypes={NodeTypes}
|
||||
fitView
|
||||
onNodeClick={onNodeClick}
|
||||
colorMode="dark"
|
||||
nodesDraggable={false}
|
||||
edgesReconnectable={false}
|
||||
nodesConnectable={false}
|
||||
connectionLineType={ConnectionLineType.SmoothStep}
|
||||
minZoom={0.001}
|
||||
proOptions={{
|
||||
hideAttribution: false // we need pro license if we want to hide
|
||||
}}
|
||||
@ -151,9 +178,17 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
)}
|
||||
{viewMode !== ViewMode.Docked && (
|
||||
<Panel position="top-right" className="flex gap-1.5">
|
||||
{viewMode !== ViewMode.Undocked && (
|
||||
<AccessTreeSecretPathInput
|
||||
placeholder="Provide a path, default is /"
|
||||
environment={environment}
|
||||
value={selectedPath}
|
||||
onChange={setSelectedPath}
|
||||
/>
|
||||
)}
|
||||
<Tooltip position="bottom" align="center" content={undockButtonLabel}>
|
||||
<IconButton
|
||||
className="mr-1 rounded"
|
||||
className="ml-1 w-10 rounded"
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={handleToggleUndockedView}
|
||||
@ -170,7 +205,7 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
</Tooltip>
|
||||
<Tooltip align="end" position="bottom" content={windowButtonLabel}>
|
||||
<IconButton
|
||||
className="rounded"
|
||||
className="w-10 rounded"
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={handleToggleModalView}
|
||||
@ -179,7 +214,7 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
viewMode === ViewMode.Modal
|
||||
? faArrowUpRightFromSquare
|
||||
? faDownLeftAndUpRightToCenter
|
||||
: faUpRightAndDownLeftFromCenter
|
||||
}
|
||||
/>
|
||||
@ -187,9 +222,28 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
|
||||
</Tooltip>
|
||||
</Panel>
|
||||
)}
|
||||
<PermissionSimulation {...accessTreeData} />
|
||||
{viewMode === ViewMode.Docked && (
|
||||
<Panel position="top-right" className="flex gap-1.5">
|
||||
<AccessTreeSecretPathInput
|
||||
placeholder="Provide a path, default is /"
|
||||
environment={environment}
|
||||
value={selectedPath}
|
||||
onChange={setSelectedPath}
|
||||
/>
|
||||
</Panel>
|
||||
)}
|
||||
<Background color="#5d5f64" bgColor="#111419" variant={BackgroundVariant.Dots} />
|
||||
<Controls position="bottom-left" />
|
||||
<Controls
|
||||
position="bottom-left"
|
||||
showInteractive={false}
|
||||
onFitView={() => fitView({ duration: 800 })}
|
||||
>
|
||||
<ControlButton onClick={goToRootNode}>
|
||||
<Tooltip position="right" content="Go to root folder">
|
||||
<FontAwesomeIcon icon={faAnglesUp} />
|
||||
</Tooltip>
|
||||
</ControlButton>
|
||||
</Controls>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -46,6 +46,12 @@ export const PermissionSimulation = ({
|
||||
className="mr-1 rounded"
|
||||
colorSchema="secondary"
|
||||
onClick={handlePermissionSimulation}
|
||||
rightIcon={
|
||||
<FontAwesomeIcon
|
||||
className="pl-1 text-sm text-bunker-300 hover:text-primary hover:opacity-80"
|
||||
icon={faChevronDown}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Permission Simulation
|
||||
</Button>
|
||||
|
@ -5,6 +5,7 @@ import { Edge, Node, useEdgesState, useNodesState } from "@xyflow/react";
|
||||
import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
||||
import { useListProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/queries";
|
||||
import { TSecretFolderWithPath } from "@app/hooks/api/secretFolders/types";
|
||||
|
||||
import { useAccessTreeContext } from "../components";
|
||||
import { PermissionAccess } from "../types";
|
||||
@ -15,8 +16,24 @@ import {
|
||||
getSubjectActionRuleMap,
|
||||
positionElements
|
||||
} from "../utils";
|
||||
import { createShowMoreNode } from "../utils/createShowMoreNode";
|
||||
|
||||
export const useAccessTree = (permissions: MongoAbility<ProjectPermissionSet, MongoQuery>) => {
|
||||
const INITIAL_FOLDERS_PER_LEVEL = 10;
|
||||
const FOLDERS_INCREMENT = 10;
|
||||
|
||||
type LevelFolderMap = Record<
|
||||
string,
|
||||
{
|
||||
folders: TSecretFolderWithPath[];
|
||||
visibleCount: number;
|
||||
hasMore: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
export const useAccessTree = (
|
||||
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>,
|
||||
searchPath: string
|
||||
) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { secretName, setSecretName, setViewMode, viewMode } = useAccessTreeContext();
|
||||
const [nodes, setNodes] = useNodesState<Node>([]);
|
||||
@ -27,19 +44,124 @@ export const useAccessTree = (permissions: MongoAbility<ProjectPermissionSet, Mo
|
||||
currentWorkspace.id
|
||||
);
|
||||
|
||||
const [levelFolderMap, setLevelFolderMap] = useState<LevelFolderMap>({});
|
||||
const [totalFolderCount, setTotalFolderCount] = useState(0);
|
||||
|
||||
const showMoreFolders = (parentId: string) => {
|
||||
setLevelFolderMap((prevMap) => {
|
||||
const level = prevMap[parentId];
|
||||
if (!level) return prevMap;
|
||||
|
||||
const newVisibleCount = Math.min(
|
||||
level.visibleCount + FOLDERS_INCREMENT,
|
||||
level.folders.length
|
||||
);
|
||||
|
||||
return {
|
||||
...prevMap,
|
||||
[parentId]: {
|
||||
...level,
|
||||
visibleCount: newVisibleCount,
|
||||
hasMore: newVisibleCount < level.folders.length
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const levelsWithMoreFolders = Object.entries(levelFolderMap)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.filter(([_, level]) => level.hasMore)
|
||||
.map(([parentId]) => parentId);
|
||||
|
||||
const getLevelCounts = (parentId: string) => {
|
||||
const level = levelFolderMap[parentId];
|
||||
if (!level) return { visibleCount: 0, totalCount: 0, hasMore: false };
|
||||
|
||||
return {
|
||||
visibleCount: level.visibleCount,
|
||||
totalCount: level.folders.length,
|
||||
hasMore: level.hasMore
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!environmentsFolders || !permissions || !environmentsFolders[environment]) return;
|
||||
|
||||
const { folders, name } = environmentsFolders[environment];
|
||||
const { folders } = environmentsFolders[environment];
|
||||
setTotalFolderCount(folders.length);
|
||||
const groupedFolders: Record<string, TSecretFolderWithPath[]> = {};
|
||||
|
||||
const filteredFolders = folders.filter((folder) => {
|
||||
if (folder.path.startsWith(searchPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
searchPath.startsWith(folder.path) &&
|
||||
(folder.path === "/" ||
|
||||
searchPath === folder.path ||
|
||||
searchPath.indexOf("/", folder.path.length) === folder.path.length)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
filteredFolders.forEach((folder) => {
|
||||
const parentId = folder.parentId || "";
|
||||
if (!groupedFolders[parentId]) {
|
||||
groupedFolders[parentId] = [];
|
||||
}
|
||||
groupedFolders[parentId].push(folder);
|
||||
});
|
||||
|
||||
const newLevelFolderMap: LevelFolderMap = {};
|
||||
|
||||
Object.entries(groupedFolders).forEach(([parentId, folderList]) => {
|
||||
const key = parentId;
|
||||
newLevelFolderMap[key] = {
|
||||
folders: folderList,
|
||||
visibleCount: Math.min(INITIAL_FOLDERS_PER_LEVEL, folderList.length),
|
||||
hasMore: folderList.length > INITIAL_FOLDERS_PER_LEVEL
|
||||
};
|
||||
});
|
||||
|
||||
setLevelFolderMap(newLevelFolderMap);
|
||||
}, [permissions, environmentsFolders, environment, subject, secretName, searchPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!environmentsFolders ||
|
||||
!permissions ||
|
||||
!environmentsFolders[environment] ||
|
||||
Object.keys(levelFolderMap).length === 0
|
||||
)
|
||||
return;
|
||||
|
||||
const { slug } = environmentsFolders[environment];
|
||||
|
||||
const roleNode = createRoleNode({
|
||||
subject,
|
||||
environment: name
|
||||
environment: slug,
|
||||
environments: environmentsFolders,
|
||||
onSubjectChange: setSubject,
|
||||
onEnvironmentChange: setEnvironment
|
||||
});
|
||||
|
||||
const actionRuleMap = getSubjectActionRuleMap(subject, permissions);
|
||||
|
||||
const folderNodes = folders.map((folder) =>
|
||||
const visibleFolders: TSecretFolderWithPath[] = [];
|
||||
Object.entries(levelFolderMap).forEach(([key, levelData]) => {
|
||||
if (key !== "__rootFolderId") {
|
||||
visibleFolders.push(...levelData.folders.slice(0, levelData.visibleCount));
|
||||
}
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const rootFolder = levelFolderMap.__rootFolderId?.folders[0];
|
||||
|
||||
const folderNodes = visibleFolders.map((folder) =>
|
||||
createFolderNode({
|
||||
folder,
|
||||
permissions,
|
||||
@ -50,10 +172,45 @@ export const useAccessTree = (permissions: MongoAbility<ProjectPermissionSet, Mo
|
||||
})
|
||||
);
|
||||
|
||||
const folderEdges = folderNodes.map(({ data: folder }) => {
|
||||
const actions = Object.values(folder.actions);
|
||||
const folderEdges: Edge[] = [];
|
||||
|
||||
if (rootFolder) {
|
||||
const rootFolderNode = folderNodes.find(
|
||||
(node) => node.data.id === rootFolder.id || node.data.path === rootFolder.path
|
||||
);
|
||||
|
||||
if (rootFolderNode) {
|
||||
const rootActions = Object.values(rootFolderNode.data.actions);
|
||||
let rootAccess: PermissionAccess;
|
||||
|
||||
if (Object.values(rootActions).some((action) => action === PermissionAccess.Full)) {
|
||||
rootAccess = PermissionAccess.Full;
|
||||
} else if (
|
||||
Object.values(rootActions).some((action) => action === PermissionAccess.Partial)
|
||||
) {
|
||||
rootAccess = PermissionAccess.Partial;
|
||||
} else {
|
||||
rootAccess = PermissionAccess.None;
|
||||
}
|
||||
|
||||
folderEdges.push(
|
||||
createBaseEdge({
|
||||
source: roleNode.id,
|
||||
target: rootFolderNode.id,
|
||||
access: rootAccess
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
folderNodes.forEach(({ data: folder }) => {
|
||||
if (rootFolder && (folder.id === rootFolder.id || folder.path === rootFolder.path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actions = Object.values(folder.actions);
|
||||
let access: PermissionAccess;
|
||||
|
||||
if (Object.values(actions).some((action) => action === PermissionAccess.Full)) {
|
||||
access = PermissionAccess.Full;
|
||||
} else if (Object.values(actions).some((action) => action === PermissionAccess.Partial)) {
|
||||
@ -62,17 +219,55 @@ export const useAccessTree = (permissions: MongoAbility<ProjectPermissionSet, Mo
|
||||
access = PermissionAccess.None;
|
||||
}
|
||||
|
||||
return createBaseEdge({
|
||||
source: folder.parentId ?? roleNode.id,
|
||||
target: folder.id,
|
||||
access
|
||||
});
|
||||
folderEdges.push(
|
||||
createBaseEdge({
|
||||
source: folder.parentId ?? roleNode.id,
|
||||
target: folder.id,
|
||||
access
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const init = positionElements([roleNode, ...folderNodes], [...folderEdges]);
|
||||
const addMoreButtons: Node[] = [];
|
||||
|
||||
Object.entries(levelFolderMap).forEach(([parentId, levelData]) => {
|
||||
if (parentId === "__rootFolderId") return;
|
||||
|
||||
const key = parentId === "null" ? null : parentId;
|
||||
|
||||
if (key && levelData.hasMore) {
|
||||
const showMoreButtonNode = createShowMoreNode({
|
||||
parentId: key,
|
||||
onClick: () => showMoreFolders(key),
|
||||
remaining: levelData.folders.length - levelData.visibleCount,
|
||||
subject
|
||||
});
|
||||
|
||||
addMoreButtons.push(showMoreButtonNode);
|
||||
|
||||
folderEdges.push(
|
||||
createBaseEdge({
|
||||
source: key,
|
||||
target: showMoreButtonNode.id,
|
||||
access: PermissionAccess.Partial
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const init = positionElements([roleNode, ...folderNodes, ...addMoreButtons], [...folderEdges]);
|
||||
setNodes(init.nodes);
|
||||
setEdges(init.edges);
|
||||
}, [permissions, environmentsFolders, environment, subject, secretName, setNodes, setEdges]);
|
||||
}, [
|
||||
levelFolderMap,
|
||||
permissions,
|
||||
environmentsFolders,
|
||||
environment,
|
||||
subject,
|
||||
secretName,
|
||||
setNodes,
|
||||
setEdges
|
||||
]);
|
||||
|
||||
return {
|
||||
nodes,
|
||||
@ -86,6 +281,11 @@ export const useAccessTree = (permissions: MongoAbility<ProjectPermissionSet, Mo
|
||||
secretName,
|
||||
setSecretName,
|
||||
viewMode,
|
||||
setViewMode
|
||||
setViewMode,
|
||||
levelFolderMap,
|
||||
showMoreFolders,
|
||||
levelsWithMoreFolders,
|
||||
getLevelCounts,
|
||||
totalFolderCount
|
||||
};
|
||||
};
|
||||
|
@ -0,0 +1,123 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Tooltip } from "@app/components/v2";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
|
||||
type AccessTreeSecretPathInputProps = {
|
||||
placeholder: string;
|
||||
environment: string;
|
||||
value: string;
|
||||
onChange: (path: string) => void;
|
||||
};
|
||||
|
||||
export const AccessTreeSecretPathInput = ({
|
||||
placeholder,
|
||||
environment,
|
||||
value,
|
||||
onChange
|
||||
}: AccessTreeSecretPathInputProps) => {
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleFocus = () => {
|
||||
setIsFocused(true);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
const timeout: NodeJS.Timeout = setTimeout(() => {
|
||||
setIsFocused(false);
|
||||
}, 200);
|
||||
return () => clearTimeout(timeout);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFocused) {
|
||||
setIsExpanded(false);
|
||||
}
|
||||
}, [isFocused]);
|
||||
|
||||
const focusInput = () => {
|
||||
const inputElement = inputRef.current?.querySelector("input");
|
||||
if (inputElement) {
|
||||
inputElement.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSearch = () => {
|
||||
setIsExpanded(!isExpanded);
|
||||
if (!isExpanded) {
|
||||
const timeout: NodeJS.Timeout = setTimeout(focusInput, 300);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
return () => {};
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef} className="relative">
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex items-center overflow-hidden rounded transition-all duration-300 ease-in-out",
|
||||
isFocused ? "bg-mineshaft-800 shadow-md" : "bg-mineshaft-700",
|
||||
isExpanded ? "w-64" : "h-10 w-10"
|
||||
)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<div
|
||||
className="flex h-10 w-10 cursor-pointer items-center justify-center text-mineshaft-300 hover:text-white"
|
||||
onClick={toggleSearch}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
toggleSearch();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSearch} />
|
||||
</div>
|
||||
) : (
|
||||
<Tooltip position="bottom" content="Search paths">
|
||||
<div
|
||||
className="flex h-10 w-10 cursor-pointer items-center justify-center text-mineshaft-300 hover:text-white"
|
||||
onClick={toggleSearch}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
toggleSearch();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSearch} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={inputRef}
|
||||
className={twMerge(
|
||||
"flex-1 transition-opacity duration-300",
|
||||
isExpanded ? "opacity-100" : "hidden"
|
||||
)}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
role="search"
|
||||
>
|
||||
<div className="custom-input-wrapper">
|
||||
<SecretPathInput
|
||||
placeholder={placeholder}
|
||||
environment={environment}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,10 +1,42 @@
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { faFileImport, faFolder, faKey, faLock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
|
||||
import { Select, SelectItem } from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { TProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/types";
|
||||
|
||||
import { createRoleNode } from "../utils";
|
||||
|
||||
const getSubjectIcon = (subject: ProjectPermissionSub) => {
|
||||
switch (subject) {
|
||||
case ProjectPermissionSub.Secrets:
|
||||
return <FontAwesomeIcon icon={faLock} className="h-4 w-4 text-yellow-700" />;
|
||||
case ProjectPermissionSub.SecretFolders:
|
||||
return <FontAwesomeIcon icon={faFolder} className="h-4 w-4 text-yellow-700" />;
|
||||
case ProjectPermissionSub.DynamicSecrets:
|
||||
return <FontAwesomeIcon icon={faKey} className="h-4 w-4 text-yellow-700" />;
|
||||
case ProjectPermissionSub.SecretImports:
|
||||
return <FontAwesomeIcon icon={faFileImport} className="h-4 w-4 text-yellow-700" />;
|
||||
default:
|
||||
return <FontAwesomeIcon icon={faLock} className="h-4 w-4 text-yellow-700" />;
|
||||
}
|
||||
};
|
||||
|
||||
const formatLabel = (text: string) => {
|
||||
return text.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
};
|
||||
|
||||
export const RoleNode = ({
|
||||
data: { subject, environment }
|
||||
}: NodeProps & { data: ReturnType<typeof createRoleNode>["data"] }) => {
|
||||
data: { subject, environment, onSubjectChange, onEnvironmentChange, environments }
|
||||
}: NodeProps & {
|
||||
data: ReturnType<typeof createRoleNode>["data"] & {
|
||||
onSubjectChange: Dispatch<SetStateAction<ProjectPermissionSub>>;
|
||||
onEnvironmentChange: (value: string) => void;
|
||||
environments: TProjectEnvironmentsFolders;
|
||||
};
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Handle
|
||||
@ -12,11 +44,60 @@ export const RoleNode = ({
|
||||
className="pointer-events-none !cursor-pointer opacity-0"
|
||||
position={Position.Top}
|
||||
/>
|
||||
<div className="flex h-full w-full flex-col items-center justify-center rounded-md border border-mineshaft bg-mineshaft-800 px-3 py-2 font-inter shadow-lg">
|
||||
<div className="flex max-w-[14rem] flex-col items-center text-xs text-mineshaft-200">
|
||||
<span className="capitalize">{subject.replace("-", " ")} Access</span>
|
||||
<div className="max-w-[14rem] whitespace-nowrap text-xs text-mineshaft-300">
|
||||
<p className="truncate capitalize">{environment}</p>
|
||||
<div className="flex w-full flex-col items-center justify-center rounded-md border-2 border-mineshaft-500 bg-gradient-to-b from-mineshaft-700 to-mineshaft-800 px-5 py-4 font-inter shadow-2xl">
|
||||
<div className="flex w-full min-w-[240px] flex-col gap-4">
|
||||
<div className="flex w-full flex-col gap-1.5">
|
||||
<div className="ml-1 text-xs font-semibold text-mineshaft-200">Subject</div>
|
||||
<Select
|
||||
value={subject}
|
||||
onValueChange={(value) => onSubjectChange(value as ProjectPermissionSub)}
|
||||
className="w-full rounded-md border border-mineshaft-600 bg-mineshaft-900/90 text-sm shadow-inner backdrop-blur-sm transition-all hover:border-amber-600/50 focus:border-amber-500"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
aria-label="Subject"
|
||||
>
|
||||
{[
|
||||
ProjectPermissionSub.Secrets,
|
||||
ProjectPermissionSub.SecretFolders,
|
||||
ProjectPermissionSub.DynamicSecrets,
|
||||
ProjectPermissionSub.SecretImports
|
||||
].map((sub) => {
|
||||
return (
|
||||
<SelectItem
|
||||
className="relative flex items-center gap-2 py-2 pl-8 pr-8 text-sm capitalize hover:bg-mineshaft-700"
|
||||
value={sub}
|
||||
key={sub}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{getSubjectIcon(sub)}
|
||||
<span className="font-medium">{formatLabel(sub)}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col gap-1.5">
|
||||
<div className="ml-1 text-xs font-semibold text-mineshaft-200">Environment</div>
|
||||
<Select
|
||||
value={environment}
|
||||
onValueChange={onEnvironmentChange}
|
||||
className="w-full rounded-md border border-mineshaft-600 bg-mineshaft-900/90 text-sm shadow-inner backdrop-blur-sm transition-all hover:border-amber-600/50 focus:border-amber-500"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
aria-label="Environment"
|
||||
>
|
||||
{Object.values(environments).map((env) => (
|
||||
<SelectItem
|
||||
key={env.slug}
|
||||
value={env.slug}
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">{env.name}</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -0,0 +1,37 @@
|
||||
import { faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
|
||||
import { Button, Tooltip } from "@app/components/v2";
|
||||
|
||||
import { createShowMoreNode } from "../utils/createShowMoreNode";
|
||||
|
||||
export const ShowMoreButtonNode = ({
|
||||
data: { onClick, remaining }
|
||||
}: NodeProps & { data: ReturnType<typeof createShowMoreNode>["data"] }) => {
|
||||
const tooltipText = `${remaining} ${remaining === 1 ? "folder is" : "folders are"} hidden. Click to show ${remaining > 10 ? "10 more" : ""}`;
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-2">
|
||||
<Handle
|
||||
type="target"
|
||||
className="pointer-events-none !cursor-pointer opacity-0"
|
||||
position={Position.Top}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-center">
|
||||
<Tooltip position="right" content={tooltipText}>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
size="xs"
|
||||
onClick={onClick}
|
||||
rightIcon={<FontAwesomeIcon icon={faChevronRight} className="ml-1" />}
|
||||
>
|
||||
Show More
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -7,7 +7,8 @@ export enum PermissionAccess {
|
||||
export enum PermissionNode {
|
||||
Role = "role",
|
||||
Folder = "folder",
|
||||
Environment = "environment"
|
||||
Environment = "environment",
|
||||
ShowMoreButton = "showMoreButton"
|
||||
}
|
||||
|
||||
export enum PermissionEdge {
|
||||
|
@ -5,11 +5,13 @@ import { PermissionAccess, PermissionEdge } from "../types";
|
||||
export const createBaseEdge = ({
|
||||
source,
|
||||
target,
|
||||
access
|
||||
access,
|
||||
hideEdge = false
|
||||
}: {
|
||||
source: string;
|
||||
target: string;
|
||||
access: PermissionAccess;
|
||||
hideEdge?: boolean;
|
||||
}) => {
|
||||
const color = access === PermissionAccess.None ? "#707174" : "#ccccce";
|
||||
return {
|
||||
@ -17,10 +19,12 @@ export const createBaseEdge = ({
|
||||
source,
|
||||
target,
|
||||
type: PermissionEdge.Base,
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
color
|
||||
},
|
||||
style: { stroke: color }
|
||||
markerEnd: hideEdge
|
||||
? undefined
|
||||
: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
color
|
||||
},
|
||||
style: { stroke: hideEdge ? "transparent" : color }
|
||||
};
|
||||
};
|
||||
|
@ -1,17 +1,31 @@
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { TProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/types";
|
||||
|
||||
import { PermissionNode } from "../types";
|
||||
|
||||
export const createRoleNode = ({
|
||||
subject,
|
||||
environment
|
||||
environment,
|
||||
environments,
|
||||
onSubjectChange,
|
||||
onEnvironmentChange
|
||||
}: {
|
||||
subject: string;
|
||||
environment: string;
|
||||
environments: TProjectEnvironmentsFolders;
|
||||
onSubjectChange: Dispatch<SetStateAction<ProjectPermissionSub>>;
|
||||
onEnvironmentChange: (value: string) => void;
|
||||
}) => ({
|
||||
id: `role-${subject}-${environment}`,
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
subject,
|
||||
environment
|
||||
environment,
|
||||
environments,
|
||||
onSubjectChange,
|
||||
onEnvironmentChange
|
||||
},
|
||||
type: PermissionNode.Role,
|
||||
height: 48,
|
||||
|
@ -0,0 +1,45 @@
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
|
||||
import { PermissionNode } from "../types";
|
||||
|
||||
export const createShowMoreNode = ({
|
||||
parentId,
|
||||
onClick,
|
||||
remaining,
|
||||
subject
|
||||
}: {
|
||||
parentId: string | null;
|
||||
onClick: () => void;
|
||||
remaining: number;
|
||||
subject: ProjectPermissionSub;
|
||||
}) => {
|
||||
let height: number;
|
||||
|
||||
switch (subject) {
|
||||
case ProjectPermissionSub.DynamicSecrets:
|
||||
height = 130;
|
||||
break;
|
||||
case ProjectPermissionSub.Secrets:
|
||||
height = 85;
|
||||
break;
|
||||
default:
|
||||
height = 64;
|
||||
}
|
||||
const id = `show-more-${parentId || "root"}`;
|
||||
return {
|
||||
id,
|
||||
type: PermissionNode.ShowMoreButton,
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
parentId,
|
||||
onClick,
|
||||
remaining
|
||||
},
|
||||
width: 150,
|
||||
height,
|
||||
style: {
|
||||
background: "transparent",
|
||||
border: "none"
|
||||
}
|
||||
};
|
||||
};
|
@ -2,27 +2,96 @@ import Dagre from "@dagrejs/dagre";
|
||||
import { Edge, Node } from "@xyflow/react";
|
||||
|
||||
export const positionElements = (nodes: Node[], edges: Edge[]) => {
|
||||
const showMoreNodes = nodes.filter((node) => node.type === "showMoreButton");
|
||||
const showMoreParentIds = new Set(
|
||||
showMoreNodes.map((node) => node.data.parentId).filter(Boolean)
|
||||
);
|
||||
|
||||
const nodeMap: Record<string, Node> = {};
|
||||
const childrenMap: Record<string, string[]> = {};
|
||||
|
||||
edges.forEach((edge) => {
|
||||
if (!childrenMap[edge.source]) {
|
||||
childrenMap[edge.source] = [];
|
||||
}
|
||||
childrenMap[edge.source].push(edge.target);
|
||||
});
|
||||
|
||||
const dagre = new Dagre.graphlib.Graph({ directed: true })
|
||||
.setDefaultEdgeLabel(() => ({}))
|
||||
.setGraph({ rankdir: "TB" });
|
||||
.setGraph({
|
||||
rankdir: "TB",
|
||||
nodesep: 50,
|
||||
ranksep: 70
|
||||
});
|
||||
|
||||
nodes.forEach((node) => {
|
||||
dagre.setNode(node.id, {
|
||||
width: node.width || 150,
|
||||
height: node.height || 40
|
||||
});
|
||||
});
|
||||
|
||||
edges.forEach((edge) => dagre.setEdge(edge.source, edge.target));
|
||||
nodes.forEach((node) => dagre.setNode(node.id, node));
|
||||
|
||||
Dagre.layout(dagre, {});
|
||||
|
||||
return {
|
||||
nodes: nodes.map((node) => {
|
||||
const { x, y } = dagre.node(node.id);
|
||||
const positionedNodes = nodes.map((node) => {
|
||||
const { x, y } = dagre.node(node.id);
|
||||
|
||||
if (node.type === "role") {
|
||||
return {
|
||||
...node,
|
||||
position: {
|
||||
x: x - (node.width ? node.width / 2 : 0),
|
||||
y: y - (node.height ? node.height / 2 : 0)
|
||||
y: y - 150
|
||||
}
|
||||
};
|
||||
}),
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
position: {
|
||||
x: x - (node.width ? node.width / 2 : 0),
|
||||
y: y - (node.height ? node.height / 2 : 0)
|
||||
},
|
||||
style: node.type === "showMoreButton" ? { ...node.style, zIndex: 10 } : node.style
|
||||
};
|
||||
});
|
||||
|
||||
positionedNodes.forEach((node) => {
|
||||
nodeMap[node.id] = node;
|
||||
});
|
||||
|
||||
Array.from(showMoreParentIds).forEach((parentId) => {
|
||||
const showMoreNodeIndex = positionedNodes.findIndex(
|
||||
(node) => node.type === "showMoreButton" && node.data.parentId === parentId
|
||||
);
|
||||
|
||||
if (showMoreNodeIndex !== -1) {
|
||||
const siblings = positionedNodes.filter(
|
||||
(node) => node.data?.parentId === parentId && node.type !== "showMoreButton"
|
||||
);
|
||||
|
||||
if (siblings.length > 0) {
|
||||
const rightmostSibling = siblings.reduce(
|
||||
(rightmost, current) => (current.position.x > rightmost.position.x ? current : rightmost),
|
||||
siblings[0]
|
||||
);
|
||||
|
||||
positionedNodes[showMoreNodeIndex] = {
|
||||
...positionedNodes[showMoreNodeIndex],
|
||||
position: {
|
||||
x: rightmostSibling.position.x + (rightmostSibling.width || 150) + 30,
|
||||
y: rightmostSibling.position.y
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
nodes: positionedNodes,
|
||||
edges
|
||||
};
|
||||
};
|
||||
|
@ -48,7 +48,6 @@ export const SecretPathInput = ({
|
||||
}, [propValue]);
|
||||
|
||||
useEffect(() => {
|
||||
// update secret path if input is valid
|
||||
if (
|
||||
(debouncedInputValue.length > 0 &&
|
||||
debouncedInputValue[debouncedInputValue.length - 1] === "/") ||
|
||||
@ -59,7 +58,6 @@ export const SecretPathInput = ({
|
||||
}, [debouncedInputValue]);
|
||||
|
||||
useEffect(() => {
|
||||
// filter suggestions based on matching
|
||||
const searchFragment = debouncedInputValue.split("/").pop() || "";
|
||||
const filteredSuggestions = folders
|
||||
.filter((suggestionEntry) =>
|
||||
@ -78,7 +76,6 @@ export const SecretPathInput = ({
|
||||
const validPaths = inputValue.split("/");
|
||||
validPaths.pop();
|
||||
|
||||
// removed trailing slash
|
||||
const newValue = `${validPaths.join("/")}/${suggestions[selectedIndex]}`;
|
||||
onChange?.(newValue);
|
||||
setInputValue(newValue);
|
||||
@ -102,7 +99,6 @@ export const SecretPathInput = ({
|
||||
};
|
||||
|
||||
const handleInputChange = (e: any) => {
|
||||
// propagate event to react-hook-form onChange
|
||||
if (onChange) {
|
||||
onChange(e.target.value);
|
||||
}
|
||||
@ -141,7 +137,7 @@ export const SecretPathInput = ({
|
||||
maxHeight: "var(--radix-select-content-available-height)"
|
||||
}}
|
||||
>
|
||||
<div className="h-full w-full flex-col items-center justify-center rounded-md text-white">
|
||||
<div className="max-h-[25vh] w-full flex-col items-center justify-center overflow-y-scroll rounded-md text-white">
|
||||
{suggestions.map((suggestion, i) => (
|
||||
<div
|
||||
tabIndex={0}
|
||||
|
@ -1,5 +1,16 @@
|
||||
import { EventType, UserAgentType } from "./enums";
|
||||
|
||||
export const secretEvents: EventType[] = [
|
||||
EventType.GET_SECRETS,
|
||||
EventType.GET_SECRET,
|
||||
EventType.DELETE_SECRETS,
|
||||
EventType.CREATE_SECRETS,
|
||||
EventType.UPDATE_SECRETS,
|
||||
EventType.CREATE_SECRET,
|
||||
EventType.UPDATE_SECRET,
|
||||
EventType.DELETE_SECRET
|
||||
];
|
||||
|
||||
export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.GET_SECRETS]: "List secrets",
|
||||
[EventType.GET_SECRET]: "Read secret",
|
||||
|
@ -12,8 +12,8 @@ export enum UserAgentType {
|
||||
CLI = "cli",
|
||||
K8_OPERATOR = "k8-operator",
|
||||
TERRAFORM = "terraform",
|
||||
NODE_SDK = "node-sdk",
|
||||
PYTHON_SDK = "python-sdk",
|
||||
NODE_SDK = "InfisicalNodeSDK",
|
||||
PYTHON_SDK = "InfisicalPythonSDK",
|
||||
OTHER = "other"
|
||||
}
|
||||
|
||||
|
@ -9,8 +9,10 @@ export type TGetAuditLogsFilter = {
|
||||
eventMetadata?: Record<string, string>;
|
||||
actorType?: ActorType;
|
||||
projectId?: string;
|
||||
environment?: string;
|
||||
actor?: string; // user ID format
|
||||
secretPath?: string;
|
||||
secretKey?: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
limit: number;
|
||||
|
@ -319,12 +319,13 @@ const fetchAccessibleSecrets = async ({
|
||||
projectId,
|
||||
secretPath,
|
||||
environment,
|
||||
filterByAction
|
||||
filterByAction,
|
||||
recursive = false
|
||||
}: TGetAccessibleSecretsDTO) => {
|
||||
const { data } = await apiRequest.get<{ secrets: SecretV3Raw[] }>(
|
||||
"/api/v1/dashboard/accessible-secrets",
|
||||
{
|
||||
params: { projectId, secretPath, environment, filterByAction }
|
||||
params: { projectId, secretPath, environment, filterByAction, recursive }
|
||||
}
|
||||
);
|
||||
|
||||
@ -399,7 +400,8 @@ export const useGetAccessibleSecrets = ({
|
||||
secretPath,
|
||||
environment,
|
||||
filterByAction,
|
||||
options
|
||||
options,
|
||||
recursive = false
|
||||
}: TGetAccessibleSecretsDTO & {
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
@ -417,8 +419,10 @@ export const useGetAccessibleSecrets = ({
|
||||
projectId,
|
||||
secretPath,
|
||||
environment,
|
||||
filterByAction
|
||||
filterByAction,
|
||||
recursive
|
||||
}),
|
||||
queryFn: () => fetchAccessibleSecrets({ projectId, secretPath, environment, filterByAction })
|
||||
queryFn: () =>
|
||||
fetchAccessibleSecrets({ projectId, secretPath, environment, filterByAction, recursive })
|
||||
});
|
||||
};
|
||||
|
@ -111,6 +111,7 @@ export type TGetAccessibleSecretsDTO = {
|
||||
projectId: string;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
recursive?: boolean;
|
||||
filterByAction:
|
||||
| ProjectPermissionSecretActions.DescribeSecret
|
||||
| ProjectPermissionSecretActions.ReadValue;
|
||||
|
@ -121,6 +121,7 @@ export type TGetProjectSecretsKey = {
|
||||
includeImports?: boolean;
|
||||
viewSecretValue?: boolean;
|
||||
expandSecretReferences?: boolean;
|
||||
recursive?: boolean;
|
||||
};
|
||||
|
||||
export type TGetProjectSecretsDTO = TGetProjectSecretsKey;
|
||||
|
@ -18,7 +18,7 @@ export const AuditLogsPage = () => {
|
||||
title="Audit logs"
|
||||
description="Audit logs for security and compliance teams to monitor information access."
|
||||
/>
|
||||
<LogsSection filterClassName="static py-2" showFilters />
|
||||
<LogsSection />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -0,0 +1,32 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Button, Tooltip } from "@app/components/v2";
|
||||
|
||||
type Props = {
|
||||
hoverTooltip?: string;
|
||||
className?: string;
|
||||
label: string;
|
||||
onClear: () => void;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const LogFilterItem = ({ label, onClear, hoverTooltip, children, className }: Props) => {
|
||||
return (
|
||||
<Tooltip className="relative top-4" content={hoverTooltip} isDisabled={!hoverTooltip}>
|
||||
<div className={twMerge("flex flex-col justify-between", className)}>
|
||||
<div className="flex items-center justify-between pr-1">
|
||||
<p className="text-xs opacity-60">{label}</p>
|
||||
<Button
|
||||
onClick={() => onClear()}
|
||||
variant="link"
|
||||
className="font-normal text-mineshaft-400 transition-all duration-75 hover:text-mineshaft-300"
|
||||
size="xs"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
@ -1,11 +1,26 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { useState } from "react";
|
||||
import { Control, Controller, UseFormReset, UseFormSetValue, UseFormWatch } from "react-hook-form";
|
||||
import { faCaretDown, faCheckCircle, faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
Control,
|
||||
Controller,
|
||||
UseFormGetFieldState,
|
||||
UseFormReset,
|
||||
UseFormResetField,
|
||||
UseFormSetValue,
|
||||
UseFormWatch
|
||||
} from "react-hook-form";
|
||||
import {
|
||||
faArrowRight,
|
||||
faCaretDown,
|
||||
faCheckCircle,
|
||||
faFilterCircleXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DatePicker,
|
||||
DropdownMenu,
|
||||
@ -19,13 +34,17 @@ import {
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useGetAuditLogActorFilterOpts, useGetUserWorkspaces } from "@app/hooks/api";
|
||||
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
|
||||
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { Actor } from "@app/hooks/api/auditLogs/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { useGetUserWorkspaces } from "@app/hooks/api";
|
||||
import {
|
||||
eventToNameMap,
|
||||
secretEvents,
|
||||
userAgentTTypeoNameMap
|
||||
} from "@app/hooks/api/auditLogs/constants";
|
||||
import { EventType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { UserAgentType } from "@app/hooks/api/auth/types";
|
||||
|
||||
import { AuditLogFilterFormData } from "./types";
|
||||
import { LogFilterItem } from "./LogFilterItem";
|
||||
import { AuditLogFilterFormData, Presets } from "./types";
|
||||
|
||||
const eventTypes = Object.entries(eventToNameMap).map(([value, label]) => ({ label, value }));
|
||||
const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label]) => ({
|
||||
@ -34,26 +53,70 @@ const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
presets?: {
|
||||
actorId?: string;
|
||||
eventType?: EventType[];
|
||||
};
|
||||
className?: string;
|
||||
isOrgAuditLogs?: boolean;
|
||||
setValue: UseFormSetValue<AuditLogFilterFormData>;
|
||||
presets?: Presets;
|
||||
control: Control<AuditLogFilterFormData>;
|
||||
reset: UseFormReset<AuditLogFilterFormData>;
|
||||
resetField: UseFormResetField<AuditLogFilterFormData>;
|
||||
watch: UseFormWatch<AuditLogFilterFormData>;
|
||||
getFieldState: UseFormGetFieldState<AuditLogFilterFormData>;
|
||||
setValue: UseFormSetValue<AuditLogFilterFormData>;
|
||||
};
|
||||
|
||||
const getActiveFilterCount = (
|
||||
getFieldState: UseFormGetFieldState<AuditLogFilterFormData>,
|
||||
watch: UseFormWatch<AuditLogFilterFormData>
|
||||
) => {
|
||||
const fields = [
|
||||
"actor",
|
||||
"project",
|
||||
"eventType",
|
||||
"startDate",
|
||||
"endDate",
|
||||
"environment",
|
||||
"secretPath",
|
||||
"userAgentType",
|
||||
"secretKey"
|
||||
] as Partial<keyof AuditLogFilterFormData>[];
|
||||
|
||||
let filterCount = 0;
|
||||
|
||||
// either start or end date should only be counted as one filter
|
||||
let dateProcessed = false;
|
||||
|
||||
fields.forEach((field) => {
|
||||
const fieldState = getFieldState(field);
|
||||
|
||||
if (
|
||||
field === "userAgentType" ||
|
||||
field === "environment" ||
|
||||
field === "secretKey" ||
|
||||
field === "secretPath"
|
||||
) {
|
||||
const value = watch(field);
|
||||
|
||||
if (value !== undefined && value !== "") {
|
||||
filterCount += 1;
|
||||
}
|
||||
} else if (fieldState.isDirty && !dateProcessed) {
|
||||
filterCount += 1;
|
||||
|
||||
if (field === "startDate" || field === "endDate") {
|
||||
dateProcessed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return filterCount;
|
||||
};
|
||||
|
||||
export const LogsFilter = ({
|
||||
presets,
|
||||
isOrgAuditLogs,
|
||||
className,
|
||||
control,
|
||||
reset,
|
||||
setValue,
|
||||
watch
|
||||
resetField,
|
||||
watch,
|
||||
getFieldState,
|
||||
setValue
|
||||
}: Props) => {
|
||||
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
||||
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
|
||||
@ -63,288 +126,423 @@ export const LogsFilter = ({
|
||||
|
||||
const workspacesInOrg = workspaces.filter((ws) => ws.orgId === currentOrg?.id);
|
||||
|
||||
const { data, isPending } = useGetAuditLogActorFilterOpts(workspaces?.[0]?.id ?? "");
|
||||
|
||||
const renderActorSelectItem = (actor: Actor) => {
|
||||
switch (actor.type) {
|
||||
case ActorType.USER:
|
||||
return (
|
||||
<SelectItem
|
||||
value={`${actor.type}-${actor.metadata.userId}`}
|
||||
key={`user-actor-filter-${actor.metadata.userId}`}
|
||||
>
|
||||
{actor.metadata.email}
|
||||
</SelectItem>
|
||||
);
|
||||
case ActorType.SERVICE:
|
||||
return (
|
||||
<SelectItem
|
||||
value={`${actor.type}-${actor.metadata.serviceId}`}
|
||||
key={`service-actor-filter-${actor.metadata.serviceId}`}
|
||||
>
|
||||
{actor.metadata.name}
|
||||
</SelectItem>
|
||||
);
|
||||
case ActorType.IDENTITY:
|
||||
return (
|
||||
<SelectItem
|
||||
value={`${actor.type}-${actor.metadata.identityId}`}
|
||||
key={`identity-filter-${actor.metadata.identityId}`}
|
||||
>
|
||||
{actor.metadata.name}
|
||||
</SelectItem>
|
||||
);
|
||||
case ActorType.KMIP_CLIENT:
|
||||
return (
|
||||
<SelectItem
|
||||
value={`${actor.type}-${actor.metadata.clientId}`}
|
||||
key={`kmip-client-filter-${actor.metadata.clientId}`}
|
||||
>
|
||||
{actor.metadata.name}
|
||||
</SelectItem>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<SelectItem value="actor-none" key="actor-none">
|
||||
N/A
|
||||
</SelectItem>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const selectedEventTypes = watch("eventType") as EventType[] | undefined;
|
||||
const selectedProject = watch("project");
|
||||
|
||||
const showSecretsSection =
|
||||
selectedEventTypes?.some(
|
||||
(eventType) => secretEvents.includes(eventType) && eventType !== EventType.GET_SECRETS
|
||||
) || selectedEventTypes?.length === 0;
|
||||
|
||||
const availableEnvironments = useMemo(() => {
|
||||
if (!selectedProject) return [];
|
||||
|
||||
return workspacesInOrg.find((ws) => ws.id === selectedProject.id)?.environments ?? [];
|
||||
}, [selectedProject, workspacesInOrg]);
|
||||
|
||||
const activeFilterCount = getActiveFilterCount(getFieldState, watch);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"sticky top-20 z-10 flex flex-wrap items-center justify-between bg-bunker-800",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
{isOrgAuditLogs && workspacesInOrg.length > 0 && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="project"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="w-64"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
isClearable
|
||||
onChange={(e) => {
|
||||
if (e === null) {
|
||||
setValue("secretPath", "");
|
||||
}
|
||||
onChange(e);
|
||||
}}
|
||||
placeholder="Select a project..."
|
||||
options={workspacesInOrg.map(({ name, id, type }) => ({ name, id, type }))}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{selectedProject?.type === ProjectType.SecretManager && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPath"
|
||||
render={({ field: { onChange, value, ...field } }) => (
|
||||
<FormControl label="Secret path" className="w-40">
|
||||
<Input {...field} value={value} onChange={(e) => onChange(e.target.value)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="eventType"
|
||||
render={({ field }) => (
|
||||
<FormControl label="Events">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="inline-flex w-full cursor-pointer items-center justify-between whitespace-nowrap rounded-md border border-mineshaft-500 bg-mineshaft-700 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||
{selectedEventTypes?.length === 1
|
||||
? eventTypes.find((eventType) => eventType.value === selectedEventTypes[0])
|
||||
?.label
|
||||
: selectedEventTypes?.length === 0
|
||||
? "All events"
|
||||
: `${selectedEventTypes?.length} events selected`}
|
||||
<FontAwesomeIcon icon={faCaretDown} className="ml-2 text-xs" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="z-[100] max-h-80 overflow-hidden">
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
{eventTypes && eventTypes.length > 0 ? (
|
||||
eventTypes.map((eventType) => {
|
||||
const isSelected = selectedEventTypes?.includes(
|
||||
eventType.value as EventType
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onSelect={(event) => eventTypes.length > 1 && event.preventDefault()}
|
||||
onClick={() => {
|
||||
if (selectedEventTypes?.includes(eventType.value as EventType)) {
|
||||
field.onChange(
|
||||
selectedEventTypes?.filter((e: string) => e !== eventType.value)
|
||||
);
|
||||
} else {
|
||||
field.onChange([...(selectedEventTypes || []), eventType.value]);
|
||||
}
|
||||
}}
|
||||
key={`event-type-${eventType.value}`}
|
||||
icon={
|
||||
isSelected ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
className="pr-0.5 text-primary"
|
||||
/>
|
||||
) : (
|
||||
<div className="pl-[1.01rem]" />
|
||||
)
|
||||
}
|
||||
iconPos="left"
|
||||
className="w-[28.4rem] text-sm"
|
||||
>
|
||||
{eventType.label}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline_bg" colorSchema="primary">
|
||||
<FontAwesomeIcon icon={faFilterCircleXmark} className="mr-3 px-[0.1rem]" />
|
||||
Filters
|
||||
{activeFilterCount > 0 && (
|
||||
<Badge className="ml-2 px-1.5 py-0.5" variant="primary">
|
||||
{activeFilterCount}
|
||||
</Badge>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!isPending && data && data.length > 0 && !presets?.actorId && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="actor"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Actor"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="w-40"
|
||||
>
|
||||
<Select
|
||||
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full border border-mineshaft-500 bg-mineshaft-700 text-mineshaft-100"
|
||||
>
|
||||
{data.map((actor) => renderActorSelectItem(actor))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="userAgentType"
|
||||
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Source"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="w-40"
|
||||
>
|
||||
<Select
|
||||
value={value === undefined ? "all" : value}
|
||||
{...field}
|
||||
onValueChange={(e) => {
|
||||
if (e === "all") onChange(undefined);
|
||||
else onChange(e);
|
||||
}}
|
||||
className={twMerge("w-full border border-mineshaft-500 bg-mineshaft-700")}
|
||||
>
|
||||
<SelectItem value="all" key="all">
|
||||
All sources
|
||||
</SelectItem>
|
||||
{userAgentTypes.map(({ label, value: userAgent }) => (
|
||||
<SelectItem value={userAgent} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="startDate"
|
||||
control={control}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl label="Start date" errorText={error?.message} isError={Boolean(error)}>
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
onChange={onChange}
|
||||
dateFormat="P"
|
||||
popUpProps={{
|
||||
open: isStartDatePickerOpen,
|
||||
onOpenChange: setIsStartDatePickerOpen
|
||||
}}
|
||||
popUpContentProps={{}}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="endDate"
|
||||
control={control}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl label="End date" errorText={error?.message} isError={Boolean(error)}>
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
onChange={onChange}
|
||||
dateFormat="P"
|
||||
popUpProps={{
|
||||
open: isEndDatePickerOpen,
|
||||
onOpenChange: setIsEndDatePickerOpen
|
||||
}}
|
||||
popUpContentProps={{}}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
isLoading={false}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mt-[0.45rem]"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} />}
|
||||
onClick={() =>
|
||||
reset({
|
||||
eventType: presets?.eventType || [],
|
||||
actor: presets?.actorId,
|
||||
userAgentType: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
project: null
|
||||
})
|
||||
}
|
||||
>
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="mt-4 py-4">
|
||||
<div className="flex min-w-64 flex-col font-inter">
|
||||
<div className="mb-3 flex items-center border-b border-b-mineshaft-500 px-3 pb-2">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Filters</span>
|
||||
<Badge className="px-1.5 py-0.5" variant="primary">
|
||||
{activeFilterCount}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
reset({
|
||||
eventType: presets?.eventType || [],
|
||||
actor: presets?.actorId,
|
||||
userAgentType: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
project: null,
|
||||
secretPath: undefined,
|
||||
secretKey: undefined
|
||||
});
|
||||
}}
|
||||
variant="link"
|
||||
className="text-mineshaft-400"
|
||||
size="xs"
|
||||
>
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-3">
|
||||
<LogFilterItem
|
||||
label="Events"
|
||||
onClear={() => {
|
||||
resetField("eventType");
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="eventType"
|
||||
render={({ field }) => (
|
||||
<FormControl>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="thin-scrollbar inline-flex w-full cursor-pointer items-center justify-between whitespace-nowrap rounded-md border border-mineshaft-500 bg-mineshaft-700 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||
{selectedEventTypes?.length === 1
|
||||
? eventTypes.find(
|
||||
(eventType) => eventType.value === selectedEventTypes[0]
|
||||
)?.label
|
||||
: selectedEventTypes?.length === 0
|
||||
? "All events"
|
||||
: `${selectedEventTypes?.length} events selected`}
|
||||
<FontAwesomeIcon icon={faCaretDown} className="ml-2 text-xs" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="thin-scrollbar z-[100] max-h-80 overflow-hidden"
|
||||
>
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
{eventTypes && eventTypes.length > 0 ? (
|
||||
eventTypes.map((eventType) => {
|
||||
const isSelected = selectedEventTypes?.includes(
|
||||
eventType.value as EventType
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onSelect={(event) =>
|
||||
eventTypes.length > 1 && event.preventDefault()
|
||||
}
|
||||
onClick={() => {
|
||||
if (
|
||||
selectedEventTypes?.includes(eventType.value as EventType)
|
||||
) {
|
||||
field.onChange(
|
||||
selectedEventTypes?.filter(
|
||||
(e: string) => e !== eventType.value
|
||||
)
|
||||
);
|
||||
} else {
|
||||
field.onChange([
|
||||
...(selectedEventTypes || []),
|
||||
eventType.value
|
||||
]);
|
||||
}
|
||||
}}
|
||||
key={`event-type-${eventType.value}`}
|
||||
icon={
|
||||
isSelected ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
className="pr-0.5 text-primary"
|
||||
/>
|
||||
) : (
|
||||
<div className="pl-[1.01rem]" />
|
||||
)
|
||||
}
|
||||
iconPos="left"
|
||||
className="w-[28.4rem] text-sm"
|
||||
>
|
||||
{eventType.label}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</LogFilterItem>
|
||||
<LogFilterItem
|
||||
label="Source"
|
||||
onClear={() => {
|
||||
resetField("userAgentType");
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="userAgentType"
|
||||
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="w-full"
|
||||
>
|
||||
<Select
|
||||
value={value === undefined ? "all" : value}
|
||||
{...field}
|
||||
onValueChange={(e) => {
|
||||
if (e === "all") onChange(undefined);
|
||||
else setValue("userAgentType", e as UserAgentType, { shouldDirty: true });
|
||||
}}
|
||||
className={twMerge("w-full border border-mineshaft-500 bg-mineshaft-700")}
|
||||
>
|
||||
<SelectItem value="all" key="all">
|
||||
All sources
|
||||
</SelectItem>
|
||||
{userAgentTypes.map(({ label, value: userAgent }) => (
|
||||
<SelectItem value={userAgent} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</LogFilterItem>
|
||||
|
||||
<LogFilterItem
|
||||
label="Date"
|
||||
onClear={() => {
|
||||
resetField("startDate");
|
||||
resetField("endDate");
|
||||
}}
|
||||
>
|
||||
<div className="flex h-10 w-full items-center justify-between gap-2">
|
||||
<Controller
|
||||
name="startDate"
|
||||
control={control}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="relative top-2"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
onChange={onChange}
|
||||
dateFormat="P"
|
||||
popUpProps={{
|
||||
open: isStartDatePickerOpen,
|
||||
onOpenChange: setIsStartDatePickerOpen
|
||||
}}
|
||||
popUpContentProps={{}}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex items-center -space-x-3">
|
||||
<div className="h-[2px] w-[20px] rounded-full bg-mineshaft-500" />
|
||||
<FontAwesomeIcon icon={faArrowRight} className="text-mineshaft-500" />
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
name="endDate"
|
||||
control={control}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="relative top-2"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
onChange={onChange}
|
||||
dateFormat="P"
|
||||
popUpProps={{
|
||||
open: isEndDatePickerOpen,
|
||||
onOpenChange: setIsEndDatePickerOpen
|
||||
}}
|
||||
popUpContentProps={{}}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</LogFilterItem>
|
||||
<AnimatePresence initial={false}>
|
||||
{showSecretsSection && (
|
||||
<motion.div
|
||||
className="mt-2 overflow-hidden"
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="mb-3 mt-2">
|
||||
<p className="text-xs opacity-60">Secrets</p>
|
||||
<div className="h-[1px] w-full rounded-full bg-mineshaft-500" />
|
||||
</div>
|
||||
|
||||
<LogFilterItem
|
||||
label="Project"
|
||||
onClear={() => {
|
||||
resetField("project");
|
||||
resetField("environment");
|
||||
setValue("secretPath", "");
|
||||
setValue("secretKey", "");
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="project"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="w-full"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
isClearable
|
||||
onChange={(e) => {
|
||||
if (e === null) {
|
||||
setValue("secretPath", "");
|
||||
setValue("secretKey", "");
|
||||
}
|
||||
resetField("environment");
|
||||
onChange(e);
|
||||
}}
|
||||
placeholder="All projects"
|
||||
options={workspacesInOrg.map(({ name, id, type }) => ({
|
||||
name,
|
||||
id,
|
||||
type
|
||||
}))}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</LogFilterItem>
|
||||
|
||||
<LogFilterItem
|
||||
label="Environment"
|
||||
hoverTooltip={
|
||||
!selectedProject
|
||||
? "Select a project before filtering by environment."
|
||||
: undefined
|
||||
}
|
||||
className={twMerge(!selectedProject && "opacity-50")}
|
||||
onClear={() => {
|
||||
resetField("environment");
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="w-full"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
key={value?.name || "filter-environment"}
|
||||
isClearable
|
||||
isDisabled={!selectedProject}
|
||||
onChange={(e) => onChange(e)}
|
||||
placeholder="All environments"
|
||||
options={availableEnvironments.map(({ name, slug }) => ({
|
||||
name,
|
||||
slug
|
||||
}))}
|
||||
getOptionValue={(option) => option.slug}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</LogFilterItem>
|
||||
<LogFilterItem
|
||||
label="Secret Path"
|
||||
hoverTooltip={
|
||||
!selectedProject
|
||||
? "Select a project before filtering by secret path."
|
||||
: undefined
|
||||
}
|
||||
className={twMerge(!selectedProject && "opacity-50")}
|
||||
onClear={() => {
|
||||
setValue("secretPath", "");
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPath"
|
||||
render={({ field: { onChange, value, ...field } }) => (
|
||||
<FormControl
|
||||
tooltipText="Filter audit logs related to events that occurred on a specific secret path."
|
||||
className="w-full"
|
||||
>
|
||||
<Input
|
||||
placeholder="Enter secret path"
|
||||
className="disabled:cursor-not-allowed"
|
||||
isDisabled={!selectedProject}
|
||||
{...field}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</LogFilterItem>
|
||||
|
||||
<LogFilterItem
|
||||
hoverTooltip={
|
||||
!selectedProject
|
||||
? "Select a project before filtering by secret key."
|
||||
: undefined
|
||||
}
|
||||
className={twMerge(!selectedProject && "opacity-50")}
|
||||
label="Secret Key"
|
||||
onClear={() => {
|
||||
setValue("secretKey", "");
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretKey"
|
||||
render={({ field: { onChange, value, ...field } }) => (
|
||||
<FormControl
|
||||
tooltipText="Filter audit logs related to a specific secret."
|
||||
className="w-full"
|
||||
>
|
||||
<Input
|
||||
isDisabled={!selectedProject}
|
||||
{...field}
|
||||
placeholder="Enter secret key"
|
||||
className="disabled:cursor-not-allowed"
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
setValue("secretKey", e.target.value, { shouldDirty: true })
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</LogFilterItem>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
@ -6,46 +6,40 @@ import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { useDebounce } from "@app/hooks";
|
||||
import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { LogsFilter } from "./LogsFilter";
|
||||
import { LogsTable } from "./LogsTable";
|
||||
import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types";
|
||||
import { AuditLogFilterFormData, auditLogFilterFormSchema, Presets } from "./types";
|
||||
|
||||
type Props = {
|
||||
presets?: {
|
||||
actorId?: string;
|
||||
eventType?: EventType[];
|
||||
actorType?: ActorType;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
eventMetadata?: Record<string, string>;
|
||||
};
|
||||
|
||||
showFilters?: boolean;
|
||||
filterClassName?: string;
|
||||
presets?: Presets;
|
||||
refetchInterval?: number;
|
||||
showFilters?: boolean;
|
||||
};
|
||||
|
||||
export const LogsSection = withPermission(
|
||||
({ presets, filterClassName, refetchInterval, showFilters }: Props) => {
|
||||
({ presets, refetchInterval, showFilters = true }: Props) => {
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
||||
|
||||
const { control, reset, watch, setValue } = useForm<AuditLogFilterFormData>({
|
||||
resolver: zodResolver(auditLogFilterFormSchema),
|
||||
defaultValues: {
|
||||
project: null,
|
||||
actor: presets?.actorId,
|
||||
eventType: presets?.eventType || [],
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
startDate: presets?.startDate ?? new Date(new Date().setDate(new Date().getDate() - 1)), // day before today
|
||||
endDate: presets?.endDate ?? new Date(new Date(Date.now()).setHours(23, 59, 59, 999)) // end of today
|
||||
}
|
||||
});
|
||||
const { control, reset, watch, getFieldState, resetField, setValue } =
|
||||
useForm<AuditLogFilterFormData>({
|
||||
resolver: zodResolver(auditLogFilterFormSchema),
|
||||
defaultValues: {
|
||||
project: null,
|
||||
environment: undefined,
|
||||
secretKey: "",
|
||||
secretPath: "",
|
||||
actor: presets?.actorId,
|
||||
eventType: presets?.eventType || [],
|
||||
userAgentType: undefined,
|
||||
startDate: presets?.startDate ?? new Date(new Date().setDate(new Date().getDate() - 1)),
|
||||
endDate: presets?.endDate ?? new Date(new Date(Date.now()).setHours(23, 59, 59, 999))
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (subscription && !subscription.auditLogs) {
|
||||
@ -57,30 +51,37 @@ export const LogsSection = withPermission(
|
||||
const userAgentType = watch("userAgentType") as UserAgentType | undefined;
|
||||
const actor = watch("actor");
|
||||
const projectId = watch("project")?.id;
|
||||
const environment = watch("environment")?.slug;
|
||||
const secretPath = watch("secretPath");
|
||||
const secretKey = watch("secretKey");
|
||||
|
||||
const startDate = watch("startDate");
|
||||
const endDate = watch("endDate");
|
||||
|
||||
const [debouncedSecretPath] = useDebounce<string>(secretPath!, 500);
|
||||
const [debouncedSecretKey] = useDebounce<string>(secretKey!, 500);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showFilters && (
|
||||
<LogsFilter
|
||||
isOrgAuditLogs
|
||||
className={filterClassName}
|
||||
presets={presets}
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
watch={watch}
|
||||
reset={reset}
|
||||
/>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<div className="flex w-full justify-end">
|
||||
{showFilters && (
|
||||
<LogsFilter
|
||||
presets={presets}
|
||||
control={control}
|
||||
watch={watch}
|
||||
reset={reset}
|
||||
resetField={resetField}
|
||||
getFieldState={getFieldState}
|
||||
setValue={setValue}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<LogsTable
|
||||
refetchInterval={refetchInterval}
|
||||
filter={{
|
||||
secretPath: debouncedSecretPath || undefined,
|
||||
secretKey: debouncedSecretKey || undefined,
|
||||
eventMetadata: presets?.eventMetadata,
|
||||
projectId,
|
||||
actorType: presets?.actorType,
|
||||
@ -89,6 +90,7 @@ export const LogsSection = withPermission(
|
||||
userAgentType,
|
||||
startDate,
|
||||
endDate,
|
||||
environment,
|
||||
actor
|
||||
}}
|
||||
/>
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { Fragment } from "react";
|
||||
import { faFile, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
Spinner,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
@ -52,7 +54,9 @@ export const LogsTable = ({ filter, refetchInterval }: Props) => {
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-24" />
|
||||
<Th className="w-24">
|
||||
<Spinner size="xs" className={twMerge(isPending ? "opacity-100" : "opacity-0")} />
|
||||
</Th>
|
||||
<Th className="w-64">
|
||||
Timestamp
|
||||
<Tooltip
|
||||
@ -94,7 +98,7 @@ export const LogsTable = ({ filter, refetchInterval }: Props) => {
|
||||
<Button
|
||||
className="mb-20 mt-4 px-4 py-3 text-sm"
|
||||
isFullWidth
|
||||
variant="star"
|
||||
variant="outline_bg"
|
||||
isLoading={isFetchingNextPage}
|
||||
isDisabled={isFetchingNextPage || !hasNextPage}
|
||||
onClick={() => fetchNextPage()}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
export const auditLogFilterFormSchema = z
|
||||
@ -10,10 +10,12 @@ export const auditLogFilterFormSchema = z
|
||||
.object({ id: z.string(), name: z.string(), type: z.nativeEnum(ProjectType) })
|
||||
.optional()
|
||||
.nullable(),
|
||||
environment: z.object({ name: z.string(), slug: z.string() }).optional().nullable(),
|
||||
eventType: z.nativeEnum(EventType).array(),
|
||||
actor: z.string().optional(),
|
||||
userAgentType: z.nativeEnum(UserAgentType),
|
||||
secretPath: z.string().optional(),
|
||||
secretKey: z.string().optional(),
|
||||
startDate: z.date().optional(),
|
||||
endDate: z.date().optional(),
|
||||
page: z.coerce.number().optional(),
|
||||
@ -39,3 +41,12 @@ export type SetValueType = (
|
||||
shouldDirty?: boolean;
|
||||
}
|
||||
) => void;
|
||||
|
||||
export type Presets = {
|
||||
actorId?: string;
|
||||
eventType?: EventType[];
|
||||
actorType?: ActorType;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
eventMetadata?: Record<string, string>;
|
||||
};
|
||||
|
@ -1,8 +1,3 @@
|
||||
import { useState } from "react";
|
||||
import { faFilter } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { IconButton, Tooltip } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { OrgUser } from "@app/hooks/api/types";
|
||||
@ -14,7 +9,6 @@ type Props = {
|
||||
|
||||
export const UserAuditLogsSection = withPermission(
|
||||
({ orgMembership }: Props) => {
|
||||
const [showFilter, setShowFilter] = useState(false);
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
@ -23,25 +17,8 @@ export const UserAuditLogsSection = withPermission(
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<p className="text-lg font-semibold text-gray-200">Audit Logs</p>
|
||||
|
||||
<Tooltip content="Show audit log filters">
|
||||
<IconButton
|
||||
colorSchema="primary"
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={() => setShowFilter(!showFilter)}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<p>Filter</p>
|
||||
<FontAwesomeIcon icon={faFilter} />
|
||||
</div>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<LogsSection
|
||||
showFilters={showFilter}
|
||||
filterClassName="bg-mineshaft-900 static"
|
||||
presets={{
|
||||
actorId: orgMembership.user.id
|
||||
}}
|
||||
|
@ -35,7 +35,6 @@ export const IntegrationAuditLogsSection = ({ integration }: Props) => {
|
||||
startDate: new Date(new Date().setDate(new Date().getDate() - auditLogsRetentionDays)),
|
||||
eventType: INTEGRATION_EVENTS
|
||||
}}
|
||||
filterClassName="bg-mineshaft-900 static"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
@ -17,10 +17,12 @@ import {
|
||||
faKey,
|
||||
faLock,
|
||||
faMinusSquare,
|
||||
faPaste,
|
||||
faPlus,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { AxiosError } from "axios";
|
||||
import FileSaver from "file-saver";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
@ -53,8 +55,19 @@ import {
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api";
|
||||
import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries";
|
||||
import {
|
||||
useCreateFolder,
|
||||
useCreateSecretBatch,
|
||||
useDeleteSecretBatch,
|
||||
useMoveSecrets,
|
||||
useUpdateSecretBatch
|
||||
} from "@app/hooks/api";
|
||||
import {
|
||||
dashboardKeys,
|
||||
fetchDashboardProjectSecretsByKeys
|
||||
} from "@app/hooks/api/dashboard/queries";
|
||||
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
|
||||
import { fetchProjectSecrets, secretKeys } from "@app/hooks/api/secrets/queries";
|
||||
import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types";
|
||||
import { SecretSearchInput } from "@app/pages/secret-manager/OverviewPage/components/SecretSearchInput";
|
||||
|
||||
@ -65,11 +78,19 @@ import {
|
||||
useSelectedSecrets
|
||||
} from "../../SecretMainPage.store";
|
||||
import { Filter, RowType } from "../../SecretMainPage.types";
|
||||
import { ReplicateFolderFromBoard } from "./ReplicateFolderFromBoard/ReplicateFolderFromBoard";
|
||||
import { CreateDynamicSecretForm } from "./CreateDynamicSecretForm";
|
||||
import { CreateSecretImportForm } from "./CreateSecretImportForm";
|
||||
import { FolderForm } from "./FolderForm";
|
||||
import { MoveSecretsModal } from "./MoveSecretsModal";
|
||||
|
||||
type TParsedEnv = Record<string, { value: string; comments: string[]; secretPath?: string }>;
|
||||
type TParsedFolderEnv = Record<
|
||||
string,
|
||||
Record<string, { value: string; comments: string[]; secretPath?: string }>
|
||||
>;
|
||||
type TSecOverwriteOpt = { update: TParsedEnv; create: TParsedEnv };
|
||||
|
||||
type Props = {
|
||||
// switch the secrets type as it gets decrypted after api call
|
||||
environment: string;
|
||||
@ -114,7 +135,9 @@ export const ActionBar = ({
|
||||
"bulkDeleteSecrets",
|
||||
"moveSecrets",
|
||||
"misc",
|
||||
"upgradePlan"
|
||||
"upgradePlan",
|
||||
"replicateFolder",
|
||||
"confirmUpload"
|
||||
] as const);
|
||||
const isProtectedBranch = Boolean(protectedBranchPolicyName);
|
||||
const { subscription } = useSubscription();
|
||||
@ -122,6 +145,13 @@ export const ActionBar = ({
|
||||
const { mutateAsync: createFolder } = useCreateFolder();
|
||||
const { mutateAsync: deleteBatchSecretV3 } = useDeleteSecretBatch();
|
||||
const { mutateAsync: moveSecrets } = useMoveSecrets();
|
||||
const { mutateAsync: updateSecretBatch, isPending: isUpdatingSecrets } = useUpdateSecretBatch({
|
||||
options: { onSuccess: undefined }
|
||||
});
|
||||
const { mutateAsync: createSecretBatch, isPending: isCreatingSecrets } = useCreateSecretBatch({
|
||||
options: { onSuccess: undefined }
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const selectedSecrets = useSelectedSecrets();
|
||||
const { reset: resetSelectedSecret } = useSelectedSecretActions();
|
||||
@ -293,6 +323,285 @@ export const ActionBar = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Replicate Folder Logic
|
||||
const createSecretCount = Object.keys(
|
||||
(popUp.confirmUpload?.data as TSecOverwriteOpt)?.create || {}
|
||||
).length;
|
||||
|
||||
const updateSecretCount = Object.keys(
|
||||
(popUp.confirmUpload?.data as TSecOverwriteOpt)?.update || {}
|
||||
).length;
|
||||
|
||||
const isNonConflictingUpload = !updateSecretCount;
|
||||
const isSubmitting = isCreatingSecrets || isUpdatingSecrets;
|
||||
|
||||
const handleParsedEnvMultiFolder = async (envByPath: TParsedFolderEnv) => {
|
||||
if (Object.keys(envByPath).length === 0) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to find secrets"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const allUpdateSecrets: TParsedEnv = {};
|
||||
const allCreateSecrets: TParsedEnv = {};
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(envByPath).map(async ([folderPath, secrets]) => {
|
||||
// Normalize the path
|
||||
let normalizedPath = folderPath;
|
||||
|
||||
// If the path is "/", use the current secretPath
|
||||
if (normalizedPath === "/") {
|
||||
normalizedPath = secretPath;
|
||||
} else {
|
||||
// Otherwise, concatenate with the current secretPath, avoiding double slashes
|
||||
const baseSecretPath = secretPath.endsWith("/") ? secretPath.slice(0, -1) : secretPath;
|
||||
// Remove leading slash from folder path if present to avoid double slashes
|
||||
const cleanFolderPath = folderPath.startsWith("/")
|
||||
? folderPath.substring(1)
|
||||
: folderPath;
|
||||
normalizedPath = `${baseSecretPath}/${cleanFolderPath}`;
|
||||
}
|
||||
|
||||
const secretFolderKeys = Object.keys(secrets);
|
||||
|
||||
if (secretFolderKeys.length === 0) return;
|
||||
|
||||
// Check which secrets already exist in this path
|
||||
const batchSize = 50;
|
||||
const secretBatches = Array.from(
|
||||
{ length: Math.ceil(secretFolderKeys.length / batchSize) },
|
||||
(_, i) => secretFolderKeys.slice(i * batchSize, (i + 1) * batchSize)
|
||||
);
|
||||
|
||||
const existingSecretLookup: Record<string, boolean> = {};
|
||||
|
||||
const processBatches = async () => {
|
||||
await secretBatches.reduce(async (previous, batch) => {
|
||||
await previous;
|
||||
|
||||
const { secrets: batchSecrets } = await fetchDashboardProjectSecretsByKeys({
|
||||
secretPath: normalizedPath,
|
||||
environment,
|
||||
projectId: workspaceId,
|
||||
keys: batch
|
||||
});
|
||||
|
||||
batchSecrets.forEach((secret) => {
|
||||
existingSecretLookup[secret.secretKey] = true;
|
||||
});
|
||||
}, Promise.resolve());
|
||||
};
|
||||
|
||||
await processBatches();
|
||||
|
||||
// Categorize each secret as update or create
|
||||
secretFolderKeys.forEach((secretKey) => {
|
||||
const secretData = secrets[secretKey];
|
||||
|
||||
// Store the path with the secret for later batch processing
|
||||
const secretWithPath = {
|
||||
...secretData,
|
||||
secretPath: normalizedPath
|
||||
};
|
||||
|
||||
if (existingSecretLookup[secretKey]) {
|
||||
allUpdateSecrets[secretKey] = secretWithPath;
|
||||
} else {
|
||||
allCreateSecrets[secretKey] = secretWithPath;
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
handlePopUpOpen("confirmUpload", {
|
||||
update: allUpdateSecrets,
|
||||
create: allCreateSecrets
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
createNotification({
|
||||
text: "Failed to check for secret conflicts",
|
||||
type: "error"
|
||||
});
|
||||
handlePopUpClose("confirmUpload");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveFolderImport = async () => {
|
||||
const { update, create } = popUp?.confirmUpload?.data as TSecOverwriteOpt;
|
||||
try {
|
||||
// Group secrets by their path for batch operations
|
||||
const groupedCreateSecrets: Record<
|
||||
string,
|
||||
Array<{
|
||||
type: SecretType;
|
||||
secretComment: string;
|
||||
secretValue: string;
|
||||
secretKey: string;
|
||||
}>
|
||||
> = {};
|
||||
|
||||
const groupedUpdateSecrets: Record<
|
||||
string,
|
||||
Array<{
|
||||
type: SecretType;
|
||||
secretComment: string;
|
||||
secretValue: string;
|
||||
secretKey: string;
|
||||
}>
|
||||
> = {};
|
||||
|
||||
// Collect all unique paths that need folders to be created
|
||||
const allPaths = new Set<string>();
|
||||
|
||||
// Add paths from create secrets
|
||||
Object.values(create || {}).forEach((secData) => {
|
||||
if (secData.secretPath && secData.secretPath !== secretPath) {
|
||||
allPaths.add(secData.secretPath);
|
||||
}
|
||||
});
|
||||
|
||||
// Create a map of folder paths to their folder name (last segment)
|
||||
const folderPaths = Array.from(allPaths).map((path) => {
|
||||
// Remove trailing slash if it exists
|
||||
const normalizedPath = path.endsWith("/") ? path.slice(0, -1) : path;
|
||||
// Split by '/' to get path segments
|
||||
const segments = normalizedPath.split("/");
|
||||
// Get the last segment as the folder name
|
||||
const folderName = segments[segments.length - 1];
|
||||
// Get the parent path (everything except the last segment)
|
||||
const parentPath = segments.slice(0, -1).join("/");
|
||||
|
||||
return {
|
||||
folderName,
|
||||
fullPath: normalizedPath,
|
||||
parentPath: parentPath || "/"
|
||||
};
|
||||
});
|
||||
|
||||
// Sort paths by depth (shortest first) to ensure parent folders are created before children
|
||||
folderPaths.sort(
|
||||
(a, b) => (a.fullPath.match(/\//g) || []).length - (b.fullPath.match(/\//g) || []).length
|
||||
);
|
||||
|
||||
// Track created folders to avoid duplicates
|
||||
const createdFolders = new Set<string>();
|
||||
|
||||
// Create all necessary folders in order using Promise.all and reduce
|
||||
await folderPaths.reduce(async (previousPromise, { folderName, fullPath, parentPath }) => {
|
||||
// Wait for the previous promise to complete
|
||||
await previousPromise;
|
||||
|
||||
// Skip if we've already created this folder
|
||||
if (createdFolders.has(fullPath)) return Promise.resolve();
|
||||
|
||||
try {
|
||||
await createFolder({
|
||||
name: folderName,
|
||||
path: parentPath,
|
||||
environment,
|
||||
projectId: workspaceId
|
||||
});
|
||||
|
||||
createdFolders.add(fullPath);
|
||||
} catch (err) {
|
||||
console.log(`Folder ${folderName} may already exist:`, err);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}, Promise.resolve());
|
||||
|
||||
if (Object.keys(create || {}).length > 0) {
|
||||
Object.entries(create).forEach(([secretKey, secData]) => {
|
||||
// Use the stored secretPath or fall back to the current secretPath
|
||||
const path = secData.secretPath || secretPath;
|
||||
|
||||
if (!groupedCreateSecrets[path]) {
|
||||
groupedCreateSecrets[path] = [];
|
||||
}
|
||||
|
||||
groupedCreateSecrets[path].push({
|
||||
type: SecretType.Shared,
|
||||
secretComment: secData.comments.join("\n"),
|
||||
secretValue: secData.value,
|
||||
secretKey
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(groupedCreateSecrets).map(([path, secrets]) =>
|
||||
createSecretBatch({
|
||||
secretPath: path,
|
||||
workspaceId,
|
||||
environment,
|
||||
secrets
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (Object.keys(update || {}).length > 0) {
|
||||
Object.entries(update).forEach(([secretKey, secData]) => {
|
||||
// Use the stored secretPath or fall back to the current secretPath
|
||||
const path = secData.secretPath || secretPath;
|
||||
|
||||
if (!groupedUpdateSecrets[path]) {
|
||||
groupedUpdateSecrets[path] = [];
|
||||
}
|
||||
|
||||
groupedUpdateSecrets[path].push({
|
||||
type: SecretType.Shared,
|
||||
secretComment: secData.comments.join("\n"),
|
||||
secretValue: secData.value,
|
||||
secretKey
|
||||
});
|
||||
});
|
||||
|
||||
// Update secrets for each path in parallel
|
||||
await Promise.all(
|
||||
Object.entries(groupedUpdateSecrets).map(([path, secrets]) =>
|
||||
updateSecretBatch({
|
||||
secretPath: path,
|
||||
workspaceId,
|
||||
environment,
|
||||
secrets
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Invalidate appropriate queries to refresh UI
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath })
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: secretApprovalRequestKeys.count({ workspaceId })
|
||||
});
|
||||
|
||||
// Close the modal and show notification
|
||||
handlePopUpClose("confirmUpload");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: isProtectedBranch
|
||||
? "Uploaded changes have been sent for review"
|
||||
: "Successfully uploaded secrets"
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to upload secrets"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-4 flex items-center space-x-2">
|
||||
@ -570,6 +879,29 @@ export const ActionBar = ({
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={subject(ProjectPermissionSub.SecretFolders, {
|
||||
environment,
|
||||
secretPath
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPaste} className="pr-2" />}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("replicateFolder");
|
||||
handlePopUpClose("misc");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
variant="outline_bg"
|
||||
className="h-10 text-left"
|
||||
isFullWidth
|
||||
>
|
||||
Replicate Folder
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@ -679,6 +1011,15 @@ export const ActionBar = ({
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
onMoveApproved={handleSecretsMove}
|
||||
/>
|
||||
<ReplicateFolderFromBoard
|
||||
isOpen={popUp.replicateFolder.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("replicateFolder", isOpen)}
|
||||
onParsedEnv={handleParsedEnvMultiFolder}
|
||||
environment={environment}
|
||||
environments={currentWorkspace.environments}
|
||||
workspaceId={workspaceId}
|
||||
secretPath={secretPath}
|
||||
/>
|
||||
{subscription && (
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
@ -690,6 +1031,58 @@ export const ActionBar = ({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Modal
|
||||
isOpen={popUp?.confirmUpload?.isOpen}
|
||||
onOpenChange={(open) => handlePopUpToggle("confirmUpload", open)}
|
||||
>
|
||||
<ModalContent
|
||||
title="Confirm Secret Upload"
|
||||
footerContent={[
|
||||
<Button
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
colorSchema={isNonConflictingUpload ? "primary" : "danger"}
|
||||
key="overwrite-btn"
|
||||
onClick={handleSaveFolderImport}
|
||||
>
|
||||
{isNonConflictingUpload ? "Upload" : "Overwrite"}
|
||||
</Button>,
|
||||
<Button
|
||||
key="keep-old-btn"
|
||||
className="ml-4"
|
||||
onClick={() => handlePopUpClose("confirmUpload")}
|
||||
variant="outline_bg"
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
{isNonConflictingUpload ? (
|
||||
<div>
|
||||
Are you sure you want to import {createSecretCount} secret
|
||||
{createSecretCount > 1 ? "s" : ""} to this environment?
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col text-gray-300">
|
||||
<div>Your project already contains the following {updateSecretCount} secrets:</div>
|
||||
<div className="mt-2 text-sm text-gray-400">
|
||||
{Object.keys((popUp?.confirmUpload?.data as TSecOverwriteOpt)?.update || {})
|
||||
?.map((key) => key)
|
||||
.join(", ")}
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
Are you sure you want to overwrite these secrets
|
||||
{createSecretCount > 0
|
||||
? ` and import ${createSecretCount} new
|
||||
one${createSecretCount > 1 ? "s" : ""}`
|
||||
: ""}
|
||||
?
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,307 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faClone } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Switch
|
||||
} from "@app/components/v2";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { useDebounce } from "@app/hooks";
|
||||
import { useGetAccessibleSecrets } from "@app/hooks/api/dashboard";
|
||||
import { SecretV3Raw } from "@app/hooks/api/types";
|
||||
|
||||
import { SecretTreeView } from "./SecretTreeView";
|
||||
|
||||
const formSchema = z.object({
|
||||
environment: z.object({ name: z.string(), slug: z.string() }),
|
||||
secretPath: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((val) =>
|
||||
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
|
||||
),
|
||||
secrets: z
|
||||
.object({
|
||||
secretKey: z.string(),
|
||||
secretValue: z.string().optional(),
|
||||
secretPath: z.string()
|
||||
})
|
||||
.array()
|
||||
.min(1, "Select one or more secrets to copy")
|
||||
});
|
||||
|
||||
type TFormSchema = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
onToggle: (isOpen: boolean) => void;
|
||||
onParsedEnv: (
|
||||
env: Record<string, Record<string, { value: string; comments: string[]; secretPath?: string }>>
|
||||
) => void;
|
||||
environments?: { name: string; slug: string }[];
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
};
|
||||
|
||||
type SecretFolder = {
|
||||
items: Partial<SecretV3Raw>[];
|
||||
subFolders: Record<string, SecretFolder>;
|
||||
};
|
||||
|
||||
type SecretStructure = {
|
||||
[rootPath: string]: SecretFolder;
|
||||
};
|
||||
|
||||
export const ReplicateFolderFromBoard = ({
|
||||
environments = [],
|
||||
workspaceId,
|
||||
isOpen,
|
||||
onToggle,
|
||||
onParsedEnv
|
||||
}: Props) => {
|
||||
const [shouldIncludeValues, setShouldIncludeValues] = useState(true);
|
||||
|
||||
const { handleSubmit, control, watch, reset, setValue } = useForm<TFormSchema>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: { secretPath: "/", environment: environments?.[0], secrets: [] }
|
||||
});
|
||||
|
||||
const envCopySecPath = watch("secretPath");
|
||||
const selectedEnvSlug = watch("environment");
|
||||
const selectedSecrets = watch("secrets");
|
||||
const [debouncedEnvCopySecretPath] = useDebounce(envCopySecPath);
|
||||
|
||||
const { data: accessibleSecrets } = useGetAccessibleSecrets({
|
||||
projectId: workspaceId,
|
||||
secretPath: "/",
|
||||
environment: selectedEnvSlug.slug,
|
||||
recursive: true,
|
||||
filterByAction: shouldIncludeValues
|
||||
? ProjectPermissionSecretActions.ReadValue
|
||||
: ProjectPermissionSecretActions.DescribeSecret,
|
||||
options: { enabled: Boolean(workspaceId) && Boolean(selectedEnvSlug) && isOpen }
|
||||
});
|
||||
|
||||
const restructureSecrets = useMemo(() => {
|
||||
if (!accessibleSecrets) return {};
|
||||
|
||||
const result: SecretStructure = {};
|
||||
result["/"] = {
|
||||
items: [],
|
||||
subFolders: {}
|
||||
};
|
||||
|
||||
accessibleSecrets.forEach((secret) => {
|
||||
const path = secret.secretPath || "/";
|
||||
|
||||
if (path === "/") {
|
||||
result["/"]?.items.push(secret);
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedPath = path.startsWith("/") ? path.substring(1) : path;
|
||||
const pathParts = normalizedPath.split("/");
|
||||
|
||||
let currentFolder = result["/"];
|
||||
|
||||
for (let i = 0; i < pathParts.length; i += 1) {
|
||||
const part = pathParts[i];
|
||||
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!part) continue;
|
||||
|
||||
if (i === pathParts.length - 1) {
|
||||
if (!currentFolder.subFolders[part]) {
|
||||
currentFolder.subFolders[part] = {
|
||||
items: [],
|
||||
subFolders: {}
|
||||
};
|
||||
}
|
||||
currentFolder.subFolders[part].items.push(secret);
|
||||
} else {
|
||||
if (!currentFolder.subFolders[part]) {
|
||||
currentFolder.subFolders[part] = {
|
||||
items: [],
|
||||
subFolders: {}
|
||||
};
|
||||
}
|
||||
currentFolder = currentFolder.subFolders[part];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [accessibleSecrets, selectedEnvSlug]);
|
||||
|
||||
const secretsFilteredByPath = useMemo(() => {
|
||||
let normalizedPath = debouncedEnvCopySecretPath;
|
||||
normalizedPath = debouncedEnvCopySecretPath.startsWith("/")
|
||||
? debouncedEnvCopySecretPath
|
||||
: `/${debouncedEnvCopySecretPath}`;
|
||||
if (normalizedPath.length > 1 && normalizedPath.endsWith("/")) {
|
||||
normalizedPath = debouncedEnvCopySecretPath.slice(0, -1);
|
||||
}
|
||||
|
||||
if (normalizedPath === "/") {
|
||||
return restructureSecrets["/"];
|
||||
}
|
||||
|
||||
const segments = normalizedPath.split("/").filter((segment) => segment !== "");
|
||||
|
||||
let currentLevel = restructureSecrets["/"];
|
||||
let result = null;
|
||||
let currentPath = "";
|
||||
|
||||
if (!currentLevel) {
|
||||
setValue("secretPath", "/");
|
||||
return null;
|
||||
}
|
||||
|
||||
for (let i = 0; i < segments.length; i += 1) {
|
||||
const segment = segments[i];
|
||||
currentPath += `/${segment}`;
|
||||
|
||||
if (currentLevel?.subFolders?.[segment]) {
|
||||
currentLevel = currentLevel.subFolders[segment];
|
||||
|
||||
if (currentPath === normalizedPath) {
|
||||
result = currentLevel;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [restructureSecrets, debouncedEnvCopySecretPath]);
|
||||
|
||||
useEffect(() => {
|
||||
setValue("secrets", []);
|
||||
}, [debouncedEnvCopySecretPath, selectedEnvSlug]);
|
||||
|
||||
const handleFormSubmit = async (data: TFormSchema) => {
|
||||
const secretsToBePulled: Record<
|
||||
string,
|
||||
Record<string, { value: string; comments: string[]; secretPath: string }>
|
||||
> = {};
|
||||
data.secrets.forEach(({ secretKey, secretValue, secretPath: secretPathToRecreate }) => {
|
||||
const normalizedPath = secretPathToRecreate.startsWith(envCopySecPath)
|
||||
? secretPathToRecreate.slice(envCopySecPath.length)
|
||||
: secretPathToRecreate;
|
||||
|
||||
if (!secretsToBePulled[normalizedPath]) {
|
||||
secretsToBePulled[normalizedPath] = {};
|
||||
}
|
||||
|
||||
secretsToBePulled[normalizedPath][secretKey] = {
|
||||
value: (shouldIncludeValues && secretValue) || "",
|
||||
comments: [""],
|
||||
secretPath: normalizedPath
|
||||
};
|
||||
});
|
||||
onParsedEnv(secretsToBePulled);
|
||||
onToggle(false);
|
||||
reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={(state) => {
|
||||
onToggle(state);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
bodyClassName="overflow-visible"
|
||||
className="max-w-2xl"
|
||||
title="Replicate Folder Content From An Environment"
|
||||
subTitle="Replicate folder content from other environments into this context"
|
||||
>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<FormControl label="Environment" isRequired className="w-1/3">
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={environments}
|
||||
placeholder="Select environment..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPath"
|
||||
render={({ field }) => (
|
||||
<FormControl label="Secret Path" className="flex-grow" isRequired>
|
||||
<SecretPathInput
|
||||
{...field}
|
||||
placeholder="Provide a path, default is /"
|
||||
environment={selectedEnvSlug?.slug}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t border-mineshaft-600 pt-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="secrets"
|
||||
render={({ field: { onChange } }) => (
|
||||
<FormControl className="flex-grow" isRequired>
|
||||
<SecretTreeView
|
||||
data={secretsFilteredByPath}
|
||||
basePath={debouncedEnvCopySecretPath}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="my-6 ml-2">
|
||||
<Switch
|
||||
id="populate-include-value"
|
||||
isChecked={shouldIncludeValues}
|
||||
onCheckedChange={(isChecked) => {
|
||||
setValue("secrets", []);
|
||||
setShouldIncludeValues(isChecked as boolean);
|
||||
}}
|
||||
>
|
||||
Include secret values
|
||||
</Switch>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faClone} />}
|
||||
type="submit"
|
||||
isDisabled={!selectedSecrets || selectedSecrets.length === 0}
|
||||
>
|
||||
Replicate Folder
|
||||
</Button>
|
||||
<Button variant="plain" colorSchema="secondary" onClick={() => onToggle(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -0,0 +1,321 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
faChevronDown,
|
||||
faChevronRight,
|
||||
faFolder,
|
||||
faFolderOpen,
|
||||
faFolderTree,
|
||||
faKey
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
||||
|
||||
import { Checkbox } from "@app/components/v2";
|
||||
|
||||
interface SecretItem {
|
||||
id?: string;
|
||||
secretKey?: string;
|
||||
secretValue?: string;
|
||||
secretPath?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface FolderStructure {
|
||||
items: SecretItem[];
|
||||
subFolders: {
|
||||
[key: string]: FolderStructure;
|
||||
};
|
||||
}
|
||||
|
||||
interface TreeData {
|
||||
[key: string]: FolderStructure | null;
|
||||
}
|
||||
|
||||
interface FolderProps {
|
||||
name: string;
|
||||
structure: FolderStructure;
|
||||
path: string;
|
||||
selectedItems: SecretItem[];
|
||||
onItemSelect: (item: SecretItem, isChecked: boolean) => void;
|
||||
onFolderSelect: (folderPath: string, isChecked: boolean) => void;
|
||||
isExpanded?: boolean;
|
||||
level: number;
|
||||
basePath?: string;
|
||||
}
|
||||
|
||||
interface TreeViewProps {
|
||||
data: FolderStructure | null;
|
||||
basePath?: string;
|
||||
className?: string;
|
||||
onChange: (items: SecretItem[]) => void;
|
||||
}
|
||||
|
||||
const getAllItemsInFolder = (folder: FolderStructure): SecretItem[] => {
|
||||
let items: SecretItem[] = [];
|
||||
|
||||
items = items.concat(folder.items);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
Object.entries(folder.subFolders).forEach(([_, subFolder]) => {
|
||||
items = items.concat(getAllItemsInFolder(subFolder));
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
const getDisplayName = (name: string): string => {
|
||||
const parts = name.split("/");
|
||||
return parts[parts.length - 1];
|
||||
};
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root;
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.Trigger;
|
||||
const CollapsibleContent = CollapsiblePrimitive.Content;
|
||||
|
||||
const Folder: React.FC<FolderProps> = ({
|
||||
name,
|
||||
structure,
|
||||
path,
|
||||
selectedItems,
|
||||
onItemSelect,
|
||||
onFolderSelect,
|
||||
isExpanded = false,
|
||||
level,
|
||||
basePath
|
||||
}) => {
|
||||
const [open, setOpen] = useState(isExpanded);
|
||||
const displayName = useMemo(() => getDisplayName(name), [name]);
|
||||
|
||||
const allItems = useMemo(() => getAllItemsInFolder(structure), [structure]);
|
||||
const allItemIds = useMemo(() => allItems.map((item) => item.id), [allItems]);
|
||||
const selectedItemIds = useMemo(() => selectedItems.map((item) => item.id), [selectedItems]);
|
||||
const allSelected = useMemo(
|
||||
() => allItemIds.length > 0 && allItemIds.every((id) => selectedItemIds.includes(id)),
|
||||
[allItemIds, selectedItemIds]
|
||||
);
|
||||
const someSelected = useMemo(
|
||||
() => allItemIds.some((id) => selectedItemIds.includes(id)) && !allSelected,
|
||||
[allItemIds, selectedItemIds, allSelected]
|
||||
);
|
||||
const hasContents = structure.items.length > 0 || Object.keys(structure.subFolders).length > 0;
|
||||
|
||||
const handleFolderSelect = (checked: boolean) => {
|
||||
onFolderSelect(path, checked);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`folder-container ml-${level > 0 ? "4" : 0}`}>
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<div className="group flex items-center rounded px-2 py-1">
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="mr-1 flex h-6 w-6 items-center justify-center rounded focus:outline-none"
|
||||
disabled={!hasContents}
|
||||
aria-label={open ? "Collapse folder" : "Expand folder"}
|
||||
>
|
||||
{hasContents && (
|
||||
<FontAwesomeIcon icon={open ? faChevronDown : faChevronRight} className="h-3 w-3" />
|
||||
)}
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<div className="mr-2">
|
||||
<FontAwesomeIcon
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
icon={level > 0 ? (open ? faFolderOpen : faFolder) : faFolderTree}
|
||||
className={`h-4 w-4 text-${level === 0 ? "mineshaft-300" : "yellow"}`}
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
id="folder-root"
|
||||
className="data-[state=indeterminate]:bg-secondary data-[state=checked]:bg-primary"
|
||||
isChecked={allSelected || someSelected}
|
||||
onCheckedChange={handleFolderSelect}
|
||||
isIndeterminate={someSelected && !allSelected}
|
||||
/>
|
||||
|
||||
<label
|
||||
htmlFor={`folder-${path}`}
|
||||
className={`ml-2 flex-1 cursor-pointer truncate ${basePath ? "italic text-mineshaft-300" : ""}`}
|
||||
title={displayName}
|
||||
>
|
||||
{displayName || `${basePath}`}
|
||||
</label>
|
||||
|
||||
{allItemIds.length > 0 && (
|
||||
<span className="ml-2 text-xs text-mineshaft-400">
|
||||
{allItemIds.length} {allItemIds.length === 1 ? "item" : "items"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<CollapsibleContent className="overflow-hidden transition-all duration-300 ease-in-out">
|
||||
<div className="relative mt-1">
|
||||
<div className="absolute bottom-0 left-5 top-0 w-px bg-mineshaft-600" />
|
||||
{structure.items.map((item) => (
|
||||
<div key={item.id} className="group ml-6 flex items-center rounded px-2 py-1">
|
||||
<div className="ml-6 mr-2">
|
||||
<FontAwesomeIcon icon={faKey} className="h-3 w-3" />
|
||||
</div>
|
||||
<Checkbox
|
||||
id={`folder-${item.id}`}
|
||||
className="data-[state=indeterminate]:bg-secondary data-[state=checked]:bg-primary"
|
||||
isChecked={selectedItemIds.includes(item.id)}
|
||||
onCheckedChange={(checked) => onItemSelect(item, !!checked)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={item.id}
|
||||
className="ml-2 flex-1 cursor-pointer truncate"
|
||||
title={item.secretKey}
|
||||
>
|
||||
{item.secretKey}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{Object.entries(structure.subFolders).map(([subName, subStructure]) => (
|
||||
<Folder
|
||||
key={subName}
|
||||
name={subName}
|
||||
structure={subStructure}
|
||||
path={path ? `${path}/${subName}` : subName}
|
||||
selectedItems={selectedItems}
|
||||
onItemSelect={onItemSelect}
|
||||
onFolderSelect={onFolderSelect}
|
||||
level={level + 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SecretTreeView: React.FC<TreeViewProps> = ({
|
||||
data,
|
||||
basePath = "/",
|
||||
className = "",
|
||||
onChange
|
||||
}) => {
|
||||
const [selectedItems, setSelectedItems] = useState<SecretItem[]>([]);
|
||||
const rootPath = "/";
|
||||
const treeData: TreeData = data ? { [rootPath]: data as FolderStructure } : { [rootPath]: null };
|
||||
|
||||
const rootFolders = useMemo(() => {
|
||||
return Object.entries(treeData);
|
||||
}, [treeData]);
|
||||
|
||||
const isEmptyData = useMemo(() => {
|
||||
return (
|
||||
!data || (typeof data === "object" && Object.keys(data).length === 0) || !rootFolders.length
|
||||
);
|
||||
}, [data, rootFolders]);
|
||||
|
||||
const handleItemSelect = (item: SecretItem, isChecked: boolean) => {
|
||||
if (isChecked) {
|
||||
setSelectedItems((prev) => [...prev, item]);
|
||||
} else {
|
||||
setSelectedItems((prev) => prev.filter((i) => i.id !== item.id));
|
||||
}
|
||||
};
|
||||
|
||||
const handleFolderSelect = (folderPath: string, isChecked: boolean) => {
|
||||
const getFolderFromPath = (tree: TreeData, path: string): FolderStructure | null => {
|
||||
if (rootFolders.length === 1 && rootFolders[0][0] === path) {
|
||||
return rootFolders[0][1];
|
||||
}
|
||||
|
||||
let adjustedPath = path;
|
||||
if (!path.startsWith(rootPath)) {
|
||||
adjustedPath = rootPath === path ? rootPath : `${rootPath}/${path}`;
|
||||
}
|
||||
|
||||
if (adjustedPath === "/") return tree["/"];
|
||||
|
||||
const parts = adjustedPath.split("/").filter((p) => p !== "");
|
||||
|
||||
let current: any;
|
||||
current = tree["/"];
|
||||
|
||||
const targetExists = parts.every((part) => {
|
||||
if (current?.subFolders?.[part]) {
|
||||
current = current.subFolders[part];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!targetExists) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return current;
|
||||
};
|
||||
|
||||
const folder = getFolderFromPath(treeData, folderPath);
|
||||
if (!folder) return;
|
||||
|
||||
const folderItems = getAllItemsInFolder(folder);
|
||||
const folderItemIds = folderItems.map((item) => item.id);
|
||||
|
||||
if (isChecked) {
|
||||
setSelectedItems((prev) => {
|
||||
const prevIds = prev.map((item) => item.id);
|
||||
const newItems = [...prev];
|
||||
folderItems.forEach((item) => {
|
||||
if (!prevIds.includes(item.id)) {
|
||||
newItems.push(item);
|
||||
}
|
||||
});
|
||||
return newItems;
|
||||
});
|
||||
} else {
|
||||
setSelectedItems((prev) => prev.filter((item) => !folderItemIds.includes(item.id)));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedItems([]);
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(selectedItems);
|
||||
}, [selectedItems]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-start gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900">
|
||||
<div className={`w-full rounded-lg shadow-sm ${className}`}>
|
||||
<div className="h-[25vh] overflow-auto p-3">
|
||||
{isEmptyData ? (
|
||||
<div className="flex h-full w-full items-center justify-center text-center text-mineshaft-300">
|
||||
<p>No secrets or folders available</p>
|
||||
</div>
|
||||
) : (
|
||||
rootFolders.map(([folderName, folderStructure]) => (
|
||||
<Folder
|
||||
basePath={basePath}
|
||||
key={folderName}
|
||||
name={folderName}
|
||||
structure={folderStructure || { items: [], subFolders: {} }}
|
||||
path={folderName}
|
||||
selectedItems={selectedItems}
|
||||
onItemSelect={handleItemSelect}
|
||||
onFolderSelect={handleFolderSelect}
|
||||
isExpanded
|
||||
level={0}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pb-2 pr-2 pt-2">
|
||||
<h3 className="flex items-center text-mineshaft-400">
|
||||
{selectedItems.length} Item{selectedItems.length === 1 ? "" : "s"} Selected
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -64,7 +64,8 @@ export const SecretDropzone = ({
|
||||
const { mutateAsync: createSecretBatch, isPending: isCreatingSecrets } = useCreateSecretBatch({
|
||||
options: { onSuccess: undefined }
|
||||
});
|
||||
|
||||
// hide copy secrets from board due to import folders feature
|
||||
const shouldRenderCopySecrets = false;
|
||||
const isSubmitting = isCreatingSecrets || isUpdatingSecrets;
|
||||
|
||||
const handleDrag = (e: DragEvent) => {
|
||||
@ -308,16 +309,18 @@ export const SecretDropzone = ({
|
||||
secretPath={secretPath}
|
||||
isSmaller={isSmaller}
|
||||
/>
|
||||
<CopySecretsFromBoard
|
||||
isOpen={popUp.importSecEnv.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("importSecEnv", isOpen)}
|
||||
onParsedEnv={handleParsedEnv}
|
||||
environment={environment}
|
||||
environments={environments}
|
||||
workspaceId={workspaceId}
|
||||
secretPath={secretPath}
|
||||
isSmaller={isSmaller}
|
||||
/>
|
||||
{shouldRenderCopySecrets && (
|
||||
<CopySecretsFromBoard
|
||||
isOpen={popUp.importSecEnv.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("importSecEnv", isOpen)}
|
||||
onParsedEnv={handleParsedEnv}
|
||||
environment={environment}
|
||||
environments={environments}
|
||||
workspaceId={workspaceId}
|
||||
secretPath={secretPath}
|
||||
isSmaller={isSmaller}
|
||||
/>
|
||||
)}
|
||||
{!isSmaller && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
|
@ -41,7 +41,6 @@ export const SecretSyncAuditLogsSection = ({ secretSync }: Props) => {
|
||||
startDate: new Date(new Date().setDate(new Date().getDate() - auditLogsRetentionDays)),
|
||||
eventType: INTEGRATION_EVENTS
|
||||
}}
|
||||
filterClassName="bg-mineshaft-900 static"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center rounded-lg bg-mineshaft-800 text-sm text-mineshaft-200">
|
||||
|
@ -13,9 +13,9 @@ type: application
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: v0.8.15
|
||||
version: v0.9.0
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "v0.8.15"
|
||||
appVersion: "v0.9.0"
|
||||
|
@ -137,6 +137,19 @@ spec:
|
||||
secretNamespace:
|
||||
description: The name space where the Kubernetes Secret is located
|
||||
type: string
|
||||
template:
|
||||
properties:
|
||||
data:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: The template key values
|
||||
type: object
|
||||
includeAllSecrets:
|
||||
description: This injects all retrieved secrets into the
|
||||
top level of your template. Secrets defined in the template
|
||||
will take precedence over the injected ones.
|
||||
type: boolean
|
||||
type: object
|
||||
required:
|
||||
- secretName
|
||||
- secretNamespace
|
||||
|
@ -32,7 +32,7 @@ controllerManager:
|
||||
- ALL
|
||||
image:
|
||||
repository: infisical/kubernetes-operator
|
||||
tag: <helm-pr-will-update-this-automatically>
|
||||
tag: v0.9.0
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
|
@ -105,7 +105,7 @@ type ManagedKubeSecretConfig struct {
|
||||
|
||||
// The template to transform the secret data
|
||||
// +kubebuilder:validation:Optional
|
||||
Template *InfisicalSecretTemplate `json:"template,omitempty"`
|
||||
Template *SecretTemplate `json:"template,omitempty"`
|
||||
}
|
||||
|
||||
type ManagedKubeConfigMapConfig struct {
|
||||
@ -127,5 +127,15 @@ type ManagedKubeConfigMapConfig struct {
|
||||
|
||||
// The template to transform the secret data
|
||||
// +kubebuilder:validation:Optional
|
||||
Template *InfisicalSecretTemplate `json:"template,omitempty"`
|
||||
Template *SecretTemplate `json:"template,omitempty"`
|
||||
}
|
||||
|
||||
type SecretTemplate struct {
|
||||
// This injects all retrieved secrets into the top level of your template.
|
||||
// Secrets defined in the template will take precedence over the injected ones.
|
||||
// +kubebuilder:validation:Optional
|
||||
IncludeAllSecrets bool `json:"includeAllSecrets"`
|
||||
// The template key values
|
||||
// +kubebuilder:validation:Optional
|
||||
Data map[string]string `json:"data,omitempty"`
|
||||
}
|
||||
|
@ -16,9 +16,22 @@ type InfisicalPushSecretDestination struct {
|
||||
ProjectID string `json:"projectId"`
|
||||
}
|
||||
|
||||
type InfisicalPushSecretSecretSource struct {
|
||||
// The name of the Kubernetes Secret
|
||||
// +kubebuilder:validation:Required
|
||||
SecretName string `json:"secretName"`
|
||||
|
||||
// The name space where the Kubernetes Secret is located
|
||||
// +kubebuilder:validation:Required
|
||||
SecretNamespace string `json:"secretNamespace"`
|
||||
|
||||
// +kubebuilder:validation:Optional
|
||||
Template *SecretTemplate `json:"template,omitempty"`
|
||||
}
|
||||
|
||||
type SecretPush struct {
|
||||
// +kubebuilder:validation:Required
|
||||
Secret KubeSecretReference `json:"secret"`
|
||||
Secret InfisicalPushSecretSecretSource `json:"secret"`
|
||||
}
|
||||
|
||||
// InfisicalPushSecretSpec defines the desired state of InfisicalPushSecret
|
||||
|
@ -116,16 +116,6 @@ type MachineIdentityScopeInWorkspace struct {
|
||||
Recursive bool `json:"recursive"`
|
||||
}
|
||||
|
||||
type InfisicalSecretTemplate struct {
|
||||
// This injects all retrieved secrets into the top level of your template.
|
||||
// Secrets defined in the template will take precedence over the injected ones.
|
||||
// +kubebuilder:validation:Optional
|
||||
IncludeAllSecrets bool `json:"includeAllSecrets"`
|
||||
// The template key values
|
||||
// +kubebuilder:validation:Optional
|
||||
Data map[string]string `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// InfisicalSecretSpec defines the desired state of InfisicalSecret
|
||||
type InfisicalSecretSpec struct {
|
||||
// +kubebuilder:validation:Optional
|
||||
|
@ -383,7 +383,7 @@ func (in *InfisicalPushSecret) DeepCopyInto(out *InfisicalPushSecret) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
out.Spec = in.Spec
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
@ -452,12 +452,32 @@ func (in *InfisicalPushSecretList) DeepCopyObject() runtime.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *InfisicalPushSecretSecretSource) DeepCopyInto(out *InfisicalPushSecretSecretSource) {
|
||||
*out = *in
|
||||
if in.Template != nil {
|
||||
in, out := &in.Template, &out.Template
|
||||
*out = new(SecretTemplate)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalPushSecretSecretSource.
|
||||
func (in *InfisicalPushSecretSecretSource) DeepCopy() *InfisicalPushSecretSecretSource {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(InfisicalPushSecretSecretSource)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *InfisicalPushSecretSpec) DeepCopyInto(out *InfisicalPushSecretSpec) {
|
||||
*out = *in
|
||||
out.Destination = in.Destination
|
||||
out.Authentication = in.Authentication
|
||||
out.Push = in.Push
|
||||
in.Push.DeepCopyInto(&out.Push)
|
||||
out.TLS = in.TLS
|
||||
}
|
||||
|
||||
@ -614,28 +634,6 @@ func (in *InfisicalSecretStatus) DeepCopy() *InfisicalSecretStatus {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *InfisicalSecretTemplate) DeepCopyInto(out *InfisicalSecretTemplate) {
|
||||
*out = *in
|
||||
if in.Data != nil {
|
||||
in, out := &in.Data, &out.Data
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalSecretTemplate.
|
||||
func (in *InfisicalSecretTemplate) DeepCopy() *InfisicalSecretTemplate {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(InfisicalSecretTemplate)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *KubeSecretReference) DeepCopyInto(out *KubeSecretReference) {
|
||||
*out = *in
|
||||
@ -703,7 +701,7 @@ func (in *ManagedKubeConfigMapConfig) DeepCopyInto(out *ManagedKubeConfigMapConf
|
||||
*out = *in
|
||||
if in.Template != nil {
|
||||
in, out := &in.Template, &out.Template
|
||||
*out = new(InfisicalSecretTemplate)
|
||||
*out = new(SecretTemplate)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
@ -723,7 +721,7 @@ func (in *ManagedKubeSecretConfig) DeepCopyInto(out *ManagedKubeSecretConfig) {
|
||||
*out = *in
|
||||
if in.Template != nil {
|
||||
in, out := &in.Template, &out.Template
|
||||
*out = new(InfisicalSecretTemplate)
|
||||
*out = new(SecretTemplate)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
@ -741,7 +739,7 @@ func (in *ManagedKubeSecretConfig) DeepCopy() *ManagedKubeSecretConfig {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SecretPush) DeepCopyInto(out *SecretPush) {
|
||||
*out = *in
|
||||
out.Secret = in.Secret
|
||||
in.Secret.DeepCopyInto(&out.Secret)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretPush.
|
||||
@ -769,6 +767,28 @@ func (in *SecretScopeInWorkspace) DeepCopy() *SecretScopeInWorkspace {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SecretTemplate) DeepCopyInto(out *SecretTemplate) {
|
||||
*out = *in
|
||||
if in.Data != nil {
|
||||
in, out := &in.Data, &out.Data
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretTemplate.
|
||||
func (in *SecretTemplate) DeepCopy() *SecretTemplate {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SecretTemplate)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ServiceAccountDetails) DeepCopyInto(out *ServiceAccountDetails) {
|
||||
*out = *in
|
||||
|
@ -137,6 +137,19 @@ spec:
|
||||
description: The name space where the Kubernetes Secret is
|
||||
located
|
||||
type: string
|
||||
template:
|
||||
properties:
|
||||
data:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: The template key values
|
||||
type: object
|
||||
includeAllSecrets:
|
||||
description: This injects all retrieved secrets into the
|
||||
top level of your template. Secrets defined in the template
|
||||
will take precedence over the injected ones.
|
||||
type: boolean
|
||||
type: object
|
||||
required:
|
||||
- secretName
|
||||
- secretNamespace
|
||||
|
@ -41,6 +41,9 @@ spec:
|
||||
|
||||
# Native Kubernetes Auth
|
||||
kubernetesAuth:
|
||||
serviceAccountRef:
|
||||
name: <secret-name>
|
||||
namespace: <secret-namespace>
|
||||
identityId: <machine-identity-id>
|
||||
serviceAccountTokenPath: "/path/to/your/service-account/token" # Optional, defaults to /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
|
||||
@ -97,18 +100,13 @@ spec:
|
||||
secretsPath: "/path"
|
||||
recursive: true
|
||||
|
||||
managedSecretReference:
|
||||
secretName: managed-secret
|
||||
secretNamespace: default
|
||||
template:
|
||||
includeAllSecrets: true
|
||||
data:
|
||||
SSH_KEY: "{{ .KEY.SecretPath }} {{ .KEY.Value }}"
|
||||
BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}"
|
||||
creationPolicy: "Orphan" ## Owner | Orphan
|
||||
# secretType: kubernetes.io/dockerconfigjson
|
||||
|
||||
# # To be depreciated soon
|
||||
# tokenSecretReference:
|
||||
# secretName: service-token
|
||||
# secretNamespace: default
|
||||
managedKubeSecretReferences:
|
||||
- secretName: managed-secret
|
||||
secretNamespace: default
|
||||
creationPolicy: "Orphan" ## Owner | Orphan
|
||||
# secretType: kubernetes.io/dockerconfigjson
|
||||
template:
|
||||
includeAllSecrets: true
|
||||
data:
|
||||
SSH_KEY: "{{ .KEY.SecretPath }} {{ .KEY.Value }}"
|
||||
BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}"
|
||||
|
@ -41,6 +41,9 @@ spec:
|
||||
|
||||
# Native Kubernetes Auth
|
||||
kubernetesAuth:
|
||||
serviceAccountRef:
|
||||
name: <secret-name>
|
||||
namespace: <secret-namespace>
|
||||
identityId: <machine-identity-id>
|
||||
serviceAccountTokenPath: "/path/to/your/service-account/token" # Optional, defaults to /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
|
||||
@ -97,7 +100,7 @@ spec:
|
||||
secretsPath: "/path"
|
||||
recursive: true
|
||||
|
||||
managedSecretReferences:
|
||||
managedKubeSecretReferences:
|
||||
- secretName: managed-secret
|
||||
secretNamespace: default
|
||||
creationPolicy: "Orphan" ## Owner | Orphan
|
||||
|
@ -0,0 +1,91 @@
|
||||
apiVersion: secrets.infisical.com/v1alpha1
|
||||
kind: InfisicalPushSecret
|
||||
metadata:
|
||||
name: infisical-api-secret-sample-push
|
||||
spec:
|
||||
resyncInterval: 1m
|
||||
hostAPI: https://app.infisical.com/api # This is the default hostAPI for the Infisical API
|
||||
|
||||
# Optional, defaults to replacement.
|
||||
updatePolicy: Replace # If set to replace, existing secrets inside Infisical will be replaced by the value of the PushSecret on sync.
|
||||
|
||||
# Optional, defaults to no deletion.
|
||||
deletionPolicy: Delete # If set to delete, the secret(s) inside Infisical managed by the operator, will be deleted if the InfisicalPushSecret CRD is deleted.
|
||||
|
||||
destination:
|
||||
projectId: <project-id>
|
||||
environmentSlug: <env-slug>
|
||||
secretsPath: <secret-path>
|
||||
|
||||
push:
|
||||
secret:
|
||||
secretName: push-secret-demo-with-templating
|
||||
secretNamespace: default
|
||||
template:
|
||||
includeAllSecrets: false
|
||||
data:
|
||||
PKCS12_CERT_NO_PASSWORD: "{{ .PKCS12_CONTENT_NO_PASSWORD.Value | decodeBase64ToBytes | pkcs12cert }}"
|
||||
PKCS12_KEY_NO_PASSWORD: "{{ .PKCS12_CONTENT_NO_PASSWORD.Value | decodeBase64ToBytes | pkcs12key }}"
|
||||
|
||||
PKCS12_CERT_WITH_PASSWORD: '{{ .PKCS12_CONTENT_WITH_PASSWORD.Value | decodeBase64ToBytes | pkcs12certPass "123456" }}'
|
||||
PKCS12_KEY_WITH_PASSWORD: '{{ .PKCS12_CONTENT_WITH_PASSWORD.Value | decodeBase64ToBytes | pkcs12keyPass "123456" }}'
|
||||
|
||||
PEM_TO_PKCS12_PASS: '{{ pemToPkcs12Pass
|
||||
(.PKCS12_CONTENT_WITH_PASSWORD.Value | decodeBase64ToBytes | pkcs12certPass "123456")
|
||||
(.PKCS12_CONTENT_WITH_PASSWORD.Value | decodeBase64ToBytes | pkcs12keyPass "123456")
|
||||
"123456" }}'
|
||||
PEM_TO_PKCS12_NO_PASSWORD: "{{ pemToPkcs12
|
||||
(.PKCS12_CONTENT_NO_PASSWORD.Value | decodeBase64ToBytes | pkcs12cert)
|
||||
(.PKCS12_CONTENT_NO_PASSWORD.Value | decodeBase64ToBytes | pkcs12key)
|
||||
}}"
|
||||
|
||||
FULL_PEM_TO_PKCS12_PASS: '{{ fullPemToPkcs12Pass
|
||||
(.PKCS12_CONTENT_WITH_PASSWORD.Value | decodeBase64ToBytes | pkcs12certPass "123456")
|
||||
(.PKCS12_CONTENT_WITH_PASSWORD.Value | decodeBase64ToBytes | pkcs12keyPass "123456")
|
||||
"123456" }}'
|
||||
FULL_PEM_TO_PKCS12_NO_PASSWORD: "{{ fullPemToPkcs12
|
||||
(.PKCS12_CONTENT_NO_PASSWORD.Value | decodeBase64ToBytes | pkcs12cert)
|
||||
(.PKCS12_CONTENT_NO_PASSWORD.Value | decodeBase64ToBytes | pkcs12key)
|
||||
}}"
|
||||
|
||||
FILTERED_PEM_CERT: '{{ filterPEM "CERTIFICATE" (printf "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" .JWK_PRIVATE_RSA_PKCS8.Value .PKCS12_CONTENT_NO_PASSWORD.Value) }}'
|
||||
FILTERED_PEM_KEY: '{{ filterPEM "PRIVATE KEY" (printf "-----BEGIN PRIVATE KEY-----\n%s\n-----END PRIVATE KEY-----\n-----BEGIN PRIVATE KEY-----\n%s\n-----END PRIVATE KEY-----" .JWK_PRIVATE_RSA_PKCS8.Value .PKCS12_CONTENT_NO_PASSWORD.Value) }}'
|
||||
|
||||
# Will be empty with our current test data as there is no chain
|
||||
CERT_CHAIN: '{{ filterCertChain "CERTIFICATE" (.PKCS12_CONTENT_NO_PASSWORD.Value | decodeBase64ToBytes | pkcs12cert) }}'
|
||||
|
||||
JWK_RSA_PUBLIC_PEM: "{{ jwkPublicKeyPem .JWK_PUB_RSA.Value }}"
|
||||
JWK_ECDSA_PUBLIC_PEM: "{{ jwkPublicKeyPem .JWK_PUB_ECDSA.Value }}"
|
||||
|
||||
JWK_ECDSA_PRIVATE_PEM: "{{ jwkPrivateKeyPem .JWK_PRIV_ECDSA.Value }}"
|
||||
JWK_RSA_PRIVATE_PEM: "{{ jwkPrivateKeyPem .JWK_PRIV_RSA.Value }}"
|
||||
|
||||
JSON_STR_TO_YAML: "{{ .TEST_JSON_DATA.Value | fromJsonStringToJson | toYaml }}"
|
||||
|
||||
FROM_YAML_TO_JSON: "{{ .TEST_YAML_STRING.Value | fromYaml | toJson }}"
|
||||
YAML_ROUNDTRIP: "{{ .TEST_YAML_STRING.Value | fromYaml | toYaml }}"
|
||||
|
||||
TEST_LOWERCASE_STRING: "{{ .TEST_LOWERCASE_STRING.Value | upper }}"
|
||||
|
||||
# Only have one authentication method defined or you are likely to run into authentication issues.
|
||||
# Remove all except one authentication method.
|
||||
authentication:
|
||||
awsIamAuth:
|
||||
identityId: <machine-identity-id>
|
||||
azureAuth:
|
||||
identityId: <machine-identity-id>
|
||||
gcpIamAuth:
|
||||
identityId: <machine-identity-id>
|
||||
serviceAccountKeyFilePath: </path-to-service-account-key-file.json>
|
||||
gcpIdTokenAuth:
|
||||
identityId: <machine-identity-id>
|
||||
kubernetesAuth:
|
||||
identityId: <machine-identity-id>
|
||||
serviceAccountRef:
|
||||
name: <secret-name>
|
||||
namespace: <secret-namespace>
|
||||
universalAuth:
|
||||
credentialsRef:
|
||||
secretName: <secret-name> # universal-auth-credentials
|
||||
secretNamespace: <secret-namespace> # default
|
||||
|
@ -19,7 +19,7 @@ spec:
|
||||
|
||||
push:
|
||||
secret:
|
||||
secretName: push-secret-demo # Secret CRD
|
||||
secretName: push-secret-source-secret # Secret CRD
|
||||
secretNamespace: default
|
||||
|
||||
# Only have one authentication method defined or you are likely to run into authentication issues.
|
@ -0,0 +1,91 @@
|
||||
# This is the source secret that you can use to demo the advanced templating functionality seen in `push-secret-with-template.yaml`
|
||||
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: push-secret-demo-with-templating
|
||||
namespace: default
|
||||
stringData:
|
||||
PKCS12_CONTENT_NO_PASSWORD: MIIJYQIBAzCCCScGCSqGSIb3DQEHAaCCCRgEggkUMIIJEDCCA8cGCSqGSIb3DQEHBqCCA7gwggO0AgEAMIIDrQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQInZmyWpNTPS4CAggAgIIDgPzZTmogBRiLP0NJZEUghZ3Oh1aqHJJ32HKgXUpD5BJ/5AvpUL9FC7m6a3GD++P1On/35J9N50bDjfBJjJrl2zpA143bzltPQBOK30cBJjNsCeN2Dq1dcsvJZfEy20z75NduXjMF6/qs4BbE+1E6nYFYVNHUybFnaQwSx7+2/2OMbXbcFpt4bv3HTw0YLw2pZeW/4/4A9d+tC9UdVQTTyNbI8l9nf1aeaaPsw1keVLmHurmTihfwh469FvjgwiHUP/P3ZCn1tOpWDR8ck0j+ru6imVP2hn+Kvk6svllmYqo3A5DnDRoF/Cl9R0DAPyS0lw7BeGskgTm7B79mzVitTbzRnIUP+sGJjc1AVghnitfcX4ffv8gq5xWaKGucO/IZXbPBoe7tMhKZmsirKzD4RBhC3nMyrwaHJB6PqUwxMQGMLbuHe7GlWhJAyFlcOTt5dgNl+axIkWdisoKNinYYeOuxudqyX6yPfsyaRCV5MEez3Wu+59MENGlGDRWbw61QuwsZkr1bAT2SJrQ/zHn5aGAluQZ1csJhKQ34iy1Ml9K9F4Zh3/2OWPs0u6+JCb1PC1vChBkguqcqQtEcikRwR9dNF9cdMB1T1Xk5GqlmOPaigkYzGWLgtl8cV5/Zl0m2j77mX9x4HVCTercAABGf9JcCLzSCo04c5OwIYtWUXBkux5n2VI2ZIuS1KF+r6JNyL3lg/D8LColzDUP/6tQCBVVgMar3iLblM17wPMTDMR5Bn+NvenwJj6FWaGGMtdjygtN+oSHpNDbVygfGQy+jEgUtK7yw0uh/WKBMWVw1E6iNuhb8HIyCFtQon8sDkuZ81czOpR3Ta1SWUWrZD+pjpL2Z4y8Nc2wt9pVPvLFOTn+GDFVqGpde3kovh3GfJjYCG/HI5rXZyziflDOoSy0SyG6aVCG4ZqW2LTymoVN/kxf+skqAweX1vxvvJniiv8HgYfEASFUWear4uT641d1YwcEIawNv4n+GKBilK/7ODl2QL86svwqIcbyiJrneyU2tHymKzGcU2VxmSgf8EnjqGuIEo7WXOpk0oUMcvYrM73cgzZ3BchUDIN0KWSDI+vDcVY82dbI39KM6dtOJFAx3kEdms/gdSqZtmHUIeArGp+8caCCAK/W+4wTOvtisK+6MtzdMz6P93N78N4Vo6cs3dkj6t/6tgNog5SCfwlOEyUpmMIIFQQYJKoZIhvcNAQcBoIIFMgSCBS4wggUqMIIFJgYLKoZIhvcNAQwKAQKgggTuMIIE6jAcBgoqhkiG9w0BDAEDMA4ECHVnarQ94cqlAgIIAASCBMgUvEVKsUcqEvYJEJ9JixgB0W3uhSi/Espt931a/mwx5Ja2K7vjlttaOct3Zc8umVrP5C322tmHz9QDVPj3Bln8CGfofC/8Nb6+SDeofmYaQYReOZpZGksEBs4P3yURl8wQpIkG31Oyf3urDTJdplfDrzu6XpEpIf7RicIR+Zh4Q1+F75XwPo52/yNs8q/kVV8H97gSRqQ2GixIdyNu+JLtNjdwAERHy4DeQjwgiMCdL+xMfN+WJyIvkLZDoy9bacXeG4IcQM+n84272C6j1a0BPaOm0K5A7I0H1zpXOJiWfn3MrT4LHDudrQoIWUOvcJjWaIM/KyghotDN50THKN9qCEE9SmtfWXGGFaJmyxbUDFizBIAsFshNtMs/47PoInTSNwzxNvUUQ3ap93iquGZ9EaZAMY2HQHW/QJIQ70IbtcHU28Bus/hrMcV0X9D1p4UeHuk37W7aCrL6hS+ac9pmzwmcDBwZUliyInxRmqCCerjg2ojAM9SVg8FrpQUErP+BOaoCBwQqLLiz9BM+3tUQc/8MyaBHq+c2dUoPfvipDIQXYiq66CkjmPHxPFEL1l9d9oBFoIGkt6SIHDjWnTPc5q5SvJ9tz8Dp1k/1HQSA8OUS6j+XySYuGe8xTvN/oUpVRswef2Qd/kxZlc1FJ4lVAXvbW7C7772l14BJv/WULcFH4Sn83rlL3YwHr4vJMf6wLahn7oQPI0VFSQiiOOb/+gkiTrwO3Gz+HXOkUwaKnW85PeoIt3/q1u0CRl64mUjqCegi7RMY9Q9tRMlD5yx0RsH7mc4b6Eg/3IwGu8VQmZCO5W2unCpfzzyrOx7OaGGaW4RJ2Mx7bJ8uV9HU8MbbNntmc9oxebPdDnBmbt8p8t4ZZxC+zcqcXi3TxACXmwnasogQEi0d0ttXkB5cnDCG00Y8WPdNIWfJdIQh8Hj16LAMYWUacz/J0kLP99ENQntZibVw/Q3zZtHSF5tmsYp7o1HglBpRwLTcd026YTrxB+VCEiUYy4hH6a38oEEpY7wTIiRmEBQPIRM0HUOqVh4z6TNzRx6iIhrQEvg06B8U6iVPqy8FGDkhf3P55Ed95/Rw6uSdlMTHng+Q4aG00k4qKdKOyv55IXPcvEzAeVNBuesknaS8x7Eb/I5mHSoZU3RYAEFGbehUkvkhNr3Xq7/W/400AKiliravJq8j/qKIZ9hAVUWOps09F/4peYfLXM1AhxWWGa5QqvwFkClM+uRyqIRGJwl2Z7asl4sWVXbwtb+Axio+mYGdzxIki5iwJvRCwKapoZplndXKTrn2nYBuhxW2+fRHa8WYdsm/wn0K+jYMlZhquVjNXyL70/Sym6DkzCtJvveQs2CfcEWQuedjRSGFVFT2jV/s5F8L2TV7nQNVj6dEJSNM5JCdZ//OpiMHMCbPNeSxY9koGplUqFhP54F1WU9x+8xiFjEp8WKxQYKHUtj+ace0lLF4CDGXhFR/0k7Icarpax3hYnvagd2OpZyRJdavKBSs5U7/NPuO6sNhZ2NpzsOiul9Iu8bu3UHCECNKkwN4wF4alTlG9sAAbS4ns4wb9XTajG+OPYoDQZmuJfc71McN6m8KBHEnXU8r4epdR7xREe/w+h2MwtPhLvbxwO592tUxJTAjBgkqhkiG9w0BCRUxFgQUOEXV6IFYGpCSHi0MPHz4b3W0KOQwMTAhMAkGBSsOAwIaBQAEFAjyBCA+mr+5UkKuQ1jGw90ASfbVBAjbvqJJZikDPgICCAA=
|
||||
PKCS12_CONTENT_WITH_PASSWORD: MIIJYQIBAzCCCScGCSqGSIb3DQEHAaCCCRgEggkUMIIJEDCCA8cGCSqGSIb3DQEHBqCCA7gwggO0AgEAMIIDrQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQI2eZRJ7Ar+JQCAggAgIIDgFTbOtkFPjqxAoYRHoq1SbyXKf/NRbBA5AqxQlv9aFVT4VcxUSrMGaSWifX2UjsVWQzn134yoLLbdJ0jTorVD+EuBUmtb3xXbBwLqtFZxwcWodYA5WhPQdDcQo0cD3o1vrsXPQARQR6ISSFnhFjPYdH9cO2LqUKV5pjFhIs2/1VPDS2eY7SWZN52DK3QknSj23S3ZW2s4TFEj/5C4ssbO7cWNWBjjaORnd17FMNgVtcRw8ITmLdGBOpFUwP8wIdiLGrXiyjfMLns74nztRelV30/v0DPlz0pZtOPygi/dy0qpbil3wtOFrtQBLEdvLNmt9ikQgGs3pJBS68eMJLu3jAU6rCIKycq0+E0eMXeHcseyMwgguTj2h4t+E4S7nU11lViBFqkSBKxE28+9fNlPvCsZ4WhQZ6TAW3E/jDy/ZSqmak5V7/khMlRPvtrxz71ivksH0iipPdJJkGi7SDEvETySBETiqIslUmsF0ZYeHR5wIBkB5V8zmi8RRZtpvDGbzuQ22V6sNk2mTDh+BRus7gNCoSGWYXWqNNp1PnznuYCJp9T+0mObcAijE7IQuhpYMeQPF+MUIlG5lmpNouzuygTf++xrKIjzP36DcthnMPeD/8LYWfzkuAeRodjl7Z1G6XLvBD5h+Xlq23WPjMcXUiiWYXxTREAQ1EWUf4A9twGcxHJ5AatbvQY3QUoS4a7LNuy17lF7G+O1SFDtGeHZXHHecaVpuAtqZEYeUpqy6ZzMJXtXE1JNl/UR9TtTordb1V5Pf45JTYKLI+SwxVQbRiTgfhulNc+E3tV1AEELZt4CKmh1OFJoJRtyREMfdVuP4rx7ywIoMYuWw8CRqJ3qSmdwz2kmL2pfJNn6vDfN6rNa+sXWErOJ7naSAKQH2CJfnkCOFxkKfwjbOcNRbnGKah8NyWu6xqlv7Y4xEJYqmPahGHmrSfdAt3mpc5zD74DvetLIczcadKaLH7mp6h98xHayvXh0UE7ERHeskfSPrLxL9A3V1RZXDOtcZFWSKHzokuXMDF9OnrcMYDzYgtzof4ReY2t1ldGF7phYINhDlUNyhzyjwyWQbdkxr/+FtWq8Sbm7o2zMTby48c25gnqD9U8RTAO+bY3oV3dQ4bpAOzWdzVmFjESUFx0xVwbTSJGXdkH4YmD5He+xwxTa0Je0HE5+ui5dbP1gxUY+pCGLOhGMIIFQQYJKoZIhvcNAQcBoIIFMgSCBS4wggUqMIIFJgYLKoZIhvcNAQwKAQKgggTuMIIE6jAcBgoqhkiG9w0BDAEDMA4ECGYAccNFWW6kAgIIAASCBMgbXw69iyx73RGWS3FYeuvF5L1VWXQGrDLdUGwa3SdovXxmP1pKBAb82UuiydXDpKSVCmefeEQTs6ucEFRmXrDIldNanqUwbydy3GSJ+4iNsVQHbFgNppH4e90L7BlLcZ3MzSrVEwxWVMHq+XLqTKf3X3QyrmA3mmF96+Nd+icpDIktd+/h2BHnSXHNP0QfVH27H4DwbMDijttXY0JB+8qP9m25Wn4zkmOPEUhrY4Ptv2I08eHFAuNI0jWUwfRhC4FDbUdwFb0aZjA3Te6uYTsu2zAlmg9HuqsD/Ef/wkBEKZLBkjiXa/niFVrwELXhWZDPBAuo+/1UbzXglsW4QDU4LbUutcs6DLag1vLe40a2LO1ODQm7Zw0bxLkb3f/ry6ZFYvO78XmHo4c/oQf4KPUtM2bLz5q7uOxAx07vHYaU2BVt3NjgiIO5VVKjw0075GdgFxwPvYncv1fsC5jSIkX43GuzEtoBTpJKDYb2nhKbN9XWixwGOhUBTK3WYBhn+uaMJs4l3EgkDtK9tsUs5VQQHawj0WrGS1mQhaBfcyZzv4wSn0d3JUO2CN0e9EReJcQvsEnwUvohilOvjDHHhTq8Kp4XU4jbq7TAKqxs3TOmdoskRykn9oKUPExJVhJQonFT3ietV5BHrnN/QoDCSeOR80ZxvWHrQDz3Hm1ygiHd8LYmN4IjiD8b28ZrCALifWxh0WmIYtLZrUjMZavPh+caWH9IG32fTxV9b1bgJD8vWqscj9jCjeMJvkKQo8PFg1kMAxt1u+bIyktTq42O9qxwGrdqEMeBzXxDJMMaRIH3m9LNZ/P5Nk4/hMURhCZJtRtNfOVTK+Q6kKgsdK2EHcuEnp/qBefZjve+xmitbF1W7C4+B7b2JNBacdIm1nE56DwglT/IUk65JrNFP3rf4c5ic76LCQrvyfLiKCGaqcihM9siLVFPYdrnr8TlGbCFnGbpBqMQA5MtZQaDUug50PJtdxlgfwWH4qliimgchCaZbSTcgN5YTguSe16uUSusHD+r6XdtI0939uDILXJjQMczhIKNw8w0Tn4Z3/g2KlB6cwbtaglnnO4a/USh0cPC1a581byNqeFoMi+mAhqfKkwdDuti4GX7OrhkUOkiRjEUXdcckpmmIsyamH/g1dq3CNFXFNIgRRrzIDo4Opr3Ip2VE/4BDQoo/+Rybzxh8bsHgCEujQf8urGxjGyd2ulHoXzHWhz7pPPuY5UN6dC9WZmOQDVous/1nhYThoLVVc61Rk6d83+Ac7iRg4bY5q/73J4HvPMmrTOOOqqn3wc9Pe5ibEy4tFaYnim4p1ZRm8YcwosZmuFPdsP6G5l5qt6uOyr2+qNpXIBkDpG7I6Ls10O7L3PQAX9zRGfcz6Ds0KtuDrLpaVvhuXpewsBwpo1lmhv9bAa4ppBuWznmKigX+vYojSxd/eCRAtMs+Lx6ppZsYNVhbdEIGKXSGwG98sSTZkoLHBMkUW7S8jpeSCHZWEFBUOPJQzAr5cW1w+RAs33cGUygZ5XEEx4DeW8MnO4lCuP+VDOwu3TAKhzAD+qCyXbLEzWiyL5fq3XL+YJtoAc8Mra9lK6jDqzq4u+PLNoYY+kWTBhCyRZ+PfzcXLry8pxuP5E6VtRgfYcxJTAjBgkqhkiG9w0BCRUxFgQUOEXV6IFYGpCSHi0MPHz4b3W0KOQwMTAhMAkGBSsOAwIaBQAEFBa+SV9FU2UObo+nYKdyt/kZVw6FBAgey4GonFtJ2gICCAA=
|
||||
JWK_PRIVATE_RSA_PKCS8: "-----BEGIN PRIVATE KEY-----
|
||||
MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQCmN2yzxloN8Qfo
|
||||
rpTsZ5bafEOpHgg/Tj1+TV8rSWd2KZswxUF0+/+FKmbxPwS0EPGtR2LU4dl8yFSL
|
||||
EZq637edDgYb2czbj2jGEK3Gqo28ReuZBEapzPIvG6H58qf0WD76FL1SlrMel9UA
|
||||
WcHloJ9eg2E+4jygHLIUowpo5WAc2o/k0ESppuIt+1kPdb+WwUI8a7OvhWnRhLvN
|
||||
LaENhJwLag4y7isZTUtwxl/f2nfXncKrttLZeHpj6/DmnDMVhl2NDEOfzHwEbd8n
|
||||
qPxMYtdCxsofXbXz8dxQlG8zB2ltRAbme8DYZdWoup3CnTngvOT38H9/WVWuY4q4
|
||||
eNM0erjzAgMBAAECggEBAJLA5rnHTCV5BRmcYqJjR566DmcXvAJgywxjtb4bPjzm
|
||||
uT2TO5rVD6J8cI1ZrYZqW2c5WvpIOeThXzu2HF4YPh5tjlkysJu9/6y4dyWr2h47
|
||||
warFSrqK191d0WJEq6Oh8mCMxSdRJO7C8W4w0XAzo+Inr0l9KDfZfiWYWg2JT5XI
|
||||
ubibKKq6P2KxND0UVlYbRsp3fv2loEL9WM5H2bjA/oSbQ4tSJtobpjlsQOHmaxbP
|
||||
XhvsIV3Dr2ksDuLEhm0vfXnEGRzNk3HV3gLNT741YEP3Sp2ZRjd5U1qFn0D+eWe0
|
||||
4LfDX9auGQCnfjZTHvu4qghX7JxcF40omjmtgkRmZ/kCgYEA4A5nU4ahEww7B65y
|
||||
uzmGeCUUi8ikWzv1C81pSyUKvKzu8CX41hp9J6oRaLGesKImYiuVQK47FhZ++wwf
|
||||
pRwHvSxtNU9qXb8ewo+BvadyO1eVrIk4tNV543QlSe7pQAoJGkxCia5rfznAE3In
|
||||
KF4JvIlchyqs0RQ8wx7lULqwnn0CgYEAven83GM6SfrmO+TBHbjTk6JhP/3CMsIv
|
||||
mSdo4KrbQNvp4vHO3w1/0zJ3URkmkYGhz2tgPlfd7v1l2I6QkIh4Bumdj6FyFZEB
|
||||
pxjE4MpfdNVcNINvVj87cLyTRmIcaGxmfylY7QErP8GFA+k4UoH/eQmGKGK44TRz
|
||||
Yj5hZYGWIC8CgYEAlmmU/AG5SGxBhJqb8wxfNXDPJjf//i92BgJT2Vp4pskBbr5P
|
||||
GoyV0HbfUQVMnw977RONEurkR6O6gxZUeCclGt4kQlGZ+m0/XSWx13v9t9DIbheA
|
||||
tgVJ2mQyVDvK4m7aRYlEceFh0PsX8vYDS5o1txgPwb3oXkPTtrmbAGMUBpECgYEA
|
||||
mxRTU3QDyR2EnCv0Nl0TCF90oliJGAHR9HJmBe//EjuCBbwHfcT8OG3hWOv8vpzo
|
||||
kQPRl5cQt3NckzX3fs6xlJN4Ai2Hh2zduKFVQ2p+AF2p6Yfahscjtq+GY9cB85Nx
|
||||
Ly2IXCC0PF++Sq9LOrTE9QV988SJy/yUrAjcZ5MmECkCgYEAldHXIrEmMZVaNwGz
|
||||
DF9WG8sHj2mOZmQpw9yrjLK9hAsmsNr5LTyqWAqJIYZSwPTYWhY4nu2O0EY9G9uY
|
||||
iqewXfCKw/UngrJt8Xwfq1Zruz0YY869zPN4GiE9+9rzdZB33RBw8kIOquY3MK74
|
||||
FMwCihYx/LiU2YTHkaoJ3ncvtvg=
|
||||
-----END PRIVATE KEY-----"
|
||||
JWK_PUB_RSA: '{"kid":"ex","kty":"RSA","key_ops":["sign","verify","wrapKey","unwrapKey","encrypt","decrypt"],"n":"p2VQo8qCfWAZmdWBVaYuYb-a-tWWm78K6Sr9poCvNcmv8rUPSLACxitQWR8gZaSH1DklVkqz-Ed8Cdlf8lkDg4Ex5tkB64jRdC1Uvn4CDpOH6cp-N2s8hTFLqy9_YaDmyQS7HiqthOi9oVjil1VMeWfaAbClGtFt6UnKD0Vb_DvLoWYQSqlhgBArFJi966b4E1pOq5Ad02K8pHBDThlIIx7unibLehhDU6q3DCwNH_OOLx6bgNtmvGYJDd1cywpkLQ3YzNCUPWnfMBJRP3iQP_WI21uP6cvo0DqBPBM4wvVzHbCT0vnIflwkbgEWkq1FprqAitZlop9KjLqzjp9vyQ","e":"AQAB"}'
|
||||
JWK_PUB_ECDSA: '{"kid":"https://kv-test-mj.vault.azure.net/keys/ec-p-521/e3d0e9c179b54988860c69c6ae172c65","kty":"EC","key_ops":["sign","verify"],"crv":"P-521","x":"AedOAtb7H7Oz1C_cPKI_R4CN_eai5nteY6KFW07FOoaqgQfVCSkQDK22fCOiMT_28c8LZYJRsiIFz_IIbQUW7bXj","y":"AOnchHnmBphIWXvanmMAmcCDkaED6ycW8GsAl9fQ43BMVZTqcTkJYn6vGnhn7MObizmkNSmgZYTwG-vZkIg03HHs"}'
|
||||
|
||||
JWK_PRIV_RSA: '{"kty" : "RSA","kid" : "cc34c0a0-bd5a-4a3c-a50d-a2a7db7643df","use" : "sig","n" : "pjdss8ZaDfEH6K6U7GeW2nxDqR4IP049fk1fK0lndimbMMVBdPv_hSpm8T8EtBDxrUdi1OHZfMhUixGaut-3nQ4GG9nM249oxhCtxqqNvEXrmQRGqczyLxuh-fKn9Fg--hS9UpazHpfVAFnB5aCfXoNhPuI8oByyFKMKaOVgHNqP5NBEqabiLftZD3W_lsFCPGuzr4Vp0YS7zS2hDYScC2oOMu4rGU1LcMZf39p3153Cq7bS2Xh6Y-vw5pwzFYZdjQxDn8x8BG3fJ6j8TGLXQsbKH1218_HcUJRvMwdpbUQG5nvA2GXVqLqdwp054Lzk9_B_f1lVrmOKuHjTNHq48w","e" : "AQAB","d" : "ksDmucdMJXkFGZxiomNHnroOZxe8AmDLDGO1vhs-POa5PZM7mtUPonxwjVmthmpbZzla-kg55OFfO7YcXhg-Hm2OWTKwm73_rLh3JavaHjvBqsVKuorX3V3RYkSro6HyYIzFJ1Ek7sLxbjDRcDOj4ievSX0oN9l-JZhaDYlPlci5uJsoqro_YrE0PRRWVhtGynd-_aWgQv1YzkfZuMD-hJtDi1Im2humOWxA4eZrFs9eG-whXcOvaSwO4sSGbS99ecQZHM2TcdXeAs1PvjVgQ_dKnZlGN3lTWoWfQP55Z7Tgt8Nf1q4ZAKd-NlMe-7iqCFfsnFwXjSiaOa2CRGZn-Q","p" : "4A5nU4ahEww7B65yuzmGeCUUi8ikWzv1C81pSyUKvKzu8CX41hp9J6oRaLGesKImYiuVQK47FhZ--wwfpRwHvSxtNU9qXb8ewo-BvadyO1eVrIk4tNV543QlSe7pQAoJGkxCia5rfznAE3InKF4JvIlchyqs0RQ8wx7lULqwnn0","q" : "ven83GM6SfrmO-TBHbjTk6JhP_3CMsIvmSdo4KrbQNvp4vHO3w1_0zJ3URkmkYGhz2tgPlfd7v1l2I6QkIh4Bumdj6FyFZEBpxjE4MpfdNVcNINvVj87cLyTRmIcaGxmfylY7QErP8GFA-k4UoH_eQmGKGK44TRzYj5hZYGWIC8","dp" : "lmmU_AG5SGxBhJqb8wxfNXDPJjf__i92BgJT2Vp4pskBbr5PGoyV0HbfUQVMnw977RONEurkR6O6gxZUeCclGt4kQlGZ-m0_XSWx13v9t9DIbheAtgVJ2mQyVDvK4m7aRYlEceFh0PsX8vYDS5o1txgPwb3oXkPTtrmbAGMUBpE","dq" : "mxRTU3QDyR2EnCv0Nl0TCF90oliJGAHR9HJmBe__EjuCBbwHfcT8OG3hWOv8vpzokQPRl5cQt3NckzX3fs6xlJN4Ai2Hh2zduKFVQ2p-AF2p6Yfahscjtq-GY9cB85NxLy2IXCC0PF--Sq9LOrTE9QV988SJy_yUrAjcZ5MmECk","qi" : "ldHXIrEmMZVaNwGzDF9WG8sHj2mOZmQpw9yrjLK9hAsmsNr5LTyqWAqJIYZSwPTYWhY4nu2O0EY9G9uYiqewXfCKw_UngrJt8Xwfq1Zruz0YY869zPN4GiE9-9rzdZB33RBw8kIOquY3MK74FMwCihYx_LiU2YTHkaoJ3ncvtvg"}'
|
||||
JWK_PRIV_ECDSA: '{"kty": "EC","kid": "rie3pHe8u8gjSa0IaJfqk7_iEfHeYfDYx-Bqi7vQc0s","crv": "P-256","x": "fDjg3Nq4jPf8IOZ0277aPVal_8iXySnzLUJAZghUzZM","y": "d863PeyBOK_Q4duiSmWwgIRzi1RPlFZTR-vACMlPg-Q","d": "jJs5xsoHUetdMabtt8H2KyX5T92nGul1chFeMT5hlr0"}'
|
||||
|
||||
TEST_LOWERCASE_STRING: i am a lowercase string
|
||||
|
||||
TEST_YAML_STRING: |
|
||||
name: test-object
|
||||
type: example
|
||||
properties:
|
||||
key1: value1
|
||||
key2: value2
|
||||
numbers:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
|
||||
TEST_JSON_DATA: '{
|
||||
"id": "f1bc87fc-99cf-48d2-b289-7cf578152f9a",
|
||||
"name": "test",
|
||||
"description": "",
|
||||
"type": "secret-manager",
|
||||
"slug": "test-o-bto",
|
||||
"autoCapitalization": false,
|
||||
"orgId": "601815be-6884-4ee4-86c7-bfc6415f2123",
|
||||
"createdAt": "2025-03-26T01:32:39.890Z",
|
||||
"updatedAt": "2025-03-26T01:32:41.688Z",
|
||||
"version": 3,
|
||||
"upgradeStatus": null,
|
||||
"pitVersionLimit": 10,
|
||||
"kmsCertificateKeyId": null,
|
||||
"auditLogsRetentionDays": null,
|
||||
"_id": "f1bc87fc-99cf-48d2-b289-7cf578152f9a",
|
||||
"environments": [
|
||||
{
|
||||
"name": "Development",
|
||||
"slug": "dev",
|
||||
"id": "cbb62f88-44cb-4c29-975a-871f8d7d303b"
|
||||
},
|
||||
{
|
||||
"name": "Staging",
|
||||
"slug": "staging",
|
||||
"id": "c933a63d-418a-4d5c-a7d1-91b74d3ee2eb"
|
||||
},
|
||||
{
|
||||
"name": "Production",
|
||||
"slug": "prod",
|
||||
"id": "0b70125e-47d5-46e8-a03e-a3105df05d37"
|
||||
}
|
||||
]
|
||||
}'
|
@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: push-secret-source-secret
|
||||
namespace: default
|
||||
stringData:
|
||||
ENCRYPTION_KEY: secret-encryption-key
|
||||
API_URL: https://example.com/api
|
||||
REGION: us-east-1
|
@ -1,9 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: push-secret-demo
|
||||
namespace: default
|
||||
stringData: # can also be "data", but needs to be base64 encoded
|
||||
API_KEY: some-api-key
|
||||
DATABASE_URL: postgres://127.0.0.1:5432
|
||||
ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab
|
@ -65,6 +65,8 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl.
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
logger.Info("Infisical Push Secret CRD not found")
|
||||
r.DeleteManagedSecrets(ctx, logger, infisicalPushSecretCRD)
|
||||
|
||||
return ctrl.Result{
|
||||
Requeue: false,
|
||||
}, nil
|
||||
|
@ -1,16 +1,21 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
tpl "text/template"
|
||||
|
||||
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/api"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/constants"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/model"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/template"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/util"
|
||||
"github.com/go-logr/logr"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
@ -101,6 +106,48 @@ func (r *InfisicalPushSecretReconciler) updateResourceVariables(infisicalPushSec
|
||||
infisicalPushSecretResourceVariablesMap[string(infisicalPushSecret.UID)] = resourceVariables
|
||||
}
|
||||
|
||||
func (r *InfisicalPushSecretReconciler) processTemplatedSecrets(infisicalPushSecret v1alpha1.InfisicalPushSecret, kubePushSecret *corev1.Secret, destination v1alpha1.InfisicalPushSecretDestination) (map[string]string, error) {
|
||||
|
||||
processedSecrets := make(map[string]string)
|
||||
|
||||
sourceSecrets := make(map[string]model.SecretTemplateOptions)
|
||||
for key, value := range kubePushSecret.Data {
|
||||
|
||||
sourceSecrets[key] = model.SecretTemplateOptions{
|
||||
Value: string(value),
|
||||
SecretPath: destination.SecretsPath,
|
||||
}
|
||||
}
|
||||
|
||||
if infisicalPushSecret.Spec.Push.Secret.Template == nil || (infisicalPushSecret.Spec.Push.Secret.Template != nil && infisicalPushSecret.Spec.Push.Secret.Template.IncludeAllSecrets) {
|
||||
for key, value := range kubePushSecret.Data {
|
||||
processedSecrets[key] = string(value)
|
||||
}
|
||||
}
|
||||
|
||||
if infisicalPushSecret.Spec.Push.Secret.Template != nil &&
|
||||
len(infisicalPushSecret.Spec.Push.Secret.Template.Data) > 0 {
|
||||
|
||||
for templateKey, userTemplate := range infisicalPushSecret.Spec.Push.Secret.Template.Data {
|
||||
|
||||
tmpl, err := tpl.New("push-secret-templates").Funcs(template.GetTemplateFunctions()).Parse(userTemplate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to compile template: %s [err=%v]", templateKey, err)
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
err = tmpl.Execute(buf, sourceSecrets)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to execute template: %s [err=%v]", templateKey, err)
|
||||
}
|
||||
|
||||
processedSecrets[templateKey] = buf.String()
|
||||
}
|
||||
}
|
||||
|
||||
return processedSecrets, nil
|
||||
}
|
||||
|
||||
func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context.Context, logger logr.Logger, infisicalPushSecret v1alpha1.InfisicalPushSecret) error {
|
||||
|
||||
resourceVariables := r.getResourceVariables(infisicalPushSecret)
|
||||
@ -134,10 +181,9 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context
|
||||
return fmt.Errorf("unable to fetch kube secret [err=%s]", err)
|
||||
}
|
||||
|
||||
var kubeSecrets = make(map[string]string)
|
||||
|
||||
for key, value := range kubePushSecret.Data {
|
||||
kubeSecrets[key] = string(value)
|
||||
processedSecrets, err := r.processTemplatedSecrets(infisicalPushSecret, kubePushSecret, infisicalPushSecret.Spec.Destination)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to process templated secrets [err=%s]", err)
|
||||
}
|
||||
|
||||
destination := infisicalPushSecret.Spec.Destination
|
||||
@ -191,7 +237,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context
|
||||
|
||||
infisicalPushSecret.Status.ManagedSecrets = make(map[string]string) // (string[id], string[key] )
|
||||
|
||||
for secretKey, secretValue := range kubeSecrets {
|
||||
for secretKey, secretValue := range processedSecrets {
|
||||
if exists := getExistingSecretByKey(secretKey); exists != nil {
|
||||
|
||||
if updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) {
|
||||
@ -280,7 +326,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context
|
||||
// We need to check if any of the secrets have been removed in the new kube secret
|
||||
for _, managedSecretKey := range infisicalPushSecret.Status.ManagedSecrets {
|
||||
|
||||
if _, ok := kubeSecrets[managedSecretKey]; !ok {
|
||||
if _, ok := processedSecrets[managedSecretKey]; !ok {
|
||||
|
||||
// Secret has been removed, verify that the secret is managed by the operator
|
||||
if getExistingSecretByKey(managedSecretKey) != nil {
|
||||
@ -305,7 +351,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context
|
||||
}
|
||||
|
||||
// We need to check if any new secrets have been added in the kube secret
|
||||
for currentSecretKey := range kubeSecrets {
|
||||
for currentSecretKey := range processedSecrets {
|
||||
|
||||
if exists := getExistingSecretByKey(currentSecretKey); exists == nil {
|
||||
|
||||
@ -317,7 +363,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context
|
||||
|
||||
createdSecret, err := infisicalClient.Secrets().Create(infisicalSdk.CreateSecretOptions{
|
||||
SecretKey: currentSecretKey,
|
||||
SecretValue: kubeSecrets[currentSecretKey],
|
||||
SecretValue: processedSecrets[currentSecretKey],
|
||||
ProjectID: destination.ProjectID,
|
||||
Environment: destination.EnvironmentSlug,
|
||||
SecretPath: destination.SecretsPath,
|
||||
@ -336,12 +382,12 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context
|
||||
|
||||
existingSecret := getExistingSecretByKey(currentSecretKey)
|
||||
|
||||
if existingSecret != nil && existingSecret.SecretValue != kubeSecrets[currentSecretKey] {
|
||||
if existingSecret != nil && existingSecret.SecretValue != processedSecrets[currentSecretKey] {
|
||||
logger.Info(fmt.Sprintf("Secret with key [key=%s] has changed value. Updating secret in Infisical", currentSecretKey))
|
||||
|
||||
updatedSecret, err := infisicalClient.Secrets().Update(infisicalSdk.UpdateSecretOptions{
|
||||
SecretKey: currentSecretKey,
|
||||
NewSecretValue: kubeSecrets[currentSecretKey],
|
||||
NewSecretValue: processedSecrets[currentSecretKey],
|
||||
ProjectID: destination.ProjectID,
|
||||
Environment: destination.EnvironmentSlug,
|
||||
SecretPath: destination.SecretsPath,
|
||||
@ -353,7 +399,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context
|
||||
continue
|
||||
}
|
||||
|
||||
updateExistingSecretByKey(currentSecretKey, kubeSecrets[currentSecretKey])
|
||||
updateExistingSecretByKey(currentSecretKey, processedSecrets[currentSecretKey])
|
||||
infisicalPushSecret.Status.ManagedSecrets[updatedSecret.ID] = currentSecretKey
|
||||
}
|
||||
}
|
||||
@ -361,7 +407,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context
|
||||
}
|
||||
|
||||
// Check if any of the existing secrets values have changed
|
||||
for secretKey, secretValue := range kubeSecrets {
|
||||
for secretKey, secretValue := range processedSecrets {
|
||||
|
||||
existingSecret := getExistingSecretByKey(secretKey)
|
||||
|
||||
@ -440,9 +486,28 @@ func (r *InfisicalPushSecretReconciler) DeleteManagedSecrets(ctx context.Context
|
||||
|
||||
resourceVariables := r.getResourceVariables(infisicalPushSecret)
|
||||
infisicalClient := resourceVariables.InfisicalClient
|
||||
cancelCtx := resourceVariables.CancelCtx
|
||||
authDetails := resourceVariables.AuthDetails
|
||||
var err error
|
||||
|
||||
if authDetails.AuthStrategy == "" {
|
||||
logger.Info("No authentication strategy found. Attempting to authenticate")
|
||||
authDetails, err = r.handleAuthentication(ctx, infisicalPushSecret, infisicalClient)
|
||||
r.SetAuthenticatedStatusCondition(ctx, &infisicalPushSecret, err)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to authenticate [err=%s]", err)
|
||||
}
|
||||
|
||||
r.updateResourceVariables(infisicalPushSecret, util.ResourceVariables{
|
||||
InfisicalClient: infisicalClient,
|
||||
CancelCtx: cancelCtx,
|
||||
AuthDetails: authDetails,
|
||||
})
|
||||
}
|
||||
|
||||
destination := infisicalPushSecret.Spec.Destination
|
||||
existingSecrets, err := infisicalClient.Secrets().List(infisicalSdk.ListSecretsOptions{
|
||||
existingSecrets, err := resourceVariables.InfisicalClient.Secrets().List(infisicalSdk.ListSecretsOptions{
|
||||
ProjectID: destination.ProjectID,
|
||||
Environment: destination.EnvironmentSlug,
|
||||
SecretPath: destination.SecretsPath,
|
||||
|
@ -3,17 +3,17 @@ package controllers
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
tpl "text/template"
|
||||
|
||||
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/api"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/constants"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/crypto"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/model"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/template"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/util"
|
||||
"github.com/go-logr/logr"
|
||||
|
||||
@ -156,16 +156,6 @@ func (r *InfisicalSecretReconciler) getInfisicalServiceAccountCredentialsFromKub
|
||||
return model.ServiceAccountDetails{AccessKey: string(accessKeyFromSecret), PrivateKey: string(privateKeyFromSecret), PublicKey: string(publicKeyFromSecret)}, nil
|
||||
}
|
||||
|
||||
var infisicalSecretTemplateFunctions = template.FuncMap{
|
||||
"decodeBase64ToBytes": func(encodedString string) string {
|
||||
decoded, err := base64.StdEncoding.DecodeString(encodedString)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
return string(decoded)
|
||||
},
|
||||
}
|
||||
|
||||
func convertBinaryToStringMap(binaryMap map[string][]byte) map[string]string {
|
||||
stringMap := make(map[string]string)
|
||||
for k, v := range binaryMap {
|
||||
@ -177,7 +167,7 @@ func convertBinaryToStringMap(binaryMap map[string][]byte) map[string]string {
|
||||
func (r *InfisicalSecretReconciler) createInfisicalManagedKubeResource(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret, managedSecretReferenceInterface interface{}, secretsFromAPI []model.SingleEnvironmentVariable, ETag string, resourceType constants.ManagedKubeResourceType) error {
|
||||
plainProcessedSecrets := make(map[string][]byte)
|
||||
|
||||
var managedTemplateData *v1alpha1.InfisicalSecretTemplate
|
||||
var managedTemplateData *v1alpha1.SecretTemplate
|
||||
|
||||
if resourceType == constants.MANAGED_KUBE_RESOURCE_TYPE_SECRET {
|
||||
managedTemplateData = managedSecretReferenceInterface.(v1alpha1.ManagedKubeSecretConfig).Template
|
||||
@ -201,7 +191,7 @@ func (r *InfisicalSecretReconciler) createInfisicalManagedKubeResource(ctx conte
|
||||
}
|
||||
|
||||
for templateKey, userTemplate := range managedTemplateData.Data {
|
||||
tmpl, err := template.New("secret-templates").Funcs(infisicalSecretTemplateFunctions).Parse(userTemplate)
|
||||
tmpl, err := tpl.New("secret-templates").Funcs(template.GetTemplateFunctions()).Parse(userTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to compile template: %s [err=%v]", templateKey, err)
|
||||
}
|
||||
@ -322,7 +312,7 @@ func (r *InfisicalSecretReconciler) updateInfisicalManagedKubeSecret(ctx context
|
||||
}
|
||||
|
||||
for templateKey, userTemplate := range managedTemplateData.Data {
|
||||
tmpl, err := template.New("secret-templates").Funcs(infisicalSecretTemplateFunctions).Parse(userTemplate)
|
||||
tmpl, err := tpl.New("secret-templates").Funcs(template.GetTemplateFunctions()).Parse(userTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to compile template: %s [err=%v]", templateKey, err)
|
||||
}
|
||||
@ -373,7 +363,7 @@ func (r *InfisicalSecretReconciler) updateInfisicalManagedConfigMap(ctx context.
|
||||
}
|
||||
|
||||
for templateKey, userTemplate := range managedTemplateData.Data {
|
||||
tmpl, err := template.New("secret-templates").Funcs(infisicalSecretTemplateFunctions).Parse(userTemplate)
|
||||
tmpl, err := tpl.New("secret-templates").Funcs(template.GetTemplateFunctions()).Parse(userTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to compile template: %s [err=%v]", templateKey, err)
|
||||
}
|
||||
|
@ -3,12 +3,15 @@ module github.com/Infisical/infisical/k8-operator
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/infisical/go-sdk v0.4.4
|
||||
github.com/lestrrat-go/jwx/v2 v2.1.4
|
||||
github.com/onsi/ginkgo/v2 v2.6.0
|
||||
github.com/onsi/gomega v1.24.1
|
||||
k8s.io/apimachinery v0.26.1
|
||||
k8s.io/client-go v0.26.1
|
||||
sigs.k8s.io/controller-runtime v0.14.4
|
||||
software.sslmate.com/src/go-pkcs12 v0.5.0
|
||||
)
|
||||
|
||||
require (
|
||||
@ -16,6 +19,9 @@ require (
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.4.0 // indirect
|
||||
cloud.google.com/go/iam v1.1.11 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.24 // indirect
|
||||
@ -29,18 +35,31 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.1 // indirect
|
||||
github.com/aws/smithy-go v1.20.3 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.5 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httprc v1.0.6 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
|
||||
go.opentelemetry.io/otel v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
google.golang.org/api v0.188.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240708141625-4ad9e859172b // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect
|
||||
@ -84,18 +103,18 @@ require (
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/zap v1.24.0 // indirect
|
||||
golang.org/x/crypto v0.25.0
|
||||
golang.org/x/crypto v0.32.0
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/oauth2 v0.21.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/term v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/term v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/api v0.26.1
|
||||
k8s.io/apiextensions-apiserver v0.26.1 // indirect
|
||||
k8s.io/component-base v0.26.1 // indirect
|
||||
|
@ -38,9 +38,17 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
|
||||
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
@ -92,6 +100,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE=
|
||||
github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
@ -106,6 +116,8 @@ github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJ
|
||||
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
@ -138,6 +150,8 @@ github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/
|
||||
github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g=
|
||||
github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
@ -214,6 +228,8 @@ github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBY
|
||||
github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
@ -239,10 +255,24 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
|
||||
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||
github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k=
|
||||
github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||
github.com/lestrrat-go/jwx/v2 v2.1.4 h1:uBCMmJX8oRZStmKuMMOFb0Yh9xmEMgNJLgjuKKt4/qc=
|
||||
github.com/lestrrat-go/jwx/v2 v2.1.4/go.mod h1:nWRbDFR1ALG2Z6GJbBXzfQaYyvn751KuuyySN2yR6is=
|
||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
@ -250,6 +280,10 @@ github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2 h1:hAHbPm5IJGijwng3PWk09JkG9WeqChjprR5s9bBZ+OM=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@ -300,9 +334,17 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
|
||||
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
||||
@ -319,8 +361,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@ -362,8 +404,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -458,8 +500,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -506,16 +548,16 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -527,8 +569,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@ -728,3 +770,5 @@ sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kF
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M=
|
||||
software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
infisicalDynamicSecretController "github.com/Infisical/infisical/k8-operator/controllers/infisicaldynamicsecret"
|
||||
infisicalPushSecretController "github.com/Infisical/infisical/k8-operator/controllers/infisicalpushsecret"
|
||||
infisicalSecretController "github.com/Infisical/infisical/k8-operator/controllers/infisicalsecret"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/template"
|
||||
//+kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
@ -86,6 +87,8 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
template.InitializeTemplateFunctions()
|
||||
|
||||
if err = (&infisicalSecretController.InfisicalSecretReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
|
18
k8-operator/packages/template/base64.go
Normal file
18
k8-operator/packages/template/base64.go
Normal file
@ -0,0 +1,18 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func decodeBase64ToBytes(encodedString string) string {
|
||||
decoded, err := base64.StdEncoding.DecodeString(encodedString)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
return string(decoded)
|
||||
}
|
||||
|
||||
func encodeBase64(plainString string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(plainString))
|
||||
}
|
43
k8-operator/packages/template/jwk.go
Normal file
43
k8-operator/packages/template/jwk.go
Normal file
@ -0,0 +1,43 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||
)
|
||||
|
||||
func jwkPublicKeyPem(jwkjson string) string {
|
||||
k, err := jwk.ParseKey([]byte(jwkjson))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("[jwkPublicKeyPem] Error: %v", err))
|
||||
}
|
||||
var rawkey any
|
||||
err = k.Raw(&rawkey)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("[jwkPublicKeyPem] Error: %v", err))
|
||||
}
|
||||
mpk, err := x509.MarshalPKIXPublicKey(rawkey)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("[jwkPublicKeyPem] Error: %v", err))
|
||||
}
|
||||
return pemEncode(mpk, "PUBLIC KEY")
|
||||
}
|
||||
|
||||
func jwkPrivateKeyPem(jwkjson string) string {
|
||||
k, err := jwk.ParseKey([]byte(jwkjson))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("[jwkPrivateKeyPem] Error: %v", err))
|
||||
}
|
||||
var mpk []byte
|
||||
var pk any
|
||||
err = k.Raw(&pk)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("[jwkPrivateKeyPem] Error: %v", err))
|
||||
}
|
||||
mpk, err = x509.MarshalPKCS8PrivateKey(pk)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("[jwkPrivateKeyPem] Error: %v", err))
|
||||
}
|
||||
return pemEncode(mpk, "PRIVATE KEY")
|
||||
}
|
98
k8-operator/packages/template/pem.go
Normal file
98
k8-operator/packages/template/pem.go
Normal file
@ -0,0 +1,98 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
errJunk = "error filtering pem: found junk"
|
||||
|
||||
certTypeLeaf = "leaf"
|
||||
certTypeIntermediate = "intermediate"
|
||||
certTypeRoot = "root"
|
||||
)
|
||||
|
||||
func filterPEM(pemType, input string) string {
|
||||
data := []byte(input)
|
||||
var blocks []byte
|
||||
var block *pem.Block
|
||||
var rest []byte
|
||||
for {
|
||||
block, rest = pem.Decode(data)
|
||||
data = rest
|
||||
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
if !strings.EqualFold(block.Type, pemType) {
|
||||
continue
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := pem.Encode(&buf, block)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("[filterPEM] Error: %v", err))
|
||||
}
|
||||
blocks = append(blocks, buf.Bytes()...)
|
||||
}
|
||||
|
||||
if len(blocks) == 0 && len(rest) != 0 {
|
||||
panic(fmt.Sprintf("[filterPEM] Error: %v", errJunk))
|
||||
}
|
||||
|
||||
return string(blocks)
|
||||
}
|
||||
|
||||
func filterCertChain(certType, input string) string {
|
||||
ordered := fetchX509CertChains([]byte(input))
|
||||
|
||||
switch certType {
|
||||
case certTypeLeaf:
|
||||
cert := ordered[0]
|
||||
if cert.AuthorityKeyId != nil && !bytes.Equal(cert.AuthorityKeyId, cert.SubjectKeyId) {
|
||||
return pemEncode(ordered[0].Raw, pemTypeCertificate)
|
||||
}
|
||||
case certTypeIntermediate:
|
||||
if len(ordered) < 2 {
|
||||
return ""
|
||||
}
|
||||
var pemData []byte
|
||||
for _, cert := range ordered[1:] {
|
||||
if isRootCertificate(cert) {
|
||||
break
|
||||
}
|
||||
b := &pem.Block{
|
||||
Type: pemTypeCertificate,
|
||||
Bytes: cert.Raw,
|
||||
}
|
||||
pemData = append(pemData, pem.EncodeToMemory(b)...)
|
||||
}
|
||||
return string(pemData)
|
||||
case certTypeRoot:
|
||||
cert := ordered[len(ordered)-1]
|
||||
if isRootCertificate(cert) {
|
||||
return pemEncode(cert.Raw, pemTypeCertificate)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func isRootCertificate(cert *x509.Certificate) bool {
|
||||
return cert.AuthorityKeyId == nil || bytes.Equal(cert.AuthorityKeyId, cert.SubjectKeyId)
|
||||
}
|
||||
|
||||
func pemEncode(thing []byte, kind string) string {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
err := pem.Encode(buf, &pem.Block{Type: kind, Bytes: thing})
|
||||
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("[pemEncode] Error: %v", err))
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
117
k8-operator/packages/template/pem_chain.go
Normal file
117
k8-operator/packages/template/pem_chain.go
Normal file
@ -0,0 +1,117 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
errNilCert = "certificate is nil"
|
||||
errFoundDisjunctCert = "found multiple leaf or disjunct certificates"
|
||||
errNoLeafFound = "no leaf certificate found"
|
||||
errChainCycle = "constructing chain resulted in cycle"
|
||||
)
|
||||
|
||||
type node struct {
|
||||
cert *x509.Certificate
|
||||
parent *node
|
||||
isParent bool
|
||||
}
|
||||
|
||||
func fetchX509CertChains(data []byte) []*x509.Certificate {
|
||||
var newCertChain []*x509.Certificate
|
||||
nodes := pemToNodes(data)
|
||||
|
||||
// at the end of this computation, the output will be a single linked list
|
||||
// the tail of the list will be the root node (which has no parents)
|
||||
// the head of the list will be the leaf node (whose parent will be intermediate certs)
|
||||
// (head) leaf -> intermediates -> root (tail)
|
||||
for i := range nodes {
|
||||
for j := range nodes {
|
||||
// ignore same node to prevent generating a cycle
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
// if ith node AuthorityKeyId is same as jth node SubjectKeyId, jth node was used
|
||||
// to sign the ith certificate
|
||||
if bytes.Equal(nodes[i].cert.AuthorityKeyId, nodes[j].cert.SubjectKeyId) {
|
||||
nodes[j].isParent = true
|
||||
nodes[i].parent = nodes[j]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var foundLeaf bool
|
||||
var leaf *node
|
||||
for i := range nodes {
|
||||
if !nodes[i].isParent {
|
||||
if foundLeaf {
|
||||
panic(fmt.Sprintf("[fetchX509CertChains] Error: %v", errFoundDisjunctCert))
|
||||
}
|
||||
// this is the leaf node as it's not a parent for any other node
|
||||
leaf = nodes[i]
|
||||
foundLeaf = true
|
||||
}
|
||||
}
|
||||
|
||||
if leaf == nil {
|
||||
panic(fmt.Sprintf("[fetchX509CertChains] Error: %v", errNoLeafFound))
|
||||
}
|
||||
|
||||
processedNodes := 0
|
||||
// iterate through the directed list and append the nodes to new cert chain
|
||||
for leaf != nil {
|
||||
processedNodes++
|
||||
// ensure we aren't stuck in a cyclic loop
|
||||
if processedNodes > len(nodes) {
|
||||
panic(fmt.Sprintf("[fetchX509CertChains] Error: %v", errChainCycle))
|
||||
}
|
||||
newCertChain = append(newCertChain, leaf.cert)
|
||||
leaf = leaf.parent
|
||||
}
|
||||
return newCertChain
|
||||
}
|
||||
|
||||
func fetchCertChains(data []byte) []byte {
|
||||
var pemData []byte
|
||||
newCertChain := fetchX509CertChains(data)
|
||||
|
||||
for _, cert := range newCertChain {
|
||||
b := &pem.Block{
|
||||
Type: pemTypeCertificate,
|
||||
Bytes: cert.Raw,
|
||||
}
|
||||
pemData = append(pemData, pem.EncodeToMemory(b)...)
|
||||
}
|
||||
return pemData
|
||||
}
|
||||
|
||||
func pemToNodes(data []byte) []*node {
|
||||
nodes := make([]*node, 0)
|
||||
for {
|
||||
// decode pem to der first
|
||||
block, rest := pem.Decode(data)
|
||||
data = rest
|
||||
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("[pemToNodes] Error: %v", err))
|
||||
}
|
||||
|
||||
if cert == nil {
|
||||
panic(fmt.Sprintf("[pemToNodes] Error: %v", errNilCert))
|
||||
}
|
||||
nodes = append(nodes, &node{
|
||||
cert: cert,
|
||||
parent: nil,
|
||||
isParent: false,
|
||||
})
|
||||
}
|
||||
return nodes
|
||||
}
|
144
k8-operator/packages/template/pkcs12.go
Normal file
144
k8-operator/packages/template/pkcs12.go
Normal file
@ -0,0 +1,144 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
|
||||
gopkcs12 "software.sslmate.com/src/go-pkcs12"
|
||||
)
|
||||
|
||||
func pkcs12keyPass(pass, input string) string {
|
||||
privateKey, _, _, err := gopkcs12.DecodeChain([]byte(input), pass)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
|
||||
marshalPrivateKey, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := pem.Encode(&buf, &pem.Block{
|
||||
Type: pemTypeKey,
|
||||
Bytes: marshalPrivateKey,
|
||||
}); err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func parsePrivateKey(block []byte) any {
|
||||
if k, err := x509.ParsePKCS1PrivateKey(block); err == nil {
|
||||
return k
|
||||
}
|
||||
if k, err := x509.ParsePKCS8PrivateKey(block); err == nil {
|
||||
return k
|
||||
}
|
||||
if k, err := x509.ParseECPrivateKey(block); err == nil {
|
||||
return k
|
||||
}
|
||||
panic("Error: unable to parse private key")
|
||||
}
|
||||
|
||||
func pkcs12key(input string) string {
|
||||
return pkcs12keyPass("", input)
|
||||
}
|
||||
|
||||
func pkcs12certPass(pass, input string) string {
|
||||
_, certificate, caCerts, err := gopkcs12.DecodeChain([]byte(input), pass)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
|
||||
var pemData []byte
|
||||
var buf bytes.Buffer
|
||||
if err := pem.Encode(&buf, &pem.Block{
|
||||
Type: pemTypeCertificate,
|
||||
Bytes: certificate.Raw,
|
||||
}); err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
|
||||
pemData = append(pemData, buf.Bytes()...)
|
||||
|
||||
for _, ca := range caCerts {
|
||||
var buf bytes.Buffer
|
||||
if err := pem.Encode(&buf, &pem.Block{
|
||||
Type: pemTypeCertificate,
|
||||
Bytes: ca.Raw,
|
||||
}); err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
pemData = append(pemData, buf.Bytes()...)
|
||||
}
|
||||
|
||||
// try to order certificate chain. If it fails we return
|
||||
// the unordered raw pem data.
|
||||
// This fails if multiple leaf or disjunct certs are provided.
|
||||
ordered := fetchCertChains(pemData)
|
||||
|
||||
return string(ordered)
|
||||
}
|
||||
|
||||
func pkcs12cert(input string) string {
|
||||
return pkcs12certPass("", input)
|
||||
}
|
||||
|
||||
func pemToPkcs12(cert, key string) string {
|
||||
return pemToPkcs12Pass(cert, key, "")
|
||||
}
|
||||
|
||||
func pemToPkcs12Pass(cert, key, pass string) string {
|
||||
certPem, _ := pem.Decode([]byte(cert))
|
||||
|
||||
parsedCert, err := x509.ParseCertificate(certPem.Bytes)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
|
||||
return certsToPkcs12(parsedCert, key, nil, pass)
|
||||
}
|
||||
|
||||
func fullPemToPkcs12(cert, key string) string {
|
||||
return fullPemToPkcs12Pass(cert, key, "")
|
||||
}
|
||||
|
||||
func fullPemToPkcs12Pass(cert, key, pass string) string {
|
||||
certPem, rest := pem.Decode([]byte(cert))
|
||||
|
||||
parsedCert, err := x509.ParseCertificate(certPem.Bytes)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
|
||||
caCerts := make([]*x509.Certificate, 0)
|
||||
for len(rest) > 0 {
|
||||
caPem, restBytes := pem.Decode(rest)
|
||||
rest = restBytes
|
||||
|
||||
caCert, err := x509.ParseCertificate(caPem.Bytes)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
|
||||
caCerts = append(caCerts, caCert)
|
||||
}
|
||||
|
||||
return certsToPkcs12(parsedCert, key, caCerts, pass)
|
||||
}
|
||||
|
||||
func certsToPkcs12(cert *x509.Certificate, key string, caCerts []*x509.Certificate, password string) string {
|
||||
keyPem, _ := pem.Decode([]byte(key))
|
||||
parsedKey := parsePrivateKey(keyPem.Bytes)
|
||||
|
||||
pfx, err := gopkcs12.Modern.Encode(parsedKey, cert, caCerts, password)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(pfx)
|
||||
}
|
67
k8-operator/packages/template/template.go
Normal file
67
k8-operator/packages/template/template.go
Normal file
@ -0,0 +1,67 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
tpl "text/template"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
)
|
||||
|
||||
var customInfisicalSecretTemplateFunctions = tpl.FuncMap{
|
||||
"pkcs12key": pkcs12key,
|
||||
"pkcs12keyPass": pkcs12keyPass,
|
||||
"pkcs12cert": pkcs12cert,
|
||||
"pkcs12certPass": pkcs12certPass,
|
||||
|
||||
"pemToPkcs12": pemToPkcs12,
|
||||
"pemToPkcs12Pass": pemToPkcs12Pass,
|
||||
"fullPemToPkcs12": fullPemToPkcs12,
|
||||
"fullPemToPkcs12Pass": fullPemToPkcs12Pass,
|
||||
|
||||
"filterPEM": filterPEM,
|
||||
"filterCertChain": filterCertChain,
|
||||
|
||||
"jwkPublicKeyPem": jwkPublicKeyPem,
|
||||
"jwkPrivateKeyPem": jwkPrivateKeyPem,
|
||||
|
||||
"toYaml": toYAML,
|
||||
"fromYaml": fromYAML,
|
||||
|
||||
"decodeBase64ToBytes": decodeBase64ToBytes,
|
||||
"encodeBase64": encodeBase64,
|
||||
}
|
||||
|
||||
const (
|
||||
errParse = "unable to parse template at key %s: %s"
|
||||
errExecute = "unable to execute template at key %s: %s"
|
||||
errDecodePKCS12WithPass = "unable to decode pkcs12 with password: %s"
|
||||
errDecodeCertWithPass = "unable to decode pkcs12 certificate with password: %s"
|
||||
errParsePrivKey = "unable to parse private key type"
|
||||
errUnmarshalJSON = "unable to unmarshal json: %s"
|
||||
errMarshalJSON = "unable to marshal json: %s"
|
||||
|
||||
pemTypeCertificate = "CERTIFICATE"
|
||||
pemTypeKey = "PRIVATE KEY"
|
||||
)
|
||||
|
||||
func InitializeTemplateFunctions() {
|
||||
templates := customInfisicalSecretTemplateFunctions
|
||||
|
||||
sprigFuncs := sprig.TxtFuncMap()
|
||||
// removed for security reasons
|
||||
delete(sprigFuncs, "env")
|
||||
delete(sprigFuncs, "expandenv")
|
||||
|
||||
for k, v := range sprigFuncs {
|
||||
// make sure we aren't overwriting any of our own functions
|
||||
_, exists := templates[k]
|
||||
if !exists {
|
||||
templates[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
customInfisicalSecretTemplateFunctions = templates
|
||||
}
|
||||
|
||||
func GetTemplateFunctions() tpl.FuncMap {
|
||||
return customInfisicalSecretTemplateFunctions
|
||||
}
|
30
k8-operator/packages/template/yaml.go
Normal file
30
k8-operator/packages/template/yaml.go
Normal file
@ -0,0 +1,30 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func toYAML(v any) string {
|
||||
data, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
|
||||
}
|
||||
return strings.TrimSuffix(string(data), "\n")
|
||||
}
|
||||
|
||||
// fromYAML converts a YAML document into a map[string]any.
|
||||
//
|
||||
// This is not a general-purpose YAML parser, and will not parse all valid
|
||||
// YAML documents.
|
||||
func fromYAML(str string) map[string]any {
|
||||
mapData := map[string]any{}
|
||||
|
||||
if err := yaml.Unmarshal([]byte(str), &mapData); err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
return mapData
|
||||
}
|
Reference in New Issue
Block a user