1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-04-17 19:37:38 +00:00

Compare commits

...

83 Commits

Author SHA1 Message Date
3c89a69410 Update infisical-dynamic-secret-crd.mdx 2025-01-10 01:39:12 -08:00
9cde1995c7 Merge pull request from Infisical/fix/address-indefinite-hang-for-run-watch-failure
fix: address infinite hang when infisical run watch fails
2025-01-10 11:54:51 +08:00
3ed3856c85 Merge pull request from akhilmhdh/fix/broken-secret-creation
feat: added validation for secret name to disallow spaces and overview page
2025-01-09 15:38:25 -05:00
=
8054d93851 feat: added validation for secret name to disallow spaces and overview page fix for folder creation 2025-01-10 02:00:39 +05:30
02dc23425c fix: address infinite hang when infisical run watch fails 2025-01-10 02:29:39 +08:00
01534c3525 Merge pull request from Infisical/daniel/k8s-dynamic-secrets
feat(k8-operator): dynamic secrets
2025-01-09 12:45:30 -05:00
325ce73b9f fix typo 2025-01-09 12:36:52 -05:00
1639bda3f6 remove copy-paste and make redeploy docs specific 2025-01-09 12:23:46 -05:00
b5b91c929f fix kubernetes docs 2025-01-09 12:11:48 -05:00
9bc549ca8c Merge pull request from Infisical/feat/add-initial-sync-behavior-for-gitlab
feat: add initial sync behavior for gitlab
2025-01-10 01:01:17 +08:00
cedc88d83a misc: removed comment 2025-01-10 00:42:16 +08:00
f866a810c1 Merge pull request from Infisical/feat/secret-access-list
feat: secret access list
2025-01-10 00:40:56 +08:00
0c034d69ac misc: removed raw from api 2025-01-10 00:32:31 +08:00
e29f7f656c docs(k8s): better documentation layout 2025-01-09 16:07:07 +01:00
858e569d4d feat: add initial sync behavior for gitlab 2025-01-09 20:43:14 +08:00
5d8f32b774 Merge pull request from thomas-infisical/patch-1
Doc: Update Nov & Dec Changelogs
2025-01-08 21:42:49 -08:00
bb71f5eb7e Merge pull request from Infisical/daniel/update-k8s-manifest
fix: k8's manifest out of sync
2025-01-08 17:25:21 -05:00
30b431a255 Merge pull request from Infisical/bump-ecs-deploy-task-definition
ci: Bump aws-actions/amazon-ecs-deploy-task-definition to v2
2025-01-08 16:22:35 -05:00
fd32118685 Bump aws-actions/amazon-ecs-deploy-task-definition to v2
Bump aws-actions/amazon-ecs-deploy-task-definition to resolve:

```
Error: Failed to register task definition in ECS: Unexpected key 'enableFaultInjection' found in params
Error: Unexpected key 'enableFaultInjection' found in params
```

errors.
2025-01-08 13:20:41 -08:00
8528f3fd2a Merge pull request from Infisical/feat/resource-metadata
feat: resource metadata
2025-01-08 14:47:01 -05:00
eb358bcafd Update install-secrets-operator.yaml 2025-01-08 16:26:50 +01:00
ffbd29c575 doc: update nov & dec changelog
First try at updating the Changelog from these past months.

Let me know if you think of anything I might have forgotten.
2025-01-08 15:50:53 +01:00
a74f0170da Merge pull request from Infisical/fix/addressed-reported-migration-related-ui-issues
fix: resolved conditions permission not getting added from new policy…
2025-01-08 22:26:45 +08:00
=
a0fad34a6d fix: resolved conditions permission not getting added from new policy button 2025-01-08 19:48:13 +05:30
f0dc5ec876 misc: add secret access insights to license fns 2025-01-08 20:56:33 +08:00
c2453f0c84 misc: finalized ui 2025-01-08 20:50:20 +08:00
2819c8519e misc: added license checks 2025-01-08 20:31:04 +08:00
616b013b12 Merge remote-tracking branch 'origin/main' into feat/secret-access-list 2025-01-08 20:08:16 +08:00
0b9d890a51 Merge pull request from Infisical/fix/addressed-reported-migration-related-ui-issues
fix: address sso redirect and project role creation
2025-01-08 19:20:19 +08:00
5ba507bc1c misc: made installation ID optional for github 2025-01-08 19:17:38 +08:00
=
0ecc196e5d feat: added missing coerce in azure key vault 2025-01-08 16:30:46 +05:30
=
ddac9f7cc4 feat: made all redirect route to coerce it 2025-01-08 16:27:56 +05:30
f5adc4d9f3 misc: removed unnecessary type assertion 2025-01-08 18:44:58 +08:00
34354994d8 fix: address sso redirect and project role creation 2025-01-08 18:29:25 +08:00
d7c3192099 feat: frontend integration 2025-01-08 18:24:40 +08:00
1576358805 Merge pull request from Infisical/auth0-saml
Add Support for Auth0 SAML
2025-01-07 15:04:44 -05:00
8bf8bc77c9 Merge pull request from akhilmhdh/fix/broken-image
feat: resolved app not loading on org no access
2025-01-07 14:29:47 -05:00
=
3219723149 feat: resolved app not loading on org no access 2025-01-08 00:55:22 +05:30
74b95d92ab feat: backend setup 2025-01-08 02:48:47 +08:00
6d3793beff Merge pull request from akhilmhdh/fix/broken-image
Fixed broke image
2025-01-08 00:18:38 +05:30
0df41f3391 Merge pull request from Infisical/fix-user-groups-plan
Improvement: Clarify Enterprise Plan for User Group Feature Upgrade Modal
2025-01-07 10:44:32 -08:00
=
1acac9d479 feat: resolved broken gitlab image and hidden the standalone endpoints from api documentation 2025-01-08 00:14:31 +05:30
5e9dc0b98d clarify enterprise plan for user group feature upgrade modal 2025-01-07 10:38:09 -08:00
291d29ec41 Merge remote-tracking branch 'origin/main' into feat/resource-metadata 2025-01-07 19:15:33 +08:00
aa39451bc2 fix: generated files 2025-01-06 22:03:56 +01:00
f5548b3e8c Merge branch 'heads/main' into daniel/k8s-dynamic-secrets 2025-01-06 22:00:21 +01:00
e763a6f683 misc: added default for metadataSyncMode 2024-12-23 21:29:51 +08:00
cb1b006118 misc: add secret metadata to batch update 2024-12-23 14:46:23 +08:00
356e7c5958 feat: added approval handling for metadata in replicate 2024-12-23 14:36:41 +08:00
1a68765f15 feat: secret approval and replication 2024-12-21 02:51:47 +08:00
ae07d38c19 Merge remote-tracking branch 'origin/main' into feat/resource-metadata 2024-12-20 14:23:14 +08:00
025b4b8761 feat: aws secret manager metadata sync 2024-12-19 23:54:47 +08:00
ef688efc8d feat: integrated secret metadata to ui 2024-12-19 22:02:20 +08:00
8c98565715 feat: completed basic CRUD 2024-12-19 17:47:39 +08:00
e9358cd1d8 misc: backend setup + create secret metadata 2024-12-19 15:05:16 +08:00
4daaf80caa fix: better naming 2024-12-18 02:56:13 +01:00
cf7768d8e5 Merge branch 'daniel/k8-push-secret' into daniel/k8s-dynamic-secrets 2024-12-18 01:41:17 +01:00
e76d2f58ea fix: move fixes from different branch 2024-12-18 01:39:32 +01:00
36a13d182f requested changes 2024-12-12 06:13:55 +04:00
8b26670d73 fix(k8s): dynamic secret structual change 2024-12-11 22:25:02 +04:00
35d3581e23 fix(k8s): fixed dynamic secret bugs 2024-12-11 22:22:52 +04:00
0edf0dac98 fix(k8-operator): PushSecret CRd causing endless snapshot updates 2024-12-10 04:31:55 +04:00
a757ea22a1 fix(k8-operator): improvements for dynamic secrets 2024-12-09 00:22:22 +04:00
74df374998 Update infisicaldynamicsecret-crd.yaml 2024-12-08 23:24:38 +04:00
925a594a1b feat(k8-operator): dynamic secrets status conditions logging 2024-12-08 23:24:30 +04:00
36af975594 docs(k8-operator): k8's dynamic secret docs 2024-12-08 22:42:29 +04:00
ee54d460a0 fix(k8-operator): update charts 2024-12-08 21:30:28 +04:00
3c32d8dd90 fix(k8-operator): helm 2024-12-08 21:30:28 +04:00
9b50d451ec fix(k8-operator): common types support 2024-12-08 21:30:28 +04:00
7ede4e2cf5 fix(k8-operator): moved template 2024-12-08 21:30:28 +04:00
4552f0efa4 feat(k8-operator): dynamic secrets 2024-12-08 21:30:28 +04:00
0d35273857 feat(k8-operator): push secrets 2024-12-08 21:30:09 +04:00
5ad8dab250 fix(k8-operator): resource-based finalizer names 2024-12-08 21:29:45 +04:00
92a80b3314 fix(k8-operator): helm and cleanup 2024-12-08 21:21:14 +04:00
01dcbb0122 updated env slugs 2024-12-07 05:22:21 +04:00
adb0819102 Added RBAC 2024-12-07 05:22:21 +04:00
41ba111a69 Update PROJECT 2024-12-07 05:22:21 +04:00
1b48ce21be Update conditions.go 2024-12-07 05:22:21 +04:00
2f922d6343 fix: requested changes 2024-12-07 05:22:21 +04:00
e67b0540dd cleanup and resource seperation 2024-12-07 05:22:21 +04:00
a78455fde6 remove print 2024-12-07 05:22:21 +04:00
967dac9be6 docs(k8-operator): push secrets 2024-12-07 05:22:21 +04:00
922b245780 feat(k8-operator): push secrets 2024-12-07 05:22:21 +04:00
121 changed files with 5778 additions and 1144 deletions
.github/workflows
backend/src
cli/packages/cmd
docs
frontend/src
hooks/api
integrations
secretApprovalRequest
secrets
subscriptions
pages
auth/LoginPage/components/InitialStep
organization
AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection
AppConnections/GithubOauthCallbackPage
project
AccessControlPage/components
GroupsTab/components/GroupsSection
ProjectRoleListTab/components/ProjectRoleList
IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection
MemberDetailsByIDPage/components/MemberProjectAdditionalPrivilegeSection
RoleDetailsBySlugPage/components
secret-manager
IntegrationsDetailsByIDPage/components
OverviewPage
SecretApprovalsPage/components/SecretApprovalRequest/components
SecretDashboardPage/components/SecretListView
integrations
AwsSecretManagerConfigurePage
AzureAppConfigurationOauthCallbackPage
AzureKeyVaultOauthCallbackPage
BitbucketOauthCallbackPage
GcpSecretManagerOauthCallbackPage
GithubOauthCallbackPage
GitlabAuthorizePage
GitlabConfigurePage
GitlabOauthCallbackPage
HerokuOauthCallbackPage
NetlifyOauthCallbackPage
VercelOauthCallbackPage
helm-charts/secrets-operator
k8-operator

@ -97,7 +97,7 @@ jobs:
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-core-gamma-stage
@ -153,7 +153,7 @@ jobs:
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-core-platform
@ -204,7 +204,7 @@ jobs:
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-core-platform

@ -218,6 +218,9 @@ import {
TRateLimit,
TRateLimitInsert,
TRateLimitUpdate,
TResourceMetadata,
TResourceMetadataInsert,
TResourceMetadataUpdate,
TSamlConfigs,
TSamlConfigsInsert,
TSamlConfigsUpdate,
@ -887,6 +890,11 @@ declare module "knex/types/tables" {
TProjectSplitBackfillIdsInsert,
TProjectSplitBackfillIdsUpdate
>;
[TableName.ResourceMetadata]: KnexOriginal.CompositeTableType<
TResourceMetadata,
TResourceMetadataInsert,
TResourceMetadataUpdate
>;
[TableName.AppConnection]: KnexOriginal.CompositeTableType<
TAppConnections,
TAppConnectionsInsert,

@ -0,0 +1,40 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.ResourceMetadata))) {
await knex.schema.createTable(TableName.ResourceMetadata, (tb) => {
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
tb.string("key").notNullable();
tb.string("value", 1020).notNullable();
tb.uuid("orgId").notNullable();
tb.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
tb.uuid("userId");
tb.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
tb.uuid("identityId");
tb.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
tb.uuid("secretId");
tb.foreign("secretId").references("id").inTable(TableName.SecretV2).onDelete("CASCADE");
tb.timestamps(true, true, true);
});
}
const hasSecretMetadataField = await knex.schema.hasColumn(TableName.SecretApprovalRequestSecretV2, "secretMetadata");
if (!hasSecretMetadataField) {
await knex.schema.alterTable(TableName.SecretApprovalRequestSecretV2, (t) => {
t.jsonb("secretMetadata");
});
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.ResourceMetadata);
const hasSecretMetadataField = await knex.schema.hasColumn(TableName.SecretApprovalRequestSecretV2, "secretMetadata");
if (hasSecretMetadataField) {
await knex.schema.alterTable(TableName.SecretApprovalRequestSecretV2, (t) => {
t.dropColumn("secretMetadata");
});
}
}

@ -71,6 +71,7 @@ export * from "./project-user-additional-privilege";
export * from "./project-user-membership-roles";
export * from "./projects";
export * from "./rate-limit";
export * from "./resource-metadata";
export * from "./saml-configs";
export * from "./scim-tokens";
export * from "./secret-approval-policies";

