Merge branch 'main' into feat/vercelSecretSyncIntegration

This commit is contained in:
carlosmonastyrski
2025-04-03 08:30:24 -03:00
90 changed files with 3977 additions and 916 deletions

View File

@ -4,6 +4,10 @@ on:
tags:
- "infisical-k8-operator/v*.*.*"
permissions:
contents: write
pull-requests: write
jobs:
release-image:
name: Generate Helm Chart PR

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.",

View File

@ -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,

View File

@ -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,

View File

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

View File

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

View File

@ -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()

View File

@ -1818,7 +1818,8 @@ export const certificateAuthorityServiceFactory = ({
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
issuingCaCertificate,
serialNumber,
ca
ca,
commonName: cn
};
};

View File

@ -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: {

View File

@ -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) {

View File

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

View File

@ -46,6 +46,7 @@ export type TGetFolderDTO = {
limit?: number;
offset?: number;
recursive?: boolean;
lastSecretModified?: string;
} & TProjectPermission;
export type TGetFolderByIdDTO = {

View File

@ -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 = "",

View File

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

View File

@ -356,5 +356,6 @@ export type TGetAccessibleSecretsDTO = {
environment: string;
projectId: string;
secretPath: string;
recursive?: boolean;
filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
} & TProjectPermission;

View File

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

View File

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

View File

@ -184,6 +184,7 @@ export enum SecretsOrderBy {
export type TGetAccessibleSecretsDTO = {
secretPath: string;
environment: string;
recursive?: boolean;
filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
} & TProjectPermission;

View File

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

View File

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

View File

@ -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:

View File

@ -11,10 +11,11 @@ This means that updating the value of a base secret propagates directly to other
![secret referencing](../../images/platform/secret-references-imports/secret-reference.png)
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
![reorder secret import](../../images/platform/secret-references-imports/secret-import-reorder.png)
<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>

View File

@ -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.

View File

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

View File

@ -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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,8 @@ export enum PermissionAccess {
export enum PermissionNode {
Role = "role",
Folder = "folder",
Environment = "environment"
Environment = "environment",
ShowMoreButton = "showMoreButton"
}
export enum PermissionEdge {

View File

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

View File

@ -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,

View File

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

View File

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

View File

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

View File

@ -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",

View File

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

View File

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

View File

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

View File

@ -111,6 +111,7 @@ export type TGetAccessibleSecretsDTO = {
projectId: string;
secretPath: string;
environment: string;
recursive?: boolean;
filterByAction:
| ProjectPermissionSecretActions.DescribeSecret
| ProjectPermissionSecretActions.ReadValue;

View File

@ -121,6 +121,7 @@ export type TGetProjectSecretsKey = {
includeImports?: boolean;
viewSecretValue?: boolean;
expandSecretReferences?: boolean;
recursive?: boolean;
};
export type TGetProjectSecretsDTO = TGetProjectSecretsKey;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
) : (

View File

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

View File

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

View File

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

View File

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

View File

@ -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">

View File

@ -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"

View File

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

View File

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

View File

@ -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"`
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.

View File

@ -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"
}
]
}'

View File

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

View File

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

View File

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

View File

@ -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,

View File

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

View File

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

View File

@ -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=

View File

@ -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(),

View 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))
}

View 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")
}

View 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()
}

View 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
}

View 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)
}

View 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
}

View 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
}