@ -80,6 +80,7 @@ export enum TableName {
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
// used by both identity and users
IdentityMetadata = "identity_metadata",
ResourceMetadata = "resource_metadata",
ScimToken = "scim_tokens",
AccessApprovalPolicy = "access_approval_policies",
AccessApprovalPolicyApprover = "access_approval_policies_approvers",

@ -0,0 +1,24 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const ResourceMetadataSchema = z.object({
id: z.string().uuid(),
key: z.string(),
value: z.string(),
orgId: z.string().uuid(),
userId: z.string().uuid().nullable().optional(),
identityId: z.string().uuid().nullable().optional(),
secretId: z.string().uuid().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TResourceMetadata = z.infer<typeof ResourceMetadataSchema>;
export type TResourceMetadataInsert = Omit<z.input<typeof ResourceMetadataSchema>, TImmutableDBKeys>;
export type TResourceMetadataUpdate = Partial<Omit<z.input<typeof ResourceMetadataSchema>, TImmutableDBKeys>>;

@ -24,7 +24,8 @@ export const SecretApprovalRequestsSecretsV2Schema = z.object({
requestId: z.string().uuid(),
op: z.string(),
secretId: z.string().uuid().nullable().optional(),
secretVersion: z.string().uuid().nullable().optional()
secretVersion: z.string().uuid().nullable().optional(),
secretMetadata: z.unknown().nullable().optional()
});
export type TSecretApprovalRequestsSecretsV2 = z.infer<typeof SecretApprovalRequestsSecretsV2Schema>;

@ -22,6 +22,7 @@ import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-rou
import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router";
import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router";
import { registerSecretRotationRouter } from "./secret-rotation-router";
import { registerSecretRouter } from "./secret-router";
import { registerSecretScanningRouter } from "./secret-scanning-router";
import { registerSecretVersionRouter } from "./secret-version-router";
import { registerSnapshotRouter } from "./snapshot-router";
@ -92,6 +93,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register(registerLdapRouter, { prefix: "/ldap" });
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
await server.register(registerSecretRouter, { prefix: "/secrets" });
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
await server.register(registerGroupRouter, { prefix: "/groups" });
await server.register(registerAuditLogStreamRouter, { prefix: "/audit-log-streams" });

@ -12,6 +12,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
const approvalRequestUser = z.object({ userId: z.string().nullable().optional() }).merge(
UsersSchema.pick({
@ -274,6 +275,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
.extend({
op: z.string(),
tags: tagSchema,
secretMetadata: ResourceMetadataSchema.nullish(),
secret: z
.object({
id: z.string(),
@ -291,7 +293,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
secretKey: z.string(),
secretValue: z.string().optional(),
secretComment: z.string().optional(),
tags: tagSchema
tags: tagSchema,
secretMetadata: ResourceMetadataSchema.nullish()
})
.optional()
})

@ -0,0 +1,71 @@
import z from "zod";
import { ProjectPermissionActions } from "@app/ee/services/permission/project-permission";
import { RAW_SECRETS } from "@app/lib/api-docs";
import { removeTrailingSlash } from "@app/lib/fn";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const AccessListEntrySchema = z
.object({
allowedActions: z.nativeEnum(ProjectPermissionActions).array(),
id: z.string(),
membershipId: z.string(),
name: z.string()
})
.array();
export const registerSecretRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:secretName/access-list",
config: {
rateLimit: readLimit
},
schema: {
description: "Get list of users, machine identities, and groups with access to a secret",
security: [
{
bearerAuth: []
}
],
params: z.object({
secretName: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.secretName)
}),
querystring: z.object({
workspaceId: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.workspaceId),
environment: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.environment),
secretPath: z
.string()
.trim()
.default("/")
.transform(removeTrailingSlash)
.describe(RAW_SECRETS.GET_ACCESS_LIST.secretPath)
}),
response: {
200: z.object({
groups: AccessListEntrySchema,
identities: AccessListEntrySchema,
users: AccessListEntrySchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { secretName } = req.params;
const { secretPath, environment, workspaceId: projectId } = req.query;
return server.services.secret.getSecretAccessList({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretPath,
environment,
projectId,
secretName
});
}
});
};

@ -24,6 +24,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
rbac: false,
customRateLimits: false,
customAlerts: false,
secretAccessInsights: false,
auditLogs: false,
auditLogsRetentionDays: 0,
auditLogStreams: false,

@ -246,8 +246,7 @@ export const licenseServiceFactory = ({
};
const getOrgPlan = async ({ orgId, actor, actorId, actorOrgId, actorAuthMethod, projectId }: TOrgPlanDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
const plan = await getPlan(orgId, projectId);
return plan;
};

@ -48,6 +48,7 @@ export type TFeatureSet = {
samlSSO: false;
hsm: false;
oidcSSO: false;
secretAccessInsights: false;
scim: false;
ldap: false;
groups: false;

@ -125,6 +125,404 @@ export const permissionDALFactory = (db: TDbClient) => {
}
};
const getProjectGroupPermissions = async (projectId: string) => {
try {
const docs = await db
.replicaNode()(TableName.GroupProjectMembership)
.join(TableName.Groups, `${TableName.Groups}.id`, `${TableName.GroupProjectMembership}.groupId`)
.join(
TableName.GroupProjectMembershipRole,
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
`${TableName.GroupProjectMembership}.id`
)
.leftJoin<TProjectRoles>(
{ groupCustomRoles: TableName.ProjectRoles },
`${TableName.GroupProjectMembershipRole}.customRoleId`,
`groupCustomRoles.id`
)
.where(`${TableName.GroupProjectMembership}.projectId`, "=", projectId)
.select(
db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"),
db.ref("id").withSchema(TableName.Groups).as("groupId"),
db.ref("name").withSchema(TableName.Groups).as("groupName"),
db.ref("slug").withSchema("groupCustomRoles").as("groupProjectMembershipRoleCustomRoleSlug"),
db.ref("permissions").withSchema("groupCustomRoles").as("groupProjectMembershipRolePermission"),
db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("groupProjectMembershipRoleId"),
db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("groupProjectMembershipRole"),
db
.ref("customRoleId")
.withSchema(TableName.GroupProjectMembershipRole)
.as("groupProjectMembershipRoleCustomRoleId"),
db
.ref("isTemporary")
.withSchema(TableName.GroupProjectMembershipRole)
.as("groupProjectMembershipRoleIsTemporary"),
db
.ref("temporaryMode")
.withSchema(TableName.GroupProjectMembershipRole)
.as("groupProjectMembershipRoleTemporaryMode"),
db
.ref("temporaryRange")
.withSchema(TableName.GroupProjectMembershipRole)
.as("groupProjectMembershipRoleTemporaryRange"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.GroupProjectMembershipRole)
.as("groupProjectMembershipRoleTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.GroupProjectMembershipRole)
.as("groupProjectMembershipRoleTemporaryAccessEndTime")
);
const groupPermissions = sqlNestRelationships({
data: docs,
key: "groupId",
parentMapper: ({ groupId, groupName, membershipId }) => ({
groupId,
username: groupName,
id: membershipId
}),
childrenMapper: [
{
key: "groupProjectMembershipRoleId",
label: "groupRoles" as const,
mapper: ({
groupProjectMembershipRoleId,
groupProjectMembershipRole,
groupProjectMembershipRolePermission,
groupProjectMembershipRoleCustomRoleSlug,
groupProjectMembershipRoleIsTemporary,
groupProjectMembershipRoleTemporaryMode,
groupProjectMembershipRoleTemporaryAccessEndTime,
groupProjectMembershipRoleTemporaryAccessStartTime,
groupProjectMembershipRoleTemporaryRange
}) => ({
id: groupProjectMembershipRoleId,
role: groupProjectMembershipRole,
customRoleSlug: groupProjectMembershipRoleCustomRoleSlug,
permissions: groupProjectMembershipRolePermission,
temporaryRange: groupProjectMembershipRoleTemporaryRange,
temporaryMode: groupProjectMembershipRoleTemporaryMode,
temporaryAccessStartTime: groupProjectMembershipRoleTemporaryAccessStartTime,
temporaryAccessEndTime: groupProjectMembershipRoleTemporaryAccessEndTime,
isTemporary: groupProjectMembershipRoleIsTemporary
})
}
]
});
return groupPermissions
.map((groupPermission) => {
if (!groupPermission) return undefined;
const activeGroupRoles =
groupPermission?.groupRoles?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
) ?? [];
return {
...groupPermission,
roles: activeGroupRoles
};
})
.filter((item): item is NonNullable<typeof item> => Boolean(item));
} catch (error) {
throw new DatabaseError({ error, name: "GetProjectGroupPermissions" });
}
};
const getProjectUserPermissions = async (projectId: string) => {
try {
const docs = await db
.replicaNode()(TableName.Users)
.where("isGhost", "=", false)
.leftJoin(TableName.GroupProjectMembership, (queryBuilder) => {
void queryBuilder.on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId]));
})
.leftJoin(
TableName.GroupProjectMembershipRole,
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
`${TableName.GroupProjectMembership}.id`
)
.leftJoin<TProjectRoles>(
{ groupCustomRoles: TableName.ProjectRoles },
`${TableName.GroupProjectMembershipRole}.customRoleId`,
`groupCustomRoles.id`
)
.join(TableName.ProjectMembership, (queryBuilder) => {
void queryBuilder
.on(`${TableName.ProjectMembership}.projectId`, db.raw("?", [projectId]))
.andOn(`${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`);
})
.leftJoin(
TableName.ProjectUserMembershipRole,
`${TableName.ProjectUserMembershipRole}.projectMembershipId`,
`${TableName.ProjectMembership}.id`
)
.leftJoin(
TableName.ProjectRoles,
`${TableName.ProjectUserMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id`
)
.leftJoin(TableName.ProjectUserAdditionalPrivilege, (queryBuilder) => {
void queryBuilder
.on(`${TableName.ProjectUserAdditionalPrivilege}.projectId`, db.raw("?", [projectId]))
.andOn(`${TableName.ProjectUserAdditionalPrivilege}.userId`, `${TableName.Users}.id`);
})
.join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId]))
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder
.on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`)
.andOn(`${TableName.Organization}.id`, `${TableName.IdentityMetadata}.orgId`);
})
.select(
db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("username").withSchema(TableName.Users).as("username"),
// groups specific
db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"),
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipUpdatedAt"),
db.ref("slug").withSchema("groupCustomRoles").as("userGroupProjectMembershipRoleCustomRoleSlug"),
db.ref("permissions").withSchema("groupCustomRoles").as("userGroupProjectMembershipRolePermission"),
db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRoleId"),
db.ref("role").withSchema(TableName.GroupProjectMembershipRole).as("userGroupProjectMembershipRole"),
db
.ref("customRoleId")
.withSchema(TableName.GroupProjectMembershipRole)
.as("userGroupProjectMembershipRoleCustomRoleId"),
db
.ref("isTemporary")
.withSchema(TableName.GroupProjectMembershipRole)
.as("userGroupProjectMembershipRoleIsTemporary"),
db
.ref("temporaryMode")
.withSchema(TableName.GroupProjectMembershipRole)
.as("userGroupProjectMembershipRoleTemporaryMode"),
db
.ref("temporaryRange")
.withSchema(TableName.GroupProjectMembershipRole)
.as("userGroupProjectMembershipRoleTemporaryRange"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.GroupProjectMembershipRole)
.as("userGroupProjectMembershipRoleTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.GroupProjectMembershipRole)
.as("userGroupProjectMembershipRoleTemporaryAccessEndTime"),
// user specific
db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"),
db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
db.ref("slug").withSchema(TableName.ProjectRoles).as("userProjectMembershipRoleCustomRoleSlug"),
db.ref("permissions").withSchema(TableName.ProjectRoles).as("userProjectCustomRolePermission"),
db.ref("id").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRoleId"),
db.ref("role").withSchema(TableName.ProjectUserMembershipRole).as("userProjectMembershipRole"),
db
.ref("temporaryMode")
.withSchema(TableName.ProjectUserMembershipRole)
.as("userProjectMembershipRoleTemporaryMode"),
db
.ref("isTemporary")
.withSchema(TableName.ProjectUserMembershipRole)
.as("userProjectMembershipRoleIsTemporary"),
db
.ref("temporaryRange")
.withSchema(TableName.ProjectUserMembershipRole)
.as("userProjectMembershipRoleTemporaryRange"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.ProjectUserMembershipRole)
.as("userProjectMembershipRoleTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.ProjectUserMembershipRole)
.as("userProjectMembershipRoleTemporaryAccessEndTime"),
db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesId"),
db
.ref("permissions")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userAdditionalPrivilegesPermissions"),
db
.ref("temporaryMode")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userAdditionalPrivilegesTemporaryMode"),
db
.ref("isTemporary")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userAdditionalPrivilegesIsTemporary"),
db
.ref("temporaryRange")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userAdditionalPrivilegesTemporaryRange"),
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userAdditionalPrivilegesUserId"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userAdditionalPrivilegesTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userAdditionalPrivilegesTemporaryAccessEndTime"),
// general
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("orgId").withSchema(TableName.Project),
db.ref("type").withSchema(TableName.Project).as("projectType"),
db.ref("id").withSchema(TableName.Project).as("projectId")
);
const userPermissions = sqlNestRelationships({
data: docs,
key: "userId",
parentMapper: ({
orgId,
username,
orgAuthEnforced,
membershipId,
groupMembershipId,
membershipCreatedAt,
groupMembershipCreatedAt,
groupMembershipUpdatedAt,
membershipUpdatedAt,
projectType,
userId
}) => ({
orgId,
orgAuthEnforced,
userId,
projectId,
username,
projectType,
id: membershipId || groupMembershipId,
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
}),
childrenMapper: [
{
key: "userGroupProjectMembershipRoleId",
label: "userGroupRoles" as const,
mapper: ({
userGroupProjectMembershipRoleId,
userGroupProjectMembershipRole,
userGroupProjectMembershipRolePermission,
userGroupProjectMembershipRoleCustomRoleSlug,
userGroupProjectMembershipRoleIsTemporary,
userGroupProjectMembershipRoleTemporaryMode,
userGroupProjectMembershipRoleTemporaryAccessEndTime,
userGroupProjectMembershipRoleTemporaryAccessStartTime,
userGroupProjectMembershipRoleTemporaryRange
}) => ({
id: userGroupProjectMembershipRoleId,
role: userGroupProjectMembershipRole,
customRoleSlug: userGroupProjectMembershipRoleCustomRoleSlug,
permissions: userGroupProjectMembershipRolePermission,
temporaryRange: userGroupProjectMembershipRoleTemporaryRange,
temporaryMode: userGroupProjectMembershipRoleTemporaryMode,
temporaryAccessStartTime: userGroupProjectMembershipRoleTemporaryAccessStartTime,
temporaryAccessEndTime: userGroupProjectMembershipRoleTemporaryAccessEndTime,
isTemporary: userGroupProjectMembershipRoleIsTemporary
})
},
{
key: "userProjectMembershipRoleId",
label: "projectMembershipRoles" as const,
mapper: ({
userProjectMembershipRoleId,
userProjectMembershipRole,
userProjectCustomRolePermission,
userProjectMembershipRoleIsTemporary,
userProjectMembershipRoleTemporaryMode,
userProjectMembershipRoleTemporaryRange,
userProjectMembershipRoleTemporaryAccessEndTime,
userProjectMembershipRoleTemporaryAccessStartTime,
userProjectMembershipRoleCustomRoleSlug
}) => ({
id: userProjectMembershipRoleId,
role: userProjectMembershipRole,
customRoleSlug: userProjectMembershipRoleCustomRoleSlug,
permissions: userProjectCustomRolePermission,
temporaryRange: userProjectMembershipRoleTemporaryRange,
temporaryMode: userProjectMembershipRoleTemporaryMode,
temporaryAccessStartTime: userProjectMembershipRoleTemporaryAccessStartTime,
temporaryAccessEndTime: userProjectMembershipRoleTemporaryAccessEndTime,
isTemporary: userProjectMembershipRoleIsTemporary
})
},
{
key: "userAdditionalPrivilegesId",
label: "additionalPrivileges" as const,
mapper: ({
userAdditionalPrivilegesId,
userAdditionalPrivilegesPermissions,
userAdditionalPrivilegesIsTemporary,
userAdditionalPrivilegesTemporaryMode,
userAdditionalPrivilegesTemporaryRange,
userAdditionalPrivilegesTemporaryAccessEndTime,
userAdditionalPrivilegesTemporaryAccessStartTime
}) => ({
id: userAdditionalPrivilegesId,
permissions: userAdditionalPrivilegesPermissions,
temporaryRange: userAdditionalPrivilegesTemporaryRange,
temporaryMode: userAdditionalPrivilegesTemporaryMode,
temporaryAccessStartTime: userAdditionalPrivilegesTemporaryAccessStartTime,
temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime,
isTemporary: userAdditionalPrivilegesIsTemporary
})
},
{
key: "metadataId",
label: "metadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
}
]
});
return userPermissions
.map((userPermission) => {
if (!userPermission) return undefined;
if (!userPermission?.userGroupRoles?.[0] && !userPermission?.projectMembershipRoles?.[0]) return undefined;
// when introducting cron mode change it here
const activeRoles =
userPermission?.projectMembershipRoles?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
) ?? [];
const activeGroupRoles =
userPermission?.userGroupRoles?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
) ?? [];
const activeAdditionalPrivileges =
userPermission?.additionalPrivileges?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
) ?? [];
return {
...userPermission,
roles: [...activeRoles, ...activeGroupRoles],
additionalPrivileges: activeAdditionalPrivileges
};
})
.filter((item): item is NonNullable<typeof item> => Boolean(item));
} catch (error) {
throw new DatabaseError({ error, name: "GetProjectUserPermissions" });
}
};
const getProjectPermission = async (userId: string, projectId: string) => {
try {
const subQueryUserGroups = db(TableName.UserGroupMembership).where("userId", userId).select("groupId");
@ -414,6 +812,163 @@ export const permissionDALFactory = (db: TDbClient) => {
}
};
const getProjectIdentityPermissions = async (projectId: string) => {
try {
const docs = await db
.replicaNode()(TableName.IdentityProjectMembership)
.join(
TableName.IdentityProjectMembershipRole,
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
`${TableName.IdentityProjectMembership}.id`
)
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityProjectMembership}.identityId`)
.leftJoin(
TableName.ProjectRoles,
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
`${TableName.ProjectRoles}.id`
)
.leftJoin(
TableName.IdentityProjectAdditionalPrivilege,
`${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`,
`${TableName.IdentityProjectMembership}.id`
)
.join(
// Join the Project table to later select orgId
TableName.Project,
`${TableName.IdentityProjectMembership}.projectId`,
`${TableName.Project}.id`
)
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder
.on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`)
.andOn(`${TableName.Project}.orgId`, `${TableName.IdentityMetadata}.orgId`);
})
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
.select(selectAllTableCols(TableName.IdentityProjectMembershipRole))
.select(
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
db.ref("id").withSchema(TableName.Identity).as("identityId"),
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
db.ref("type").withSchema(TableName.Project).as("projectType"),
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
db.ref("permissions").withSchema(TableName.ProjectRoles),
db.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApId"),
db.ref("permissions").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApPermissions"),
db
.ref("temporaryMode")
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
.as("identityApTemporaryMode"),
db.ref("isTemporary").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApIsTemporary"),
db
.ref("temporaryRange")
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
.as("identityApTemporaryRange"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
.as("identityApTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
.as("identityApTemporaryAccessEndTime"),
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue")
);
const permissions = sqlNestRelationships({
data: docs,
key: "identityId",
parentMapper: ({
membershipId,
membershipCreatedAt,
membershipUpdatedAt,
orgId,
identityName,
projectType,
identityId
}) => ({
id: membershipId,
identityId,
username: identityName,
projectId,
createdAt: membershipCreatedAt,
updatedAt: membershipUpdatedAt,
orgId,
projectType,
// just a prefilled value
orgAuthEnforced: false
}),
childrenMapper: [
{
key: "id",
label: "roles" as const,
mapper: (data) =>
IdentityProjectMembershipRoleSchema.extend({
permissions: z.unknown(),
customRoleSlug: z.string().optional().nullable()
}).parse(data)
},
{
key: "identityApId",
label: "additionalPrivileges" as const,
mapper: ({
identityApId,
identityApPermissions,
identityApIsTemporary,
identityApTemporaryMode,
identityApTemporaryRange,
identityApTemporaryAccessEndTime,
identityApTemporaryAccessStartTime
}) => ({
id: identityApId,
permissions: identityApPermissions,
temporaryRange: identityApTemporaryRange,
temporaryMode: identityApTemporaryMode,
temporaryAccessEndTime: identityApTemporaryAccessEndTime,
temporaryAccessStartTime: identityApTemporaryAccessStartTime,
isTemporary: identityApIsTemporary
})
},
{
key: "metadataId",
label: "metadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
}
]
});
return permissions
.map((permission) => {
if (!permission) {
return undefined;
}
// when introducting cron mode change it here
const activeRoles = permission?.roles.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
);
const activeAdditionalPrivileges = permission?.additionalPrivileges?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
);
return { ...permission, roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges };
})
.filter((item): item is NonNullable<typeof item> => Boolean(item));
} catch (error) {
throw new DatabaseError({ error, name: "GetProjectIdentityPermissions" });
}
};
const getProjectIdentityPermission = async (identityId: string, projectId: string) => {
try {
const docs = await db
@ -568,6 +1123,9 @@ export const permissionDALFactory = (db: TDbClient) => {
getOrgPermission,
getOrgIdentityPermission,
getProjectPermission,
getProjectIdentityPermission
getProjectIdentityPermission,
getProjectUserPermissions,
getProjectIdentityPermissions,
getProjectGroupPermissions
};
};

@ -405,6 +405,123 @@ export const permissionServiceFactory = ({
ForbidOnInvalidProjectType: (type: ProjectType) => void;
};
const getProjectPermissions = async (projectId: string) => {
// fetch user permissions
const rawUserProjectPermissions = await permissionDAL.getProjectUserPermissions(projectId);
const userPermissions = rawUserProjectPermissions.map((userProjectPermission) => {
const rolePermissions =
userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
const additionalPrivileges =
userProjectPermission.additionalPrivileges?.map(({ permissions }) => ({
role: ProjectMembershipRole.Custom,
permissions
})) || [];
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
objectify(
userProjectPermission.metadata,
(i) => i.key,
(i) => i.value
)
);
const interpolateRules = templatedRules(
{
identity: {
id: userProjectPermission.userId,
username: userProjectPermission.username,
metadata: metadataKeyValuePair
}
},
{ data: false }
);
const permission = createMongoAbility<ProjectPermissionSet>(
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
{
conditionsMatcher
}
);
return {
permission,
id: userProjectPermission.userId,
name: userProjectPermission.username,
membershipId: userProjectPermission.id
};
});
// fetch identity permissions
const rawIdentityProjectPermissions = await permissionDAL.getProjectIdentityPermissions(projectId);
const identityPermissions = rawIdentityProjectPermissions.map((identityProjectPermission) => {
const rolePermissions =
identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
const additionalPrivileges =
identityProjectPermission.additionalPrivileges?.map(({ permissions }) => ({
role: ProjectMembershipRole.Custom,
permissions
})) || [];
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
objectify(
identityProjectPermission.metadata,
(i) => i.key,
(i) => i.value
)
);
const interpolateRules = templatedRules(
{
identity: {
id: identityProjectPermission.identityId,
username: identityProjectPermission.username,
metadata: metadataKeyValuePair
}
},
{ data: false }
);
const permission = createMongoAbility<ProjectPermissionSet>(
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
{
conditionsMatcher
}
);
return {
permission,
id: identityProjectPermission.identityId,
name: identityProjectPermission.username,
membershipId: identityProjectPermission.id
};
});
// fetch group permissions
const rawGroupProjectPermissions = await permissionDAL.getProjectGroupPermissions(projectId);
const groupPermissions = rawGroupProjectPermissions.map((groupProjectPermission) => {
const rolePermissions =
groupProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
const rules = buildProjectPermissionRules(rolePermissions);
const permission = createMongoAbility<ProjectPermissionSet>(rules, {
conditionsMatcher
});
return {
permission,
id: groupProjectPermission.groupId,
name: groupProjectPermission.username,
membershipId: groupProjectPermission.id
};
});
return {
userPermissions,
identityPermissions,
groupPermissions
};
};
const getProjectPermission = async <T extends ActorType>(
type: T,
id: string,
@ -455,6 +572,7 @@ export const permissionServiceFactory = ({
getOrgPermission,
getUserProjectPermission,
getProjectPermission,
getProjectPermissions,
getOrgPermissionByRole,
getProjectPermissionByRole,
buildOrgPermission,

@ -256,6 +256,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
db.ref("id").withSchema("secVerTag")
)
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2))
.select({
secVerTagId: "secVerTag.id",
@ -279,6 +280,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
db.ref("key").withSchema(TableName.SecretVersionV2).as("secVerKey"),
db.ref("encryptedValue").withSchema(TableName.SecretVersionV2).as("secVerValue"),
db.ref("encryptedComment").withSchema(TableName.SecretVersionV2).as("secVerComment")
)
.select(
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
);
const formatedDoc = sqlNestRelationships({
data: doc,
@ -338,9 +344,19 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
})
}
]
},
{
key: "metadataId",
label: "oldSecretMetadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
}
]
});
return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({
...el,
secret: secret?.[0],

@ -22,6 +22,8 @@ import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import {
decryptSecretWithBot,
@ -91,6 +93,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
smtpService: Pick<TSmtpService, "sendMail">;
userDAL: Pick<TUserDALFactory, "find" | "findOne" | "findById">;
@ -138,7 +141,8 @@ export const secretApprovalRequestServiceFactory = ({
secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL,
licenseService,
projectSlackConfigDAL
projectSlackConfigDAL,
resourceMetadataDAL
}: TSecretApprovalRequestServiceFactoryDep) => {
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
@ -241,6 +245,7 @@ export const secretApprovalRequestServiceFactory = ({
secretKey: el.key,
id: el.id,
version: el.version,
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
@ -269,7 +274,8 @@ export const secretApprovalRequestServiceFactory = ({
secretComment: el.secretVersion.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
: "",
tags: el.secretVersion.tags
tags: el.secretVersion.tags,
secretMetadata: el.oldSecretMetadata as ResourceMetadataDTO
}
: undefined
}));
@ -543,6 +549,7 @@ export const secretApprovalRequestServiceFactory = ({
? await fnSecretV2BridgeBulkInsert({
tx,
folderId,
orgId: actorOrgId,
inputSecrets: secretCreationCommits.map((el) => ({
tagIds: el?.tags.map(({ id }) => id),
version: 1,
@ -550,6 +557,7 @@ export const secretApprovalRequestServiceFactory = ({
encryptedValue: el.encryptedValue,
skipMultilineEncoding: el.skipMultilineEncoding,
key: el.key,
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
references: el.encryptedValue
? getAllSecretReferencesV2Bridge(
secretManagerDecryptor({
@ -559,6 +567,7 @@ export const secretApprovalRequestServiceFactory = ({
: [],
type: SecretType.Shared
})),
resourceMetadataDAL,
secretDAL: secretV2BridgeDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL,
@ -568,6 +577,7 @@ export const secretApprovalRequestServiceFactory = ({
const updatedSecrets = secretUpdationCommits.length
? await fnSecretV2BridgeBulkUpdate({
folderId,
orgId: actorOrgId,
tx,
inputSecrets: secretUpdationCommits.map((el) => {
const encryptedValue =
@ -592,6 +602,7 @@ export const secretApprovalRequestServiceFactory = ({
skipMultilineEncoding: el.skipMultilineEncoding,
key: el.key,
tags: el?.tags.map(({ id }) => id),
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
...encryptedValue
}
};
@ -599,7 +610,8 @@ export const secretApprovalRequestServiceFactory = ({
secretDAL: secretV2BridgeDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
resourceMetadataDAL
})
: [];
const deletedSecret = secretDeletionCommits.length
@ -824,6 +836,7 @@ export const secretApprovalRequestServiceFactory = ({
}
await secretQueueService.syncSecrets({
projectId,
orgId: actorOrgId,
secretPath: folder.path,
environmentSlug: folder.environmentSlug,
actorId,
@ -1208,6 +1221,7 @@ export const secretApprovalRequestServiceFactory = ({
),
skipMultilineEncoding: createdSecret.skipMultilineEncoding,
key: createdSecret.secretKey,
secretMetadata: createdSecret.secretMetadata,
type: SecretType.Shared
}))
);
@ -1263,12 +1277,14 @@ export const secretApprovalRequestServiceFactory = ({
reminderNote,
secretComment,
metadata,
skipMultilineEncoding
skipMultilineEncoding,
secretMetadata
}) => {
const secretId = updatingSecretsGroupByKey[secretKey][0].id;
if (tagIds?.length) commitTagIds[secretKey] = tagIds;
return {
...latestSecretVersions[secretId],
secretMetadata,
key: newSecretName || secretKey,
encryptedComment: setKnexStringValue(
secretComment,
@ -1370,7 +1386,8 @@ export const secretApprovalRequestServiceFactory = ({
reminderRepeatDays,
encryptedValue,
secretId,
secretVersion
secretVersion,
secretMetadata
}) => ({
version,
requestId: doc.id,
@ -1383,7 +1400,8 @@ export const secretApprovalRequestServiceFactory = ({
reminderRepeatDays,
reminderNote,
encryptedComment,
key
key,
secretMetadata: JSON.stringify(secretMetadata)
})
),
tx

@ -1,5 +1,6 @@
import { TImmutableDBKeys, TSecretApprovalPolicies, TSecretApprovalRequestsSecrets } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretOperations } from "@app/services/secret/secret-types";
export enum RequestState {
@ -34,6 +35,7 @@ export type TApprovalCreateSecretV2Bridge = {
reminderRepeatDays?: number | null;
skipMultilineEncoding?: boolean;
metadata?: Record<string, string>;
secretMetadata?: ResourceMetadataDTO;
tagIds?: string[];
};

@ -13,6 +13,8 @@ import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { fnSecretBulkInsert, fnSecretBulkUpdate } from "@app/services/secret/secret-fns";
import { TSecretQueueFactory, uniqueSecretQueueKey } from "@app/services/secret/secret-queue";
@ -56,6 +58,7 @@ type TSecretReplicationServiceFactoryDep = {
>;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "find" | "insertMany">;
secretVersionV2TagBridgeDAL: Pick<TSecretVersionV2TagDALFactory, "find" | "insertMany">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "replicateSecrets">;
queueService: Pick<TQueueServiceFactory, "start" | "listen" | "queue" | "stopJobById">;
secretApprovalPolicyService: Pick<TSecretApprovalPolicyServiceFactory, "getSecretApprovalPolicy">;
@ -121,7 +124,8 @@ export const secretReplicationServiceFactory = ({
secretVersionV2TagBridgeDAL,
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
kmsService
kmsService,
resourceMetadataDAL
}: TSecretReplicationServiceFactoryDep) => {
const $getReplicatedSecrets = (
botKey: string,
@ -151,8 +155,10 @@ export const secretReplicationServiceFactory = ({
};
const $getReplicatedSecretsV2 = (
localSecrets: (TSecretsV2 & { secretKey: string; secretValue?: string })[],
importedSecrets: { secrets: (TSecretsV2 & { secretKey: string; secretValue?: string })[] }[]
localSecrets: (TSecretsV2 & { secretKey: string; secretValue?: string; secretMetadata?: ResourceMetadataDTO })[],
importedSecrets: {
secrets: (TSecretsV2 & { secretKey: string; secretValue?: string; secretMetadata?: ResourceMetadataDTO })[];
}[]
) => {
const deDupe = new Set<string>();
const secrets = [...localSecrets];
@ -178,6 +184,7 @@ export const secretReplicationServiceFactory = ({
secretPath,
environmentSlug,
projectId,
orgId,
actorId,
actor,
pickOnlyImportIds,
@ -222,6 +229,7 @@ export const secretReplicationServiceFactory = ({
.map(({ folderId }) =>
secretQueueService.replicateSecrets({
projectId,
orgId,
secretPath: foldersGroupedById[folderId][0]?.path as string,
environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string,
actorId,
@ -267,6 +275,7 @@ export const secretReplicationServiceFactory = ({
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: undefined
}));
const sourceSecrets = $getReplicatedSecretsV2(sourceDecryptedLocalSecrets, sourceImportedSecrets);
const sourceSecretsGroupByKey = groupBy(sourceSecrets, (i) => i.key);
@ -333,13 +342,29 @@ export const secretReplicationServiceFactory = ({
.map((el) => ({ ...el, operation: SecretOperations.Create })); // rewrite update ops to create
const locallyUpdatedSecrets = sourceSecrets
.filter(
({ key, secretKey, secretValue }) =>
.filter(({ key, secretKey, secretValue, secretMetadata }) => {
const sourceSecretMetadataJson = JSON.stringify(
(secretMetadata ?? []).map((entry) => ({
key: entry.key,
value: entry.value
}))
);
const destinationSecretMetadataJson = JSON.stringify(
(destinationLocalSecretsGroupedByKey[key]?.[0]?.secretMetadata ?? []).map((entry) => ({
key: entry.key,
value: entry.value
}))
);
return (
destinationLocalSecretsGroupedByKey[key]?.[0] &&
// if key or value changed
(destinationLocalSecretsGroupedByKey[key]?.[0]?.secretKey !== secretKey ||
destinationLocalSecretsGroupedByKey[key]?.[0]?.secretValue !== secretValue)
)
destinationLocalSecretsGroupedByKey[key]?.[0]?.secretValue !== secretValue ||
sourceSecretMetadataJson !== destinationSecretMetadataJson)
);
})
.map((el) => ({ ...el, operation: SecretOperations.Update })); // rewrite update ops to create
const locallyDeletedSecrets = destinationLocalSecrets
@ -387,6 +412,7 @@ export const secretReplicationServiceFactory = ({
op: operation,
requestId: approvalRequestDoc.id,
metadata: doc.metadata,
secretMetadata: JSON.stringify(doc.secretMetadata),
key: doc.key,
encryptedValue: doc.encryptedValue,
encryptedComment: doc.encryptedComment,
@ -406,10 +432,12 @@ export const secretReplicationServiceFactory = ({
if (locallyCreatedSecrets.length) {
await fnSecretV2BridgeBulkInsert({
folderId: destinationReplicationFolderId,
orgId,
secretVersionDAL: secretVersionV2BridgeDAL,
secretDAL: secretV2BridgeDAL,
tx,
secretTagDAL,
resourceMetadataDAL,
secretVersionTagDAL: secretVersionV2TagBridgeDAL,
inputSecrets: locallyCreatedSecrets.map((doc) => {
return {
@ -419,6 +447,7 @@ export const secretReplicationServiceFactory = ({
encryptedValue: doc.encryptedValue,
encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding,
secretMetadata: doc.secretMetadata,
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
};
})
@ -426,10 +455,12 @@ export const secretReplicationServiceFactory = ({
}
if (locallyUpdatedSecrets.length) {
await fnSecretV2BridgeBulkUpdate({
orgId,
folderId: destinationReplicationFolderId,
secretVersionDAL: secretVersionV2BridgeDAL,
secretDAL: secretV2BridgeDAL,
tx,
resourceMetadataDAL,
secretTagDAL,
secretVersionTagDAL: secretVersionV2TagBridgeDAL,
inputSecrets: locallyUpdatedSecrets.map((doc) => {
@ -445,6 +476,7 @@ export const secretReplicationServiceFactory = ({
encryptedValue: doc.encryptedValue as Buffer,
encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding,
secretMetadata: doc.secretMetadata,
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
}
};
@ -466,6 +498,7 @@ export const secretReplicationServiceFactory = ({
await secretQueueService.syncSecrets({
projectId,
orgId,
secretPath: destinationFolder.path,
environmentSlug: destinationFolder.environmentSlug,
actorId,
@ -751,6 +784,7 @@ export const secretReplicationServiceFactory = ({
await secretQueueService.syncSecrets({
projectId,
orgId,
secretPath: destinationFolder.path,
environmentSlug: destinationFolder.environmentSlug,
actorId,

@ -741,6 +741,12 @@ export const RAW_SECRETS = {
workspaceId: "The ID of the project where the secret is located.",
environment: "The slug of the environment where the the secret is located.",
secretPath: "The folder path where the secret is located."
},
GET_ACCESS_LIST: {
secretName: "The name of the secret to get the access list for.",
workspaceId: "The ID of the project where the secret is located.",
environment: "The slug of the environment where the the secret is located.",
secretPath: "The folder path where the secret is located."
}
} as const;
@ -1150,7 +1156,8 @@ export const INTEGRATION = {
shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.",
shouldProtectSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Protected'.",
shouldEnableDelete: "The flag to enable deletion of secrets.",
octopusDeployScopeValues: "Specifies the scope values to set on synced secrets to Octopus Deploy."
octopusDeployScopeValues: "Specifies the scope values to set on synced secrets to Octopus Deploy.",
metadataSyncMode: "The mode for syncing metadata to external system"
}
},
UPDATE: {

@ -21,6 +21,9 @@ export const registerServeUI = async (
server.route({
method: "GET",
url: "/runtime-ui-env.js",
schema: {
hide: true
},
handler: (_req, res) => {
const appCfg = getConfig();
void res.type("application/javascript");
@ -43,12 +46,19 @@ export const registerServeUI = async (
wildcard: false
});
server.get("/*", (request, reply) => {
if (request.url.startsWith("/api")) {
reply.callNotFound();
return;
server.route({
method: "GET",
url: "/*",
schema: {
hide: true
},
handler: (request, reply) => {
if (request.url.startsWith("/api")) {
reply.callNotFound();
return;
}
void reply.sendFile("index.html");
}
void reply.sendFile("index.html");
});
}
};

@ -181,6 +181,7 @@ import { projectUserMembershipRoleDALFactory } from "@app/services/project-membe
import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal";
import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service";
import { dailyResourceCleanUpQueueServiceFactory } from "@app/services/resource-cleanup/resource-cleanup-queue";
import { resourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
import { secretDALFactory } from "@app/services/secret/secret-dal";
import { secretQueueFactory } from "@app/services/secret/secret-queue";
import { secretServiceFactory } from "@app/services/secret/secret-service";
@ -374,6 +375,7 @@ export const registerRoutes = async (
const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db);
const projectTemplateDAL = projectTemplateDALFactory(db);
const resourceMetadataDAL = resourceMetadataDALFactory(db);
const permissionService = permissionServiceFactory({
permissionDAL,
@ -854,7 +856,8 @@ export const registerRoutes = async (
secretApprovalRequestDAL,
projectKeyDAL,
projectUserMembershipRoleDAL,
orgService
orgService,
resourceMetadataDAL
});
const projectService = projectServiceFactory({
@ -980,7 +983,8 @@ export const registerRoutes = async (
secretApprovalPolicyService,
secretApprovalRequestSecretDAL,
kmsService,
snapshotService
snapshotService,
resourceMetadataDAL
});
const secretApprovalRequestService = secretApprovalRequestServiceFactory({
@ -1007,7 +1011,8 @@ export const registerRoutes = async (
projectEnvDAL,
userDAL,
licenseService,
projectSlackConfigDAL
projectSlackConfigDAL,
resourceMetadataDAL
});
const secretService = secretServiceFactory({
@ -1028,7 +1033,8 @@ export const registerRoutes = async (
secretApprovalRequestDAL,
secretApprovalRequestSecretDAL,
secretV2BridgeService,
secretApprovalRequestService
secretApprovalRequestService,
licenseService
});
const secretSharingService = secretSharingServiceFactory({
@ -1086,8 +1092,10 @@ export const registerRoutes = async (
kmsService,
secretV2BridgeDAL,
secretVersionV2TagBridgeDAL: secretVersionTagV2BridgeDAL,
secretVersionV2BridgeDAL
secretVersionV2BridgeDAL,
resourceMetadataDAL
});
const secretRotationQueue = secretRotationQueueFactory({
telemetryService,
secretRotationDAL,
@ -1339,7 +1347,8 @@ export const registerRoutes = async (
folderDAL,
secretDAL: secretV2BridgeDAL,
queueService,
secretV2BridgeService
secretV2BridgeService,
resourceMetadataDAL
});
const migrationService = externalMigrationServiceFactory({

@ -17,6 +17,7 @@ import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedDynamicSecretSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
@ -116,6 +117,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema
.extend({
secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
@ -408,6 +410,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema
.extend({
secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
@ -693,6 +696,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema
.extend({
secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
@ -864,6 +868,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema
.extend({
secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,

@ -18,6 +18,7 @@ import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { ProjectFilterType } from "@app/services/project/project-types";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretOperations, SecretProtectionType } from "@app/services/secret/secret-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
@ -35,6 +36,12 @@ const SecretReferenceNodeTree: z.ZodType<TSecretReferenceNode> = SecretReference
children: z.lazy(() => SecretReferenceNodeTree.array())
});
const SecretNameSchema = z
.string()
.trim()
.min(1)
.refine((el) => !el.includes(" "), "Secret name cannot contain spaces.");
export const registerSecretRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
@ -50,7 +57,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}
],
params: z.object({
secretName: z.string().trim().describe(SECRETS.ATTACH_TAGS.secretName)
secretName: SecretNameSchema.describe(SECRETS.ATTACH_TAGS.secretName)
}),
body: z.object({
projectSlug: z.string().trim().describe(SECRETS.ATTACH_TAGS.projectSlug),
@ -113,7 +120,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}
],
params: z.object({
secretName: z.string().trim().describe(SECRETS.DETACH_TAGS.secretName)
secretName: z.string().describe(SECRETS.DETACH_TAGS.secretName)
}),
body: z.object({
projectSlug: z.string().trim().describe(SECRETS.DETACH_TAGS.projectSlug),
@ -205,6 +212,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema
.extend({
secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
@ -220,7 +228,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretPath: z.string(),
environment: z.string(),
folderId: z.string().optional(),
secrets: secretRawSchema.omit({ createdAt: true, updatedAt: true }).array()
secrets: secretRawSchema
.omit({ createdAt: true, updatedAt: true })
.extend({
secretMetadata: ResourceMetadataSchema.optional()
})
.array()
})
.array()
.optional()
@ -348,7 +361,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
})
.extend({ name: z.string() })
.array()
.optional()
.optional(),
secretMetadata: ResourceMetadataSchema.optional()
})
})
}
@ -434,7 +448,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}
],
params: z.object({
secretName: z.string().trim().describe(RAW_SECRETS.CREATE.secretName)
secretName: SecretNameSchema.describe(RAW_SECRETS.CREATE.secretName)
}),
body: z.object({
workspaceId: z.string().trim().describe(RAW_SECRETS.CREATE.workspaceId),
@ -450,6 +464,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
.describe(RAW_SECRETS.CREATE.secretValue),
secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment),
secretMetadata: ResourceMetadataSchema.optional(),
tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds),
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding),
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.CREATE.type),
@ -484,6 +499,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretValue: req.body.secretValue,
skipMultilineEncoding: req.body.skipMultilineEncoding,
secretComment: req.body.secretComment,
secretMetadata: req.body.secretMetadata,
tagIds: req.body.tagIds,
secretReminderNote: req.body.secretReminderNote,
secretReminderRepeatDays: req.body.secretReminderRepeatDays
@ -539,7 +555,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}
],
params: z.object({
secretName: z.string().trim().describe(RAW_SECRETS.UPDATE.secretName)
secretName: SecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName)
}),
body: z.object({
workspaceId: z.string().trim().describe(RAW_SECRETS.UPDATE.workspaceId),
@ -558,13 +574,14 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.UPDATE.type),
tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds),
metadata: z.record(z.string()).optional(),
secretMetadata: ResourceMetadataSchema.optional(),
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote),
secretReminderRepeatDays: z
.number()
.optional()
.nullable()
.describe(RAW_SECRETS.UPDATE.secretReminderRepeatDays),
newSecretName: z.string().min(1).optional().describe(RAW_SECRETS.UPDATE.newSecretName),
newSecretName: SecretNameSchema.optional().describe(RAW_SECRETS.UPDATE.newSecretName),
secretComment: z.string().optional().describe(RAW_SECRETS.UPDATE.secretComment)
}),
response: {
@ -595,8 +612,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretReminderNote: req.body.secretReminderNote,
metadata: req.body.metadata,
newSecretName: req.body.newSecretName,
secretComment: req.body.secretComment
secretComment: req.body.secretComment,
secretMetadata: req.body.secretMetadata
});
if (secretOperation.type === SecretProtectionType.Approval) {
return { approval: secretOperation.approval };
}
@ -647,7 +666,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}
],
params: z.object({
secretName: z.string().trim().describe(RAW_SECRETS.DELETE.secretName)
secretName: z.string().min(1).describe(RAW_SECRETS.DELETE.secretName)
}),
body: z.object({
workspaceId: z.string().trim().describe(RAW_SECRETS.DELETE.workspaceId),
@ -1842,7 +1861,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
.describe(RAW_SECRETS.CREATE.secretPath),
secrets: z
.object({
secretKey: z.string().trim().describe(RAW_SECRETS.CREATE.secretName),
secretKey: SecretNameSchema.describe(RAW_SECRETS.CREATE.secretName),
secretValue: z
.string()
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
@ -1850,6 +1869,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment),
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding),
metadata: z.record(z.string()).optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds)
})
.array()
@ -1942,16 +1962,17 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
.describe(RAW_SECRETS.UPDATE.secretPath),
secrets: z
.object({
secretKey: z.string().trim().describe(RAW_SECRETS.UPDATE.secretName),
secretKey: SecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName),
secretValue: z
.string()
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
.describe(RAW_SECRETS.UPDATE.secretValue),
secretComment: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.secretComment),
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding),
newSecretName: z.string().min(1).optional().describe(RAW_SECRETS.UPDATE.newSecretName),
newSecretName: SecretNameSchema.optional().describe(RAW_SECRETS.UPDATE.newSecretName),
tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds),
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote),
secretMetadata: ResourceMetadataSchema.optional(),
secretReminderRepeatDays: z
.number()
.optional()
@ -2047,7 +2068,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
.describe(RAW_SECRETS.DELETE.secretPath),
secrets: z
.object({
secretKey: z.string().trim().describe(RAW_SECRETS.DELETE.secretName),
secretKey: z.string().describe(RAW_SECRETS.DELETE.secretName),
type: z.nativeEnum(SecretType).default(SecretType.Shared)
})
.array()

@ -16,6 +16,7 @@ import { TProjectDALFactory } from "../project/project-dal";
import { TProjectServiceFactory } from "../project/project-service";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
@ -35,6 +36,8 @@ export type TImportDataIntoInfisicalDTO = {
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
folderDAL: Pick<TSecretFolderDALFactory, "create" | "findBySecretPath" | "findById">;
projectService: Pick<TProjectServiceFactory, "createProject">;
projectEnvService: Pick<TProjectEnvServiceFactory, "createEnvironment">;
@ -503,6 +506,7 @@ export const importDataIntoInfisicalFn = async ({
secretTagDAL,
secretVersionTagDAL,
folderDAL,
resourceMetadataDAL,
input: { data, actor, actorId, actorOrgId, actorAuthMethod }
}: TImportDataIntoInfisicalDTO) => {
// Import data to infisical
@ -762,6 +766,8 @@ export const importDataIntoInfisicalFn = async ({
};
}),
folderId: selectedFolder.id,
orgId: actorOrgId,
resourceMetadataDAL,
secretDAL,
secretVersionDAL,
secretTagDAL,

@ -8,6 +8,7 @@ import { TProjectDALFactory } from "../project/project-dal";
import { TProjectServiceFactory } from "../project/project-service";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
@ -35,6 +36,8 @@ export type TExternalMigrationQueueFactoryDep = {
projectService: Pick<TProjectServiceFactory, "createProject">;
projectEnvService: Pick<TProjectEnvServiceFactory, "createEnvironment">;
secretV2BridgeService: Pick<TSecretV2BridgeServiceFactory, "createManySecret">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
};
export type TExternalMigrationQueueFactory = ReturnType<typeof externalMigrationQueueFactory>;
@ -52,7 +55,8 @@ export const externalMigrationQueueFactory = ({
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL
folderDAL,
resourceMetadataDAL
}: TExternalMigrationQueueFactoryDep) => {
const startImport = async (dto: {
actorEmail: string;
@ -109,7 +113,8 @@ export const externalMigrationQueueFactory = ({
kmsService,
projectService,
projectEnvService,
secretV2BridgeService
secretV2BridgeService,
resourceMetadataDAL
});
if (projectsNotImported.length) {

@ -427,3 +427,8 @@ export const getIntegrationOptions = async () => {
return INTEGRATION_OPTIONS;
};
export enum IntegrationMetadataSyncMode {
CUSTOM = "custom",
SECRET_METADATA = "secret-metadata"
}

@ -38,6 +38,7 @@ import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/
import { TIntegrationDALFactory } from "../integration/integration-dal";
import { IntegrationMetadataSchema } from "../integration/integration-schema";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { IntegrationAuthMetadataSchema } from "./integration-auth-schema";
import {
CircleCiScope,
@ -48,6 +49,7 @@ import {
import {
IntegrationInitialSyncBehavior,
IntegrationMappingBehavior,
IntegrationMetadataSyncMode,
Integrations,
IntegrationUrls
} from "./integration-list";
@ -1082,14 +1084,14 @@ const syncSecretsAWSSecretManager = async ({
projectId
}: {
integration: TIntegrations;
secrets: Record<string, { value: string; comment?: string }>;
secrets: Record<string, { value: string; comment?: string; secretMetadata?: ResourceMetadataDTO }>;
accessId: string | null;
accessToken: string;
awsAssumeRoleArn: string | null;
projectId?: string;
}) => {
const appCfg = getConfig();
const metadata = z.record(z.any()).parse(integration.metadata || {});
const metadata = IntegrationMetadataSchema.parse(integration.metadata || {});
if (!accessId && !awsAssumeRoleArn) {
throw new Error("AWS access ID/AWS Assume Role is required");
@ -1137,8 +1139,25 @@ const syncSecretsAWSSecretManager = async ({
const processAwsSecret = async (
secretId: string,
secretValue: Record<string, string | null | undefined> | string
secretValue: Record<string, string | null | undefined> | string,
secretMetadata?: ResourceMetadataDTO
) => {
const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined;
const shouldTag =
(secretAWSTag && secretAWSTag.length) ||
(metadata.metadataSyncMode === IntegrationMetadataSyncMode.SECRET_METADATA &&
metadata.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE);
const tagArray =
(metadata.metadataSyncMode === IntegrationMetadataSyncMode.SECRET_METADATA ? secretMetadata : secretAWSTag) ?? [];
const integrationTagObj = tagArray.reduce(
(acc, item) => {
acc[item.key] = item.value;
return acc;
},
{} as Record<string, string>
);
try {
const awsSecretManagerSecret = await secretsManager.send(
new GetSecretValueCommand({
@ -1174,9 +1193,7 @@ const syncSecretsAWSSecretManager = async ({
}
}
const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined;
if (secretAWSTag && secretAWSTag.length) {
if (shouldTag) {
const describedSecret = await secretsManager.send(
// requires secretsmanager:DescribeSecret policy
new DescribeSecretCommand({
@ -1186,14 +1203,6 @@ const syncSecretsAWSSecretManager = async ({
if (!describedSecret.Tags) return;
const integrationTagObj = secretAWSTag.reduce(
(acc, item) => {
acc[item.key] = item.value;
return acc;
},
{} as Record<string, string>
);
const awsTagObj = (describedSecret.Tags || []).reduce(
(acc, item) => {
if (item.Key && item.Value) {
@ -1225,7 +1234,7 @@ const syncSecretsAWSSecretManager = async ({
}
});
secretAWSTag?.forEach((tag) => {
tagArray.forEach((tag) => {
if (!(tag.key in awsTagObj)) {
// create tag in AWS secret manager
tagsToUpdate.push({
@ -1262,8 +1271,8 @@ const syncSecretsAWSSecretManager = async ({
Name: secretId,
SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue),
...(metadata.kmsKeyId && { KmsKeyId: metadata.kmsKeyId }),
Tags: metadata.secretAWSTag
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({
Tags: shouldTag
? tagArray.map((tag: { key: string; value: string }) => ({
Key: tag.key,
Value: tag.value
}))
@ -1280,7 +1289,7 @@ const syncSecretsAWSSecretManager = async ({
if (metadata.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE) {
for await (const [key, value] of Object.entries(secrets)) {
await processAwsSecret(key, value.value);
await processAwsSecret(key, value.value, value.secretMetadata);
}
} else {
await processAwsSecret(integration.app as string, getSecretKeyValuePair(secrets));
@ -2763,13 +2772,23 @@ const syncSecretsAzureDevops = async ({
* Sync/push [secrets] to GitLab repo with name [integration.app]
*/
const syncSecretsGitLab = async ({
createManySecretsRawFn,
integrationAuth,
integration,
secrets,
accessToken
}: {
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<{ id: string }>>;
integrationAuth: TIntegrationAuths;
integration: TIntegrations;
integration: TIntegrations & {
projectId: string;
environment: {
id: string;
name: string;
slug: string;
};
secretPath: string;
};
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
}) => {
@ -2826,6 +2845,81 @@ const syncSecretsGitLab = async ({
return isValid;
});
if (!integration.lastUsed) {
const secretsToAddToInfisical: { [key: string]: GitLabSecret } = {};
const secretsToRemoveInGitlab: GitLabSecret[] = [];
if (!metadata.initialSyncBehavior) {
metadata.initialSyncBehavior = IntegrationInitialSyncBehavior.OVERWRITE_TARGET;
}
getSecretsRes.forEach((gitlabSecret) => {
// first time using integration
// -> apply initial sync behavior
switch (metadata.initialSyncBehavior) {
// Override all the secrets in GitLab
case IntegrationInitialSyncBehavior.OVERWRITE_TARGET: {
if (!(gitlabSecret.key in secrets)) {
secretsToRemoveInGitlab.push(gitlabSecret);
}
break;
}
case IntegrationInitialSyncBehavior.PREFER_SOURCE: {
// if the secret is not in infisical, we need to add it to infisical
if (!(gitlabSecret.key in secrets)) {
secrets[gitlabSecret.key] = {
value: gitlabSecret.value
};
// need to remove prefix and suffix from what we're saving to Infisical
const prefix = metadata?.secretPrefix || "";
const suffix = metadata?.secretSuffix || "";
let processedKey = gitlabSecret.key;
// Remove prefix if it exists at the start
if (prefix && processedKey.startsWith(prefix)) {
processedKey = processedKey.slice(prefix.length);
}
// Remove suffix if it exists at the end
if (suffix && processedKey.endsWith(suffix)) {
processedKey = processedKey.slice(0, -suffix.length);
}
secretsToAddToInfisical[processedKey] = gitlabSecret;
}
break;
}
default: {
throw new Error(`Invalid initial sync behavior: ${metadata.initialSyncBehavior}`);
}
}
});
if (Object.keys(secretsToAddToInfisical).length) {
await createManySecretsRawFn({
projectId: integration.projectId,
environment: integration.environment.slug,
path: integration.secretPath,
secrets: Object.keys(secretsToAddToInfisical).map((key) => ({
secretName: key,
secretValue: secretsToAddToInfisical[key].value,
type: SecretType.Shared
}))
});
}
for await (const gitlabSecret of secretsToRemoveInGitlab) {
await request.delete(
`${gitLabApiUrl}/v4/projects/${integration?.appId}/variables/${gitlabSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`,
{
headers: {
Authorization: `Bearer ${accessToken}`
}
}
);
}
}
for await (const key of Object.keys(secrets)) {
const existingSecret = getSecretsRes.find((s) => s.key === key);
if (!existingSecret) {
@ -4440,7 +4534,7 @@ export const syncIntegrationSecrets = async ({
secretPath: string;
};
integrationAuth: TIntegrationAuths;
secrets: Record<string, { value: string; comment?: string }>;
secrets: Record<string, { value: string; comment?: string; secretMetadata?: ResourceMetadataDTO }>;
accessId: string | null;
awsAssumeRoleArn: string | null;
accessToken: string;
@ -4545,7 +4639,8 @@ export const syncIntegrationSecrets = async ({
integrationAuth,
integration,
secrets,
accessToken
accessToken,
createManySecretsRawFn
});
break;
case Integrations.RENDER:

@ -2,7 +2,7 @@ import { z } from "zod";
import { INTEGRATION } from "@app/lib/api-docs";
import { IntegrationMappingBehavior } from "../integration-auth/integration-list";
import { IntegrationMappingBehavior, IntegrationMetadataSyncMode } from "../integration-auth/integration-list";
export const IntegrationMetadataSchema = z.object({
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
@ -50,6 +50,11 @@ export const IntegrationMetadataSchema = z.object({
shouldMaskSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldMaskSecrets),
shouldProtectSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldProtectSecrets),
metadataSyncMode: z
.nativeEnum(IntegrationMetadataSyncMode)
.optional()
.describe(INTEGRATION.CREATE.metadata.metadataSyncMode),
octopusDeployScopeValues: z
.object({
// in Octopus Deploy Scope Value Format

@ -0,0 +1,11 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TResourceMetadataDALFactory = ReturnType<typeof resourceMetadataDALFactory>;
export const resourceMetadataDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.ResourceMetadata);
return orm;
};

@ -0,0 +1,10 @@
import z from "zod";
export const ResourceMetadataSchema = z
.object({
key: z.string().trim().min(1),
value: z.string().trim().default("")
})
.array();
export type ResourceMetadataDTO = z.infer<typeof ResourceMetadataSchema>;

@ -1,6 +1,7 @@
import { SecretType, TSecretImports, TSecrets, TSecretsV2 } from "@app/db/schemas";
import { groupBy, unique } from "@app/lib/fn";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { TSecretDALFactory } from "../secret/secret-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
@ -39,6 +40,7 @@ type TSecretImportSecretsV2 = {
// But for somereason ts consider ? and undefined explicit as different just ts things
secretValue: string;
secretComment: string;
secretMetadata?: ResourceMetadataDTO;
})[];
};

@ -160,6 +160,7 @@ export const secretImportServiceFactory = ({
if (secImport.isReplication && sourceFolder) {
await secretQueueService.replicateSecrets({
secretPath: secImport.importPath,
orgId: actorOrgId,
projectId,
environmentSlug: importEnv.slug,
pickOnlyImportIds: [secImport.id],
@ -169,6 +170,7 @@ export const secretImportServiceFactory = ({
} else {
await secretQueueService.syncSecrets({
secretPath,
orgId: actorOrgId,
projectId,
environmentSlug: environment,
actorId,
@ -340,6 +342,7 @@ export const secretImportServiceFactory = ({
await secretQueueService.syncSecrets({
secretPath,
orgId: actorOrgId,
projectId,
environmentSlug: environment,
actor,
@ -415,6 +418,7 @@ export const secretImportServiceFactory = ({
if (membership && sourceFolder) {
await secretQueueService.replicateSecrets({
orgId: actorOrgId,
secretPath: secretImportDoc.importPath,
projectId,
environmentSlug: secretImportDoc.importEnv.slug,

@ -78,6 +78,12 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.select(
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
)
.select(selectAllTableCols(TableName.SecretV2))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
@ -103,6 +109,15 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
slug,
name: slug
})
},
{
key: "metadataId",
label: "secretMetadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
}
]
});
@ -221,7 +236,9 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
const secs = await (tx || db.replicaNode())(TableName.SecretV2)
.where({ folderId })
.where((bd) => {
void bd.whereNull("userId").orWhere({ userId: userId || null });
void bd
.whereNull(`${TableName.SecretV2}.userId`)
.orWhere({ [`${TableName.SecretV2}.userId` as "userId"]: userId || null });
})
.leftJoin(
TableName.SecretV2JnTag,
@ -233,10 +250,16 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.select(selectAllTableCols(TableName.SecretV2))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
)
.orderBy("id", "asc");
const data = sqlNestRelationships({
@ -253,6 +276,15 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
slug,
name: slug
})
},
{
key: "metadataId",
label: "secretMetadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
}
]
});
@ -367,7 +399,9 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
}
})
.where((bd) => {
void bd.whereNull(`${TableName.SecretV2}.userId`).orWhere({ userId: userId || null });
void bd
.whereNull(`${TableName.SecretV2}.userId`)
.orWhere({ [`${TableName.SecretV2}.userId` as "userId"]: userId || null });
})
.leftJoin(
TableName.SecretV2JnTag,
@ -379,13 +413,23 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.select(
selectAllTableCols(TableName.SecretV2),
db.raw(`DENSE_RANK() OVER (ORDER BY "key" ${filters?.orderDirection ?? OrderByDirection.ASC}) as rank`)
db.raw(
`DENSE_RANK() OVER (ORDER BY "${TableName.SecretV2}".key ${
filters?.orderDirection ?? OrderByDirection.ASC
}) as rank`
)
)
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
)
.where((bd) => {
const slugs = filters?.tagSlugs?.filter(Boolean);
if (slugs && slugs.length > 0) {
@ -425,6 +469,15 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
slug,
name: slug
})
},
{
key: "metadataId",
label: "secretMetadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
}
]
});
@ -545,10 +598,17 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.select(selectAllTableCols(TableName.SecretV2))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
);
const docs = sqlNestRelationships({
data: rawDocs,
key: "id",
@ -563,6 +623,15 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
slug,
name: slug
})
},
{
key: "metadataId",
label: "secretMetadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
}
]
});

@ -6,6 +6,7 @@ import { groupBy } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
@ -54,9 +55,11 @@ export const getAllSecretReferences = (maybeSecretReference: string) => {
export const fnSecretBulkInsert = async ({
// TODO: Pick types here
folderId,
orgId,
inputSecrets,
secretDAL,
secretVersionDAL,
resourceMetadataDAL,
secretTagDAL,
secretVersionTagDAL,
tx
@ -91,6 +94,7 @@ export const fnSecretBulkInsert = async ({
sanitizedInputSecrets.map((el) => ({ ...el, folderId })),
tx
);
const newSecretGroupedByKeyName = groupBy(newSecrets, (item) => item.key);
const newSecretTags = inputSecrets.flatMap(({ tagIds: secretTags = [], key }) =>
secretTags.map((tag) => ({
@ -106,6 +110,7 @@ export const fnSecretBulkInsert = async ({
})),
tx
);
await secretDAL.upsertSecretReferences(
inputSecrets.map(({ references = [], key }) => ({
secretId: newSecretGroupedByKeyName[key][0].id,
@ -113,6 +118,22 @@ export const fnSecretBulkInsert = async ({
})),
tx
);
await resourceMetadataDAL.insertMany(
inputSecrets.flatMap(({ key: secretKey, secretMetadata }) => {
if (secretMetadata) {
return secretMetadata.map(({ key, value }) => ({
key,
value,
secretId: newSecretGroupedByKeyName[secretKey][0].id,
orgId
}));
}
return [];
}),
tx
);
if (newSecretTags.length) {
const secTags = await secretTagDAL.saveTagsToSecretV2(newSecretTags, tx);
const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId);
@ -120,6 +141,7 @@ export const fnSecretBulkInsert = async ({
[`${TableName.SecretVersionV2}Id` as const]: secVersionsGroupBySecId[secrets_v2Id][0].id,
[`${TableName.SecretTag}Id` as const]: secret_tagsId
}));
await secretVersionTagDAL.insertMany(newSecretVersionTags, tx);
}
@ -130,10 +152,12 @@ export const fnSecretBulkUpdate = async ({
tx,
inputSecrets,
folderId,
orgId,
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL
secretVersionTagDAL,
resourceMetadataDAL
}: TFnSecretBulkUpdate) => {
const sanitizedInputSecrets = inputSecrets.map(
({
@ -231,6 +255,34 @@ export const fnSecretBulkUpdate = async ({
}
}
const inputSecretIdsWithMetadata = inputSecrets
.filter((sec) => Boolean(sec.data.secretMetadata))
.map((sec) => sec.filter.id);
await resourceMetadataDAL.delete(
{
$in: {
secretId: inputSecretIdsWithMetadata
}
},
tx
);
await resourceMetadataDAL.insertMany(
inputSecrets.flatMap(({ filter: { id }, data: { secretMetadata } }) => {
if (secretMetadata) {
return secretMetadata.map(({ key, value }) => ({
key,
value,
secretId: id,
orgId
}));
}
return [];
}),
tx
);
return newSecrets.map((secret) => ({ ...secret, _id: secret.id }));
};
@ -570,6 +622,7 @@ export const reshapeBridgeSecret = (
color?: string | null;
name: string;
}[];
secretMetadata?: ResourceMetadataDTO;
}
) => ({
secretKey: secret.key,
@ -588,6 +641,7 @@ export const reshapeBridgeSecret = (
secretReminderRepeatDays: secret.reminderRepeatDays,
secretReminderNote: secret.reminderNote,
metadata: secret.metadata,
secretMetadata: secret.secretMetadata,
createdAt: secret.createdAt,
updatedAt: secret.updatedAt
});

@ -18,6 +18,7 @@ import { ActorType } from "../auth/auth-type";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
import { TSecretQueueFactory } from "../secret/secret-queue";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
@ -74,6 +75,7 @@ type TSecretV2BridgeServiceFactoryDep = {
"insertV2Bridge" | "insertApprovalSecretV2Tags"
>;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
};
export type TSecretV2BridgeServiceFactory = ReturnType<typeof secretV2BridgeServiceFactory>;
@ -95,7 +97,8 @@ export const secretV2BridgeServiceFactory = ({
secretApprovalPolicyService,
secretApprovalRequestDAL,
secretApprovalRequestSecretDAL,
kmsService
kmsService,
resourceMetadataDAL
}: TSecretV2BridgeServiceFactoryDep) => {
const $validateSecretReferences = async (
projectId: string,
@ -141,7 +144,7 @@ export const secretV2BridgeServiceFactory = ({
},
{
operator: "eq",
field: "key",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
}
]
@ -186,6 +189,7 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod,
projectId,
secretPath,
secretMetadata,
...inputSecret
}: TCreateSecretDTO) => {
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
@ -255,6 +259,7 @@ export const secretV2BridgeServiceFactory = ({
const secret = await secretDAL.transaction((tx) =>
fnSecretBulkInsert({
folderId,
orgId: actorOrgId,
inputSecrets: [
{
version: 1,
@ -272,9 +277,11 @@ export const secretV2BridgeServiceFactory = ({
key: secretName,
userId: inputSecret.type === SecretType.Personal ? actorId : null,
tagIds: inputSecret.tagIds,
references: nestedReferences
references: nestedReferences,
secretMetadata
}
],
resourceMetadataDAL,
secretDAL,
secretVersionDAL,
secretTagDAL,
@ -287,6 +294,7 @@ export const secretV2BridgeServiceFactory = ({
await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({
secretPath,
orgId: actorOrgId,
actorId,
actor,
projectId,
@ -309,6 +317,7 @@ export const secretV2BridgeServiceFactory = ({
actorAuthMethod,
projectId,
secretPath,
secretMetadata,
...inputSecret
}: TUpdateSecretDTO) => {
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
@ -435,6 +444,8 @@ export const secretV2BridgeServiceFactory = ({
const updatedSecret = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({
folderId,
orgId: actorOrgId,
resourceMetadataDAL,
inputSecrets: [
{
filter: { id: secretId },
@ -448,6 +459,7 @@ export const secretV2BridgeServiceFactory = ({
skipMultilineEncoding: inputSecret.skipMultilineEncoding,
key: inputSecret.newSecretName || secretName,
tags: inputSecret.tagIds,
secretMetadata,
...encryptedValue
}
}
@ -475,6 +487,7 @@ export const secretV2BridgeServiceFactory = ({
actorId,
actor,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
}
@ -562,6 +575,7 @@ export const secretV2BridgeServiceFactory = ({
actorId,
actor,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
}
@ -961,8 +975,8 @@ export const secretV2BridgeServiceFactory = ({
? secretDAL.findOneWithTags({
folderId,
type: secretType,
key: secretName,
userId: secretType === SecretType.Personal ? actorId : null
[`${TableName.SecretV2}.key` as "key"]: secretName,
[`${TableName.SecretV2}.userId` as "userId"]: secretType === SecretType.Personal ? actorId : null
})
: secretVersionDAL
.findOne({
@ -1113,7 +1127,7 @@ export const secretV2BridgeServiceFactory = ({
value: [
{
operator: "eq",
field: "key",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
},
{
@ -1185,11 +1199,14 @@ export const secretV2BridgeServiceFactory = ({
key: el.secretKey,
tagIds: el.tagIds,
references,
secretMetadata: el.secretMetadata,
type: SecretType.Shared
};
}),
folderId,
orgId: actorOrgId,
secretDAL,
resourceMetadataDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
@ -1203,6 +1220,7 @@ export const secretV2BridgeServiceFactory = ({
actorId,
secretPath,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
@ -1254,7 +1272,7 @@ export const secretV2BridgeServiceFactory = ({
value: [
{
operator: "eq",
field: "key",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
},
{
@ -1319,7 +1337,7 @@ export const secretV2BridgeServiceFactory = ({
value: [
{
operator: "eq",
field: "key",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
},
{
@ -1371,6 +1389,7 @@ export const secretV2BridgeServiceFactory = ({
const secrets = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({
folderId,
orgId: actorOrgId,
tx,
inputSecrets: inputSecrets.map((el) => {
const originalSecret = secretsToUpdateInDBGroupedByKey[el.secretKey][0];
@ -1394,6 +1413,7 @@ export const secretV2BridgeServiceFactory = ({
skipMultilineEncoding: el.skipMultilineEncoding,
key: el.newSecretName || el.secretKey,
tags: el.tagIds,
secretMetadata: el.secretMetadata,
...encryptedValue
}
};
@ -1401,7 +1421,8 @@ export const secretV2BridgeServiceFactory = ({
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL
secretVersionTagDAL,
resourceMetadataDAL
})
);
await snapshotService.performSnapshot(folderId);
@ -1410,6 +1431,7 @@ export const secretV2BridgeServiceFactory = ({
actorId,
secretPath,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
@ -1461,7 +1483,7 @@ export const secretV2BridgeServiceFactory = ({
value: [
{
operator: "eq",
field: "key",
field: `${TableName.SecretV2}.key` as "key",
value: el.secretKey
},
{
@ -1512,6 +1534,7 @@ export const secretV2BridgeServiceFactory = ({
actorId,
secretPath,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
@ -1815,10 +1838,12 @@ export const secretV2BridgeServiceFactory = ({
if (locallyCreatedSecrets.length) {
await fnSecretBulkInsert({
folderId: destinationFolder.id,
orgId: actorOrgId,
secretVersionDAL,
secretDAL,
tx,
secretTagDAL,
resourceMetadataDAL,
secretVersionTagDAL,
inputSecrets: locallyCreatedSecrets.map((doc) => {
return {
@ -1830,6 +1855,7 @@ export const secretV2BridgeServiceFactory = ({
skipMultilineEncoding: doc.skipMultilineEncoding,
reminderNote: doc.reminderNote,
reminderRepeatDays: doc.reminderRepeatDays,
secretMetadata: doc.secretMetadata,
references: doc.value ? getAllSecretReferences(doc.value).nestedReferences : []
};
})
@ -1838,6 +1864,8 @@ export const secretV2BridgeServiceFactory = ({
if (locallyUpdatedSecrets.length) {
await fnSecretBulkUpdate({
folderId: destinationFolder.id,
orgId: actorOrgId,
resourceMetadataDAL,
secretVersionDAL,
secretDAL,
tx,
@ -1855,6 +1883,7 @@ export const secretV2BridgeServiceFactory = ({
encryptedComment: doc.encryptedComment,
skipMultilineEncoding: doc.skipMultilineEncoding,
reminderNote: doc.reminderNote,
secretMetadata: doc.secretMetadata,
reminderRepeatDays: doc.reminderRepeatDays,
...(doc.encryptedValue
? {
@ -1938,6 +1967,7 @@ export const secretV2BridgeServiceFactory = ({
await snapshotService.performSnapshot(destinationFolder.id);
await secretQueueService.syncSecrets({
projectId,
orgId: actorOrgId,
secretPath: destinationFolder.path,
environmentSlug: destinationFolder.environment.slug,
actorId,
@ -1949,6 +1979,7 @@ export const secretV2BridgeServiceFactory = ({
await snapshotService.performSnapshot(sourceFolder.id);
await secretQueueService.syncSecrets({
projectId,
orgId: actorOrgId,
secretPath: sourceFolder.path,
environmentSlug: sourceFolder.environment.slug,
actorId,

@ -7,6 +7,8 @@ import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
import { TSecretVersionV2DALFactory } from "./secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "./secret-version-tag-dal";
@ -58,6 +60,7 @@ export type TCreateSecretDTO = TProjectPermission & {
skipMultilineEncoding?: boolean;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
secretMetadata?: ResourceMetadataDTO;
};
export type TUpdateSecretDTO = TProjectPermission & {
@ -75,6 +78,7 @@ export type TUpdateSecretDTO = TProjectPermission & {
metadata?: {
source?: string;
};
secretMetadata?: ResourceMetadataDTO;
};
export type TDeleteSecretDTO = TProjectPermission & {
@ -94,6 +98,7 @@ export type TCreateManySecretDTO = Omit<TProjectPermission, "projectId"> & {
secretComment?: string;
skipMultilineEncoding?: boolean;
tagIds?: string[];
secretMetadata?: ResourceMetadataDTO;
metadata?: {
source?: string;
};
@ -113,6 +118,7 @@ export type TUpdateManySecretDTO = Omit<TProjectPermission, "projectId"> & {
tagIds?: string[];
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
secretMetadata?: ResourceMetadataDTO;
}[];
};
@ -136,8 +142,16 @@ export type TSecretReference = { environment: string; secretPath: string; secret
export type TFnSecretBulkInsert = {
folderId: string;
orgId: string;
tx?: Knex;
inputSecrets: Array<Omit<TSecretsV2Insert, "folderId"> & { tagIds?: string[]; references: TSecretReference[] }>;
inputSecrets: Array<
Omit<TSecretsV2Insert, "folderId"> & {
tagIds?: string[];
references: TSecretReference[];
secretMetadata?: ResourceMetadataDTO;
}
>;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2">;
@ -156,10 +170,12 @@ type TRequireReferenceIfValue =
export type TFnSecretBulkUpdate = {
folderId: string;
orgId: string;
inputSecrets: {
filter: Partial<TSecretsV2>;
data: TRequireReferenceIfValue & { tags?: string[] };
data: TRequireReferenceIfValue & { tags?: string[]; secretMetadata?: ResourceMetadataDTO };
}[];
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "upsertSecretReferences">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2">;

@ -749,7 +749,8 @@ export const createManySecretsRawFnFactory = ({
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
secretVersionTagV2BridgeDAL,
kmsService
kmsService,
resourceMetadataDAL
}: TCreateManySecretsRawFnFactory) => {
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL);
const createManySecretsRawFn = async ({
@ -760,7 +761,7 @@ export const createManySecretsRawFnFactory = ({
userId
}: TCreateManySecretsRawFn) => {
const { botKey, shouldUseSecretV2Bridge } = await getBotKeyFn(projectId);
const project = await projectDAL.findById(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
throw new NotFoundError({
@ -814,7 +815,9 @@ export const createManySecretsRawFnFactory = ({
tagIds: el.tags
})),
folderId,
orgId: project.orgId,
secretDAL: secretV2BridgeDAL,
resourceMetadataDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
@ -909,6 +912,7 @@ export const updateManySecretsRawFnFactory = ({
secretVersionTagV2BridgeDAL,
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
resourceMetadataDAL,
kmsService
}: TUpdateManySecretsRawFnFactory) => {
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL);
@ -920,6 +924,7 @@ export const updateManySecretsRawFnFactory = ({
userId
}: TUpdateManySecretsRawFn): Promise<Array<{ id: string }>> => {
const { botKey, shouldUseSecretV2Bridge } = await getBotKeyFn(projectId);
const project = await projectDAL.findById(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
@ -988,11 +993,13 @@ export const updateManySecretsRawFnFactory = ({
const updatedSecrets = await secretDAL.transaction(async (tx) =>
fnSecretV2BridgeBulkUpdate({
folderId,
orgId: project.orgId,
tx,
inputSecrets: inputSecrets.map((el) => ({
filter: { id: secretsToUpdateInDBGroupedByKey[el.key][0].id, type: SecretType.Shared },
data: el
})),
resourceMetadataDAL,
secretDAL: secretV2BridgeDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretTagDAL,

@ -47,6 +47,8 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
@ -104,6 +106,7 @@ type TSecretQueueFactoryDep = {
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
};
export type TGetSecrets = {
@ -120,7 +123,12 @@ export const uniqueSecretQueueKey = (environment: string, secretPath: string) =>
type TIntegrationSecret = Record<
string,
{ value: string; comment?: string; skipMultilineEncoding?: boolean | null | undefined }
{
value: string;
comment?: string;
skipMultilineEncoding?: boolean | null | undefined;
secretMetadata?: ResourceMetadataDTO;
}
>;
// TODO(akhilmhdh): split this into multiple queue
@ -157,7 +165,8 @@ export const secretQueueFactory = ({
auditLogService,
orgService,
projectUserMembershipRoleDAL,
projectKeyDAL
projectKeyDAL,
resourceMetadataDAL
}: TSecretQueueFactoryDep) => {
const integrationMeter = opentelemetry.metrics.getMeter("Integrations");
const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", {
@ -306,7 +315,8 @@ export const secretQueueFactory = ({
kmsService,
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
secretVersionTagV2BridgeDAL
secretVersionTagV2BridgeDAL,
resourceMetadataDAL
});
const updateManySecretsRawFn = updateManySecretsRawFnFactory({
@ -321,7 +331,8 @@ export const secretQueueFactory = ({
kmsService,
secretVersionV2BridgeDAL,
secretV2BridgeDAL,
secretVersionTagV2BridgeDAL
secretVersionTagV2BridgeDAL,
resourceMetadataDAL
});
/**
@ -372,6 +383,7 @@ export const secretQueueFactory = ({
}
content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
content[secretKey].secretMetadata = secret.secretMetadata;
})
);
@ -397,7 +409,8 @@ export const secretQueueFactory = ({
content[importedSecret.key] = {
skipMultilineEncoding: importedSecret.skipMultilineEncoding,
comment: importedSecret.secretComment,
value: importedSecret.secretValue || ""
value: importedSecret.secretValue || "",
secretMetadata: importedSecret.secretMetadata
};
}
}
@ -597,6 +610,7 @@ export const secretQueueFactory = ({
_depth: depth,
secretPath,
projectId,
orgId,
environmentSlug: environment,
excludeReplication,
actorId,
@ -625,6 +639,7 @@ export const secretQueueFactory = ({
_deDupeReplicationQueue: deDupeReplicationQueue,
_depth: depth,
projectId,
orgId,
secretPath,
actorId,
actor,
@ -681,6 +696,7 @@ export const secretQueueFactory = ({
if (!folder) {
throw new Error("Secret path not found");
}
const project = await projectDAL.findById(projectId);
// find all imports made with the given environment and secret path
const linkSourceDto = {
@ -715,6 +731,7 @@ export const secretQueueFactory = ({
.map(({ folderId }) =>
syncSecrets({
projectId,
orgId: project.orgId,
secretPath: foldersGroupedById[folderId][0]?.path as string,
environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string,
_deDupeQueue: deDupeQueue,
@ -767,6 +784,7 @@ export const secretQueueFactory = ({
.map((folderId) =>
syncSecrets({
projectId,
orgId: project.orgId,
secretPath: referencedFoldersGroupedById[folderId][0]?.path as string,
environmentSlug: referencedFoldersGroupedById[folderId][0]?.environmentSlug as string,
_deDupeQueue: deDupeQueue,

@ -11,6 +11,7 @@ import {
SecretsSchema,
SecretType
} from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
@ -69,6 +70,7 @@ import {
TDeleteSecretRawDTO,
TGetASecretDTO,
TGetASecretRawDTO,
TGetSecretAccessListDTO,
TGetSecretsDTO,
TGetSecretsRawDTO,
TGetSecretVersionsDTO,
@ -94,7 +96,7 @@ type TSecretServiceFactoryDep = {
>;
secretV2BridgeService: TSecretV2BridgeServiceFactory;
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissions">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
secretQueueService: Pick<
TSecretQueueFactory,
@ -113,6 +115,7 @@ type TSecretServiceFactoryDep = {
TSecretApprovalRequestSecretDALFactory,
"insertMany" | "insertApprovalSecretTags"
>;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TSecretServiceFactory = ReturnType<typeof secretServiceFactory>;
@ -134,7 +137,8 @@ export const secretServiceFactory = ({
secretApprovalRequestDAL,
secretApprovalRequestSecretDAL,
secretV2BridgeService,
secretApprovalRequestService
secretApprovalRequestService,
licenseService
}: TSecretServiceFactoryDep) => {
const getSecretReference = async (projectId: string) => {
// if bot key missing means e2e still exist
@ -288,6 +292,7 @@ export const secretServiceFactory = ({
actorId,
actor,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
}
@ -429,6 +434,7 @@ export const secretServiceFactory = ({
await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({
secretPath: path,
orgId: actorOrgId,
actorId,
actor,
projectId,
@ -526,6 +532,7 @@ export const secretServiceFactory = ({
actorId,
actor,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
}
@ -820,6 +827,7 @@ export const secretServiceFactory = ({
actorId,
secretPath: path,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
@ -928,6 +936,7 @@ export const secretServiceFactory = ({
actorId,
secretPath: path,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
@ -1014,6 +1023,7 @@ export const secretServiceFactory = ({
actorId,
secretPath: path,
projectId,
orgId: actorOrgId,
environmentSlug: folder.environment.slug
});
@ -1147,6 +1157,71 @@ export const secretServiceFactory = ({
return secretV2BridgeService.getSecretReferenceTree(dto);
};
const getSecretAccessList = async (dto: TGetSecretAccessListDTO) => {
const { environment, secretPath, secretName, projectId } = dto;
const plan = await licenseService.getPlan(dto.actorOrgId);
if (!plan.secretAccessInsights) {
throw new BadRequestError({
message: "Failed to fetch secret access list due to plan restriction. Upgrade your plan."
});
}
const secret = await secretV2BridgeService.getSecretByName({
actor: dto.actor,
actorId: dto.actorId,
actorOrgId: dto.actorOrgId,
actorAuthMethod: dto.actorAuthMethod,
projectId,
secretName,
path: secretPath,
environment,
type: "shared"
});
const { userPermissions, identityPermissions, groupPermissions } = await permissionService.getProjectPermissions(
dto.projectId
);
const attachAllowedActions = (
entityPermission:
| (typeof userPermissions)[number]
| (typeof identityPermissions)[number]
| (typeof groupPermissions)[number]
) => {
const allowedActions = [
ProjectPermissionActions.Read,
ProjectPermissionActions.Delete,
ProjectPermissionActions.Create,
ProjectPermissionActions.Edit
].filter((action) =>
entityPermission.permission.can(
action,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: secret?.tags?.map((el) => el.slug)
})
)
);
return {
...entityPermission,
allowedActions
};
};
const usersWithAccess = userPermissions.map(attachAllowedActions).filter((user) => user.allowedActions.length > 0);
const identitiesWithAccess = identityPermissions
.map(attachAllowedActions)
.filter((identity) => identity.allowedActions.length > 0);
const groupsWithAccess = groupPermissions
.map(attachAllowedActions)
.filter((group) => group.allowedActions.length > 0);
return { users: usersWithAccess, identities: identitiesWithAccess, groups: groupsWithAccess };
};
const getSecretsRaw = async ({
projectId,
path,
@ -1385,7 +1460,8 @@ export const secretServiceFactory = ({
skipMultilineEncoding,
tagIds,
secretReminderNote,
secretReminderRepeatDays
secretReminderRepeatDays,
secretMetadata
}: TCreateSecretRawDTO) => {
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
const policy =
@ -1412,7 +1488,8 @@ export const secretServiceFactory = ({
secretValue,
tagIds,
reminderNote: secretReminderNote,
reminderRepeatDays: secretReminderRepeatDays
reminderRepeatDays: secretReminderRepeatDays,
secretMetadata
}
]
}
@ -1435,7 +1512,8 @@ export const secretServiceFactory = ({
tagIds,
secretReminderNote,
skipMultilineEncoding,
secretReminderRepeatDays
secretReminderRepeatDays,
secretMetadata
});
return { secret, type: SecretProtectionType.Direct as const };
}
@ -1525,7 +1603,8 @@ export const secretServiceFactory = ({
secretReminderRepeatDays,
metadata,
secretComment,
newSecretName
newSecretName,
secretMetadata
}: TUpdateSecretRawDTO) => {
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
const policy =
@ -1553,7 +1632,8 @@ export const secretServiceFactory = ({
secretValue,
tagIds,
reminderNote: secretReminderNote,
reminderRepeatDays: secretReminderRepeatDays
reminderRepeatDays: secretReminderRepeatDays,
secretMetadata
}
]
}
@ -1577,7 +1657,8 @@ export const secretServiceFactory = ({
secretName,
newSecretName,
metadata,
secretValue
secretValue,
secretMetadata
});
return { type: SecretProtectionType.Direct as const, secret };
}
@ -1793,7 +1874,8 @@ export const secretServiceFactory = ({
secretComment: el.secretComment,
metadata: el.metadata,
skipMultilineEncoding: el.skipMultilineEncoding,
secretKey: el.secretKey
secretKey: el.secretKey,
secretMetadata: el.secretMetadata
}))
}
});
@ -1919,7 +2001,8 @@ export const secretServiceFactory = ({
secretValue: el.secretValue,
secretComment: el.secretComment,
skipMultilineEncoding: el.skipMultilineEncoding,
secretKey: el.secretKey
secretKey: el.secretKey,
secretMetadata: el.secretMetadata
}))
}
});
@ -2262,6 +2345,7 @@ export const secretServiceFactory = ({
await secretQueueService.syncSecrets({
secretPath,
projectId: project.id,
orgId: project.orgId,
environmentSlug: environment,
excludeReplication: true
});
@ -2370,6 +2454,7 @@ export const secretServiceFactory = ({
await secretQueueService.syncSecrets({
secretPath,
projectId: project.id,
orgId: project.orgId,
environmentSlug: environment,
excludeReplication: true
});
@ -2828,6 +2913,7 @@ export const secretServiceFactory = ({
await snapshotService.performSnapshot(destinationFolder.id);
await secretQueueService.syncSecrets({
projectId: project.id,
orgId: project.orgId,
secretPath: destinationFolder.path,
environmentSlug: destinationFolder.environment.slug,
actorId,
@ -2839,6 +2925,7 @@ export const secretServiceFactory = ({
await snapshotService.performSnapshot(sourceFolder.id);
await secretQueueService.syncSecrets({
projectId: project.id,
orgId: project.orgId,
secretPath: sourceFolder.path,
environmentSlug: sourceFolder.environment.slug,
actorId,
@ -2928,6 +3015,7 @@ export const secretServiceFactory = ({
getSecretsCountMultiEnv,
getSecretsRawMultiEnv,
getSecretReferenceTree,
getSecretsRawByFolderMappings
getSecretsRawByFolderMappings,
getSecretAccessList
};
};

@ -14,6 +14,8 @@ import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { ActorType } from "../auth/auth-type";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
@ -188,6 +190,12 @@ export type TGetSecretsRawDTO = {
keys?: string[];
} & TProjectPermission;
export type TGetSecretAccessListDTO = {
environment: string;
secretPath: string;
secretName: string;
} & TProjectPermission;
export type TGetASecretRawDTO = {
secretName: string;
path: string;
@ -211,6 +219,7 @@ export type TCreateSecretRawDTO = TProjectPermission & {
skipMultilineEncoding?: boolean;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
secretMetadata?: ResourceMetadataDTO;
};
export type TUpdateSecretRawDTO = TProjectPermission & {
@ -228,6 +237,7 @@ export type TUpdateSecretRawDTO = TProjectPermission & {
metadata?: {
source?: string;
};
secretMetadata?: ResourceMetadataDTO;
};
export type TDeleteSecretRawDTO = TProjectPermission & {
@ -248,6 +258,7 @@ export type TCreateManySecretRawDTO = Omit<TProjectPermission, "projectId"> & {
secretComment?: string;
skipMultilineEncoding?: boolean;
tagIds?: string[];
secretMetadata?: ResourceMetadataDTO;
metadata?: {
source?: string;
};
@ -266,6 +277,7 @@ export type TUpdateManySecretRawDTO = Omit<TProjectPermission, "projectId"> & {
secretComment?: string;
skipMultilineEncoding?: boolean;
tagIds?: string[];
secretMetadata?: ResourceMetadataDTO;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
}[];
@ -293,7 +305,13 @@ export type TSecretReference = { environment: string; secretPath: string };
export type TFnSecretBulkInsert = {
folderId: string;
tx?: Knex;
inputSecrets: Array<Omit<TSecretsInsert, "folderId"> & { tags?: string[]; references?: TSecretReference[] }>;
inputSecrets: Array<
Omit<TSecretsInsert, "folderId"> & {
tags?: string[];
references?: TSecretReference[];
secretMetadata?: ResourceMetadataDTO;
}
>;
secretDAL: Pick<TSecretDALFactory, "insertMany" | "upsertSecretReferences">;
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret">;
@ -389,6 +407,7 @@ export type TCreateManySecretsRawFnFactory = {
>;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
};
export type TCreateManySecretsRawFn = {
@ -425,6 +444,7 @@ export type TUpdateManySecretsRawFnFactory = {
>;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
};
export type TUpdateManySecretsRawFn = {
@ -460,6 +480,7 @@ export type TSyncSecretsDTO<T extends boolean = false> = {
_depth?: number;
secretPath: string;
projectId: string;
orgId: string;
environmentSlug: string;
// cases for just doing sync integration and webhook
excludeReplication?: T;

@ -419,22 +419,23 @@ func executeCommandWithWatchMode(commandFlag string, args []string, watchModeInt
for {
<-recheckSecretsChannel
watchMutex.Lock()
func() {
watchMutex.Lock()
defer watchMutex.Unlock()
newEnvironmentVariables, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, token)
if err != nil {
log.Error().Err(err).Msg("[HOT RELOAD] Failed to fetch secrets")
continue
}
newEnvironmentVariables, err := fetchAndFormatSecretsForShell(request, projectConfigDir, secretOverriding, token)
if err != nil {
log.Error().Err(err).Msg("[HOT RELOAD] Failed to fetch secrets")
return
}
if newEnvironmentVariables.ETag != currentETag {
runCommandWithWatcher(newEnvironmentVariables)
} else {
log.Debug().Msg("[HOT RELOAD] No changes detected in secrets, not reloading process")
}
watchMutex.Unlock()
if newEnvironmentVariables.ETag != currentETag {
runCommandWithWatcher(newEnvironmentVariables)
} else {
log.Debug().Msg("[HOT RELOAD] No changes detected in secrets, not reloading process")
}
}()
}
}

@ -4,6 +4,27 @@ title: "Changelog"
The changelog below reflects new product developments and updates on a monthly basis.
## December 2024
- Added [GCP KMS](https://infisical.com/docs/documentation/platform/kms/overview) integration support.
- Added support for [K8s CSI integration](https://infisical.com/docs/integrations/platforms/kubernetes-csi) and ability to point K8s operator to specific secret versions.
- Fixed [Java SDK](https://github.com/Infisical/java-sdk) compatibility issues with Alpine Linux.
- Fixed SCIM group role assignment issues.
- Added Group View Page for improved team management.
- Added instance URL to email verification for Infisical accounts.
- Added ability to copy full path of nested folders.
- Added custom templating support for K8s operator, allowing flexible secret key mapping and additional fields
- Optimized secrets versions table performance.
## November 2024
- Improved EnvKey migration functionality with support for Blocks, Inheritance, and Branches.
- Added [Hardware Security Module (HSM) Encryption](https://infisical.com/docs/documentation/platform/kms/hsm-integration) support.
- Updated permissions handling in [Infisical Terraform Provider](https://registry.terraform.io/providers/Infisical/infisical/latest/docs) to use lists instead of sets.
- Enhanced [SCIM](https://infisical.com/docs/documentation/platform/scim/overview) implementation to remove SAML dependency.
- Enhanced [OIDC Authentication](https://infisical.com/docs/documentation/platform/identities/oidc-auth/general) implementation and added Default Org Slug support.
- Added support for multiple authentication methods per identity.
- Added AWS Parameter Store integration sync improvements.
- Added new screen and API for managing additional privileges.
- Added Dynamic Secrets support for SQL Server.
## October 2024
- Significantly improved performance of audit log operations in UI.

Binary file not shown.

Before

(image error) Size: 74 KiB

After

(image error) Size: 580 KiB

@ -17,6 +17,7 @@ Prerequisites:
If your instance is deployed on AWS, the aws-sdk will automatically retrieve the credentials. Ensure that you assign the provided permission policy to your deployed instance, such as ECS or EC2.
The following steps are for instances not deployed on AWS
<Steps>
<Step title="Create an IAM User">
Navigate to [Create IAM User](https://console.aws.amazon.com/iamv2/home#/users/create) in your AWS Console.
@ -40,9 +41,10 @@ The following steps are for instances not deployed on AWS
<Step title="Obtain the IAM User Credentials">
Obtain the AWS access key ID and secret access key for your IAM User by navigating to IAM > Users > [Your User] > Security credentials > Access keys.
![Access Key Step 1](../../images/integrations/aws/integrations-aws-access-key-1.png)
![Access Key Step 2](../../images/integrations/aws/integrations-aws-access-key-2.png)
![Access Key Step 3](../../images/integrations/aws/integrations-aws-access-key-3.png)
![Access Key Step 1](../../images/integrations/aws/integrations-aws-access-key-1.png)
![Access Key Step 2](../../images/integrations/aws/integrations-aws-access-key-2.png)
![Access Key Step 3](../../images/integrations/aws/integrations-aws-access-key-3.png)
</Step>
<Step title="Set Up Integration Keys">
1. Set the access key as **CLIENT_ID_AWS_INTEGRATION**.
@ -59,6 +61,7 @@ The following steps are for instances not deployed on AWS
2. Select **AWS Account** as the **Trusted Entity Type**.
3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead.
4. Optionally, enable **Require external ID** and enter your **project ID** to further enhance security.
</Step>
<Step title="Add Required Permissions for the IAM Role">
@ -89,11 +92,13 @@ The following steps are for instances not deployed on AWS
]
}
```
</Step>
<Step title="Copy the AWS IAM Role ARN">
![Copy IAM Role ARN](../../images/integrations/aws/integration-aws-iam-assume-arn.png)
</Step>
<Step title="Copy the AWS IAM Role ARN">
![Copy IAM Role
ARN](../../images/integrations/aws/integration-aws-iam-assume-arn.png)
</Step>
<Step title="Authorize Infisical for AWS Secrets Manager">
1. Navigate to your project's integrations tab in Infisical.
@ -104,6 +109,7 @@ The following steps are for instances not deployed on AWS
![Select Assume Role](../../images/integrations/aws/integration-aws-iam-assume-select.png)
4. Provide the **AWS IAM Role ARN** obtained from the previous step.
</Step> <Step title="Start integration">
Select how you want to integration to work by specifying a number of parameters:
@ -127,6 +133,12 @@ The following steps are for instances not deployed on AWS
Optionally, you can add tags or specify the encryption key of all the secrets created via this integration:
<ParamField path="Tag Sync Mode" type="string" optional>
The sync mode for AWS tags. The supported options are `Secret Metadata` and `Custom`. If `Secret Metadata` is selected,
the metadata of the Infisical secrets are used as tags in AWS. If custom is selected, then the key/value of the **Secret Tag** field is used. `Secret Metadata` mode
is only supported for one-to-one integrations.
</ParamField>
<ParamField path="Secret Tag" type="string" optional>
The Key/Value of a tag that will be added to secrets in AWS. Please note that it is possible to add multiple tags via API.
</ParamField>

@ -0,0 +1,437 @@
---
sidebarTitle: "InfisicalDynamicSecret CRD"
title: "InfisicalDynamicSecret CRD"
description: "Learn how to generate dynamic secret leases in Infisical and sync them to your Kubernetes cluster."
---
## Overview
The **InfisicalDynamicSecret** CRD allows you to easily create and manage dynamic secret leases in Infisical and automatically sync them to your Kubernetes cluster as native **Kubernetes Secret** resources.
This means any Pod, Deployment, or other Kubernetes resource can make use of dynamic secrets from Infisical just like any other K8s secret.
This CRD offers the following features:
- **Generate a dynamic secret lease** in Infisical and track its lifecycle.
- **Write** the dynamic secret from Infisical to your cluster as native Kubernetes secret.
- **Automatically rotate** the dynamic secret value before it expires to make sure your cluster always has valid credentials.
- **Optionally trigger redeployments** of any workloads that consume the secret if you enable auto-reload.
### Prerequisites
- The operator is installed on to your Kubernetes cluster
- You have already configured a dynamic secret in Infisical
## Configure Dynamic Secret CRD
The example below shows a sample **InfisicalDynamicSecret** CRD that creates a dynamic secret lease in Infisical, and syncs the lease to your Kubernetes cluster.
```yaml dynamic-secret-crd.yaml
apiVersion: secrets.infisical.com/v1alpha1
kind: InfisicalDynamicSecret
metadata:
name: infisicaldynamicsecret
spec:
hostAPI: https://app.infisical.com/api # Optional, defaults to https://app.infisical.com/api
dynamicSecret:
secretName: <dynamic-secret-name>
projectId: <project-id>
secretsPath: <path/to/dynamic-secret> # Root directory is /
environmentSlug: <env-slug>
# Lease revocation policy defines what should happen to leases created by the operator if the CRD is deleted.
# If set to "Revoke", leases will be revoked when the InfisicalDynamicSecret CRD is deleted.
leaseRevocationPolicy: Revoke
# Lease TTL defines how long the lease should last for the dynamic secret.
# This value must be less than 1 day, and if a max TTL is defined on the dynamic secret, it must be below the max TTL.
leaseTTL: 1m
# A reference to the secret that the dynamic secret lease should be stored in.
# If the secret doesn't exist, it will automatically be created.
managedSecretReference:
secretName: <secret-name>
secretNamespace: default # Must be the same namespace as the InfisicalDynamicSecret CRD.
creationPolicy: Orphan
# 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
```
Apply the InfisicalDynamicSecret CRD to your cluster.
```bash
kubectl apply -f dynamic-secret-crd.yaml
```
After applying the InfisicalDynamicSecret CRD, you should notice that the dynamic secret lease has been created in Infisical and synced to your Kubernetes cluster. You can verify that the lease has been created by doing:
```bash
kubectl get secret <managed-secret-name> -o yaml
```
After getting the secret, you should should see that the secret has data that contains the lease credentials.
```yaml
apiVersion: v1
data:
DB_PASSWORD: VHhETjZ4c2xsTXpOSWdPYW5LLlRyNEc2alVKYml6WiQjQS0tNTdodyREM3ZLZWtYSi4hTkdyS0F+TVFsLU9CSA==
DB_USERNAME: cHg4Z0dJTUVBcHdtTW1aYnV3ZWRsekJRRll6cW4wFEE=
kind: Secret
# .....
```
### InfisicalDynamicSecret CRD properties
<Accordion title="hostAPI">
If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to
` https://your-self-hosted-instace.com/api`
When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud.
<Accordion title="Advanced use case">
If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet.
To achieve this, use the following address for the hostAPI field:
``` bash
http://<backend-svc-name>.<namespace>.svc.cluster.local:4000/api
```
Make sure to replace `<backend-svc-name>` and `<namespace>` with the appropriate values for your backend service and namespace.
</Accordion>
</Accordion>
<Accordion title="leaseTTL">
The `leaseTTL` is a string-formatted duration that defines the time the lease should last for the dynamic secret.
The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time.
The following units are supported:
- `s` for seconds (must be at least 5 seconds)
- `m` for minutes
- `h` for hours
- `d` for days
<Note>
The lease duration at most be 1 day (24 hours). And the TTL must be less than the max TTL defined on the dynamic secret.
</Note>
</Accordion>
<Accordion title="managedSecretReference">
The `managedSecretReference` field is used to define the Kubernetes secret where the dynamic secret lease should be stored. The required fields are `secretName` and `secretNamespace`.
```yaml
spec:
managedSecretReference:
secretName: <secret-name>
secretNamespace: default
```
<Accordion title="managedSecretReference.secretName">
The name of the Kubernetes secret where the dynamic secret lease should be stored.
</Accordion>
<Accordion title="managedSecretReference.secretNamespace">
The namespace of the Kubernetes secret where the dynamic secret lease should be stored.
</Accordion>
<Accordion title="managedSecretReference.creationPolicy">
Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator.
This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically.
#### Available options
- `Orphan` (default)
- `Owner`
<Tip>
When creation policy is set to `Owner`, the `InfisicalSecret` CRD must be in
the same namespace as where the managed kubernetes secret.
</Tip>
This field is optional.
</Accordion>
<Accordion title="managedSecretReference.secretType">
Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets.
This field is optional.
</Accordion>
</Accordion>
<Accordion title="leaseRevocationPolicy">
The field is optional and will default to `None` if not defined.
The lease revocation policy defines what the operator should do with the leases created by the operator, when the InfisicalDynamicSecret CRD is deleted.
Valid values are `None` and `Revoke`.
Behavior of each policy:
- `None`: The operator will not override existing secrets in Infisical. If a secret with the same key already exists, the operator will skip pushing that secret, and the secret will not be managed by the operator.
- `Revoke`: The operator will revoke the leases created by the operator when the InfisicalDynamicSecret CRD is deleted.
```yaml
spec:
leaseRevocationPolicy: Revoke
```
</Accordion>
<Accordion title="dynamicSecret">
The `dynamicSecret` field is used to specify which dynamic secret to create leases for. The required fields are `secretName`, `projectId`, `secretsPath`, and `environmentSlug`.
```yaml
spec:
dynamicSecret:
secretName: <dynamic-secret-name>
projectId: <project-id>
environmentSlug: <env-slug>
secretsPath: <secrets-path>
```
<Accordion title="dynamicSecret.secretName">
The name of the dynamic secret.
</Accordion>
<Accordion title="dynamicSecret.projectId">
The project ID of where the dynamic secret is stored in Infisical.
</Accordion>
<Accordion title="dynamicSecret.environmentSlug">
The environment slug of where the dynamic secret is stored in Infisical.
</Accordion>
<Accordion title="dynamicSecret.secretsPath">
The path of where the dynamic secret is stored in Infisical. The root path is `/`.
</Accordion>
</Accordion>
<Accordion title="authentication">
The `authentication` field dictates which authentication method to use when pushing secrets to Infisical.
The available authentication methods are `universalAuth`, `kubernetesAuth`, `awsIamAuth`, `azureAuth`, `gcpIdTokenAuth`, and `gcpIamAuth`.
<Accordion title="universalAuth">
The universal authentication method is one of the easiest ways to get started with Infisical. Universal Auth works anywhere and is not tied to any specific cloud provider.
[Read more about Universal Auth](/documentation/platform/identities/universal-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
- `credentialsRef`: The name and namespace of the Kubernetes secret that stores the service token.
- `credentialsRef.secretName`: The name of the Kubernetes secret.
- `credentialsRef.secretNamespace`: The namespace of the Kubernetes secret.
Example:
```yaml
# infisical-push-secret.yaml
spec:
universalAuth:
credentialsRef:
secretName: <secret-name>
secretNamespace: <secret-namespace>
```
```yaml
# machine-identity-credentials.yaml
apiVersion: v1
kind: Secret
metadata:
name: universal-auth-credentials
type: Opaque
stringData:
clientId: <machine-identity-client-id>
clientSecret: <machine-identity-client-secret>
```
</Accordion>
<Accordion title="kubernetesAuth">
The Kubernetes machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within a Kubernetes environment.
[Read more about Kubernetes Auth](/documentation/platform/identities/kubernetes-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
- `serviceAccountRef`: The name and namespace of the service account that will be used to authenticate with Infisical.
- `serviceAccountRef.name`: The name of the service account.
- `serviceAccountRef.namespace`: The namespace of the service account.
Example:
```yaml
spec:
kubernetesAuth:
identityId: <machine-identity-id>
serviceAccountRef:
name: <secret-name>
namespace: <secret-namespace>
```
</Accordion>
<Accordion title="awsIamAuth">
The AWS IAM machine identity authentication method is used to authenticate with Infisical.
[Read more about AWS IAM Auth](/documentation/platform/identities/aws-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
Example:
```yaml
spec:
authentication:
awsIamAuth:
identityId: <machine-identity-id>
```
</Accordion>
<Accordion title="azureAuth">
The AWS IAM machine identity authentication method is used to authenticate with Infisical. Azure Auth can only be used from within an Azure environment.
[Read more about Azure Auth](/documentation/platform/identities/azure-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
Example:
```yaml
spec:
authentication:
azureAuth:
identityId: <machine-identity-id>
```
</Accordion>
<Accordion title="gcpIamAuth">
The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used both within and outside GCP environments.
[Read more about Azure Auth](/documentation/platform/identities/gcp-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
- `serviceAccountKeyFilePath`: The path to the GCP service account key file.
Example:
```yaml
spec:
gcpIamAuth:
identityId: <machine-identity-id>
serviceAccountKeyFilePath: </path-to-service-account-key-file.json>
```
</Accordion>
<Accordion title="gcpIdTokenAuth">
The GCP ID Token machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within GCP environments.
[Read more about Azure Auth](/documentation/platform/identities/gcp-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
Example:
```yaml
spec:
gcpIdTokenAuth:
identityId: <machine-identity-id>
```
</Accordion>
</Accordion>
<Accordion title="tls">
This block defines the TLS settings to use for connecting to the Infisical
instance.
Fields:
<Accordion title="caRef">
This block defines the reference to the CA certificate to use for connecting to the Infisical instance with SSL/TLS.
Valid fields:
- `secretName`: The name of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS.
- `secretNamespace`: The namespace of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS.
- `key`: The name of the key in the Kubernetes secret which contains the value of the CA certificate to use for connecting to the Infisical instance with SSL/TLS.
Example:
```yaml
tls:
caRef:
secretName: custom-ca-certificate
secretNamespace: default
key: ca.crt
```
</Accordion>
</Accordion>
### Applying the InfisicalDynamicSecret CRD to your cluster
Once you have configured the `InfisicalDynamicSecret` CRD with the required fields, you can apply it to your cluster. After applying, you should notice that a lease has been created in Infisical and synced to your Kubernetes cluster.
```bash
kubectl apply -f dynamic-secret-crd.yaml
```
## Auto redeployment
Deployments referring to Kubernetes secrets containing Infisical dynamic secrets don't automatically reload when the dynamic secret lease expires. This means your deployment may use expired dynamic secrets unless manually redeployed.
To address this, we've added functionality to automatically redeploy your deployment when the associated Kubernetes secret containing your Infisical dynamic secret updates.
#### Enabling auto redeploy
To enable auto redeployment you simply have to add the following annotation to the deployment that consumes a managed secret
```yaml
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:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
envFrom:
- secretRef:
name: managed-secret # The name of your managed secret, the same that you're using in your InfisicalDynamicSecret CRD (spec.managedSecretReference.secretName)
ports:
- containerPort: 80
```
</Accordion>
<Info>
#### How it works
When the lease changes, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update.
Then, for each deployment that has this annotation present, a rolling update will be triggered. A redeployment won't happen if the lease is renewed, only if it's recreated.
</Info>

@ -0,0 +1,400 @@
---
sidebarTitle: "InfisicalPushSecret CRD"
title: "Using the InfisicalPushSecret CRD"
description: "Learn how to use the InfisicalPushSecret CRD to push and manage secrets in Infisical."
---
## Push Secrets to Infisical
### Example usage
Below is a sample InfisicalPushSecret CRD that pushes secrets defined in a Kubernetes secret to Infisical.
After filling out the fields in the InfisicalPushSecret CRD, you can apply it directly to your cluster.
Before applying the InfisicalPushSecret CRD, you need to create a Kubernetes secret containing the secrets you want to push to Infisical. An example can be seen below the InfisicalPushSecret CRD.
```yaml infisical-push-secret.yaml
apiVersion: secrets.infisical.com/v1alpha1
kind: InfisicalPushSecret
metadata:
name: infisical-push-secret-demo
spec:
resyncInterval: 1m
hostAPI: https://app.infisical.com/api
# Optional, defaults to no 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 # Secret CRD
secretNamespace: default
# 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
```
```yaml source-secret.yaml
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
```
```bash
kubectl apply -f source-secret.yaml
```
After applying the soruce-secret.yaml file, you are ready to apply the InfisicalPushSecret CRD.
```bash
kubectl apply -f infisical-push-secret.yaml
```
After applying the InfisicalPushSecret CRD, you should notice that the secrets you have defined in your source-secret.yaml file have been pushed to your specified destination in Infisical.
### InfisicalPushSecret CRD properties
<Accordion title="hostAPI">
If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to
` https://your-self-hosted-instace.com/api`
When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud.
<Accordion title="Advanced use case">
If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet.
To achieve this, use the following address for the hostAPI field:
``` bash
http://<backend-svc-name>.<namespace>.svc.cluster.local:4000/api
```
Make sure to replace `<backend-svc-name>` and `<namespace>` with the appropriate values for your backend service and namespace.
</Accordion>
</Accordion>
<Accordion title="resyncInterval">
The `resyncInterval` is a string-formatted duration that defines the time between each resync.
The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time.
The following units are supported:
- `s` for seconds (must be at least 5 seconds)
- `m` for minutes
- `h` for hours
- `d` for days
- `w` for weeks
The default value is `1m` (1 minute).
Valid intervals examples:
```yaml
resyncInterval: 5s # 10 seconds
resyncInterval: 10s # 10 seconds
resyncInterval: 5m # 5 minutes
resyncInterval: 1h # 1 hour
resyncInterval: 1d # 1 day
```
</Accordion>
<Accordion title="updatePolicy">
The field is optional and will default to `None` if not defined.
The update policy defines how the operator should handle conflicting secrets when pushing secrets to Infisical.
Valid values are `None` and `Replace`.
Behavior of each policy:
- `None`: The operator will not override existing secrets in Infisical. If a secret with the same key already exists, the operator will skip pushing that secret, and the secret will not be managed by the operator.
- `Replace`: The operator will replace existing secrets in Infisical with the new secrets. If a secret with the same key already exists, the operator will update the secret with the new value.
```yaml
spec:
updatePolicy: Replace
```
</Accordion>
<Accordion title="deletionPolicy">
This field is optional and will default to `None` if not defined.
The deletion policy defines what the operator should do in case the InfisicalPushSecret CRD is deleted.
Valid values are `None` and `Delete`.
Behavior of each policy:
- `None`: The operator will not delete the secrets in Infisical when the InfisicalPushSecret CRD is deleted.
- `Delete`: The operator will delete the secrets in Infisical that are managed by the operator when the InfisicalPushSecret CRD is deleted.
```yaml
spec:
deletionPolicy: Delete
```
</Accordion>
<Accordion title="destination">
The `destination` field is used to specify where you want to create the secrets in Infisical. The required fields are `projectId`, `environmentSlug`, and `secretsPath`.
```yaml
spec:
destination:
projectId: <project-id>
environmentSlug: <env-slug>
secretsPath: <secrets-path>
```
<Accordion title="destination.projectId">
The project ID where you want to create the secrets in Infisical.
</Accordion>
<Accordion title="destination.environmentSlug">
The environment slug where you want to create the secrets in Infisical.
</Accordion>
<Accordion title="destination.secretsPath">
The path where you want to create the secrets in Infisical. The root path is `/`.
</Accordion>
</Accordion>
<Accordion title="push">
The `push` field is used to define what you want to push to Infisical. Currently the operator only supports pushing Kubernetes secrets to Infisical. An example of the `push` field is shown below.
<Accordion title="secret">
The `secret` field is used to define the Kubernetes secret you want to push to Infisical. The required fields are `secretName` and `secretNamespace`.
Example usage of the `push.secret` field:
```yaml infisical-push-secret.yaml
push:
secret:
secretName: push-secret-demo
secretNamespace: default
```
```yaml push-secret-demo.yaml
apiVersion: v1
kind: Secret
metadata:
name: push-secret-demo
namespace: default
# Pass in the secrets you wish to push to Infisical
stringData:
API_KEY: some-api-key
DATABASE_URL: postgres://127.0.0.1:5432
ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab
```
</Accordion>
</Accordion>
<Accordion title="authentication">
The `authentication` field dictates which authentication method to use when pushing secrets to Infisical.
The available authentication methods are `universalAuth`, `kubernetesAuth`, `awsIamAuth`, `azureAuth`, `gcpIdTokenAuth`, and `gcpIamAuth`.
<Accordion title="universalAuth">
The universal authentication method is one of the easiest ways to get started with Infisical. Universal Auth works anywhere and is not tied to any specific cloud provider.
[Read more about Universal Auth](/documentation/platform/identities/universal-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
- `credentialsRef`: The name and namespace of the Kubernetes secret that stores the service token.
- `credentialsRef.secretName`: The name of the Kubernetes secret.
- `credentialsRef.secretNamespace`: The namespace of the Kubernetes secret.
Example:
```yaml
# infisical-push-secret.yaml
spec:
universalAuth:
credentialsRef:
secretName: <secret-name>
secretNamespace: <secret-namespace>
```
```yaml
# machine-identity-credentials.yaml
apiVersion: v1
kind: Secret
metadata:
name: universal-auth-credentials
type: Opaque
stringData:
clientId: <machine-identity-client-id>
clientSecret: <machine-identity-client-secret>
```
</Accordion>
<Accordion title="kubernetesAuth">
The Kubernetes machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within a Kubernetes environment.
[Read more about Kubernetes Auth](/documentation/platform/identities/kubernetes-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
- `serviceAccountRef`: The name and namespace of the service account that will be used to authenticate with Infisical.
- `serviceAccountRef.name`: The name of the service account.
- `serviceAccountRef.namespace`: The namespace of the service account.
Example:
```yaml
spec:
kubernetesAuth:
identityId: <machine-identity-id>
serviceAccountRef:
name: <secret-name>
namespace: <secret-namespace>
```
</Accordion>
<Accordion title="awsIamAuth">
The AWS IAM machine identity authentication method is used to authenticate with Infisical.
[Read more about AWS IAM Auth](/documentation/platform/identities/aws-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
Example:
```yaml
spec:
authentication:
awsIamAuth:
identityId: <machine-identity-id>
```
</Accordion>
<Accordion title="azureAuth">
The AWS IAM machine identity authentication method is used to authenticate with Infisical. Azure Auth can only be used from within an Azure environment.
[Read more about Azure Auth](/documentation/platform/identities/azure-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
Example:
```yaml
spec:
authentication:
azureAuth:
identityId: <machine-identity-id>
```
</Accordion>
<Accordion title="gcpIamAuth">
The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used both within and outside GCP environments.
[Read more about Azure Auth](/documentation/platform/identities/gcp-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
- `serviceAccountKeyFilePath`: The path to the GCP service account key file.
Example:
```yaml
spec:
gcpIamAuth:
identityId: <machine-identity-id>
serviceAccountKeyFilePath: </path-to-service-account-key-file.json>
```
</Accordion>
<Accordion title="gcpIdTokenAuth">
The GCP ID Token machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within GCP environments.
[Read more about Azure Auth](/documentation/platform/identities/gcp-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
Example:
```yaml
spec:
gcpIdTokenAuth:
identityId: <machine-identity-id>
```
</Accordion>
</Accordion>
<Accordion title="tls">
This block defines the TLS settings to use for connecting to the Infisical
instance.
Fields:
<Accordion title="caRef">
This block defines the reference to the CA certificate to use for connecting to the Infisical instance with SSL/TLS.
Valid fields:
- `secretName`: The name of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS.
- `secretNamespace`: The namespace of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS.
- `key`: The name of the key in the Kubernetes secret which contains the value of the CA certificate to use for connecting to the Infisical instance with SSL/TLS.
Example:
```yaml
tls:
caRef:
secretName: custom-ca-certificate
secretNamespace: default
key: ca.crt
```
</Accordion>
</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.
After applying, you should notice that the secrets have been pushed to Infisical.
```bash
kubectl apply -f source-push-secret.yaml # The secret that you're referencing in the InfisicalPushSecret CRD push.secret field
kubectl apply -f example-infisical-push-secret-crd.yaml # The InfisicalPushSecret CRD itself
```

@ -1,109 +1,9 @@
---
title: "Kubernetes Operator"
description: "How to use Infisical to inject secrets into Kubernetes clusters."
sidebarTitle: "InfisicalSecret CRD"
title: "InfisicalSecret CRD"
description: "Learn how to use the InfisicalSecret CRD to fetch secrets from Infisical and store them as native Kubernetes secret resource"
---
![title](../../images/k8-diagram.png)
The Infisical Secrets Operator is a Kubernetes controller that retrieves secrets from Infisical and stores them in a designated cluster.
It uses an `InfisicalSecret` resource to specify authentication and storage methods.
The operator continuously updates secrets and can also reload dependent deployments automatically.
<Note>
If you are already using the External Secrets operator, you can view the
integration documentation for it
[here](https://external-secrets.io/latest/provider/infisical/).
</Note>
## Install Operator
The operator can be install via [Helm](https://helm.sh) or [kubectl](https://github.com/kubernetes/kubectl)
<Tabs>
<Tab title="Helm (recommended)">
**Install the latest Infisical Helm repository**
```bash
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
helm repo update
```
**Install the Helm chart**
To select a specific version, view the application versions [here](https://hub.docker.com/r/infisical/kubernetes-operator/tags) and chart versions [here](https://cloudsmith.io/~infisical/repos/helm-charts/packages/detail/helm/secrets-operator/#versions)
```bash
helm install --generate-name infisical-helm-charts/secrets-operator
```
```bash
# Example installing app version v0.2.0 and chart version 0.1.4
helm install --generate-name infisical-helm-charts/secrets-operator --version=0.1.4 --set controllerManager.manager.image.tag=v0.2.0
```
**Namespace-scoped Installation**
The operator can be configured to watch and manage secrets in a specific namespace instead of having cluster-wide access. This is useful for:
- **Enhanced Security**: Limit the operator's permissions to only specific namespaces instead of cluster-wide access
- **Multi-tenant Clusters**: Run separate operator instances for different teams or applications
- **Resource Isolation**: Ensure operators in different namespaces don't interfere with each other
- **Development & Testing**: Run development and production operators side by side in isolated namespaces
**Note**: For multiple namespace-scoped installations, only the first installation should install CRDs. Subsequent installations should set `installCRDs: false` to avoid conflicts.
```bash
# First namespace installation (with CRDs)
helm install operator-namespace1 infisical-helm-charts/secrets-operator \
--namespace first-namespace \
--set scopedNamespace=first-namespace \
--set scopedRBAC=true
# Subsequent namespace installations
helm install operator-namespace2 infisical-helm-charts/secrets-operator \
--namespace another-namespace \
--set scopedNamespace=another-namespace \
--set scopedRBAC=true \
--set installCRDs=false
```
When scoped to a namespace, the operator will:
- Only watch InfisicalSecrets in the specified namespace
- Only create/update Kubernetes secrets in that namespace
- Only access deployments in that namespace
The default configuration gives cluster-wide access:
```yaml
installCRDs: true # Install CRDs (set to false for additional namespace installations)
scopedNamespace: "" # Empty for cluster-wide access
scopedRBAC: false # Cluster-wide permissions
```
If you want to install operators in multiple namespaces simultaneously:
- Make sure to set `installCRDs: false` for all but one of the installations to avoid conflicts, as CRDs are cluster-wide resources.
- Use unique release names for each installation (e.g., operator-namespace1, operator-namespace2).
</Tab>
<Tab title="Kubectl">
For production deployments, it is highly recommended to set the version of the Kubernetes operator manually instead of pointing to the latest version.
Doing so will help you avoid accidental updates to the newest release which may introduce unintended breaking changes. View all application versions [here](https://hub.docker.com/r/infisical/kubernetes-operator/tags).
The command below will install the most recent version of the Kubernetes operator.
However, to set the version manually, download the manifest and set the image tag version of `infisical/kubernetes-operator` according to your desired version.
Once you apply the manifest, the operator will be installed in `infisical-operator-system` namespace.
```
kubectl apply -f https://raw.githubusercontent.com/Infisical/infisical/main/k8-operator/kubectl-install/install-secrets-operator.yaml
```
</Tab>
</Tabs>
## Sync Infisical Secrets to your cluster
Once you have installed the operator to your cluster, you'll need to create a `InfisicalSecret` custom resource definition (CRD).
```yaml example-infisical-secret-crd.yaml
@ -789,48 +689,6 @@ This is useful for tools such as ArgoCD, where every resource requires an owner
</Accordion>
### Propagating labels & annotations
The operator will transfer all labels & annotations present on the `InfisicalSecret` CRD to the managed Kubernetes secret to be created.
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:
...
managedSecretReference:
...
```
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
```
</Accordion>
### Apply the InfisicalSecret CRD to your cluster
Once you have configured the InfisicalSecret CRD with the required fields, you can apply it to your cluster.
@ -854,7 +712,7 @@ kubectl get secrets -n <namespace of managed secret>
1 minutes.
</Info>
### Using managed secret in your deployment
## Using managed secret in your deployment
Incorporating the managed secret created by the operator into your deployment can be achieved through several methods.
Here, we will highlight three of the most common ways to utilize it. Learn more about Kubernetes secrets [here](https://kubernetes.io/docs/concepts/configuration/secret/)
@ -996,25 +854,6 @@ spec:
</Accordion>
### Connecting to instances with private/self-signed certificate
To connect to Infisical instances behind a private/self-signed certificate, you can configure the TLS settings in the `InfisicalSecret` CRD
to point to a CA certificate stored in a Kubernetes secret resource.
```yaml
---
spec:
hostAPI: https://app.infisical.com/api
resyncInterval: 10
tls:
caRef:
secretName: custom-ca-certificate
secretNamespace: default
key: ca.crt
authentication:
---
```
The definition file of the Kubernetes secret for the CA certificate can be structured like the following:
```yaml
@ -1081,503 +920,44 @@ spec:
Then, for each deployment that has this annotation present, a rolling update will be triggered.
</Info>
## Push Secrets to Infisical
## Propagating labels & annotations
The operator will transfer all labels & annotations present on the `InfisicalSecret` CRD to the managed Kubernetes secret to be created.
Thus, if a specific label is required on the resulting secret, it can be applied as demonstrated in the following example:
### Example usage
Below is a sample InfisicalPushSecret CRD that pushes secrets defined in a Kubernetes secret to Infisical.
After filling out the fields in the InfisicalPushSecret CRD, you can apply it directly to your cluster.
Before applying the InfisicalPushSecret CRD, you need to create a Kubernetes secret containing the secrets you want to push to Infisical. An example can be seen below the InfisicalPushSecret CRD.
```bash
kubectl apply -f source-secret.yaml
```
After applying the soruce-secret.yaml file, you are ready to apply the InfisicalPushSecret CRD.
```bash
kubectl apply -f infisical-push-secret.yaml
```
After applying the InfisicalPushSecret CRD, you should notice that the secrets you have defined in your source-secret.yaml file have been pushed to your specified destination in Infisical.
```yaml infisical-push-secret.yaml
apiVersion: secrets.infisical.com/v1alpha1
kind: InfisicalPushSecret
metadata:
name: infisical-push-secret-demo
spec:
resyncInterval: 1m
hostAPI: https://app.infisical.com/api
# Optional, defaults to no 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 # Secret CRD
secretNamespace: default
# 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
```
```yaml source-secret.yaml
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
```
### InfisicalPushSecret CRD properties
<Accordion title="hostAPI">
If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to
` https://your-self-hosted-instace.com/api`
When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud.
<Accordion title="Advanced use case">
If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet.
To achieve this, use the following address for the hostAPI field:
``` bash
http://<backend-svc-name>.<namespace>.svc.cluster.local:4000/api
```
Make sure to replace `<backend-svc-name>` and `<namespace>` with the appropriate values for your backend service and namespace.
</Accordion>
</Accordion>
<Accordion title="resyncInterval">
The `resyncInterval` is a string-formatted duration that defines the time between each resync.
The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time.
The following units are supported:
- `s` for seconds (must be at least 5 seconds)
- `m` for minutes
- `h` for hours
- `d` for days
- `w` for weeks
The default value is `1m` (1 minute).
Valid intervals examples:
```yaml
resyncInterval: 5s # 10 seconds
resyncInterval: 10s # 10 seconds
resyncInterval: 5m # 5 minutes
resyncInterval: 1h # 1 hour
resyncInterval: 1d # 1 day
```
</Accordion>
<Accordion title="updatePolicy">
The field is optional and will default to `None` if not defined.
The update policy defines how the operator should handle conflicting secrets when pushing secrets to Infisical.
Valid values are `None` and `Replace`.
Behavior of each policy:
- `None`: The operator will not override existing secrets in Infisical. If a secret with the same key already exists, the operator will skip pushing that secret, and the secret will not be managed by the operator.
- `Replace`: The operator will replace existing secrets in Infisical with the new secrets. If a secret with the same key already exists, the operator will update the secret with the new value.
```yaml
spec:
updatePolicy: Replace
```
</Accordion>
<Accordion title="deletionPolicy">
This field is optional and will default to `None` if not defined.
The deletion policy defines what the operator should do in case the InfisicalPushSecret CRD is deleted.
Valid values are `None` and `Delete`.
Behavior of each policy:
- `None`: The operator will not delete the secrets in Infisical when the InfisicalPushSecret CRD is deleted.
- `Delete`: The operator will delete the secrets in Infisical that are managed by the operator when the InfisicalPushSecret CRD is deleted.
```yaml
spec:
deletionPolicy: Delete
```
</Accordion>
<Accordion title="destination">
The `destination` field is used to specify where you want to create the secrets in Infisical. The required fields are `projectId`, `environmentSlug`, and `secretsPath`.
```yaml
spec:
destination:
projectId: <project-id>
environmentSlug: <env-slug>
secretsPath: <secrets-path>
```
<Accordion title="destination.projectId">
The project ID where you want to create the secrets in Infisical.
</Accordion>
<Accordion title="destination.environmentSlug">
The environment slug where you want to create the secrets in Infisical.
</Accordion>
<Accordion title="destination.secretsPath">
The path where you want to create the secrets in Infisical. The root path is `/`.
</Accordion>
</Accordion>
<Accordion title="push">
The `push` field is used to define what you want to push to Infisical. Currently the operator only supports pushing Kubernetes secrets to Infisical. An example of the `push` field is shown below.
<Accordion title="secret">
The `secret` field is used to define the Kubernetes secret you want to push to Infisical. The required fields are `secretName` and `secretNamespace`.
Example usage of the `push.secret` field:
```yaml infisical-push-secret.yaml
push:
secret:
secretName: push-secret-demo
secretNamespace: default
```
```yaml push-secret-demo.yaml
apiVersion: v1
kind: Secret
metadata:
name: push-secret-demo
namespace: default
# Pass in the secrets you wish to push to Infisical
stringData:
API_KEY: some-api-key
DATABASE_URL: postgres://127.0.0.1:5432
ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab
```
</Accordion>
</Accordion>
<Accordion title="authentication">
The `authentication` field dictates which authentication method to use when pushing secrets to Infisical.
The available authentication methods are `universalAuth`, `kubernetesAuth`, `awsIamAuth`, `azureAuth`, `gcpIdTokenAuth`, and `gcpIamAuth`.
<Accordion title="universalAuth">
The universal authentication method is one of the easiest ways to get started with Infisical. Universal Auth works anywhere and is not tied to any specific cloud provider.
[Read more about Universal Auth](/documentation/platform/identities/universal-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
- `credentialsRef`: The name and namespace of the Kubernetes secret that stores the service token.
- `credentialsRef.secretName`: The name of the Kubernetes secret.
- `credentialsRef.secretNamespace`: The namespace of the Kubernetes secret.
Example:
```yaml
# infisical-push-secret.yaml
spec:
universalAuth:
credentialsRef:
secretName: <secret-name>
secretNamespace: <secret-namespace>
```
```yaml
# machine-identity-credentials.yaml
apiVersion: v1
kind: Secret
metadata:
name: universal-auth-credentials
type: Opaque
stringData:
clientId: <machine-identity-client-id>
clientSecret: <machine-identity-client-secret>
```
</Accordion>
<Accordion title="kubernetesAuth">
The Kubernetes machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within a Kubernetes environment.
[Read more about Kubernetes Auth](/documentation/platform/identities/kubernetes-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
- `serviceAccountRef`: The name and namespace of the service account that will be used to authenticate with Infisical.
- `serviceAccountRef.name`: The name of the service account.
- `serviceAccountRef.namespace`: The namespace of the service account.
Example:
```yaml
spec:
kubernetesAuth:
identityId: <machine-identity-id>
serviceAccountRef:
name: <secret-name>
namespace: <secret-namespace>
```
</Accordion>
<Accordion title="awsIamAuth">
The AWS IAM machine identity authentication method is used to authenticate with Infisical.
[Read more about AWS IAM Auth](/documentation/platform/identities/aws-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
Example:
```yaml
spec:
authentication:
awsIamAuth:
identityId: <machine-identity-id>
```
</Accordion>
<Accordion title="azureAuth">
The AWS IAM machine identity authentication method is used to authenticate with Infisical. Azure Auth can only be used from within an Azure environment.
[Read more about Azure Auth](/documentation/platform/identities/azure-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
Example:
```yaml
spec:
authentication:
azureAuth:
identityId: <machine-identity-id>
```
</Accordion>
<Accordion title="gcpIamAuth">
The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used both within and outside GCP environments.
[Read more about Azure Auth](/documentation/platform/identities/gcp-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
- `serviceAccountKeyFilePath`: The path to the GCP service account key file.
Example:
```yaml
spec:
gcpIamAuth:
identityId: <machine-identity-id>
serviceAccountKeyFilePath: </path-to-service-account-key-file.json>
```
</Accordion>
<Accordion title="gcpIdTokenAuth">
The GCP ID Token machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within GCP environments.
[Read more about Azure Auth](/documentation/platform/identities/gcp-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
Example:
```yaml
spec:
gcpIdTokenAuth:
identityId: <machine-identity-id>
```
</Accordion>
</Accordion>
<Accordion title="tls">
This block defines the TLS settings to use for connecting to the Infisical
instance.
Fields:
<Accordion title="caRef">
This block defines the reference to the CA certificate to use for connecting to the Infisical instance with SSL/TLS.
Valid fields:
- `secretName`: The name of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS.
- `secretNamespace`: The namespace of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS.
- `key`: The name of the key in the Kubernetes secret which contains the value of the CA certificate to use for connecting to the Infisical instance with SSL/TLS.
Example:
```yaml
tls:
caRef:
secretName: custom-ca-certificate
secretNamespace: default
key: ca.crt
```
</Accordion>
</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.
After applying, you should notice that the secrets have been pushed to Infisical.
```bash
kubectl apply -f source-push-secret.yaml # The secret that you're referencing in the InfisicalPushSecret CRD push.secret field
kubectl apply -f example-infisical-push-secret-crd.yaml # The InfisicalPushSecret CRD itself
```
### Connecting to instances with private/self-signed certificate
To connect to Infisical instances behind a private/self-signed certificate, you can configure the TLS settings in the `InfisicalPushSecret` CRD
to point to a CA certificate stored in a Kubernetes secret resource.
<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:
hostAPI: https://app.infisical.com/api
resyncInterval: 30s
tls:
caRef:
secretName: custom-ca-certificate
secretNamespace: default
key: ca.crt
..
authentication:
# ...
...
managedSecretReference:
...
```
## Global configuration
To configure global settings that will apply to all instances of `InfisicalSecret`, you can define these configurations in a Kubernetes ConfigMap.
For example, you can configure all `InfisicalSecret` instances to fetch secrets from a single backend API without specifying the `hostAPI` parameter for each instance.
### Available global properties
| Property | Description | Default value |
| -------- | --------------------------------------------------------------------------------- | ----------------------------- |
| hostAPI | If `hostAPI` in `InfisicalSecret` instance is left empty, this value will be used | https://app.infisical.com/api |
### Applying global configurations
All global configurations must reside in a Kubernetes ConfigMap named `infisical-config` in the namespace `infisical-operator-system`.
To apply global configuration to the operator, copy the following yaml into `infisical-config.yaml` file.
```yaml infisical-config.yaml
apiVersion: v1
kind: Namespace
metadata:
name: infisical-operator-system
---
apiVersion: v1
kind: ConfigMap
metadata:
name: infisical-config
namespace: infisical-operator-system
data:
hostAPI: https://example.com/api # <-- global hostAPI
```
Then apply this change via kubectl by running the following
```bash
kubectl apply -f infisical-config.yaml
```
## Troubleshoot operator
If the operator is unable to fetch secrets from the API, it will not affect the managed Kubernetes secret.
It will continue attempting to reconnect to the API indefinitely.
The InfisicalSecret resource uses the `status.conditions` field to report its current state and any errors encountered.
This would result in the following managed secret to be created:
```yaml
$ kubectl get infisicalSecrets
NAME AGE
infisicalsecret-sample 12s
$ kubectl describe infisicalSecret infisicalsecret-sample
...
Spec:
...
Status:
Conditions:
Last Transition Time: 2022-12-18T04:29:09Z
Message: Infisical controller has located the Infisical token in provided Kubernetes secret
Reason: OK
Status: True
Type: secrets.infisical.com/LoadedInfisicalToken
Last Transition Time: 2022-12-18T04:29:10Z
Message: Failed to update secret because: 400 Bad Request
Reason: Error
Status: False
Type: secrets.infisical.com/ReadyToSyncSecrets
Events: <none>
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
```
## Uninstall Operator
The managed secret created by the operator will not be deleted when the operator is uninstalled.
<Tabs>
<Tab title="Helm">
Install Infisical Helm repository
```bash
helm uninstall <release name>
```
</Tab>
<Tab title="Kubectl">
```
kubectl delete -f https://raw.githubusercontent.com/Infisical/infisical/main/k8-operator/kubectl-install/install-secrets-operator.yaml
```
</Tab>
</Tabs>
## Useful Articles
- [Managing secrets in OpenShift with Infisical](https://xphyr.net/post/infisical_ocp/)
</Accordion>

@ -0,0 +1,195 @@
---
title: "Kubernetes Operator"
sidebarTitle: "Overview"
description: "How to use Infisical to inject, push, and manage secrets within Kubernetes clusters"
---
The Infisical Operator is a collection of Kubernetes controllers that streamline how secrets are managed between Infisical and your Kubernetes cluster.
It provides multiple Custom Resource Definitions (CRDs) which enable you to:
- **Sync** secrets from Infisical into Kubernetes (`InfisicalSecret`).
- **Push** new secrets from Kubernetes to Infisical (`InfisicalPushSecret`).
- **Manage** dynamic secrets and automatically create time-bound leases (`InfisicalDynamicSecret`).
When these CRDs are configured, the Infisical Operator will continuously monitors for changes and performs necessary updates to keep your Kubernetes secrets up to date.
It can also automatically reload dependent Deployments resources whenever relevant secrets are updated.
<Note>
If you are already using the External Secrets operator, you can view the
integration documentation for it
[here](https://external-secrets.io/latest/provider/infisical/).
</Note>
## Install
The operator can be install via [Helm](https://helm.sh). Helm is a package manager for Kubernetes that allows you to define, install, and upgrade Kubernetes applications.
**Install the latest Helm repository**
```bash
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
```
```bash
helm repo update
```
The operator can be installed either cluster-wide or restricted to a specific namespace.
If you require stronger isolation and stricter access controls, a namespace-scoped installation may make more sense.
<Tabs>
<Tab title="Cluster Wide Installation">
```bash
helm install --generate-name infisical-helm-charts/secrets-operator
```
</Tab>
<Tab title="Namespace Scoped Installation">
The operator can be configured to watch and manage secrets in a specific namespace instead of having cluster-wide access. This is useful for:
- **Enhanced Security**: Limit the operator's permissions to only specific namespaces instead of cluster-wide access
- **Multi-tenant Clusters**: Run separate operator instances for different teams or applications
- **Resource Isolation**: Ensure operators in different namespaces don't interfere with each other
- **Development & Testing**: Run development and production operators side by side in isolated namespaces
**Note**: For multiple namespace-scoped installations, only the first installation should install CRDs. Subsequent installations should set `installCRDs: false` to avoid conflicts.
```bash
# First namespace installation (with CRDs)
helm install operator-namespace1 infisical-helm-charts/secrets-operator \
--namespace first-namespace \
--set scopedNamespace=first-namespace \
--set scopedRBAC=true
# Subsequent namespace installations
helm install operator-namespace2 infisical-helm-charts/secrets-operator \
--namespace another-namespace \
--set scopedNamespace=another-namespace \
--set scopedRBAC=true \
--set installCRDs=false
```
When scoped to a namespace, the operator will:
- Only watch InfisicalSecrets in the specified namespace
- Only create/update Kubernetes secrets in that namespace
- Only access deployments in that namespace
The default configuration gives cluster-wide access:
```yaml
installCRDs: true # Install CRDs (set to false for additional namespace installations)
scopedNamespace: "" # Empty for cluster-wide access
scopedRBAC: false # Cluster-wide permissions
```
If you want to install operators in multiple namespaces simultaneously:
- Make sure to set `installCRDs: false` for all but one of the installations to avoid conflicts, as CRDs are cluster-wide resources.
- Use unique release names for each installation (e.g., operator-namespace1, operator-namespace2).
</Tab>
</Tabs>
## Custom Resource Definitions
Currently the operator supports the following CRD's. We are constantly expanding the functionality of the operator, and this list will be updated as new CRD's are added.
1. [InfisicalSecret](/integrations/platforms/kubernetes/infisical-secret-crd): Sync secrets from Infisical to a Kubernetes secret.
2. [InfisicalPushSecret](/integrations/platforms/kubernetes/infisical-push-secret-crd): Push secrets from a Kubernetes secret to Infisical.
3. [InfisicalDynamicSecret](/integrations/platforms/kubernetes/infisical-dynamic-secret-crd): Sync dynamic secrets and create leases automatically in Kubernetes.
## General Configuration
### Private/self-signed certificate
To connect to Infisical instances behind a private/self-signed certificate, you can configure the TLS settings in the CRD
to point to a CA certificate stored in a Kubernetes secret resource.
```yaml
---
spec:
hostAPI: https://app.infisical.com/api
tls:
caRef:
secretName: custom-ca-certificate
secretNamespace: default
key: ca.crt
---
```
## Global configuration
To configure global settings that will apply to all instances of `InfisicalSecret`, you can define these configurations in a Kubernetes ConfigMap.
For example, you can configure all `InfisicalSecret` instances to fetch secrets from a single backend API without specifying the `hostAPI` parameter for each instance.
### Available global properties
| Property | Description | Default value |
| -------- | --------------------------------------------------------------------------------- | ----------------------------- |
| hostAPI | If `hostAPI` in `InfisicalSecret` instance is left empty, this value will be used | https://app.infisical.com/api |
### Applying global configurations
All global configurations must reside in a Kubernetes ConfigMap named `infisical-config` in the namespace `infisical-operator-system`.
To apply global configuration to the operator, copy the following yaml into `infisical-config.yaml` file.
```yaml infisical-config.yaml
apiVersion: v1
kind: Namespace
metadata:
name: infisical-operator-system
---
apiVersion: v1
kind: ConfigMap
metadata:
name: infisical-config
namespace: infisical-operator-system
data:
hostAPI: https://example.com/api # <-- global hostAPI
```
Then apply this change via kubectl by running the following
```bash
kubectl apply -f infisical-config.yaml
```
## Troubleshoot operator
If the operator is unable to fetch secrets from the API, it will not affect the managed Kubernetes secret.
It will continue attempting to reconnect to the API indefinitely.
The InfisicalSecret resource uses the `status.conditions` field to report its current state and any errors encountered.
```yaml
$ kubectl get infisicalSecrets
NAME AGE
infisicalsecret-sample 12s
$ kubectl describe infisicalSecret infisicalsecret-sample
...
Spec:
...
Status:
Conditions:
Last Transition Time: 2022-12-18T04:29:09Z
Message: Infisical controller has located the Infisical token in provided Kubernetes secret
Reason: OK
Status: True
Type: secrets.infisical.com/LoadedInfisicalToken
Last Transition Time: 2022-12-18T04:29:10Z
Message: Failed to update secret because: 400 Bad Request
Reason: Error
Status: False
Type: secrets.infisical.com/ReadyToSyncSecrets
Events: <none>
```
## Uninstall Operator
The managed secret created by the operator will not be deleted when the operator is uninstalled.
<Tabs>
<Tab title="Helm">
Install Infisical Helm repository
```bash
helm uninstall <release name>
```
</Tab>
</Tabs>

@ -349,7 +349,15 @@
{
"group": "Container orchestrators",
"pages": [
"integrations/platforms/kubernetes",
{
"group": "Kubernetes",
"pages": [
"integrations/platforms/kubernetes/overview",
"integrations/platforms/kubernetes/infisical-secret-crd",
"integrations/platforms/kubernetes/infisical-push-secret-crd",
"integrations/platforms/kubernetes/infisical-dynamic-secret-crd"
]
},
"integrations/platforms/kubernetes-csi",
"integrations/platforms/docker-swarm-with-agent",
"integrations/platforms/ecs-with-agent"

@ -4,7 +4,12 @@ import { createNotification } from "@app/components/notifications";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace";
import { TCloudIntegration, TIntegrationWithEnv, TOctopusDeployScopeValues } from "./types";
import {
IntegrationMetadataSyncMode,
TCloudIntegration,
TIntegrationWithEnv,
TOctopusDeployScopeValues
} from "./types";
export const integrationQueryKeys = {
getIntegrations: () => ["integrations"] as const,
@ -88,6 +93,7 @@ export const useCreateIntegration = () => {
shouldProtectSecrets?: boolean;
shouldEnableDelete?: boolean;
octopusDeployScopeValues?: TOctopusDeployScopeValues;
metadataSyncMode?: IntegrationMetadataSyncMode;
};
}) => {
const {

@ -62,6 +62,7 @@ export type TIntegration = {
octopusDeployScopeValues?: TOctopusDeployScopeValues;
awsIamRole?: string;
region?: string;
metadataSyncMode?: IntegrationMetadataSyncMode;
};
};
@ -92,3 +93,8 @@ export enum IntegrationMappingBehavior {
ONE_TO_ONE = "one-to-one",
MANY_TO_ONE = "many-to-one"
}
export enum IntegrationMetadataSyncMode {
CUSTOM = "custom",
SECRET_METADATA = "secret-metadata"
}

@ -82,6 +82,7 @@ export type TSecretApprovalRequest = {
conflicts: Array<{ secretId: string; op: CommitType.UPDATE }>;
commits: ({
// if there is no secret means it was creation
secretMetadata?: { key: string; value: string }[];
secret?: { version: number };
secretVersion: SecretV3Raw;
// if there is no new version its for Delete

@ -84,7 +84,8 @@ export const useUpdateSecretV3 = ({
secretReminderRepeatDays,
secretReminderNote,
newSecretName,
skipMultilineEncoding
skipMultilineEncoding,
secretMetadata
}) => {
const { data } = await apiRequest.patch(`/api/v3/secrets/raw/${secretKey}`, {
workspaceId,
@ -97,7 +98,8 @@ export const useUpdateSecretV3 = ({
newSecretName,
secretComment,
tagIds,
secretValue
secretValue,
secretMetadata
});
return data;
},

@ -10,6 +10,7 @@ import { useToggle } from "@app/hooks/useToggle";
import { ERROR_NOT_ALLOWED_READ_SECRETS } from "./constants";
import {
GetSecretVersionsDTO,
SecretAccessListEntry,
SecretType,
SecretV3Raw,
SecretV3RawResponse,
@ -18,6 +19,7 @@ import {
TGetProjectSecretsAllEnvDTO,
TGetProjectSecretsDTO,
TGetProjectSecretsKey,
TGetSecretAccessListDTO,
TGetSecretReferenceTreeDTO,
TSecretReferenceTraceNode
} from "./types";
@ -27,6 +29,13 @@ export const secretKeys = {
getProjectSecret: ({ workspaceId, environment, secretPath }: TGetProjectSecretsKey) =>
[{ workspaceId, environment, secretPath }, "secrets"] as const,
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const,
getSecretAccessList: ({
workspaceId,
environment,
secretPath,
secretKey
}: TGetSecretAccessListDTO) =>
["secret-access-list", { workspaceId, environment, secretPath, secretKey }] as const,
getSecretReferenceTree: (dto: TGetSecretReferenceTreeDTO) => ["secret-reference-tree", dto]
};
@ -67,7 +76,8 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
updatedAt: el.updatedAt,
version: el.version,
skipMultilineEncoding: el.skipMultilineEncoding,
path: el.secretPath
path: el.secretPath,
secretMetadata: el.secretMetadata
};
if (el.type === SecretType.Personal) {
@ -231,6 +241,27 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
}, [])
});
export const useGetSecretAccessList = (dto: TGetSecretAccessListDTO) =>
useQuery({
enabled: Boolean(dto.secretKey),
queryKey: secretKeys.getSecretAccessList(dto),
queryFn: async () => {
const { data } = await apiRequest.get<{
groups: SecretAccessListEntry[];
identities: SecretAccessListEntry[];
users: SecretAccessListEntry[];
}>(`/api/v1/secrets/${dto.secretKey}/access-list`, {
params: {
workspaceId: dto.workspaceId,
environment: dto.environment,
secretPath: dto.secretPath
}
});
return data;
}
});
const fetchSecretReferenceTree = async ({
secretPath,
projectId,

@ -1,3 +1,5 @@
import { ProjectPermissionActions } from "@app/context";
import type { WsTag } from "../tags/types";
export enum SecretType {
@ -48,6 +50,7 @@ export type SecretV3RawSanitized = {
overrideAction?: string;
folderId?: string;
skipMultilineEncoding?: boolean;
secretMetadata?: { key: string; value: string }[];
};
export type SecretV3Raw = {
@ -63,6 +66,7 @@ export type SecretV3Raw = {
secretComment?: string;
secretReminderNote?: string;
secretReminderRepeatDays?: number;
secretMetadata?: { key: string; value: string }[];
skipMultilineEncoding?: boolean;
metadata?: Record<string, string>;
tags?: WsTag[];
@ -123,6 +127,13 @@ export type GetSecretVersionsDTO = {
offset: number;
};
export type TGetSecretAccessListDTO = {
workspaceId: string;
environment: string;
secretPath: string;
secretKey: string;
};
export type TCreateSecretsV3DTO = {
secretKey: string;
secretValue: string;
@ -148,6 +159,7 @@ export type TUpdateSecretsV3DTO = {
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
tagIds?: string[];
secretMetadata?: { key: string; value: string }[];
};
export type TDeleteSecretsV3DTO = {
@ -228,3 +240,10 @@ export type TSecretReferenceTraceNode = {
secretPath: string;
children: TSecretReferenceTraceNode[];
};
export type SecretAccessListEntry = {
allowedActions: ProjectPermissionActions[];
id: string;
membershipId: string;
name: string;
};

@ -23,6 +23,7 @@ export type SubscriptionPlan = {
workspacesUsed: number;
environmentLimit: number;
samlSSO: boolean;
secretAccessInsights: boolean;
hsm: boolean;
oidcSSO: boolean;
scim: boolean;

@ -48,7 +48,8 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
const redirectUrl = `/api/v1/sso/redirect/saml2/organizations/${orgSlug}${
callbackPort ? `?callback_port=${callbackPort}` : ""
}`;
navigate({ to: redirectUrl });
window.location.assign(redirectUrl);
};
const redirectToOidc = (orgSlug: string) => {
@ -56,7 +57,8 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
const redirectUrl = `/api/v1/sso/oidc/login?orgSlug=${orgSlug}${
callbackPort ? `&callbackPort=${callbackPort}` : ""
}`;
navigate({ to: redirectUrl });
window.location.assign(redirectUrl);
};
useEffect(() => {

@ -27,7 +27,7 @@ export const OrgGroupsSection = () => {
if (!subscription?.groups) {
handlePopUpOpen("upgradePlan", {
description:
"You can manage users more efficiently with groups if you upgrade your Infisical plan."
"You can manage users more efficiently with groups if you upgrade your Infisical plan to an Enterprise license."
});
} else {
handlePopUpOpen("group");

@ -5,9 +5,9 @@ import { z } from "zod";
import { GitHubOAuthCallbackPage } from "./GithubOauthCallbackPage";
const GitHubOAuthCallbackPageQueryParamsSchema = z.object({
code: z.string().catch(""),
code: z.coerce.string().catch(""),
state: z.string().catch(""),
installation_id: z.coerce.string().catch("")
installation_id: z.coerce.string().optional().catch("")
});
export const Route = createFileRoute(

@ -33,7 +33,7 @@ export const GroupsSection = () => {
if (!subscription?.groups) {
handlePopUpOpen("upgradePlan", {
description:
"You can manage users more efficiently with groups if you upgrade your Infisical plan."
"You can manage users more efficiently with groups if you upgrade your Infisical plan to an Enterprise license."
});
} else {
handlePopUpOpen("group");

@ -25,7 +25,7 @@ import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@a
import { usePopUp } from "@app/hooks";
import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api";
import { TProjectRole } from "@app/hooks/api/roles/types";
import { RoleModal } from "@app/pages/organization/RoleByIDPage/components";
import { RoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/RoleModal";
export const ProjectRoleList = () => {
const navigate = useNavigate();

@ -201,7 +201,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[...rootPolicyValue, ...[]],
[...rootPolicyValue, {}],
{ shouldDirty: true, shouldTouch: true }
);
} else {

@ -173,7 +173,7 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[...rootPolicyValue, ...[]],
[...rootPolicyValue, {}],
{ shouldDirty: true, shouldTouch: true }
);
} else {

@ -98,7 +98,7 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[...rootPolicyValue, ...[]],
[...rootPolicyValue, {}],
{ shouldDirty: true, shouldTouch: true }
);
} else {

@ -32,7 +32,8 @@ const metadataMappings: Record<keyof NonNullable<TIntegrationWithEnv["metadata"]
shouldEnableDelete: "GitHub Secret Deletion Enabled",
octopusDeployScopeValues: "Octopus Deploy Scope Values",
awsIamRole: "AWS IAM Role",
region: "Region"
region: "Region",
metadataSyncMode: "Metadata Sync Mode"
} as const;
export const IntegrationSettingsSection = ({ integration }: Props) => {

@ -340,7 +340,14 @@ export const OverviewPage = () => {
const pathSegment = secretPath.split("/").filter(Boolean);
const parentPath = `/${pathSegment.slice(0, -1).join("/")}`;
const folderName = pathSegment.at(-1);
if (folderName && parentPath) {
const canCreateFolder = permission.can(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.SecretFolders, {
environment: env,
secretPath: parentPath
})
);
if (folderName && parentPath && canCreateFolder) {
await createFolder({
projectId: workspaceId,
path: parentPath,

@ -1,4 +1,4 @@
import { faExclamationTriangle, faInfo } from "@fortawesome/free-solid-svg-icons";
import { faExclamationTriangle, faInfo, faKey } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
@ -18,7 +18,10 @@ import { CommitType, SecretV3Raw, TSecretApprovalSecChange, WsTag } from "@app/h
export type Props = {
op: CommitType;
secretVersion?: SecretV3Raw;
newVersion?: Omit<TSecretApprovalSecChange, "tags"> & { tags?: WsTag[] };
newVersion?: Omit<TSecretApprovalSecChange, "tags"> & {
tags?: WsTag[];
secretMetadata?: { key: string; value: string }[];
};
presentSecretVersionNumber: number;
hasMerged?: boolean;
conflicts: Array<{ secretId: string; op: CommitType }>;
@ -80,11 +83,12 @@ export const SecretApprovalRequestChangeItem = ({
<Table>
<THead>
<Tr>
{op === CommitType.UPDATE && <Th className="w-12" />}
<Th className="min-table-row">Secret</Th>
<Th>Value</Th>
<Th className="min-table-row">Comment</Th>
<Th className="min-table-row">Tags</Th>
{op === CommitType.UPDATE && <Th className="w-12 shrink-0" />}
<Th className="w-48 shrink-0">Secret</Th>
<Th className="min-w-0 flex-1">Value</Th>
<Th className="w-24 shrink-0">Comment</Th>
<Th className="w-24 shrink-0">Tags</Th>
<Th className="w-40 shrink-0">Metadata</Th>
</Tr>
</THead>
{op === CommitType.UPDATE ? (
@ -110,6 +114,33 @@ export const SecretApprovalRequestChangeItem = ({
</Tag>
))}
</Td>
<Td>
{secretVersion?.secretMetadata?.length ? (
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
{secretVersion.secretMetadata?.map((el) => (
<div key={el.key} className="flex items-center">
<Tag
size="xs"
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
>
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
<div>{el.key}</div>
</Tag>
<Tag
size="xs"
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
>
<div className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
{el.value}
</div>
</Tag>
</div>
))}
</div>
) : (
<p className="text-sm text-mineshaft-300">-</p>
)}
</Td>
</Tr>
<Tr>
<Td className="text-green-600">NEW</Td>
@ -132,6 +163,33 @@ export const SecretApprovalRequestChangeItem = ({
</Tag>
))}
</Td>
<Td>
{newVersion?.secretMetadata?.length ? (
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
{newVersion.secretMetadata?.map((el) => (
<div key={el.key} className="flex items-center">
<Tag
size="xs"
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
>
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
<div>{el.key}</div>
</Tag>
<Tag
size="xs"
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
>
<div className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
{el.value}
</div>
</Tag>
</div>
))}
</div>
) : (
<p className="text-sm text-mineshaft-300">-</p>
)}
</Td>
</Tr>
</TBody>
) : (
@ -173,6 +231,33 @@ export const SecretApprovalRequestChangeItem = ({
)
)}
</Td>
<Td>
{newVersion?.secretMetadata?.length ? (
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
{newVersion.secretMetadata?.map((el) => (
<div key={el.key} className="flex items-center">
<Tag
size="xs"
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
>
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
<div>{el.key}</div>
</Tag>
<Tag
size="xs"
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
>
<div className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
{el.value}
</div>
</Tag>
</div>
))}
</div>
) : (
<p className="text-sm text-mineshaft-300">-</p>
)}
</Td>
</Tr>
</TBody>
)}

@ -8,12 +8,15 @@ import {
faClock,
faPlus,
faShare,
faTag
faTag,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { Link } from "@tanstack/react-router";
import { format } from "date-fns";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
@ -26,6 +29,7 @@ import {
DropdownMenuLabel,
DropdownMenuTrigger,
FormControl,
FormLabel,
IconButton,
Input,
Switch,
@ -34,10 +38,17 @@ import {
Tooltip
} from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { useToggle } from "@app/hooks";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useProjectPermission,
useWorkspace
} from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
import { useGetSecretVersion } from "@app/hooks/api";
import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { CreateReminderForm } from "./CreateReminderForm";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
@ -85,13 +96,23 @@ export const SecretDetailSidebar = ({
values: secret
});
const { permission } = useProjectPermission();
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp([
"secretAccessUpgradePlan"
] as const);
const { fields, append, remove } = useFieldArray({
const { permission } = useProjectPermission();
const { currentWorkspace } = useWorkspace();
const tagFields = useFieldArray({
control,
name: "tags"
});
const metadataFormFields = useFieldArray({
control,
name: "secretMetadata"
});
const secretKey = secret?.key || "";
const selectedTags = watch("tags", []) || [];
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
@ -130,6 +151,13 @@ export const SecretDetailSidebar = ({
secretId: secret?.id
});
const { data: secretAccessList, isPending } = useGetSecretAccessList({
workspaceId: currentWorkspace.id,
environment,
secretPath,
secretKey
});
const handleOverrideClick = () => {
if (isOverridden) {
// override need not be flagged delete if it was never saved in server
@ -153,10 +181,10 @@ export const SecretDetailSidebar = ({
if (selectedTagsGroupById?.[tag.id]) {
const tagPos = selectedTags.findIndex(({ id }) => id === tag.id);
if (tagPos !== -1) {
remove(tagPos);
tagFields.remove(tagPos);
}
} else {
append(tag);
tagFields.append(tag);
}
};
@ -184,6 +212,13 @@ export const SecretDetailSidebar = ({
}
}}
/>
<UpgradePlanModal
isOpen={popUp.secretAccessUpgradePlan.isOpen}
onOpenChange={(isUpgradeModalOpen) =>
handlePopUpToggle("secretAccessUpgradePlan", isUpgradeModalOpen)
}
text="Secret access analysis is only available on Infisical's Pro plan and above."
/>
<Drawer
onOpenChange={(state) => {
if (isOpen && isDirty) {
@ -277,9 +312,73 @@ export const SecretDetailSidebar = ({
)}
/>
)}
<FormControl label="Metadata">
<div className="flex flex-col space-y-2">
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
<div key={metadataFieldId} className="flex items-end space-x-2">
<div className="flex-grow">
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
<Controller
control={control}
name={`secretMetadata.${i}.key`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} className="max-h-8" />
</FormControl>
)}
/>
</div>
<div className="flex-grow">
{i === 0 && (
<FormLabel
label="Value"
className="text-xs text-mineshaft-400"
isOptional
/>
)}
<Controller
control={control}
name={`secretMetadata.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} className="max-h-8" />
</FormControl>
)}
/>
</div>
<IconButton
ariaLabel="delete key"
className="bottom-0.5 max-h-8"
variant="outline_bg"
onClick={() => metadataFormFields.remove(i)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
))}
<div className="mt-2">
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => metadataFormFields.append({ key: "", value: "" })}
>
Add Key
</Button>
</div>
</div>
</FormControl>
<FormControl label="Tags" className="">
<div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2">
{fields.map(({ tagColor, id: formId, slug, id }) => (
{tagFields.fields.map(({ tagColor, id: formId, slug, id }) => (
<Tag
className="flex w-min items-center space-x-2"
key={formId}
@ -482,8 +581,117 @@ export const SecretDetailSidebar = ({
))}
</div>
</div>
<div className="dark mb-4 flex-grow text-sm text-bunker-300">
<div className="mb-2">
Access List
<Tooltip
content="Lists all users, machine identities, and groups that have been granted any permission level (read, create, edit, or delete) for this secret."
className="z-[100]"
>
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
</Tooltip>
</div>
{isPending && (
<Button className="w-full px-2 py-1" variant="outline_bg" isDisabled>
Analyze Access
</Button>
)}
{!isPending && secretAccessList === undefined && (
<Button
className="w-full px-2 py-1"
variant="outline_bg"
onClick={() => {
handlePopUpOpen("secretAccessUpgradePlan");
}}
>
Analyze Access
</Button>
)}
{!isPending && secretAccessList && (
<div className="flex max-h-72 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
{secretAccessList.users.length > 0 && (
<div className="pb-3">
<div className="mb-2 font-bold">Users</div>
<div className="flex flex-wrap gap-2">
{secretAccessList.users.map((user) => (
<div className="rounded-md bg-bunker-500 px-1">
<Tooltip content={user.allowedActions.join(", ")} className="z-[100]">
<Link
to={
`/${ProjectType.SecretManager}/$projectId/members/$membershipId` as const
}
params={{
projectId: currentWorkspace.id,
membershipId: user.membershipId
}}
className="text-secondary/80 text-sm hover:text-primary"
>
{user.name}
</Link>
</Tooltip>
</div>
))}
</div>
</div>
)}
{secretAccessList.identities.length > 0 && (
<div className="pb-3">
<div className="mb-2 font-bold">Identities</div>
<div className="flex flex-wrap gap-2">
{secretAccessList.identities.map((identity) => (
<div className="rounded-md bg-bunker-500 px-1">
<Tooltip
content={identity.allowedActions.join(", ")}
className="z-[100]"
>
<Link
to={
`/${ProjectType.SecretManager}/$projectId/identities/$identityId` as const
}
params={{
projectId: currentWorkspace.id,
identityId: identity.id
}}
className="text-secondary/80 text-sm hover:text-primary"
>
{identity.name}
</Link>
</Tooltip>
</div>
))}
</div>
</div>
)}
{secretAccessList.groups.length > 0 && (
<div className="pb-3">
<div className="mb-2 font-bold">Groups</div>
<div className="flex flex-wrap gap-2">
{secretAccessList.groups.map((group) => (
<div className="rounded-md bg-bunker-500 px-1">
<Tooltip
content={group.allowedActions.join(", ")}
className="z-[100]"
>
<Link
to={"/organization/groups/$groupId" as const}
params={{
groupId: group.id
}}
className="text-secondary/80 text-sm hover:text-primary"
>
{group.name}
</Link>
</Tooltip>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
<div className="flex flex-col space-y-4">
<div className="flex items-center space-x-4">
<div className="mb-2 flex items-center space-x-4">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {

@ -78,7 +78,8 @@ export const SecretListView = ({
tags,
skipMultilineEncoding,
newKey,
secretId
secretId,
secretMetadata
}: Partial<{
value: string;
comment: string;
@ -88,6 +89,7 @@ export const SecretListView = ({
skipMultilineEncoding: boolean;
newKey: string;
secretId: string;
secretMetadata?: { key: string; value: string }[];
}> = {}
) => {
if (operation === "delete") {
@ -115,7 +117,8 @@ export const SecretListView = ({
secretReminderRepeatDays: reminderRepeatDays,
secretReminderNote: reminderNote,
skipMultilineEncoding,
newSecretName: newKey
newSecretName: newKey,
secretMetadata
});
return;
}
@ -138,7 +141,10 @@ export const SecretListView = ({
const handleSaveSecret = useCallback(
async (
orgSecret: SecretV3RawSanitized,
modSecret: Omit<SecretV3RawSanitized, "tags"> & { tags?: { id: string }[] },
modSecret: Omit<SecretV3RawSanitized, "tags"> & {
tags?: { id: string }[];
secretMetadata?: { key: string; value: string }[];
},
cb?: () => void
) => {
const { key: oldKey } = orgSecret;
@ -151,7 +157,8 @@ export const SecretListView = ({
tags,
comment,
reminderRepeatDays,
reminderNote
reminderNote,
secretMetadata
} = modSecret;
const hasKeyChanged = oldKey !== key && key;
@ -166,7 +173,8 @@ export const SecretListView = ({
"comment",
"skipMultilineEncoding",
"reminderRepeatDays",
"reminderNote"
"reminderNote",
"secretMetadata"
] as const
).every((el) => orgSecret[el] === modSecret[el]) && isSameTags;
@ -199,7 +207,8 @@ export const SecretListView = ({
reminderNote,
secretId: orgSecret.id,
newKey: hasKeyChanged ? key : undefined,
skipMultilineEncoding: modSecret.skipMultilineEncoding
skipMultilineEncoding: modSecret.skipMultilineEncoding,
secretMetadata
});
if (cb) cb();
}

@ -47,7 +47,13 @@ export const formSchema = z.object({
.nullable()
.optional(),
reminderNote: z.string().trim().nullable().optional(),
secretMetadata: z
.object({
key: z.string().trim().min(1),
value: z.string().trim().default("")
})
.array()
.optional(),
tags: z
.object({
id: z.string(),

@ -34,7 +34,10 @@ import { useWorkspace } from "@app/context";
import { useCreateIntegration } from "@app/hooks/api";
import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth";
import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries";
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
import {
IntegrationMappingBehavior,
IntegrationMetadataSyncMode
} from "@app/hooks/api/integrations/types";
enum TabSections {
Connection = "connection",
@ -94,6 +97,7 @@ const schema = z
mappingBehavior: z.nativeEnum(IntegrationMappingBehavior),
kmsKeyId: z.string().optional(),
shouldTag: z.boolean().optional(),
metadataSyncMode: z.nativeEnum(IntegrationMetadataSyncMode).optional(),
tags: z
.object({
key: z.string(),
@ -136,6 +140,7 @@ export const AwsSecretManagerConfigurePage = () => {
});
const shouldTagState = watch("shouldTag");
const selectedMetadataSyncMode = watch("metadataSyncMode");
const selectedSourceEnvironment = watch("sourceEnvironment");
const selectedAWSRegion = watch("awsRegion");
const selectedMappingBehavior = watch("mappingBehavior");
@ -171,7 +176,8 @@ export const AwsSecretManagerConfigurePage = () => {
tags,
secretPrefix,
kmsKeyId,
mappingBehavior
mappingBehavior,
metadataSyncMode
}: TFormSchema) => {
try {
if (!integrationAuth?.id) return;
@ -186,7 +192,8 @@ export const AwsSecretManagerConfigurePage = () => {
metadata: {
...(shouldTag
? {
secretAWSTag: tags
secretAWSTag: tags,
metadataSyncMode
}
: {}),
...(secretPrefix && { secretPrefix }),
@ -339,7 +346,12 @@ export const AwsSecretManagerConfigurePage = () => {
>
<Select
defaultValue={field.value}
onValueChange={(e) => onChange(e)}
onValueChange={(e) => {
if (e === IntegrationMappingBehavior.MANY_TO_ONE) {
setValue("metadataSyncMode", IntegrationMetadataSyncMode.CUSTOM);
}
onChange(e);
}}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-full"
>
@ -386,14 +398,25 @@ export const AwsSecretManagerConfigurePage = () => {
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<div className="ml-1 mt-2">
<div className="mb-3 ml-1 mt-2">
<Controller
control={control}
name="shouldTag"
render={({ field: { onChange, value } }) => (
<Switch
id="tag-aws"
onCheckedChange={(isChecked) => onChange(isChecked)}
onCheckedChange={(isChecked) => {
if (
isChecked &&
selectedMappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE
) {
setValue(
"metadataSyncMode",
IntegrationMetadataSyncMode.SECRET_METADATA
);
}
onChange(isChecked);
}}
isChecked={value}
>
Tag in AWS Secrets Manager
@ -401,37 +424,76 @@ export const AwsSecretManagerConfigurePage = () => {
)}
/>
</div>
{shouldTagState && (
<div className="mt-4 flex justify-between">
{shouldTagState &&
selectedMappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE && (
<Controller
control={control}
name="tags.0.key"
render={({ field, fieldState: { error } }) => (
name="metadataSyncMode"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Tag Key"
label="Tag Sync Mode"
errorText={error?.message}
isError={Boolean(error)}
>
<Input placeholder="managed-by" {...field} />
<Select
defaultValue={field.value}
onValueChange={(e) => {
setValue("tags", []);
onChange(e);
}}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-full"
>
<SelectItem
value={IntegrationMetadataSyncMode.SECRET_METADATA}
className="text-left"
key={`sync-mode-${IntegrationMetadataSyncMode.SECRET_METADATA}`}
>
Secret Metadata
</SelectItem>
<SelectItem
value={IntegrationMetadataSyncMode.CUSTOM}
className="text-left"
key={`sync-mode-${IntegrationMetadataSyncMode.CUSTOM}`}
>
Custom
</SelectItem>
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="tags.0.value"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Tag Value"
errorText={error?.message}
isError={Boolean(error)}
>
<Input placeholder="infisical" {...field} />
</FormControl>
)}
/>
</div>
)}
)}
{shouldTagState &&
selectedMetadataSyncMode === IntegrationMetadataSyncMode.CUSTOM && (
<div className="mt-4 flex justify-between">
<Controller
control={control}
name="tags.0.key"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Tag Key"
errorText={error?.message}
isError={Boolean(error)}
>
<Input placeholder="managed-by" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="tags.0.value"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Tag Value"
errorText={error?.message}
isError={Boolean(error)}
>
<Input placeholder="infisical" {...field} />
</FormControl>
)}
/>
</div>
)}
<Controller
control={control}
name="secretPrefix"

@ -6,7 +6,7 @@ import { AzureAppConfigurationOauthCallbackPage } from "./AzureAppConfigurationO
export const AzureAppConfigurationOauthCallbackPageQueryParamsSchema = z.object({
state: z.string().catch(""),
code: z.string()
code: z.coerce.string().catch("")
});
export const Route = createFileRoute(

@ -6,7 +6,7 @@ import { AzureKeyVaultOauthCallbackPage } from "./AzureKeyVaultOauthCallback";
export const AzureKeyVaultOauthCallbackQueryParamsSchema = z.object({
state: z.string().catch(""),
code: z.string()
code: z.coerce.string().catch("")
});
export const Route = createFileRoute(

@ -6,7 +6,7 @@ import { BitbucketOauthCallbackPage } from "./BitbucketOauthCallbackPage";
export const BitbucketOauthCallbackQueryParamsSchema = z.object({
state: z.string().catch(""),
code: z.string()
code: z.coerce.string().catch("")
});
export const Route = createFileRoute(

@ -6,7 +6,7 @@ import { GcpSecretManagerOauthCallbackPage } from "./GcpSecretManagerOauthCallba
export const GcpSecretManagerOAuthCallbackPageQueryParamsSchema = z.object({
state: z.string().catch(""),
code: z.string()
code: z.coerce.string().catch("")
});
export const Route = createFileRoute(

@ -6,8 +6,8 @@ import { GithubOauthCallbackPage } from "./GithubOauthCallbackPage";
export const GithubOAuthCallbackPageQueryParamsSchema = z.object({
state: z.string().catch(""),
installation_id: z.coerce.string().catch(""),
code: z.string().catch("")
installation_id: z.coerce.string().optional().catch(""),
code: z.coerce.string().catch("")
});
export const Route = createFileRoute(

@ -72,7 +72,7 @@ export const GitlabAuthorizePage = () => {
>
<div className="flex flex-row items-center">
<div className="flex items-center pb-0.5">
<img src="/images/integrations/Gitlab.png" height={28} width={28} alt="Gitlab logo" />
<img src="/images/integrations/GitLab.png" height={28} width={28} alt="Gitlab logo" />
</div>
<span className="ml-2.5">GitLab Integration </span>
<a

@ -34,6 +34,7 @@ import {
useGetIntegrationAuthById,
useGetIntegrationAuthTeams
} from "@app/hooks/api/integrationAuth";
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
const gitLabEntities = [
{ name: "Individual", value: "individual" },
@ -45,6 +46,14 @@ enum TabSections {
Options = "options"
}
const initialSyncBehaviors = [
{
label: "No Import - Overwrite all values in GitLab",
value: IntegrationSyncBehavior.OVERWRITE_TARGET
},
{ label: "Import - Prefer values from Infisical", value: IntegrationSyncBehavior.PREFER_SOURCE }
];
const schema = z.object({
targetEntity: z.enum([gitLabEntities[0].value, gitLabEntities[1].value]),
targetTeamId: z.string().optional(),
@ -55,7 +64,8 @@ const schema = z.object({
secretPrefix: z.string().optional(),
secretSuffix: z.string().optional(),
shouldMaskSecrets: z.boolean().optional(),
shouldProtectSecrets: z.boolean()
shouldProtectSecrets: z.boolean().default(false),
initialSyncBehavior: z.nativeEnum(IntegrationSyncBehavior)
});
type FormData = z.infer<typeof schema>;
@ -74,7 +84,8 @@ export const GitlabConfigurePage = () => {
secretPath: "/",
secretPrefix: "",
secretSuffix: "",
selectedSourceEnvironment: currentWorkspace.environments[0].slug
selectedSourceEnvironment: currentWorkspace.environments[0].slug,
initialSyncBehavior: IntegrationSyncBehavior.PREFER_SOURCE
}
});
const selectedSourceEnvironment = watch("selectedSourceEnvironment");
@ -135,7 +146,8 @@ export const GitlabConfigurePage = () => {
secretPrefix,
secretSuffix,
shouldMaskSecrets,
shouldProtectSecrets
shouldProtectSecrets,
initialSyncBehavior
}: FormData) => {
try {
setIsLoading(true);
@ -155,7 +167,8 @@ export const GitlabConfigurePage = () => {
secretPrefix,
secretSuffix,
shouldMaskSecrets,
shouldProtectSecrets
shouldProtectSecrets,
initialSyncBehavior
}
});
@ -178,7 +191,11 @@ export const GitlabConfigurePage = () => {
integrationAuthTeams ? (
<form
onSubmit={handleSubmit((data: FormData) => {
if (!data.secretPrefix && !data.secretSuffix) {
if (
!data.secretPrefix &&
!data.secretSuffix &&
data.initialSyncBehavior === IntegrationSyncBehavior.OVERWRITE_TARGET
) {
handlePopUpOpen("confirmIntegration", data);
return;
}
@ -197,7 +214,7 @@ export const GitlabConfigurePage = () => {
>
<div className="flex flex-row items-center">
<div className="flex items-center pb-0.5">
<img src="/images/integrations/Gitlab.png" height={28} width={28} alt="Gitlab logo" />
<img src="/images/integrations/GitLab.png" height={28} width={28} alt="Gitlab logo" />
</div>
<span className="ml-2.5">GitLab Integration </span>
<a
@ -378,6 +395,36 @@ export const GitlabConfigurePage = () => {
</FormControl>
)}
/>
<Controller
control={control}
name="initialSyncBehavior"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Initial Sync Behavior"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
onValueChange={(e) => onChange(e)}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-full"
>
{initialSyncBehaviors.map((b) => {
return (
<SelectItem
value={b.value}
key={`sync-behavior-${b.value}`}
className="w-full"
>
{b.label}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
</motion.div>
</TabPanel>
<TabPanel value={TabSections.Options}>
@ -462,11 +509,6 @@ export const GitlabConfigurePage = () => {
Create Integration
</Button>
</Card>
{/* <div className="border-t border-mineshaft-800 w-full max-w-md mt-6"/>
<div className="flex flex-col bg-mineshaft-800 border border-mineshaft-600 w-full p-4 max-w-lg mt-6 rounded-md">
<div className="flex flex-row items-center"><FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-200 text-xl"/> <span className="ml-3 text-md text-mineshaft-100">Pro Tip</span></div>
<span className="text-mineshaft-300 text-sm mt-4">After creating an integration, your secrets will start syncing immediately. This might cause an unexpected override of current secrets in GitLab with secrets from Infisical.</span>
</div> */}
<Modal
isOpen={popUp.confirmIntegration?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("confirmIntegration", isOpen)}

@ -6,7 +6,7 @@ import { GitLabOAuthCallbackPage } from "./GitlabOauthCallbackPage";
export const GitlabOAuthCallbackPageQueryParamsSchema = z.object({
state: z.string().catch(""),
code: z.string()
code: z.coerce.string().catch("")
});
export const Route = createFileRoute(

@ -6,7 +6,7 @@ import { HerokuOAuthCallbackPage } from "./HerokuOauthCallbackPage";
export const HerokuOAuthCallbackPageQueryParamsSchema = z.object({
state: z.string().catch(""),
code: z.string()
code: z.coerce.string().catch("")
});
export const Route = createFileRoute(

@ -6,7 +6,7 @@ import { NetlifyOauthCallbackPage } from "./NetlifyOauthCallbackPage";
export const NetlifyOAuthCallbackPageQueryParamsSchema = z.object({
state: z.string().catch(""),
code: z.string()
code: z.coerce.string().catch("")
});
export const Route = createFileRoute(

@ -6,7 +6,7 @@ import { VercelOauthCallbackPage } from "./VercelOauthCallbackPage";
export const VercelOAuthCallbackPageQueryParamsSchema = z.object({
state: z.string().catch(""),
code: z.string()
code: z.coerce.string().catch("")
});
export const Route = createFileRoute(

@ -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.0
version: v0.8.1
# 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.0"
appVersion: "v0.8.1"

@ -0,0 +1,310 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: infisicaldynamicsecrets.secrets.infisical.com
annotations:
controller-gen.kubebuilder.io/version: v0.10.0
labels:
{{- include "secrets-operator.labels" . | nindent 4 }}
spec:
group: secrets.infisical.com
names:
kind: InfisicalDynamicSecret
listKind: InfisicalDynamicSecretList
plural: infisicaldynamicsecrets
singular: infisicaldynamicsecret
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: InfisicalDynamicSecret is the Schema for the infisicaldynamicsecrets
API.
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: InfisicalDynamicSecretSpec defines the desired state of InfisicalDynamicSecret.
properties:
authentication:
properties:
awsIamAuth:
properties:
identityId:
type: string
required:
- identityId
type: object
azureAuth:
properties:
identityId:
type: string
resource:
type: string
required:
- identityId
type: object
gcpIamAuth:
properties:
identityId:
type: string
serviceAccountKeyFilePath:
type: string
required:
- identityId
- serviceAccountKeyFilePath
type: object
gcpIdTokenAuth:
properties:
identityId:
type: string
required:
- identityId
type: object
kubernetesAuth:
properties:
identityId:
type: string
serviceAccountRef:
properties:
name:
type: string
namespace:
type: string
required:
- name
- namespace
type: object
required:
- identityId
- serviceAccountRef
type: object
universalAuth:
properties:
credentialsRef:
properties:
secretName:
description: The name of the Kubernetes Secret
type: string
secretNamespace:
description: The name space where the Kubernetes Secret
is located
type: string
required:
- secretName
- secretNamespace
type: object
required:
- credentialsRef
type: object
type: object
dynamicSecret:
properties:
environmentSlug:
type: string
projectId:
type: string
secretName:
type: string
secretsPath:
type: string
required:
- environmentSlug
- projectId
- secretName
- secretsPath
type: object
hostAPI:
type: string
leaseRevocationPolicy:
type: string
leaseTTL:
type: string
managedSecretReference:
properties:
creationPolicy:
default: Orphan
description: 'The Kubernetes Secret creation policy. Enum with values:
''Owner'', ''Orphan''. Owner creates the secret and sets .metadata.ownerReferences
of the InfisicalSecret CRD that created it. Orphan will not set
the secret owner. This will result in the secret being orphaned
and not deleted when the resource is deleted.'
type: string
secretName:
description: The name of the Kubernetes Secret
type: string
secretNamespace:
description: The name space where the Kubernetes Secret is located
type: string
secretType:
default: Opaque
description: 'The Kubernetes Secret type (experimental feature).
More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types'
type: string
template:
description: The template to transform the secret data
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
type: object
tls:
properties:
caRef:
description: Reference to secret containing CA cert
properties:
key:
description: The name of the secret property with the CA certificate
value
type: string
secretName:
description: The name of the Kubernetes Secret
type: string
secretNamespace:
description: The namespace where the Kubernetes Secret is located
type: string
required:
- key
- secretName
- secretNamespace
type: object
type: object
required:
- authentication
- dynamicSecret
- leaseRevocationPolicy
- leaseTTL
- managedSecretReference
type: object
status:
description: InfisicalDynamicSecretStatus defines the observed state of
InfisicalDynamicSecret.
properties:
conditions:
items:
description: "Condition contains details for one aspect of the current
state of this API Resource. --- This struct is intended for direct
use as an array at the field path .status.conditions. For example,
\n type FooStatus struct{ // Represents the observations of a foo's
current state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
// +listType=map // +listMapKey=type Conditions []metav1.Condition
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
properties:
lastTransitionTime:
description: lastTransitionTime is the last time the condition
transitioned from one status to another. This should be when
the underlying condition changed. If that is not known, then
using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: message is a human readable message indicating details
about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: observedGeneration represents the .metadata.generation
that the condition was set based upon. For instance, if .metadata.generation
is currently 12, but the .status.conditions[x].observedGeneration
is 9, the condition is out of date with respect to the current
state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: reason contains a programmatic identifier indicating
the reason for the condition's last transition. Producers of
specific condition types may define expected values and meanings
for this field, and whether the values are considered a guaranteed
API. The value should be a CamelCase string. This field may
not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
--- Many .condition.type values are consistent across resources
like Available, but because arbitrary conditions can be useful
(see .node.status.conditions), the ability to deconflict is
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
dynamicSecretId:
type: string
lease:
properties:
creationTimestamp:
format: date-time
type: string
expiresAt:
format: date-time
type: string
id:
type: string
version:
format: int64
type: integer
required:
- creationTimestamp
- expiresAt
- id
- version
type: object
maxTTL:
description: The MaxTTL can be null, if it's null, there's no max TTL
and we should never have to renew.
type: string
required:
- conditions
type: object
type: object
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

@ -90,7 +90,6 @@ spec:
- serviceAccountRef
type: object
universalAuth:
description: PushSecretUniversalAuth defines universal authentication
properties:
credentialsRef:
properties:

@ -51,6 +51,32 @@ rules:
- list
- update
- watch
- apiGroups:
- secrets.infisical.com
resources:
- infisicaldynamicsecrets
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- secrets.infisical.com
resources:
- infisicaldynamicsecrets/finalizers
verbs:
- update
- apiGroups:
- secrets.infisical.com
resources:
- infisicaldynamicsecrets/status
verbs:
- get
- patch
- update
- apiGroups:
- secrets.infisical.com
resources:

@ -32,7 +32,7 @@ controllerManager:
- ALL
image:
repository: infisical/kubernetes-operator
tag: v0.8.0
tag: v0.8.1
resources:
limits:
cpu: 500m

@ -26,4 +26,13 @@ resources:
kind: InfisicalPushSecretSecret
path: github.com/Infisical/infisical/k8-operator/api/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: infisical.com
group: secrets
kind: InfisicalDynamicSecret
path: github.com/Infisical/infisical/k8-operator/api/v1alpha1
version: v1alpha1
version: "3"

@ -0,0 +1,109 @@
package v1alpha1
type GenericInfisicalAuthentication struct {
// +kubebuilder:validation:Optional
UniversalAuth GenericUniversalAuth `json:"universalAuth,omitempty"`
// +kubebuilder:validation:Optional
KubernetesAuth GenericKubernetesAuth `json:"kubernetesAuth,omitempty"`
// +kubebuilder:validation:Optional
AwsIamAuth GenericAwsIamAuth `json:"awsIamAuth,omitempty"`
// +kubebuilder:validation:Optional
AzureAuth GenericAzureAuth `json:"azureAuth,omitempty"`
// +kubebuilder:validation:Optional
GcpIdTokenAuth GenericGcpIdTokenAuth `json:"gcpIdTokenAuth,omitempty"`
// +kubebuilder:validation:Optional
GcpIamAuth GenericGcpIamAuth `json:"gcpIamAuth,omitempty"`
}
type GenericUniversalAuth struct {
// +kubebuilder:validation:Required
CredentialsRef KubeSecretReference `json:"credentialsRef"`
}
type GenericAwsIamAuth struct {
// +kubebuilder:validation:Required
IdentityID string `json:"identityId"`
}
type GenericAzureAuth struct {
// +kubebuilder:validation:Required
IdentityID string `json:"identityId"`
// +kubebuilder:validation:Optional
Resource string `json:"resource,omitempty"`
}
type GenericGcpIdTokenAuth struct {
// +kubebuilder:validation:Required
IdentityID string `json:"identityId"`
}
type GenericGcpIamAuth struct {
// +kubebuilder:validation:Required
IdentityID string `json:"identityId"`
// +kubebuilder:validation:Required
ServiceAccountKeyFilePath string `json:"serviceAccountKeyFilePath"`
}
type GenericKubernetesAuth struct {
// +kubebuilder:validation:Required
IdentityID string `json:"identityId"`
// +kubebuilder:validation:Required
ServiceAccountRef KubernetesServiceAccountRef `json:"serviceAccountRef"`
}
type TLSConfig struct {
// Reference to secret containing CA cert
// +kubebuilder:validation:Optional
CaRef CaReference `json:"caRef,omitempty"`
}
type CaReference struct {
// The name of the Kubernetes Secret
// +kubebuilder:validation:Required
SecretName string `json:"secretName"`
// The namespace where the Kubernetes Secret is located
// +kubebuilder:validation:Required
SecretNamespace string `json:"secretNamespace"`
// +kubebuilder:validation:Required
// The name of the secret property with the CA certificate value
SecretKey string `json:"key"`
}
type KubeSecretReference 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"`
}
type ManagedKubeSecretConfig 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"`
// The Kubernetes Secret type (experimental feature). More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types
// +kubebuilder:validation:Optional
// +kubebuilder:default:=Opaque
SecretType string `json:"secretType"`
// The Kubernetes Secret creation policy.
// Enum with values: 'Owner', 'Orphan'.
// Owner creates the secret and sets .metadata.ownerReferences of the InfisicalSecret CRD that created it.
// Orphan will not set the secret owner. This will result in the secret being orphaned and not deleted when the resource is deleted.
// +kubebuilder:validation:Optional
// +kubebuilder:default:=Orphan
CreationPolicy string `json:"creationPolicy"`
// The template to transform the secret data
// +kubebuilder:validation:Optional
Template *InfisicalSecretTemplate `json:"template,omitempty"`
}

@ -0,0 +1,99 @@
/*
Copyright 2022.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type InfisicalDynamicSecretLease struct {
ID string `json:"id"`
Version int64 `json:"version"`
CreationTimestamp metav1.Time `json:"creationTimestamp"`
ExpiresAt metav1.Time `json:"expiresAt"`
}
type DynamicSecretDetails struct {
// +kubebuilder:validation:Required
// +kubebuilder:validation:Immutable
SecretName string `json:"secretName"`
// +kubebuilder:validation:Required
// +kubebuilder:validation:Immutable
SecretPath string `json:"secretsPath"`
// +kubebuilder:validation:Required
// +kubebuilder:validation:Immutable
EnvironmentSlug string `json:"environmentSlug"`
// +kubebuilder:validation:Required
// +kubebuilder:validation:Immutable
ProjectID string `json:"projectId"`
}
// InfisicalDynamicSecretSpec defines the desired state of InfisicalDynamicSecret.
type InfisicalDynamicSecretSpec struct {
// +kubebuilder:validation:Required
ManagedSecretReference ManagedKubeSecretConfig `json:"managedSecretReference"` // The destination to store the lease in.
// +kubebuilder:validation:Required
Authentication GenericInfisicalAuthentication `json:"authentication"` // The authentication to use for authenticating with Infisical.
// +kubebuilder:validation:Required
DynamicSecret DynamicSecretDetails `json:"dynamicSecret"` // The dynamic secret to create the lease for. Required.
LeaseRevocationPolicy string `json:"leaseRevocationPolicy"` // Revoke will revoke the lease when the resource is deleted. Optional, will default to no revocation.
LeaseTTL string `json:"leaseTTL"` // The TTL of the lease in seconds. Optional, will default to the dynamic secret default TTL.
// +kubebuilder:validation:Optional
HostAPI string `json:"hostAPI"`
// +kubebuilder:validation:Optional
TLS TLSConfig `json:"tls"`
}
// InfisicalDynamicSecretStatus defines the observed state of InfisicalDynamicSecret.
type InfisicalDynamicSecretStatus struct {
Conditions []metav1.Condition `json:"conditions"`
Lease *InfisicalDynamicSecretLease `json:"lease,omitempty"`
DynamicSecretID string `json:"dynamicSecretId,omitempty"`
// The MaxTTL can be null, if it's null, there's no max TTL and we should never have to renew.
MaxTTL string `json:"maxTTL,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// InfisicalDynamicSecret is the Schema for the infisicaldynamicsecrets API.
type InfisicalDynamicSecret struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec InfisicalDynamicSecretSpec `json:"spec,omitempty"`
Status InfisicalDynamicSecretStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// InfisicalDynamicSecretList contains a list of InfisicalDynamicSecret.
type InfisicalDynamicSecretList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []InfisicalDynamicSecret `json:"items"`
}
func init() {
SchemeBuilder.Register(&InfisicalDynamicSecret{}, &InfisicalDynamicSecretList{})
}

@ -16,64 +16,6 @@ type InfisicalPushSecretDestination struct {
ProjectID string `json:"projectId"`
}
type PushSecretTlsConfig struct {
// Reference to secret containing CA cert
// +kubebuilder:validation:Optional
CaRef CaReference `json:"caRef,omitempty"`
}
// PushSecretUniversalAuth defines universal authentication
type PushSecretUniversalAuth struct {
// +kubebuilder:validation:Required
CredentialsRef KubeSecretReference `json:"credentialsRef"`
}
type PushSecretAwsIamAuth struct {
// +kubebuilder:validation:Required
IdentityID string `json:"identityId"`
}
type PushSecretAzureAuth struct {
// +kubebuilder:validation:Required
IdentityID string `json:"identityId"`
// +kubebuilder:validation:Optional
Resource string `json:"resource,omitempty"`
}
type PushSecretGcpIdTokenAuth struct {
// +kubebuilder:validation:Required
IdentityID string `json:"identityId"`
}
type PushSecretGcpIamAuth struct {
// +kubebuilder:validation:Required
IdentityID string `json:"identityId"`
// +kubebuilder:validation:Required
ServiceAccountKeyFilePath string `json:"serviceAccountKeyFilePath"`
}
type PushSecretKubernetesAuth struct {
// +kubebuilder:validation:Required
IdentityID string `json:"identityId"`
// +kubebuilder:validation:Required
ServiceAccountRef KubernetesServiceAccountRef `json:"serviceAccountRef"`
}
type PushSecretAuthentication struct {
// +kubebuilder:validation:Optional
UniversalAuth PushSecretUniversalAuth `json:"universalAuth,omitempty"`
// +kubebuilder:validation:Optional
KubernetesAuth PushSecretKubernetesAuth `json:"kubernetesAuth,omitempty"`
// +kubebuilder:validation:Optional
AwsIamAuth PushSecretAwsIamAuth `json:"awsIamAuth,omitempty"`
// +kubebuilder:validation:Optional
AzureAuth PushSecretAzureAuth `json:"azureAuth,omitempty"`
// +kubebuilder:validation:Optional
GcpIdTokenAuth PushSecretGcpIdTokenAuth `json:"gcpIdTokenAuth,omitempty"`
// +kubebuilder:validation:Optional
GcpIamAuth PushSecretGcpIamAuth `json:"gcpIamAuth,omitempty"`
}
type SecretPush struct {
// +kubebuilder:validation:Required
Secret KubeSecretReference `json:"secret"`
@ -92,7 +34,7 @@ type InfisicalPushSecretSpec struct {
Destination InfisicalPushSecretDestination `json:"destination"`
// +kubebuilder:validation:Optional
Authentication PushSecretAuthentication `json:"authentication"`
Authentication GenericInfisicalAuthentication `json:"authentication"`
// +kubebuilder:validation:Required
Push SecretPush `json:"push"`
@ -104,7 +46,7 @@ type InfisicalPushSecretSpec struct {
HostAPI string `json:"hostAPI"`
// +kubebuilder:validation:Optional
TLS PushSecretTlsConfig `json:"tls"`
TLS TLSConfig `json:"tls"`
}
// InfisicalPushSecretStatus defines the observed state of InfisicalPushSecret

@ -116,43 +116,6 @@ type MachineIdentityScopeInWorkspace struct {
Recursive bool `json:"recursive"`
}
type KubeSecretReference 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"`
}
type MangedKubeSecretConfig 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"`
// The Kubernetes Secret type (experimental feature). More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types
// +kubebuilder:validation:Optional
// +kubebuilder:default:=Opaque
SecretType string `json:"secretType"`
// The Kubernetes Secret creation policy.
// Enum with values: 'Owner', 'Orphan'.
// Owner creates the secret and sets .metadata.ownerReferences of the InfisicalSecret CRD that created it.
// Orphan will not set the secret owner. This will result in the secret being orphaned and not deleted when the resource is deleted.
// +kubebuilder:validation:Optional
// +kubebuilder:default:=Orphan
CreationPolicy string `json:"creationPolicy"`
// The template to transform the secret data
// +kubebuilder:validation:Optional
Template *InfisicalSecretTemplate `json:"template,omitempty"`
}
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.
@ -163,26 +126,6 @@ type InfisicalSecretTemplate struct {
Data map[string]string `json:"data,omitempty"`
}
type CaReference struct {
// The name of the Kubernetes Secret
// +kubebuilder:validation:Required
SecretName string `json:"secretName"`
// The namespace where the Kubernetes Secret is located
// +kubebuilder:validation:Required
SecretNamespace string `json:"secretNamespace"`
// +kubebuilder:validation:Required
// The name of the secret property with the CA certificate value
SecretKey string `json:"key"`
}
type TLSConfig struct {
// Reference to secret containing CA cert
// +kubebuilder:validation:Optional
CaRef CaReference `json:"caRef,omitempty"`
}
// InfisicalSecretSpec defines the desired state of InfisicalSecret
type InfisicalSecretSpec struct {
// +kubebuilder:validation:Optional
@ -192,7 +135,7 @@ type InfisicalSecretSpec struct {
Authentication Authentication `json:"authentication"`
// +kubebuilder:validation:Required
ManagedSecretReference MangedKubeSecretConfig `json:"managedSecretReference"`
ManagedSecretReference ManagedKubeSecretConfig `json:"managedSecretReference"`
// +kubebuilder:default:=60
ResyncInterval int `json:"resyncInterval"`

@ -96,6 +96,21 @@ func (in *CaReference) DeepCopy() *CaReference {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DynamicSecretDetails) DeepCopyInto(out *DynamicSecretDetails) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DynamicSecretDetails.
func (in *DynamicSecretDetails) DeepCopy() *DynamicSecretDetails {
if in == nil {
return nil
}
out := new(DynamicSecretDetails)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GCPIdTokenAuthDetails) DeepCopyInto(out *GCPIdTokenAuthDetails) {
*out = *in
@ -128,6 +143,241 @@ func (in *GcpIamAuthDetails) DeepCopy() *GcpIamAuthDetails {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GenericAwsIamAuth) DeepCopyInto(out *GenericAwsIamAuth) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericAwsIamAuth.
func (in *GenericAwsIamAuth) DeepCopy() *GenericAwsIamAuth {
if in == nil {
return nil
}
out := new(GenericAwsIamAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GenericAzureAuth) DeepCopyInto(out *GenericAzureAuth) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericAzureAuth.
func (in *GenericAzureAuth) DeepCopy() *GenericAzureAuth {
if in == nil {
return nil
}
out := new(GenericAzureAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GenericGcpIamAuth) DeepCopyInto(out *GenericGcpIamAuth) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericGcpIamAuth.
func (in *GenericGcpIamAuth) DeepCopy() *GenericGcpIamAuth {
if in == nil {
return nil
}
out := new(GenericGcpIamAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GenericGcpIdTokenAuth) DeepCopyInto(out *GenericGcpIdTokenAuth) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericGcpIdTokenAuth.
func (in *GenericGcpIdTokenAuth) DeepCopy() *GenericGcpIdTokenAuth {
if in == nil {
return nil
}
out := new(GenericGcpIdTokenAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GenericInfisicalAuthentication) DeepCopyInto(out *GenericInfisicalAuthentication) {
*out = *in
out.UniversalAuth = in.UniversalAuth
out.KubernetesAuth = in.KubernetesAuth
out.AwsIamAuth = in.AwsIamAuth
out.AzureAuth = in.AzureAuth
out.GcpIdTokenAuth = in.GcpIdTokenAuth
out.GcpIamAuth = in.GcpIamAuth
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericInfisicalAuthentication.
func (in *GenericInfisicalAuthentication) DeepCopy() *GenericInfisicalAuthentication {
if in == nil {
return nil
}
out := new(GenericInfisicalAuthentication)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GenericKubernetesAuth) DeepCopyInto(out *GenericKubernetesAuth) {
*out = *in
out.ServiceAccountRef = in.ServiceAccountRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericKubernetesAuth.
func (in *GenericKubernetesAuth) DeepCopy() *GenericKubernetesAuth {
if in == nil {
return nil
}
out := new(GenericKubernetesAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GenericUniversalAuth) DeepCopyInto(out *GenericUniversalAuth) {
*out = *in
out.CredentialsRef = in.CredentialsRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericUniversalAuth.
func (in *GenericUniversalAuth) DeepCopy() *GenericUniversalAuth {
if in == nil {
return nil
}
out := new(GenericUniversalAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InfisicalDynamicSecret) DeepCopyInto(out *InfisicalDynamicSecret) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalDynamicSecret.
func (in *InfisicalDynamicSecret) DeepCopy() *InfisicalDynamicSecret {
if in == nil {
return nil
}
out := new(InfisicalDynamicSecret)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *InfisicalDynamicSecret) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InfisicalDynamicSecretLease) DeepCopyInto(out *InfisicalDynamicSecretLease) {
*out = *in
in.CreationTimestamp.DeepCopyInto(&out.CreationTimestamp)
in.ExpiresAt.DeepCopyInto(&out.ExpiresAt)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalDynamicSecretLease.
func (in *InfisicalDynamicSecretLease) DeepCopy() *InfisicalDynamicSecretLease {
if in == nil {
return nil
}
out := new(InfisicalDynamicSecretLease)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InfisicalDynamicSecretList) DeepCopyInto(out *InfisicalDynamicSecretList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]InfisicalDynamicSecret, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalDynamicSecretList.
func (in *InfisicalDynamicSecretList) DeepCopy() *InfisicalDynamicSecretList {
if in == nil {
return nil
}
out := new(InfisicalDynamicSecretList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *InfisicalDynamicSecretList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InfisicalDynamicSecretSpec) DeepCopyInto(out *InfisicalDynamicSecretSpec) {
*out = *in
in.ManagedSecretReference.DeepCopyInto(&out.ManagedSecretReference)
out.Authentication = in.Authentication
out.DynamicSecret = in.DynamicSecret
out.TLS = in.TLS
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalDynamicSecretSpec.
func (in *InfisicalDynamicSecretSpec) DeepCopy() *InfisicalDynamicSecretSpec {
if in == nil {
return nil
}
out := new(InfisicalDynamicSecretSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InfisicalDynamicSecretStatus) DeepCopyInto(out *InfisicalDynamicSecretStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Lease != nil {
in, out := &in.Lease, &out.Lease
*out = new(InfisicalDynamicSecretLease)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalDynamicSecretStatus.
func (in *InfisicalDynamicSecretStatus) DeepCopy() *InfisicalDynamicSecretStatus {
if in == nil {
return nil
}
out := new(InfisicalDynamicSecretStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InfisicalPushSecret) DeepCopyInto(out *InfisicalPushSecret) {
*out = *in
@ -435,7 +685,7 @@ func (in *MachineIdentityScopeInWorkspace) DeepCopy() *MachineIdentityScopeInWor
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MangedKubeSecretConfig) DeepCopyInto(out *MangedKubeSecretConfig) {
func (in *ManagedKubeSecretConfig) DeepCopyInto(out *ManagedKubeSecretConfig) {
*out = *in
if in.Template != nil {
in, out := &in.Template, &out.Template
@ -444,141 +694,12 @@ func (in *MangedKubeSecretConfig) DeepCopyInto(out *MangedKubeSecretConfig) {
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MangedKubeSecretConfig.
func (in *MangedKubeSecretConfig) DeepCopy() *MangedKubeSecretConfig {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedKubeSecretConfig.
func (in *ManagedKubeSecretConfig) DeepCopy() *ManagedKubeSecretConfig {
if in == nil {
return nil
}
out := new(MangedKubeSecretConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretAuthentication) DeepCopyInto(out *PushSecretAuthentication) {
*out = *in
out.UniversalAuth = in.UniversalAuth
out.KubernetesAuth = in.KubernetesAuth
out.AwsIamAuth = in.AwsIamAuth
out.AzureAuth = in.AzureAuth
out.GcpIdTokenAuth = in.GcpIdTokenAuth
out.GcpIamAuth = in.GcpIamAuth
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretAuthentication.
func (in *PushSecretAuthentication) DeepCopy() *PushSecretAuthentication {
if in == nil {
return nil
}
out := new(PushSecretAuthentication)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretAwsIamAuth) DeepCopyInto(out *PushSecretAwsIamAuth) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretAwsIamAuth.
func (in *PushSecretAwsIamAuth) DeepCopy() *PushSecretAwsIamAuth {
if in == nil {
return nil
}
out := new(PushSecretAwsIamAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretAzureAuth) DeepCopyInto(out *PushSecretAzureAuth) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretAzureAuth.
func (in *PushSecretAzureAuth) DeepCopy() *PushSecretAzureAuth {
if in == nil {
return nil
}
out := new(PushSecretAzureAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretGcpIamAuth) DeepCopyInto(out *PushSecretGcpIamAuth) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretGcpIamAuth.
func (in *PushSecretGcpIamAuth) DeepCopy() *PushSecretGcpIamAuth {
if in == nil {
return nil
}
out := new(PushSecretGcpIamAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretGcpIdTokenAuth) DeepCopyInto(out *PushSecretGcpIdTokenAuth) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretGcpIdTokenAuth.
func (in *PushSecretGcpIdTokenAuth) DeepCopy() *PushSecretGcpIdTokenAuth {
if in == nil {
return nil
}
out := new(PushSecretGcpIdTokenAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretKubernetesAuth) DeepCopyInto(out *PushSecretKubernetesAuth) {
*out = *in
out.ServiceAccountRef = in.ServiceAccountRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretKubernetesAuth.
func (in *PushSecretKubernetesAuth) DeepCopy() *PushSecretKubernetesAuth {
if in == nil {
return nil
}
out := new(PushSecretKubernetesAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretTlsConfig) DeepCopyInto(out *PushSecretTlsConfig) {
*out = *in
out.CaRef = in.CaRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretTlsConfig.
func (in *PushSecretTlsConfig) DeepCopy() *PushSecretTlsConfig {
if in == nil {
return nil
}
out := new(PushSecretTlsConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretUniversalAuth) DeepCopyInto(out *PushSecretUniversalAuth) {
*out = *in
out.CredentialsRef = in.CredentialsRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretUniversalAuth.
func (in *PushSecretUniversalAuth) DeepCopy() *PushSecretUniversalAuth {
if in == nil {
return nil
}
out := new(PushSecretUniversalAuth)
out := new(ManagedKubeSecretConfig)
in.DeepCopyInto(out)
return out
}

@ -0,0 +1,306 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.10.0
creationTimestamp: null
name: infisicaldynamicsecrets.secrets.infisical.com
spec:
group: secrets.infisical.com
names:
kind: InfisicalDynamicSecret
listKind: InfisicalDynamicSecretList
plural: infisicaldynamicsecrets
singular: infisicaldynamicsecret
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: InfisicalDynamicSecret is the Schema for the infisicaldynamicsecrets
API.
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: InfisicalDynamicSecretSpec defines the desired state of InfisicalDynamicSecret.
properties:
authentication:
properties:
awsIamAuth:
properties:
identityId:
type: string
required:
- identityId
type: object
azureAuth:
properties:
identityId:
type: string
resource:
type: string
required:
- identityId
type: object
gcpIamAuth:
properties:
identityId:
type: string
serviceAccountKeyFilePath:
type: string
required:
- identityId
- serviceAccountKeyFilePath
type: object
gcpIdTokenAuth:
properties:
identityId:
type: string
required:
- identityId
type: object
kubernetesAuth:
properties:
identityId:
type: string
serviceAccountRef:
properties:
name:
type: string
namespace:
type: string
required:
- name
- namespace
type: object
required:
- identityId
- serviceAccountRef
type: object
universalAuth:
properties:
credentialsRef:
properties:
secretName:
description: The name of the Kubernetes Secret
type: string
secretNamespace:
description: The name space where the Kubernetes Secret
is located
type: string
required:
- secretName
- secretNamespace
type: object
required:
- credentialsRef
type: object
type: object
dynamicSecret:
properties:
environmentSlug:
type: string
projectId:
type: string
secretName:
type: string
secretsPath:
type: string
required:
- environmentSlug
- projectId
- secretName
- secretsPath
type: object
hostAPI:
type: string
leaseRevocationPolicy:
type: string
leaseTTL:
type: string
managedSecretReference:
properties:
creationPolicy:
default: Orphan
description: 'The Kubernetes Secret creation policy. Enum with
values: ''Owner'', ''Orphan''. Owner creates the secret and
sets .metadata.ownerReferences of the InfisicalSecret CRD that
created it. Orphan will not set the secret owner. This will
result in the secret being orphaned and not deleted when the
resource is deleted.'
type: string
secretName:
description: The name of the Kubernetes Secret
type: string
secretNamespace:
description: The name space where the Kubernetes Secret is located
type: string
secretType:
default: Opaque
description: 'The Kubernetes Secret type (experimental feature).
More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types'
type: string
template:
description: The template to transform the secret data
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
type: object
tls:
properties:
caRef:
description: Reference to secret containing CA cert
properties:
key:
description: The name of the secret property with the CA certificate
value
type: string
secretName:
description: The name of the Kubernetes Secret
type: string
secretNamespace:
description: The namespace where the Kubernetes Secret is
located
type: string
required:
- key
- secretName
- secretNamespace
type: object
type: object
required:
- authentication
- dynamicSecret
- leaseRevocationPolicy
- leaseTTL
- managedSecretReference
type: object
status:
description: InfisicalDynamicSecretStatus defines the observed state of
InfisicalDynamicSecret.
properties:
conditions:
items:
description: "Condition contains details for one aspect of the current
state of this API Resource. --- This struct is intended for direct
use as an array at the field path .status.conditions. For example,
\n type FooStatus struct{ // Represents the observations of a
foo's current state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
// +listType=map // +listMapKey=type Conditions []metav1.Condition
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
properties:
lastTransitionTime:
description: lastTransitionTime is the last time the condition
transitioned from one status to another. This should be when
the underlying condition changed. If that is not known, then
using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: message is a human readable message indicating
details about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: observedGeneration represents the .metadata.generation
that the condition was set based upon. For instance, if .metadata.generation
is currently 12, but the .status.conditions[x].observedGeneration
is 9, the condition is out of date with respect to the current
state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: reason contains a programmatic identifier indicating
the reason for the condition's last transition. Producers
of specific condition types may define expected values and
meanings for this field, and whether the values are considered
a guaranteed API. The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
--- Many .condition.type values are consistent across resources
like Available, but because arbitrary conditions can be useful
(see .node.status.conditions), the ability to deconflict is
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
dynamicSecretId:
type: string
lease:
properties:
creationTimestamp:
format: date-time
type: string
expiresAt:
format: date-time
type: string
id:
type: string
version:
format: int64
type: integer
required:
- creationTimestamp
- expiresAt
- id
- version
type: object
maxTTL:
description: The MaxTTL can be null, if it's null, there's no max
TTL and we should never have to renew.
type: string
required:
- conditions
type: object
type: object
served: true
storage: true
subresources:
status: {}

@ -90,7 +90,6 @@ spec:
- serviceAccountRef
type: object
universalAuth:
description: PushSecretUniversalAuth defines universal authentication
properties:
credentialsRef:
properties:

@ -4,6 +4,7 @@
resources:
- bases/secrets.infisical.com_infisicalsecrets.yaml
- bases/secrets.infisical.com_infisicalpushsecrets.yaml
- bases/secrets.infisical.com_infisicaldynamicsecrets.yaml
#+kubebuilder:scaffold:crdkustomizeresource
patchesStrategicMerge:

@ -0,0 +1,27 @@
# permissions for end users to edit infisicaldynamicsecrets.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: k8-operator
app.kubernetes.io/managed-by: kustomize
name: infisicaldynamicsecret-editor-role
rules:
- apiGroups:
- secrets.infisical.com
resources:
- infisicaldynamicsecrets
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- secrets.infisical.com
resources:
- infisicaldynamicsecrets/status
verbs:
- get

@ -0,0 +1,23 @@
# permissions for end users to view infisicaldynamicsecrets.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: k8-operator
app.kubernetes.io/managed-by: kustomize
name: infisicaldynamicsecret-viewer-role
rules:
- apiGroups:
- secrets.infisical.com
resources:
- infisicaldynamicsecrets
verbs:
- get
- list
- watch
- apiGroups:
- secrets.infisical.com
resources:
- infisicaldynamicsecrets/status
verbs:
- get

@ -44,6 +44,32 @@ rules:
- list
- update
- watch
- apiGroups:
- secrets.infisical.com
resources:
- infisicaldynamicsecrets
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- secrets.infisical.com
resources:
- infisicaldynamicsecrets/finalizers
verbs:
- update
- apiGroups:
- secrets.infisical.com
resources:
- infisicaldynamicsecrets/status
verbs:
- get
- patch
- update
- apiGroups:
- secrets.infisical.com
resources:

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