Compare commits

..

32 Commits

Author SHA1 Message Date
Scott Wilson
c8109b4e84 improvement: add example paste value formats 2024-11-12 16:46:35 -08:00
Scott Wilson
1f2b0443cc improvement: address requested changes 2024-11-12 16:11:47 -08:00
Scott Wilson
ddcf5b576b improvement: improve field error message 2024-11-12 10:25:23 -08:00
Scott Wilson
7138b392f2 Feature: add ability to paste .env, .yml or .json secrets for upload and also fix upload when keys conflict but are not on current page 2024-11-12 10:21:07 -08:00
Scott Wilson
0e946f73bd Merge pull request #2713 from scott-ray-wilson/bitbucket-integration-additions
Feature: Add Support for Deployment Environment Scope for Bitbucket Integration
2024-11-11 11:27:12 -08:00
Scott Wilson
7b8551f883 fix: use constant url for bitbucket update/create secret 2024-11-11 10:56:26 -08:00
Sheen
3b1ce86ee6 Merge pull request #2704 from Infisical/feat/add-support-for-no-bootstrap-cert-est
feat: add support for EST device enrollment without bootstrap certs
2024-11-12 02:40:37 +08:00
Sheen Capadngan
c649661133 misc: remove not nullable from alter 2024-11-12 02:35:21 +08:00
Maidul Islam
70e44d04ef Merge pull request #2720 from akhilmhdh/fix/random-patch
feat: random patches
2024-11-11 11:35:04 -07:00
=
0dddd58be1 feat: random patches 2024-11-11 23:59:26 +05:30
Scott Wilson
d4c911a28f feature: add support for deployment environment scope for bitbucket and refactor bitbucket create UI 2024-11-11 09:47:23 -08:00
Maidul Islam
65d642113d Update mint.json 2024-11-10 21:06:57 -07:00
Maidul Islam
92e7e90c21 Merge pull request #2701 from scott-ray-wilson/project-templates-feature
Feature: Project Templates
2024-11-10 21:03:11 -07:00
Maidul Islam
f9f6ec0a8d Merge pull request #2717 from Infisical/misc/true-myssql-rotation-flag-default
misc: made myssql rotation flag true in example
2024-11-10 20:43:11 -07:00
Sheen
d80a70731d Merge pull request #2641 from Infisical/feat/ldap-static-dynamic-secret
feat: static ldap credentials
2024-11-10 13:01:56 +08:00
Scott Wilson
bd99b4e356 improvement: reduce json max size limit based of aws policy limit 2024-11-09 16:32:50 -08:00
Maidul Islam
7db0bd7daa Merge pull request #2702 from felixhummel/main
docs: fix link to cli
2024-11-09 09:01:08 -05:00
Maidul Islam
8bc538af93 Merge pull request #2711 from Infisical/misc/moved-aws-sm-integration-to-react-hook-form
misc: moved aws secret manager integration to react hook form
2024-11-09 08:59:38 -05:00
Scott Wilson
d5f718c6ad improve: improve template form buttons 2024-11-08 14:07:50 -08:00
Sheen Capadngan
829b399cda misc: moved to react hook form 2024-11-09 03:02:22 +08:00
Scott Wilson
829ae7d3c0 chore: revert license 2024-11-07 12:40:48 -08:00
Scott Wilson
19c26c680c improvement: address requested feedback 2024-11-07 12:38:33 -08:00
Sheen Capadngan
027b200b1a misc: renamed disable flag + docs 2024-11-08 02:02:12 +08:00
Sheen Capadngan
e761e65322 feat: add support for no bootstrap cert EST 2024-11-08 01:42:47 +08:00
Felix Hummel
370ed45abb docs: fix link to cli 2024-11-07 15:27:42 +01:00
Scott Wilson
61f786e8d8 chore: add comment explaining ID 2024-11-06 23:25:31 -08:00
Scott Wilson
26064e3a08 docs: add images 2024-11-06 23:13:13 -08:00
Scott Wilson
9b246166a1 feature: project templates with docs 2024-11-06 23:12:32 -08:00
Sheen Capadngan
ac0cb6d96f misc: updated docs 2024-10-24 20:37:39 +08:00
Sheen Capadngan
f71f894de8 feat: added rotation handling for static ldap 2024-10-24 02:45:09 +08:00
Sheen Capadngan
66d2cc8947 misc: updated ldap edit 2024-10-24 02:10:55 +08:00
Sheen Capadngan
e034aa381a feat: initial schema setup 2024-10-24 00:29:41 +08:00
134 changed files with 4993 additions and 932 deletions

View File

@@ -18,6 +18,7 @@ import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-con
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service";
import { RateLimitConfiguration } from "@app/ee/services/rate-limit/rate-limit-types";
@@ -189,6 +190,7 @@ declare module "fastify" {
cmek: TCmekServiceFactory;
migration: TExternalMigrationServiceFactory;
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
projectTemplate: TProjectTemplateServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer

View File

@@ -200,6 +200,9 @@ import {
TProjectSlackConfigsInsert,
TProjectSlackConfigsUpdate,
TProjectsUpdate,
TProjectTemplates,
TProjectTemplatesInsert,
TProjectTemplatesUpdate,
TProjectUserAdditionalPrivilege,
TProjectUserAdditionalPrivilegeInsert,
TProjectUserAdditionalPrivilegeUpdate,
@@ -818,5 +821,10 @@ declare module "knex/types/tables" {
TExternalGroupOrgRoleMappingsInsert,
TExternalGroupOrgRoleMappingsUpdate
>;
[TableName.ProjectTemplates]: KnexOriginal.CompositeTableType<
TProjectTemplates,
TProjectTemplatesInsert,
TProjectTemplatesUpdate
>;
}
}

View File

@@ -0,0 +1,28 @@
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.ProjectTemplates))) {
await knex.schema.createTable(TableName.ProjectTemplates, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("name", 32).notNullable();
t.string("description").nullable();
t.jsonb("roles").notNullable();
t.jsonb("environments").notNullable();
t.uuid("orgId").notNullable().references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.ProjectTemplates);
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.ProjectTemplates)) {
await dropOnUpdateTrigger(knex, TableName.ProjectTemplates);
await knex.schema.dropTable(TableName.ProjectTemplates);
}
}

View File

@@ -0,0 +1,35 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasDisableBootstrapCertValidationCol = await knex.schema.hasColumn(
TableName.CertificateTemplateEstConfig,
"disableBootstrapCertValidation"
);
const hasCaChainCol = await knex.schema.hasColumn(TableName.CertificateTemplateEstConfig, "encryptedCaChain");
await knex.schema.alterTable(TableName.CertificateTemplateEstConfig, (t) => {
if (!hasDisableBootstrapCertValidationCol) {
t.boolean("disableBootstrapCertValidation").defaultTo(false).notNullable();
}
if (hasCaChainCol) {
t.binary("encryptedCaChain").nullable().alter();
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasDisableBootstrapCertValidationCol = await knex.schema.hasColumn(
TableName.CertificateTemplateEstConfig,
"disableBootstrapCertValidation"
);
await knex.schema.alterTable(TableName.CertificateTemplateEstConfig, (t) => {
if (hasDisableBootstrapCertValidationCol) {
t.dropColumn("disableBootstrapCertValidation");
}
});
}

View File

@@ -12,11 +12,12 @@ import { TImmutableDBKeys } from "./models";
export const CertificateTemplateEstConfigsSchema = z.object({
id: z.string().uuid(),
certificateTemplateId: z.string().uuid(),
encryptedCaChain: zodBuffer,
encryptedCaChain: zodBuffer.nullable().optional(),
hashedPassphrase: z.string(),
isEnabled: z.boolean(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
disableBootstrapCertValidation: z.boolean().default(false)
});
export type TCertificateTemplateEstConfigs = z.infer<typeof CertificateTemplateEstConfigsSchema>;

View File

@@ -64,6 +64,7 @@ export * from "./project-keys";
export * from "./project-memberships";
export * from "./project-roles";
export * from "./project-slack-configs";
export * from "./project-templates";
export * from "./project-user-additional-privilege";
export * from "./project-user-membership-roles";
export * from "./projects";

View File

@@ -41,6 +41,7 @@ export enum TableName {
ProjectUserAdditionalPrivilege = "project_user_additional_privilege",
ProjectUserMembershipRole = "project_user_membership_roles",
ProjectKeys = "project_keys",
ProjectTemplates = "project_templates",
Secret = "secrets",
SecretReference = "secret_references",
SecretSharing = "secret_sharing",

View File

@@ -15,7 +15,8 @@ export const ProjectRolesSchema = z.object({
permissions: z.unknown(),
createdAt: z.date(),
updatedAt: z.date(),
projectId: z.string()
projectId: z.string(),
version: z.number().default(1)
});
export type TProjectRoles = z.infer<typeof ProjectRolesSchema>;

View File

@@ -0,0 +1,23 @@
// 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 ProjectTemplatesSchema = z.object({
id: z.string().uuid(),
name: z.string(),
description: z.string().nullable().optional(),
roles: z.unknown(),
environments: z.unknown(),
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TProjectTemplates = z.infer<typeof ProjectTemplatesSchema>;
export type TProjectTemplatesInsert = Omit<z.input<typeof ProjectTemplatesSchema>, TImmutableDBKeys>;
export type TProjectTemplatesUpdate = Partial<Omit<z.input<typeof ProjectTemplatesSchema>, TImmutableDBKeys>>;

View File

@@ -1,3 +1,5 @@
import { registerProjectTemplateRouter } from "@app/ee/routes/v1/project-template-router";
import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router";
import { registerAccessApprovalRequestRouter } from "./access-approval-request-router";
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
@@ -92,4 +94,6 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register(registerExternalKmsRouter, {
prefix: "/external-kms"
});
await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" });
};

View File

@@ -192,7 +192,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
roles: ProjectRolesSchema.omit({ permissions: true }).array()
roles: ProjectRolesSchema.omit({ permissions: true, version: true }).array()
})
}
},
@@ -225,7 +225,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
role: SanitizedRoleSchemaV1
role: SanitizedRoleSchemaV1.omit({ version: true })
})
}
},

View File

@@ -0,0 +1,309 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { ProjectMembershipRole, ProjectTemplatesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
import { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
import { ProjectTemplates } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
import { AuthMode } from "@app/services/auth/auth-type";
const MAX_JSON_SIZE_LIMIT_IN_BYTES = 32_768;
const SlugSchema = z
.string()
.trim()
.min(1)
.max(32)
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Must be valid slug format"
});
const isReservedRoleSlug = (slug: string) =>
Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole);
const isReservedRoleName = (name: string) =>
["custom", "admin", "viewer", "developer", "no access"].includes(name.toLowerCase());
const SanitizedProjectTemplateSchema = ProjectTemplatesSchema.extend({
roles: z
.object({
name: z.string().trim().min(1),
slug: SlugSchema,
permissions: UnpackedPermissionSchema.array()
})
.array(),
environments: z
.object({
name: z.string().trim().min(1),
slug: SlugSchema,
position: z.number().min(1)
})
.array()
});
const ProjectTemplateRolesSchema = z
.object({
name: z.string().trim().min(1),
slug: SlugSchema,
permissions: ProjectPermissionV2Schema.array()
})
.array()
.superRefine((roles, ctx) => {
if (!roles.length) return;
if (Buffer.byteLength(JSON.stringify(roles)) > MAX_JSON_SIZE_LIMIT_IN_BYTES)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Size limit exceeded" });
if (new Set(roles.map((v) => v.slug)).size !== roles.length)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Role slugs must be unique" });
if (new Set(roles.map((v) => v.name)).size !== roles.length)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Role names must be unique" });
roles.forEach((role) => {
if (isReservedRoleSlug(role.slug))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Role slug "${role.slug}" is reserved` });
if (isReservedRoleName(role.name))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Role name "${role.name}" is reserved` });
});
});
const ProjectTemplateEnvironmentsSchema = z
.object({
name: z.string().trim().min(1),
slug: SlugSchema,
position: z.number().min(1)
})
.array()
.min(1)
.superRefine((environments, ctx) => {
if (Buffer.byteLength(JSON.stringify(environments)) > MAX_JSON_SIZE_LIMIT_IN_BYTES)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Size limit exceeded" });
if (new Set(environments.map((v) => v.name)).size !== environments.length)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Environment names must be unique" });
if (new Set(environments.map((v) => v.slug)).size !== environments.length)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Environment slugs must be unique" });
if (
environments.some((env) => env.position < 1 || env.position > environments.length) ||
new Set(environments.map((env) => env.position)).size !== environments.length
)
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "One or more of the positions specified is invalid. Positions must be sequential starting from 1."
});
});
export const registerProjectTemplateRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
description: "List project templates for the current organization.",
response: {
200: z.object({
projectTemplates: SanitizedProjectTemplateSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const projectTemplates = await server.services.projectTemplate.listProjectTemplatesByOrg(req.permission);
const auditTemplates = projectTemplates.filter((template) => !isInfisicalProjectTemplate(template.name));
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.GET_PROJECT_TEMPLATES,
metadata: {
count: auditTemplates.length,
templateIds: auditTemplates.map((template) => template.id)
}
}
});
return { projectTemplates };
}
});
server.route({
method: "GET",
url: "/:templateId",
config: {
rateLimit: readLimit
},
schema: {
description: "Get a project template by ID.",
params: z.object({
templateId: z.string().uuid()
}),
response: {
200: z.object({
projectTemplate: SanitizedProjectTemplateSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const projectTemplate = await server.services.projectTemplate.findProjectTemplateById(
req.params.templateId,
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.GET_PROJECT_TEMPLATE,
metadata: {
templateId: req.params.templateId
}
}
});
return { projectTemplate };
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
description: "Create a project template.",
body: z.object({
name: SlugSchema.refine((val) => !isInfisicalProjectTemplate(val), {
message: `The requested project template name is reserved.`
}).describe(ProjectTemplates.CREATE.name),
description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description),
roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles),
environments: ProjectTemplateEnvironmentsSchema.default(ProjectTemplateDefaultEnvironments).describe(
ProjectTemplates.CREATE.environments
)
}),
response: {
200: z.object({
projectTemplate: SanitizedProjectTemplateSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const projectTemplate = await server.services.projectTemplate.createProjectTemplate(req.body, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.CREATE_PROJECT_TEMPLATE,
metadata: req.body
}
});
return { projectTemplate };
}
});
server.route({
method: "PATCH",
url: "/:templateId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Update a project template.",
params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.UPDATE.templateId) }),
body: z.object({
name: SlugSchema.refine((val) => !isInfisicalProjectTemplate(val), {
message: `The requested project template name is reserved.`
})
.optional()
.describe(ProjectTemplates.UPDATE.name),
description: z.string().max(256).trim().optional().describe(ProjectTemplates.UPDATE.description),
roles: ProjectTemplateRolesSchema.optional().describe(ProjectTemplates.UPDATE.roles),
environments: ProjectTemplateEnvironmentsSchema.optional().describe(ProjectTemplates.UPDATE.environments)
}),
response: {
200: z.object({
projectTemplate: SanitizedProjectTemplateSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const projectTemplate = await server.services.projectTemplate.updateProjectTemplateById(
req.params.templateId,
req.body,
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.UPDATE_PROJECT_TEMPLATE,
metadata: {
templateId: req.params.templateId,
...req.body
}
}
});
return { projectTemplate };
}
});
server.route({
method: "DELETE",
url: "/:templateId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Delete a project template.",
params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.DELETE.templateId) }),
response: {
200: z.object({
projectTemplate: SanitizedProjectTemplateSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const projectTemplate = await server.services.projectTemplate.deleteProjectTemplateById(
req.params.templateId,
req.permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.DELETE_PROJECT_TEMPLATE,
metadata: {
templateId: req.params.templateId
}
}
});
return { projectTemplate };
}
});
};

View File

@@ -186,7 +186,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
roles: ProjectRolesSchema.omit({ permissions: true }).array()
roles: ProjectRolesSchema.omit({ permissions: true, version: true }).array()
})
}
},
@@ -219,7 +219,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
role: SanitizedRoleSchema
role: SanitizedRoleSchema.omit({ version: true })
})
}
},

View File

@@ -1,3 +1,7 @@
import {
TCreateProjectTemplateDTO,
TUpdateProjectTemplateDTO
} from "@app/ee/services/project-template/project-template-types";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { TProjectPermission } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type";
@@ -192,7 +196,13 @@ export enum EventType {
CMEK_ENCRYPT = "cmek-encrypt",
CMEK_DECRYPT = "cmek-decrypt",
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping"
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping",
GET_PROJECT_TEMPLATES = "get-project-templates",
GET_PROJECT_TEMPLATE = "get-project-template",
CREATE_PROJECT_TEMPLATE = "create-project-template",
UPDATE_PROJECT_TEMPLATE = "update-project-template",
DELETE_PROJECT_TEMPLATE = "delete-project-template",
APPLY_PROJECT_TEMPLATE = "apply-project-template"
}
interface UserActorMetadata {
@@ -1618,6 +1628,46 @@ interface UpdateExternalGroupOrgRoleMappingsEvent {
};
}
interface GetProjectTemplatesEvent {
type: EventType.GET_PROJECT_TEMPLATES;
metadata: {
count: number;
templateIds: string[];
};
}
interface GetProjectTemplateEvent {
type: EventType.GET_PROJECT_TEMPLATE;
metadata: {
templateId: string;
};
}
interface CreateProjectTemplateEvent {
type: EventType.CREATE_PROJECT_TEMPLATE;
metadata: TCreateProjectTemplateDTO;
}
interface UpdateProjectTemplateEvent {
type: EventType.UPDATE_PROJECT_TEMPLATE;
metadata: TUpdateProjectTemplateDTO & { templateId: string };
}
interface DeleteProjectTemplateEvent {
type: EventType.DELETE_PROJECT_TEMPLATE;
metadata: {
templateId: string;
};
}
interface ApplyProjectTemplateEvent {
type: EventType.APPLY_PROJECT_TEMPLATE;
metadata: {
template: string;
projectId: string;
};
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@@ -1766,4 +1816,10 @@ export type Event =
| CmekEncryptEvent
| CmekDecryptEvent
| GetExternalGroupOrgRoleMappingsEvent
| UpdateExternalGroupOrgRoleMappingsEvent;
| UpdateExternalGroupOrgRoleMappingsEvent
| GetProjectTemplatesEvent
| GetProjectTemplateEvent
| CreateProjectTemplateEvent
| UpdateProjectTemplateEvent
| DeleteProjectTemplateEvent
| ApplyProjectTemplateEvent;

View File

@@ -171,27 +171,29 @@ export const certificateEstServiceFactory = ({
});
}
const caCerts = estConfig.caChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => {
return new x509.X509Certificate(cert);
});
if (!estConfig.disableBootstrapCertValidation) {
const caCerts = estConfig.caChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => {
return new x509.X509Certificate(cert);
});
if (!caCerts) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
if (!caCerts) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
const leafCertificate = decodeURIComponent(sslClientCert).match(
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
)?.[0];
const leafCertificate = decodeURIComponent(sslClientCert).match(
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
)?.[0];
if (!leafCertificate) {
throw new BadRequestError({ message: "Missing client certificate" });
}
if (!leafCertificate) {
throw new BadRequestError({ message: "Missing client certificate" });
}
const certObj = new x509.X509Certificate(leafCertificate);
if (!(await isCertChainValid([certObj, ...caCerts]))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
const certObj = new x509.X509Certificate(leafCertificate);
if (!(await isCertChainValid([certObj, ...caCerts]))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
}
}
const { certificate } = await certificateAuthorityService.signCertFromCa({

View File

@@ -9,7 +9,7 @@ import {
} from "@app/ee/services/permission/project-permission";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection, ProjectServiceActor } from "@app/lib/types";
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@@ -457,7 +457,7 @@ export const dynamicSecretServiceFactory = ({
const listDynamicSecretsByFolderIds = async (
{ folderMappings, filters, projectId }: TListDynamicSecretsByFolderMappingsDTO,
actor: ProjectServiceActor
actor: OrgServiceActor
) => {
const { permission } = await permissionService.getProjectPermission(
actor.type,

View File

@@ -7,7 +7,7 @@ import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { LdapSchema, TDynamicProviderFns } from "./models";
import { LdapCredentialType, LdapSchema, TDynamicProviderFns } from "./models";
const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
@@ -193,29 +193,76 @@ export const LdapProvider = (): TDynamicProviderFns => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.creationLdif });
if (providerInputs.credentialType === LdapCredentialType.Static) {
const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m);
try {
const dnArray = await executeLdif(client, generatedLdif);
if (dnMatch) {
const username = dnMatch[1];
const password = generatePassword();
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
} catch (err) {
if (providerInputs.rollbackLdif) {
const rollbackLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rollbackLdif });
await executeLdif(client, rollbackLdif);
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rotationLdif });
try {
const dnArray = await executeLdif(client, generatedLdif);
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
} catch (err) {
throw new BadRequestError({ message: (err as Error).message });
}
} else {
throw new BadRequestError({
message: "Invalid rotation LDIF, missing DN."
});
}
} else {
const username = generateUsername();
const password = generatePassword();
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.creationLdif });
try {
const dnArray = await executeLdif(client, generatedLdif);
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
} catch (err) {
if (providerInputs.rollbackLdif) {
const rollbackLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rollbackLdif });
await executeLdif(client, rollbackLdif);
}
throw new BadRequestError({ message: (err as Error).message });
}
throw new BadRequestError({ message: (err as Error).message });
}
};
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const client = await getClient(providerInputs);
if (providerInputs.credentialType === LdapCredentialType.Static) {
const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m);
if (dnMatch) {
const username = dnMatch[1];
const password = generatePassword();
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rotationLdif });
try {
const dnArray = await executeLdif(client, generatedLdif);
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
} catch (err) {
throw new BadRequestError({ message: (err as Error).message });
}
} else {
throw new BadRequestError({
message: "Invalid rotation LDIF, missing DN."
});
}
}
const revocationLdif = generateLDIF({ username: entityId, ldifTemplate: providerInputs.revocationLdif });
await executeLdif(connection, revocationLdif);
await executeLdif(client, revocationLdif);
return { entityId };
};

View File

@@ -12,6 +12,11 @@ export enum ElasticSearchAuthTypes {
ApiKey = "api-key"
}
export enum LdapCredentialType {
Dynamic = "dynamic",
Static = "static"
}
export const DynamicSecretRedisDBSchema = z.object({
host: z.string().trim().toLowerCase(),
port: z.number(),
@@ -195,16 +200,26 @@ export const AzureEntraIDSchema = z.object({
clientSecret: z.string().trim().min(1)
});
export const LdapSchema = z.object({
url: z.string().trim().min(1),
binddn: z.string().trim().min(1),
bindpass: z.string().trim().min(1),
ca: z.string().optional(),
creationLdif: z.string().min(1),
revocationLdif: z.string().min(1),
rollbackLdif: z.string().optional()
});
export const LdapSchema = z.union([
z.object({
url: z.string().trim().min(1),
binddn: z.string().trim().min(1),
bindpass: z.string().trim().min(1),
ca: z.string().optional(),
credentialType: z.literal(LdapCredentialType.Dynamic).optional().default(LdapCredentialType.Dynamic),
creationLdif: z.string().min(1),
revocationLdif: z.string().min(1),
rollbackLdif: z.string().optional()
}),
z.object({
url: z.string().trim().min(1),
binddn: z.string().trim().min(1),
bindpass: z.string().trim().min(1),
ca: z.string().optional(),
credentialType: z.literal(LdapCredentialType.Static),
rotationLdif: z.string().min(1)
})
]);
export enum DynamicSecretProviders {
SqlDatabase = "sql-database",

View File

@@ -123,7 +123,7 @@ export const groupServiceFactory = ({
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to update group due to plan restrictio Upgrade plan to update group."
message: "Failed to update group due to plan restriction Upgrade plan to update group."
});
const group = await groupDAL.findOne({ orgId: actorOrgId, id });

View File

@@ -47,7 +47,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
secretsLimit: 40
},
pkiEst: false,
enforceMfa: false
enforceMfa: false,
projectTemplates: false
});
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {

View File

@@ -65,6 +65,7 @@ export type TFeatureSet = {
};
pkiEst: boolean;
enforceMfa: boolean;
projectTemplates: false;
};
export type TOrgPlansTableDTO = {

View File

@@ -26,7 +26,8 @@ export enum OrgPermissionSubjects {
Identity = "identity",
Kms = "kms",
AdminConsole = "organization-admin-console",
AuditLogs = "audit-logs"
AuditLogs = "audit-logs",
ProjectTemplates = "project-templates"
}
export type OrgPermissionSet =
@@ -45,6 +46,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
const buildAdminPermission = () => {
@@ -118,6 +120,11 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.AuditLogs);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.AuditLogs);
can(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
can(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
return rules;

View File

@@ -0,0 +1,5 @@
export const ProjectTemplateDefaultEnvironments = [
{ name: "Development", slug: "dev", position: 1 },
{ name: "Staging", slug: "staging", position: 2 },
{ name: "Production", slug: "prod", position: 3 }
];

View File

@@ -0,0 +1,7 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TProjectTemplateDALFactory = ReturnType<typeof projectTemplateDALFactory>;
export const projectTemplateDALFactory = (db: TDbClient) => ormify(db, TableName.ProjectTemplates);

View File

@@ -0,0 +1,24 @@
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
import {
InfisicalProjectTemplate,
TUnpackedPermission
} from "@app/ee/services/project-template/project-template-types";
import { getPredefinedRoles } from "@app/services/project-role/project-role-fns";
export const getDefaultProjectTemplate = (orgId: string) => ({
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // random ID to appease zod
name: InfisicalProjectTemplate.Default,
createdAt: new Date(),
updatedAt: new Date(),
description: "Infisical's default project template",
environments: ProjectTemplateDefaultEnvironments,
roles: [...getPredefinedRoles("project-template")].map(({ name, slug, permissions }) => ({
name,
slug,
permissions: permissions as TUnpackedPermission[]
})),
orgId
});
export const isInfisicalProjectTemplate = (template: string) =>
Object.values(InfisicalProjectTemplate).includes(template as InfisicalProjectTemplate);

View File

@@ -0,0 +1,265 @@
import { ForbiddenError } from "@casl/ability";
import { packRules } from "@casl/ability/extra";
import { TProjectTemplates } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getDefaultProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
import {
TCreateProjectTemplateDTO,
TProjectTemplateEnvironment,
TProjectTemplateRole,
TUnpackedPermission,
TUpdateProjectTemplateDTO
} from "@app/ee/services/project-template/project-template-types";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { unpackPermissions } from "@app/server/routes/santizedSchemas/permission";
import { getPredefinedRoles } from "@app/services/project-role/project-role-fns";
import { TProjectTemplateDALFactory } from "./project-template-dal";
type TProjectTemplatesServiceFactoryDep = {
licenseService: TLicenseServiceFactory;
permissionService: TPermissionServiceFactory;
projectTemplateDAL: TProjectTemplateDALFactory;
};
export type TProjectTemplateServiceFactory = ReturnType<typeof projectTemplateServiceFactory>;
const $unpackProjectTemplate = ({ roles, environments, ...rest }: TProjectTemplates) => ({
...rest,
environments: environments as TProjectTemplateEnvironment[],
roles: [
...getPredefinedRoles("project-template").map(({ name, slug, permissions }) => ({
name,
slug,
permissions: permissions as TUnpackedPermission[]
})),
...(roles as TProjectTemplateRole[]).map((role) => ({
...role,
permissions: unpackPermissions(role.permissions)
}))
]
});
export const projectTemplateServiceFactory = ({
licenseService,
permissionService,
projectTemplateDAL
}: TProjectTemplatesServiceFactoryDep) => {
const listProjectTemplatesByOrg = async (actor: OrgServiceActor) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates)
throw new BadRequestError({
message: "Failed to access project templates due to plan restriction. Upgrade plan to access project templates."
});
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
const projectTemplates = await projectTemplateDAL.find({
orgId: actor.orgId
});
return [
getDefaultProjectTemplate(actor.orgId),
...projectTemplates.map((template) => $unpackProjectTemplate(template))
];
};
const findProjectTemplateByName = async (name: string, actor: OrgServiceActor) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates)
throw new BadRequestError({
message: "Failed to access project template due to plan restriction. Upgrade plan to access project templates."
});
const projectTemplate = await projectTemplateDAL.findOne({ name, orgId: actor.orgId });
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with Name "${name}"` });
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
projectTemplate.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
return {
...$unpackProjectTemplate(projectTemplate),
packedRoles: projectTemplate.roles as TProjectTemplateRole[] // preserve packed for when applying template
};
};
const findProjectTemplateById = async (id: string, actor: OrgServiceActor) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates)
throw new BadRequestError({
message: "Failed to access project template due to plan restriction. Upgrade plan to access project templates."
});
const projectTemplate = await projectTemplateDAL.findById(id);
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
projectTemplate.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
return {
...$unpackProjectTemplate(projectTemplate),
packedRoles: projectTemplate.roles as TProjectTemplateRole[] // preserve packed for when applying template
};
};
const createProjectTemplate = async (
{ roles, environments, ...params }: TCreateProjectTemplateDTO,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates)
throw new BadRequestError({
message: "Failed to create project template due to plan restriction. Upgrade plan to access project templates."
});
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates);
const isConflictingName = Boolean(
await projectTemplateDAL.findOne({
name: params.name,
orgId: actor.orgId
})
);
if (isConflictingName)
throw new BadRequestError({
message: `A project template with the name "${params.name}" already exists.`
});
const projectTemplate = await projectTemplateDAL.create({
...params,
roles: JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) }))),
environments: JSON.stringify(environments),
orgId: actor.orgId
});
return $unpackProjectTemplate(projectTemplate);
};
const updateProjectTemplateById = async (
id: string,
{ roles, environments, ...params }: TUpdateProjectTemplateDTO,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates)
throw new BadRequestError({
message: "Failed to update project template due to plan restriction. Upgrade plan to access project templates."
});
const projectTemplate = await projectTemplateDAL.findById(id);
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
projectTemplate.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
if (params.name && projectTemplate.name !== params.name) {
const isConflictingName = Boolean(
await projectTemplateDAL.findOne({
name: params.name,
orgId: projectTemplate.orgId
})
);
if (isConflictingName)
throw new BadRequestError({
message: `A project template with the name "${params.name}" already exists.`
});
}
const updatedProjectTemplate = await projectTemplateDAL.updateById(id, {
...params,
roles: roles
? JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) })))
: undefined,
environments: environments ? JSON.stringify(environments) : undefined
});
return $unpackProjectTemplate(updatedProjectTemplate);
};
const deleteProjectTemplateById = async (id: string, actor: OrgServiceActor) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates)
throw new BadRequestError({
message: "Failed to delete project template due to plan restriction. Upgrade plan to access project templates."
});
const projectTemplate = await projectTemplateDAL.findById(id);
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
projectTemplate.orgId,
actor.authMethod,
actor.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
const deletedProjectTemplate = await projectTemplateDAL.deleteById(id);
return $unpackProjectTemplate(deletedProjectTemplate);
};
return {
listProjectTemplatesByOrg,
createProjectTemplate,
updateProjectTemplateById,
deleteProjectTemplateById,
findProjectTemplateById,
findProjectTemplateByName
};
};

View File

@@ -0,0 +1,28 @@
import { z } from "zod";
import { TProjectEnvironments } from "@app/db/schemas";
import { TProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
export type TProjectTemplateEnvironment = Pick<TProjectEnvironments, "name" | "slug" | "position">;
export type TProjectTemplateRole = {
slug: string;
name: string;
permissions: TProjectPermissionV2Schema[];
};
export type TCreateProjectTemplateDTO = {
name: string;
description?: string;
roles: TProjectTemplateRole[];
environments: TProjectTemplateEnvironment[];
};
export type TUpdateProjectTemplateDTO = Partial<TCreateProjectTemplateDTO>;
export type TUnpackedPermission = z.infer<typeof UnpackedPermissionSchema>;
export enum InfisicalProjectTemplate {
Default = "default"
}

View File

@@ -391,7 +391,8 @@ export const PROJECTS = {
CREATE: {
organizationSlug: "The slug of the organization to create the project in.",
projectName: "The name of the project to create.",
slug: "An optional slug for the project."
slug: "An optional slug for the project.",
template: "The name of the project template, if specified, to apply to this project."
},
DELETE: {
workspaceId: "The ID of the project to delete."
@@ -1438,3 +1439,22 @@ export const KMS = {
ciphertext: "The ciphertext to be decrypted (base64 encoded)."
}
};
export const ProjectTemplates = {
CREATE: {
name: "The name of the project template to be created. Must be slug-friendly.",
description: "An optional description of the project template.",
roles: "The roles to be created when the template is applied to a project.",
environments: "The environments to be created when the template is applied to a project."
},
UPDATE: {
templateId: "The ID of the project template to be updated.",
name: "The updated name of the project template. Must be slug-friendly.",
description: "The updated description of the project template.",
roles: "The updated roles to be created when the template is applied to a project.",
environments: "The updated environments to be created when the template is applied to a project."
},
DELETE: {
templateId: "The ID of the project template to be deleted."
}
};

View File

@@ -58,7 +58,7 @@ export enum OrderByDirection {
DESC = "desc"
}
export type ProjectServiceActor = {
export type OrgServiceActor = {
type: ActorType;
id: string;
authMethod: ActorAuthMethod;

View File

@@ -1,2 +1,3 @@
export { isDisposableEmail } from "./validate-email";
export { isValidFolderName, isValidSecretPath } from "./validate-folder-name";
export { blockLocalAndPrivateIpAddresses } from "./validate-url";

View File

@@ -0,0 +1,8 @@
// regex to allow only alphanumeric, dash, underscore
export const isValidFolderName = (name: string) => /^[a-zA-Z0-9-_]+$/.test(name);
export const isValidSecretPath = (path: string) =>
path
.split("/")
.filter((el) => el.length)
.every((name) => isValidFolderName(name));

View File

@@ -43,6 +43,8 @@ import { oidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
import { oidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
import { permissionDALFactory } from "@app/ee/services/permission/permission-dal";
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { projectTemplateDALFactory } from "@app/ee/services/project-template/project-template-dal";
import { projectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
import { projectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
import { projectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
import { rateLimitDALFactory } from "@app/ee/services/rate-limit/rate-limit-dal";
@@ -340,6 +342,8 @@ export const registerRoutes = async (
const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db);
const projectTemplateDAL = projectTemplateDALFactory(db);
const permissionService = permissionServiceFactory({
permissionDAL,
orgRoleDAL,
@@ -732,6 +736,12 @@ export const registerRoutes = async (
permissionService
});
const projectTemplateService = projectTemplateServiceFactory({
licenseService,
permissionService,
projectTemplateDAL
});
const projectService = projectServiceFactory({
permissionService,
projectDAL,
@@ -758,7 +768,8 @@ export const registerRoutes = async (
projectBotDAL,
certificateTemplateDAL,
projectSlackConfigDAL,
slackIntegrationDAL
slackIntegrationDAL,
projectTemplateService
});
const projectEnvService = projectEnvServiceFactory({
@@ -1336,7 +1347,8 @@ export const registerRoutes = async (
slack: slackService,
workflowIntegration: workflowIntegrationService,
migration: migrationService,
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService,
projectTemplate: projectTemplateService
});
const cronJobs: CronJob[] = [];

View File

@@ -14,7 +14,8 @@ import { validateTemplateRegexField } from "@app/services/certificate-template/c
const sanitizedEstConfig = CertificateTemplateEstConfigsSchema.pick({
id: true,
certificateTemplateId: true,
isEnabled: true
isEnabled: true,
disableBootstrapCertValidation: true
});
export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => {
@@ -241,11 +242,18 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
params: z.object({
certificateTemplateId: z.string().trim()
}),
body: z.object({
caChain: z.string().trim().min(1),
passphrase: z.string().min(1),
isEnabled: z.boolean().default(true)
}),
body: z
.object({
caChain: z.string().trim().optional(),
passphrase: z.string().min(1),
isEnabled: z.boolean().default(true),
disableBootstrapCertValidation: z.boolean().default(false)
})
.refine(
({ caChain, disableBootstrapCertValidation }) =>
disableBootstrapCertValidation || (!disableBootstrapCertValidation && caChain),
"CA chain is required"
),
response: {
200: sanitizedEstConfig
}
@@ -289,8 +297,9 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
certificateTemplateId: z.string().trim()
}),
body: z.object({
caChain: z.string().trim().min(1).optional(),
caChain: z.string().trim().optional(),
passphrase: z.string().min(1).optional(),
disableBootstrapCertValidation: z.boolean().optional(),
isEnabled: z.boolean().optional()
}),
response: {

View File

@@ -840,4 +840,91 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
};
}
});
server.route({
method: "GET",
url: "/secrets-by-keys",
config: {
rateLimit: secretsLimit
},
schema: {
security: [
{
bearerAuth: []
}
],
querystring: z.object({
projectId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
keys: z.string().trim().transform(decodeURIComponent)
}),
response: {
200: z.object({
secrets: secretRawSchema
.extend({
secretPath: z.string().optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
})
.array()
.optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { secretPath, projectId, environment } = req.query;
const keys = req.query.keys?.split(",").filter((key) => Boolean(key.trim())) ?? [];
if (!keys.length) throw new BadRequestError({ message: "One or more keys required" });
const { secrets } = await server.services.secret.getSecretsRaw({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environment,
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
keys
});
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRETS,
metadata: {
environment,
secretPath,
numberOfSecrets: secrets.length
}
}
});
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretPulled,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: secrets.length,
workspaceId: projectId,
environment,
secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
...req.auditLogInfo
}
});
}
return { secrets };
}
});
};

View File

@@ -891,6 +891,48 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
}
});
server.route({
method: "GET",
url: "/:integrationAuthId/bitbucket/environments",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
integrationAuthId: z.string().trim()
}),
querystring: z.object({
workspaceSlug: z.string().trim().min(1, { message: "Workspace slug required" }),
repoSlug: z.string().trim().min(1, { message: "Repo slug required" })
}),
response: {
200: z.object({
environments: z
.object({
name: z.string(),
slug: z.string(),
uuid: z.string(),
type: z.string()
})
.array()
})
}
},
handler: async (req) => {
const environments = await server.services.integrationAuth.getBitbucketEnvironments({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId,
workspaceSlug: req.query.workspaceSlug,
repoSlug: req.query.repoSlug
});
return { environments };
}
});
server.route({
method: "GET",
url: "/:integrationAuthId/northflank/secret-groups",

View File

@@ -4,6 +4,7 @@ import { SecretFoldersSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { FOLDERS } from "@app/lib/api-docs";
import { prefixWithSlash, removeTrailingSlash } from "@app/lib/fn";
import { isValidFolderName } from "@app/lib/validator";
import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -25,7 +26,13 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
body: z.object({
workspaceId: z.string().trim().describe(FOLDERS.CREATE.workspaceId),
environment: z.string().trim().describe(FOLDERS.CREATE.environment),
name: z.string().trim().describe(FOLDERS.CREATE.name),
name: z
.string()
.trim()
.describe(FOLDERS.CREATE.name)
.refine((name) => isValidFolderName(name), {
message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed."
}),
path: z
.string()
.trim()
@@ -97,7 +104,13 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
body: z.object({
workspaceId: z.string().trim().describe(FOLDERS.UPDATE.workspaceId),
environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
name: z.string().trim().describe(FOLDERS.UPDATE.name),
name: z
.string()
.trim()
.describe(FOLDERS.UPDATE.name)
.refine((name) => isValidFolderName(name), {
message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed."
}),
path: z
.string()
.trim()
@@ -170,7 +183,13 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.object({
id: z.string().describe(FOLDERS.UPDATE.folderId),
environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
name: z.string().trim().describe(FOLDERS.UPDATE.name),
name: z
.string()
.trim()
.describe(FOLDERS.UPDATE.name)
.refine((name) => isValidFolderName(name), {
message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed."
}),
path: z
.string()
.trim()

View File

@@ -9,6 +9,7 @@ import {
ProjectKeysSchema
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
import { PROJECTS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
@@ -169,7 +170,15 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
})
.optional()
.describe(PROJECTS.CREATE.slug),
kmsKeyId: z.string().optional()
kmsKeyId: z.string().optional(),
template: z
.string()
.refine((v) => slugify(v) === v, {
message: "Template name must be in slug format"
})
.optional()
.default(InfisicalProjectTemplate.Default)
.describe(PROJECTS.CREATE.template)
}),
response: {
200: z.object({
@@ -186,7 +195,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actorAuthMethod: req.permission.authMethod,
workspaceName: req.body.projectName,
slug: req.body.slug,
kmsKeyId: req.body.kmsKeyId
kmsKeyId: req.body.kmsKeyId,
template: req.body.template
});
await server.services.telemetry.sendPostHogEvents({
@@ -199,6 +209,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}
});
if (req.body.template) {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.APPLY_PROJECT_TEMPLATE,
metadata: {
template: req.body.template,
projectId: project.id
}
}
});
}
return { project };
}
});

View File

@@ -235,7 +235,8 @@ export const certificateTemplateServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
disableBootstrapCertValidation
}: TCreateEstConfigurationDTO) => {
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.pkiEst) {
@@ -266,39 +267,45 @@ export const certificateTemplateServiceFactory = ({
const appCfg = getConfig();
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: certTemplate.projectId,
projectDAL,
kmsService
});
let encryptedCaChain: Buffer | undefined;
if (caChain) {
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: certTemplate.projectId,
projectDAL,
kmsService
});
// validate CA chain
const certificates = caChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => new x509.X509Certificate(cert));
// validate CA chain
const certificates = caChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => new x509.X509Certificate(cert));
if (!certificates) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
if (!certificates) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
if (!(await isCertChainValid(certificates))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
}
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { cipherTextBlob } = await kmsEncryptor({
plainText: Buffer.from(caChain)
});
encryptedCaChain = cipherTextBlob;
}
if (!(await isCertChainValid(certificates))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
}
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { cipherTextBlob: encryptedCaChain } = await kmsEncryptor({
plainText: Buffer.from(caChain)
});
const hashedPassphrase = await bcrypt.hash(passphrase, appCfg.SALT_ROUNDS);
const estConfig = await certificateTemplateEstConfigDAL.create({
certificateTemplateId,
hashedPassphrase,
encryptedCaChain,
isEnabled
isEnabled,
disableBootstrapCertValidation
});
return { ...estConfig, projectId: certTemplate.projectId };
@@ -312,7 +319,8 @@ export const certificateTemplateServiceFactory = ({
actorId,
actorAuthMethod,
actor,
actorOrgId
actorOrgId,
disableBootstrapCertValidation
}: TUpdateEstConfigurationDTO) => {
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.pkiEst) {
@@ -360,7 +368,8 @@ export const certificateTemplateServiceFactory = ({
});
const updatedData: TCertificateTemplateEstConfigsUpdate = {
isEnabled
isEnabled,
disableBootstrapCertValidation
};
if (caChain) {
@@ -442,18 +451,24 @@ export const certificateTemplateServiceFactory = ({
kmsId: certificateManagerKmsId
});
const decryptedCaChain = await kmsDecryptor({
cipherTextBlob: estConfig.encryptedCaChain
});
let decryptedCaChain = "";
if (estConfig.encryptedCaChain) {
decryptedCaChain = (
await kmsDecryptor({
cipherTextBlob: estConfig.encryptedCaChain
})
).toString();
}
return {
certificateTemplateId,
id: estConfig.id,
isEnabled: estConfig.isEnabled,
caChain: decryptedCaChain.toString(),
caChain: decryptedCaChain,
hashedPassphrase: estConfig.hashedPassphrase,
projectId: certTemplate.projectId,
orgId: certTemplate.orgId
orgId: certTemplate.orgId,
disableBootstrapCertValidation: estConfig.disableBootstrapCertValidation
};
};

View File

@@ -34,9 +34,10 @@ export type TDeleteCertTemplateDTO = {
export type TCreateEstConfigurationDTO = {
certificateTemplateId: string;
caChain: string;
caChain?: string;
passphrase: string;
isEnabled: boolean;
disableBootstrapCertValidation: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateEstConfigurationDTO = {
@@ -44,6 +45,7 @@ export type TUpdateEstConfigurationDTO = {
caChain?: string;
passphrase?: string;
isEnabled?: boolean;
disableBootstrapCertValidation?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TGetEstConfigurationDTO =

View File

@@ -3,7 +3,7 @@ import { ForbiddenError } from "@casl/ability";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ProjectServiceActor } from "@app/lib/types";
import { OrgServiceActor } from "@app/lib/types";
import {
TCmekDecryptDTO,
TCmekEncryptDTO,
@@ -23,7 +23,7 @@ type TCmekServiceFactoryDep = {
export type TCmekServiceFactory = ReturnType<typeof cmekServiceFactory>;
export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TCmekServiceFactoryDep) => {
const createCmek = async ({ projectId, ...dto }: TCreateCmekDTO, actor: ProjectServiceActor) => {
const createCmek = async ({ projectId, ...dto }: TCreateCmekDTO, actor: OrgServiceActor) => {
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
@@ -43,7 +43,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
return cmek;
};
const updateCmekById = async ({ keyId, ...data }: TUpdabteCmekByIdDTO, actor: ProjectServiceActor) => {
const updateCmekById = async ({ keyId, ...data }: TUpdabteCmekByIdDTO, actor: OrgServiceActor) => {
const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
@@ -65,7 +65,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
return cmek;
};
const deleteCmekById = async (keyId: string, actor: ProjectServiceActor) => {
const deleteCmekById = async (keyId: string, actor: OrgServiceActor) => {
const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
@@ -87,10 +87,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
return cmek;
};
const listCmeksByProjectId = async (
{ projectId, ...filters }: TListCmeksByProjectIdDTO,
actor: ProjectServiceActor
) => {
const listCmeksByProjectId = async ({ projectId, ...filters }: TListCmeksByProjectIdDTO, actor: OrgServiceActor) => {
const { permission } = await permissionService.getProjectPermission(
actor.type,
actor.id,
@@ -106,7 +103,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
return { cmeks, totalCount };
};
const cmekEncrypt = async ({ keyId, plaintext }: TCmekEncryptDTO, actor: ProjectServiceActor) => {
const cmekEncrypt = async ({ keyId, plaintext }: TCmekEncryptDTO, actor: OrgServiceActor) => {
const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
@@ -132,7 +129,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
return cipherTextBlob.toString("base64");
};
const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: ProjectServiceActor) => {
const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: OrgServiceActor) => {
const key = await kmsDAL.findById(keyId);
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });

View File

@@ -3,7 +3,7 @@ import { ForbiddenError } from "@casl/ability";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectServiceActor } from "@app/lib/types";
import { OrgServiceActor } from "@app/lib/types";
import { constructGroupOrgMembershipRoleMappings } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-fns";
import { TSyncExternalGroupOrgMembershipRoleMappingsDTO } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-types";
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
@@ -25,7 +25,7 @@ export const externalGroupOrgRoleMappingServiceFactory = ({
permissionService,
orgRoleDAL
}: TExternalGroupOrgRoleMappingServiceFactoryDep) => {
const listExternalGroupOrgRoleMappings = async (actor: ProjectServiceActor) => {
const listExternalGroupOrgRoleMappings = async (actor: OrgServiceActor) => {
const { permission } = await permissionService.getOrgPermission(
actor.type,
actor.id,
@@ -46,7 +46,7 @@ export const externalGroupOrgRoleMappingServiceFactory = ({
const updateExternalGroupOrgRoleMappings = async (
dto: TSyncExternalGroupOrgMembershipRoleMappingsDTO,
actor: ProjectServiceActor
actor: OrgServiceActor
) => {
const { permission } = await permissionService.getOrgPermission(
actor.type,

View File

@@ -20,6 +20,7 @@ import { getApps } from "./integration-app-list";
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
import {
TBitbucketEnvironment,
TBitbucketWorkspace,
TChecklyGroups,
TDeleteIntegrationAuthByIdDTO,
@@ -30,6 +31,7 @@ import {
THerokuPipelineCoupling,
TIntegrationAuthAppsDTO,
TIntegrationAuthAwsKmsKeyDTO,
TIntegrationAuthBitbucketEnvironmentsDTO,
TIntegrationAuthBitbucketWorkspaceDTO,
TIntegrationAuthChecklyGroupsDTO,
TIntegrationAuthGithubEnvsDTO,
@@ -1261,6 +1263,55 @@ export const integrationAuthServiceFactory = ({
return workspaces;
};
const getBitbucketEnvironments = async ({
workspaceSlug,
repoSlug,
actorId,
actor,
actorOrgId,
actorAuthMethod,
id
}: TIntegrationAuthBitbucketEnvironmentsDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integrationAuth.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const environments: TBitbucketEnvironment[] = [];
let hasNextPage = true;
let environmentsUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${workspaceSlug}/${repoSlug}/environments`;
while (hasNextPage) {
// eslint-disable-next-line
const { data }: { data: { values: TBitbucketEnvironment[]; next: string } } = await request.get(environmentsUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
});
if (data?.values.length > 0) {
environments.push(...data.values);
}
if (data.next) {
environmentsUrl = data.next;
} else {
hasNextPage = false;
}
}
return environments;
};
const getNorthFlankSecretGroups = async ({
id,
actor,
@@ -1499,6 +1550,7 @@ export const integrationAuthServiceFactory = ({
getNorthFlankSecretGroups,
getTeamcityBuildConfigs,
getBitbucketWorkspaces,
getBitbucketEnvironments,
getIntegrationAccessToken,
duplicateIntegrationAuth
};

View File

@@ -99,6 +99,12 @@ export type TIntegrationAuthBitbucketWorkspaceDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthBitbucketEnvironmentsDTO = {
workspaceSlug: string;
repoSlug: string;
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthNorthflankSecretGroupDTO = {
id: string;
appId: string;
@@ -148,6 +154,13 @@ export type TBitbucketWorkspace = {
updated_on: string;
};
export type TBitbucketEnvironment = {
type: string;
uuid: string;
name: string;
slug: string;
};
export type TNorthflankSecretGroup = {
id: string;
name: string;

View File

@@ -334,7 +334,7 @@ export const getIntegrationOptions = async () => {
docsLink: ""
},
{
name: "BitBucket",
name: "Bitbucket",
slug: "bitbucket",
image: "BitBucket.png",
isAvailable: true,

View File

@@ -3631,7 +3631,14 @@ const syncSecretsBitBucket = async ({
const res: { [key: string]: BitbucketVariable } = {};
let hasNextPage = true;
let variablesUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${integration.targetEnvironmentId}/${integration.appId}/pipelines_config/variables`;
const rootUrl = integration.targetServiceId
? // scope: deployment environment
`${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${integration.targetEnvironmentId}/${integration.appId}/deployments_config/environments/${integration.targetServiceId}/variables`
: // scope: repository
`${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${integration.targetEnvironmentId}/${integration.appId}/pipelines_config/variables`;
let variablesUrl = rootUrl;
while (hasNextPage) {
const { data }: { data: VariablesResponse } = await request.get(variablesUrl, {
@@ -3658,7 +3665,7 @@ const syncSecretsBitBucket = async ({
if (key in res) {
// update existing secret
await request.put(
`${variablesUrl}/${res[key].uuid}`,
`${rootUrl}/${res[key].uuid}`,
{
key,
value: secrets[key].value,
@@ -3674,7 +3681,7 @@ const syncSecretsBitBucket = async ({
} else {
// create new secret
await request.post(
variablesUrl,
rootUrl,
{
key,
value: secrets[key].value,

View File

@@ -6,6 +6,8 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
@@ -94,7 +96,7 @@ type TProjectServiceFactoryDep = {
orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "insertMany">;
kmsService: Pick<
TKmsServiceFactory,
| "updateProjectSecretManagerKmsKey"
@@ -104,6 +106,7 @@ type TProjectServiceFactoryDep = {
| "getProjectSecretManagerKmsKeyId"
| "deleteInternalKms"
>;
projectTemplateService: TProjectTemplateServiceFactory;
};
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
@@ -134,7 +137,8 @@ export const projectServiceFactory = ({
kmsService,
projectBotDAL,
projectSlackConfigDAL,
slackIntegrationDAL
slackIntegrationDAL,
projectTemplateService
}: TProjectServiceFactoryDep) => {
/*
* Create workspace. Make user the admin
@@ -148,7 +152,8 @@ export const projectServiceFactory = ({
slug: projectSlug,
kmsKeyId,
tx: trx,
createDefaultEnvs = true
createDefaultEnvs = true,
template = InfisicalProjectTemplate.Default
}: TCreateProjectDTO) => {
const organization = await orgDAL.findOne({ id: actorOrgId });
@@ -183,6 +188,21 @@ export const projectServiceFactory = ({
}
}
let projectTemplate: Awaited<ReturnType<typeof projectTemplateService.findProjectTemplateByName>> | null = null;
switch (template) {
case InfisicalProjectTemplate.Default:
projectTemplate = null;
break;
default:
projectTemplate = await projectTemplateService.findProjectTemplateByName(template, {
id: actorId,
orgId: organization.id,
type: actor,
authMethod: actorAuthMethod
});
}
const project = await projectDAL.create(
{
name: workspaceName,
@@ -210,7 +230,24 @@ export const projectServiceFactory = ({
// set default environments and root folder for provided environments
let envs: TProjectEnvironments[] = [];
if (createDefaultEnvs) {
if (projectTemplate) {
envs = await projectEnvDAL.insertMany(
projectTemplate.environments.map((env) => ({ ...env, projectId: project.id })),
tx
);
await folderDAL.insertMany(
envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })),
tx
);
await projectRoleDAL.insertMany(
projectTemplate.packedRoles.map((role) => ({
...role,
permissions: JSON.stringify(role.permissions),
projectId: project.id
})),
tx
);
} else if (createDefaultEnvs) {
envs = await projectEnvDAL.insertMany(
DEFAULT_PROJECT_ENVS.map((el, i) => ({ ...el, projectId: project.id, position: i + 1 })),
tx

View File

@@ -32,6 +32,7 @@ export type TCreateProjectDTO = {
slug?: string;
kmsKeyId?: string;
createDefaultEnvs?: boolean;
template?: string;
tx?: Knex;
};

View File

@@ -6,6 +6,7 @@ import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { groupBy, removeTrailingSlash } from "@app/lib/fn";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types";
import { isValidSecretPath } from "@app/lib/validator";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { TFindFoldersDeepByParentIdsDTO } from "./secret-folder-types";
@@ -214,6 +215,12 @@ export const secretFolderDALFactory = (db: TDbClient) => {
const secretFolderOrm = ormify(db, TableName.SecretFolder);
const findBySecretPath = async (projectId: string, environment: string, path: string, tx?: Knex) => {
const isValidPath = isValidSecretPath(path);
if (!isValidPath)
throw new BadRequestError({
message: "Invalid secret path. Only alphanumeric characters, dashes, and underscores are allowed."
});
try {
const folder = await sqlFindFolderByPathQuery(
tx || db.replicaNode(),
@@ -236,6 +243,12 @@ export const secretFolderDALFactory = (db: TDbClient) => {
// finds folders by path for multiple envs
const findBySecretPathMultiEnv = async (projectId: string, environments: string[], path: string, tx?: Knex) => {
const isValidPath = isValidSecretPath(path);
if (!isValidPath)
throw new BadRequestError({
message: "Invalid secret path. Only alphanumeric characters, dashes, and underscores are allowed."
});
try {
const pathDepth = removeTrailingSlash(path).split("/").filter(Boolean).length + 1;
@@ -267,6 +280,12 @@ export const secretFolderDALFactory = (db: TDbClient) => {
// even if its the original given /path1/path2
// it will stop automatically at /path2
const findClosestFolder = async (projectId: string, environment: string, path: string, tx?: Knex) => {
const isValidPath = isValidSecretPath(path);
if (!isValidPath)
throw new BadRequestError({
message: "Invalid secret path. Only alphanumeric characters, dashes, and underscores are allowed."
});
try {
const folder = await sqlFindFolderByPathQuery(
tx || db.replicaNode(),

View File

@@ -7,7 +7,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection, ProjectServiceActor } from "@app/lib/types";
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
@@ -514,7 +514,7 @@ export const secretFolderServiceFactory = ({
const getFoldersDeepByEnvs = async (
{ projectId, environments, secretPath }: TGetFoldersDeepByEnvsDTO,
actor: ProjectServiceActor
actor: OrgServiceActor
) => {
// folder list is allowed to be read by anyone
// permission to check does user have access

View File

@@ -361,6 +361,10 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
void bd.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`);
}
}
if (filters?.keys) {
void bd.whereIn(`${TableName.SecretV2}.key`, filters.keys);
}
})
.where((bd) => {
void bd.whereNull(`${TableName.SecretV2}.userId`).orWhere({ userId: userId || null });

View File

@@ -10,9 +10,9 @@ import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g;
const INTERPOLATION_SYNTAX_REG = /\${([a-zA-Z0-9-_.]+)}/g;
// akhilmhdh: JS regex with global save state in .test
const INTERPOLATION_SYNTAX_REG_NON_GLOBAL = /\${([^}]+)}/;
const INTERPOLATION_SYNTAX_REG_NON_GLOBAL = /\${([a-zA-Z0-9-_.]+)}/;
export const shouldUseSecretV2Bridge = (version: number) => version === 3;

View File

@@ -33,6 +33,7 @@ export type TGetSecretsDTO = {
offset?: number;
limit?: number;
search?: string;
keys?: string[];
} & TProjectPermission;
export type TGetASecretDTO = {
@@ -294,6 +295,7 @@ export type TFindSecretsByFolderIdsFilter = {
search?: string;
tagSlugs?: string[];
includeTagsInSearch?: boolean;
keys?: string[];
};
export type TGetSecretsRawByFolderMappingsDTO = {

View File

@@ -27,7 +27,7 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/
import { groupBy, pick } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { ProjectServiceActor } from "@app/lib/types";
import { OrgServiceActor } from "@app/lib/types";
import { TGetSecretsRawByFolderMappingsDTO } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
import { ActorType } from "../auth/auth-type";
@@ -2849,7 +2849,7 @@ export const secretServiceFactory = ({
const getSecretsRawByFolderMappings = async (
params: Omit<TGetSecretsRawByFolderMappingsDTO, "userId">,
actor: ProjectServiceActor
actor: OrgServiceActor
) => {
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(params.projectId);

View File

@@ -185,6 +185,7 @@ export type TGetSecretsRawDTO = {
offset?: number;
limit?: number;
search?: string;
keys?: string[];
} & TProjectPermission;
export type TGetASecretRawDTO = {

View File

@@ -0,0 +1,8 @@
---
title: "Create"
openapi: "POST /api/v1/project-templates"
---
<Note>
You can read more about the role's permissions field in the [permissions documentation](/internals/permissions).
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/project-templates/{templateId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get By ID"
openapi: "GET /api/v1/project-templates/{templateId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/project-templates"
---

View File

@@ -0,0 +1,8 @@
---
title: "Update"
openapi: "PATCH /api/v1/project-templates/{templateId}"
---
<Note>
You can read more about the role's permissions field in the [permissions documentation](/internals/permissions).
</Note>

View File

@@ -10,143 +10,253 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem
1. Create a user with the necessary permissions to create users in your LDAP server.
2. Ensure your LDAP server is reachable via Infisical instance.
## Set up Dynamic Secrets with LDAP
## Create LDAP Credentials
<Steps>
<Step title="Open Secret Overview Dashboard">
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
</Step>
<Step title="Click on the 'Add Dynamic Secret' button">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select 'LDAP'">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-ldap-select.png)
</Step>
<Tabs>
<Tab title="Dynamic">
<Steps>
<Step title="Open Secret Overview Dashboard">
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
</Step>
<Step title="Click on the 'Add Dynamic Secret' button">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select 'LDAP'">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-ldap-select.png)
</Step>
<Step title="Provide the inputs for dynamic secret parameters">
<ParamField path="Secret Name" type="string" required>
Name by which you want the secret to be referenced
</ParamField>
<Step title="Provide the inputs for dynamic secret parameters">
<ParamField path="Secret Name" type="string" required>
Name by which you want the secret to be referenced
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
</ParamField>
<ParamField path="Max TTL" type="string" required>
Maximum time-to-live for a generated secret.
</ParamField>
<ParamField path="Max TTL" type="string" required>
Maximum time-to-live for a generated secret.
</ParamField>
<ParamField path="URL" type="string" required>
LDAP url to connect to. _(Example: ldap://your-ldap-ip:389 or ldaps://domain:636)_
</ParamField>
<ParamField path="URL" type="string" required>
LDAP url to connect to. _(Example: ldap://your-ldap-ip:389 or ldaps://domain:636)_
</ParamField>
<ParamField path="BIND DN" type="string" required>
DN to bind to. This should have permissions to create a new users.
</ParamField>
<ParamField path="BIND DN" type="string" required>
DN to bind to. This should have permissions to create a new users.
</ParamField>
<ParamField path="BIND Password" type="string" required>
Password for the given DN.
</ParamField>
<ParamField path="BIND Password" type="string" required>
Password for the given DN.
</ParamField>
<ParamField path="CA" type="text">
CA certificate to use for TLS in case of a secure connection.
</ParamField>
<ParamField path="CA" type="text">
CA certificate to use for TLS in case of a secure connection.
</ParamField>
<ParamField path="Creation LDIF" type="text" required>
LDIF to run while creating a user in LDAP. This can include extra steps to assign the user to groups or set permissions.
Here `{{Username}}`, `{{Password}}` and `{{EncodedPassword}}` are templatized variables for the username and password generated by the dynamic secret.
<ParamField path="Credential Type" type="enum">
The type of LDAP credential - select Dynamic.
</ParamField>
`{{EncodedPassword}}` is the encoded password required for the `unicodePwd` field in Active Directory as described [here](https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/change-windows-active-directory-user-password).
<ParamField path="Creation LDIF" type="text" required>
LDIF to run while creating a user in LDAP. This can include extra steps to assign the user to groups or set permissions.
Here `{{Username}}`, `{{Password}}` and `{{EncodedPassword}}` are templatized variables for the username and password generated by the dynamic secret.
**OpenLDAP** Example:
```
dn: uid={{Username}},dc=infisical,dc=com
changetype: add
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
cn: John Doe
sn: Doe
uid: jdoe
mail: jdoe@infisical.com
userPassword: {{Password}}
```
`{{EncodedPassword}}` is the encoded password required for the `unicodePwd` field in Active Directory as described [here](https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/change-windows-active-directory-user-password).
**Active Directory** Example:
```
dn: CN={{Username}},OU=Test Create,DC=infisical,DC=com
changetype: add
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: user
userPrincipalName: {{Username}}@infisical.com
sAMAccountName: {{Username}}
unicodePwd::{{EncodedPassword}}
userAccountControl: 66048
**OpenLDAP** Example:
```
dn: uid={{Username}},dc=infisical,dc=com
changetype: add
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
cn: John Doe
sn: Doe
uid: jdoe
mail: jdoe@infisical.com
userPassword: {{Password}}
```
dn: CN=test-group,OU=Test Create,DC=infisical,DC=com
changetype: modify
add: member
member: CN={{Username}},OU=Test Create,DC=infisical,DC=com
-
```
</ParamField>
**Active Directory** Example:
```
dn: CN={{Username}},OU=Test Create,DC=infisical,DC=com
changetype: add
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: user
userPrincipalName: {{Username}}@infisical.com
sAMAccountName: {{Username}}
unicodePwd::{{EncodedPassword}}
userAccountControl: 66048
<ParamField path="Revocation LDIF" type="text" required>
LDIF to run while revoking a user in LDAP. This can include extra steps to remove the user from groups or set permissions.
Here `{{Username}}` is a templatized variable for the username generated by the dynamic secret.
dn: CN=test-group,OU=Test Create,DC=infisical,DC=com
changetype: modify
add: member
member: CN={{Username}},OU=Test Create,DC=infisical,DC=com
-
```
</ParamField>
**OpenLDAP / Active Directory** Example:
```
dn: CN={{Username}},OU=Test Create,DC=infisical,DC=com
changetype: delete
```
</ParamField>
<ParamField path="Revocation LDIF" type="text" required>
LDIF to run while revoking a user in LDAP. This can include extra steps to remove the user from groups or set permissions.
Here `{{Username}}` is a templatized variable for the username generated by the dynamic secret.
<ParamField path="Rollback LDIF" type="text">
LDIF to run incase Creation LDIF fails midway.
**OpenLDAP / Active Directory** Example:
```
dn: CN={{Username}},OU=Test Create,DC=infisical,DC=com
changetype: delete
```
</ParamField>
For the creation example shown above, if the user is created successfully but not added to a group, this LDIF can be used to remove the user.
Here `{{Username}}`, `{{Password}}` and `{{EncodedPassword}}` are templatized variables for the username generated by the dynamic secret.
<ParamField path="Rollback LDIF" type="text">
LDIF to run incase Creation LDIF fails midway.
**OpenLDAP / Active Directory** Example:
```
dn: CN={{Username}},OU=Test Create,DC=infisical,DC=com
changetype: delete
```
</ParamField>
For the creation example shown above, if the user is created successfully but not added to a group, this LDIF can be used to remove the user.
Here `{{Username}}`, `{{Password}}` and `{{EncodedPassword}}` are templatized variables for the username generated by the dynamic secret.
</Step>
**OpenLDAP / Active Directory** Example:
```
dn: CN={{Username}},OU=Test Create,DC=infisical,DC=com
changetype: delete
```
</ParamField>
</Step>
<Step title="Click `Submit`">
After submitting the form, you will see a dynamic secret created in the dashboard.
</Step>
<Step title="Generate dynamic secrets">
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
<Step title="Click `Submit`">
After submitting the form, you will see a dynamic secret created in the dashboard.
</Step>
<Step title="Generate dynamic secrets">
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png)
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png)
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png)
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png)
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png)
![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png)
<Tip>
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
</Tip>
<Tip>
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
</Tip>
Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you with an array of DN's altered depending on the Creation LDIF.
Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you with an array of DN's altered depending on the Creation LDIF.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-ldap-lease.png)
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-ldap-lease.png)
</Step>
</Step>
</Steps>
</Steps>
</Tab>
<Tab title="Static">
<Steps>
<Step title="Open Secret Overview Dashboard">
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
</Step>
<Step title="Click on the 'Add Dynamic Secret' button">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select 'LDAP'">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-ldap-select.png)
</Step>
<Step title="Provide the inputs for dynamic secret parameters">
<ParamField path="Secret Name" type="string" required>
Name by which you want the secret to be referenced
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
</ParamField>
<ParamField path="Max TTL" type="string" required>
Maximum time-to-live for a generated secret.
</ParamField>
<ParamField path="URL" type="string" required>
LDAP url to connect to. _(Example: ldap://your-ldap-ip:389 or ldaps://domain:636)_
</ParamField>
<ParamField path="BIND DN" type="string" required>
DN to bind to. This should have permissions to create a new users.
</ParamField>
<ParamField path="BIND Password" type="string" required>
Password for the given DN.
</ParamField>
<ParamField path="CA" type="text">
CA certificate to use for TLS in case of a secure connection.
</ParamField>
<ParamField path="Credential Type" type="enum">
The type of LDAP credential - select Static.
</ParamField>
<ParamField path="Rotation LDIF" type="text" required>
LDIF to run for rotating the credentals of an LDAP user. This can include extra LDAP steps based on your needs.
Here `{{Password}}` and `{{EncodedPassword}}` are templatized variables for the password generated by the dynamic secret.
Note that the `-` characters and the empty lines found at the end of the examples are necessary based on the LDIF format.
**OpenLDAP** Example:
```
dn: cn=sheencaps capadngan,ou=people,dc=acme,dc=com
changetype: modify
replace: userPassword
password: {{Password}}
-
```
**Active Directory** Example:
```
dn: cn=sheencaps capadngan,ou=people,dc=acme,dc=com
changetype: modify
replace: unicodePwd
unicodePwd::{{EncodedPassword}}
-
```
`{{EncodedPassword}}` is the encoded password required for the `unicodePwd` field in Active Directory as described [here](https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/change-windows-active-directory-user-password).
</ParamField>
</Step>
<Step title="Click `Submit`">
After submitting the form, you will see a dynamic secret created in the dashboard.
</Step>
<Step title="Generate dynamic secrets">
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png)
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png)
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png)
<Tip>
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
</Tip>
Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you with an array of DN's altered depending on the Creation LDIF.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-ldap-lease.png)
</Step>
</Steps>
</Tab>
</Tabs>
## Active Directory Integration

View File

@@ -35,6 +35,7 @@ These endpoints are exposed on port 8443 under the .well-known/est path e.g.
![est enrollment modal create](/images/platform/pki/est/template-enrollment-modal.png)
- **Disable Bootstrap Certificate Validation** - Enable this if your devices are not configured with a bootstrap certificate.
- **Certificate Authority Chain** - This is the certificate chain used to validate your devices' manufacturing/pre-installed certificates. This will be used to authenticate your devices with Infisical's EST server.
- **Passphrase** - This is also used to authenticate your devices with Infisical's EST server. When configuring the clients, use the value defined here as the EST password.

View File

@@ -0,0 +1,147 @@
---
title: "Project Templates"
sidebarTitle: "Project Templates"
description: "Learn how to manage and apply project templates"
---
## Concept
Project Templates streamline your ability to set up projects by providing customizable templates to configure projects quickly with a predefined set of environments and roles.
<Note>
Project Templates is a paid feature.
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
then you should contact team@infisical.com to purchase an enterprise license to use it.
</Note>
## Workflow
The typical workflow for using Project Templates consists of the following steps:
1. <strong>Creating a project template:</strong> As part of this step, you will configure a set of environments and roles to be created when applying this template to a project.
2. <strong>Using a project template:</strong> When creating new projects, optionally specify a project template to provision the project with the configured roles and environments.
<Note>
Note that this workflow can be executed via the Infisical UI or through the API.
</Note>
## Guide to Creating a Project Template
In the following steps, we'll explore how to set up a project template.
<Tabs>
<Tab title="Infisical UI">
<Steps>
<Step title="Creating a Project Template">
Navigate to the Project Templates tab on the Organization Settings page and tap on the **Add Template** button.
![project template add button](/images/platform/project-templates/project-template-add-button.png)
Specify your template details. Here's some guidance on each field:
- <strong>Name:</strong> A slug-friendly name for the template.
- <strong>Description:</strong> An optional description of the intended usage of this template.
![project template create modal](/images/platform/project-templates/project-template-create.png)
</Step>
<Step title="Configuring a Project Template">
Once your template is created, you'll be directed to the configuration section.
![project template edit form](/images/platform/project-templates/project-template-edit-form.png)
Customize the environments and roles to your needs.
![project template customized](/images/platform/project-templates/project-template-customized.png)
<Note>
Be sure to save your environment and role changes.
</Note>
</Step>
</Steps>
</Tab>
<Tab title="API">
To create a project template, make an API request to the [Create Project Template](/api-reference/endpoints/project-templates/create) API endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/project-templates \
--header 'Content-Type: application/json' \
--data '{
"name": "my-project-template",
"description": "...",
"environments": "[...]",
"roles": "[...]",
}'
```
### Sample response
```bash Response
{
"projectTemplate": {
"id": "<template-id>",
"name": "my-project-template",
"description": "...",
"environments": "[...]",
"roles": "[...]",
"orgId": "<org-id>",
"createdAt": "2023-11-07T05:31:56Z",
"updatedAt": "2023-11-07T05:31:56Z",
}
}
```
</Tab>
</Tabs>
## Guide to Using a Project Template
In the following steps, we'll explore how to use a project template when creating a project.
<Tabs>
<Tab title="Infisical UI">
When creating a new project, select the desired template from the dropdown menu in the create project modal.
![kms key options](/images/platform/project-templates/project-template-apply.png)
Your project will be provisioned with the configured template roles and environments.
</Tab>
<Tab title="API">
To use a project template, make an API request to the [Create Project](/api-reference/endpoints/workspaces/create-workspace) API endpoint with the specified template name included.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v2/workspace \
--header 'Content-Type: application/json' \
--data '{
"projectName": "My Project",
"template": "<template-name>", // defaults to "default"
}'
```
### Sample response
```bash Response
{
"project": {
"id": "<project-id>",
"environments": "[...]", // configured environments
...
}
}
```
<Note>
Note that configured roles are not included in the project response.
</Note>
</Tab>
</Tabs>
## FAQ
<AccordionGroup>
<Accordion title="Do changes to templates propagate to existing projects?">
No. Project templates only apply at the time of project creation.
</Accordion>
</AccordionGroup>

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 612 KiB

After

Width:  |  Height:  |  Size: 507 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1015 KiB

View File

@@ -3,29 +3,37 @@ title: "Bitbucket"
description: "How to sync secrets from Infisical to Bitbucket"
---
Infisical lets you sync secrets to Bitbucket at the repository-level and deployment environment-level.
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
<AccordionGroup>
<Accordion title="Push secrets to Bitbucket from Infisical">
<Steps>
<Step title="Authorize Infisical for Bitbucket">
Navigate to your project's integrations tab in Infisical.
<Steps>
<Step title="Authorize Infisical for Bitbucket">
Navigate to your project's integrations tab in Infisical.
![integrations](../../images/integrations.png)
![integrations](/images/integrations.png)
Press on the Bitbucket tile and grant Infisical access to your Bitbucket account.
Press on the Bitbucket tile and grant Infisical access to your Bitbucket account.
![integrations bitbucket authorization](../../images/integrations/bitbucket/integrations-bitbucket-auth.png)
![integrations bitbucket authorization](/images/integrations/bitbucket/integrations-bitbucket.png)
</Step>
<Step title='Configure integration'>
Select which workspace, repository, and optionally, deployment environment, you'd like to sync your secrets
to.
![integrations configure
bitbucket](/images/integrations/bitbucket/integrations-bitbucket-configuration.png)
</Step>
<Step title="Start integration">
Select which Infisical environment secrets you want to sync to which Bitbucket repo and press start integration to start syncing secrets to the repo.
Once created, your integration will begin syncing secrets to the configured repository or deployment
environment.
![integrations bitbucket](../../images/integrations/bitbucket/integrations-bitbucket.png)
</Step>
</Steps>
![integrations bitbucket](/images/integrations/bitbucket/integrations-bitbucket.png)
</Step>
</Steps>
</Accordion>
<Accordion title="Pull secrets in Bitbucket pipelines from Infisical">
@@ -36,7 +44,7 @@ Prerequisites:
<Step title="Initialize Bitbucket variables">
Create Bitbucket variables (can be either workspace, repository, or deployment-level) to store Machine Identity Client ID and Client Secret.
![integrations bitbucket](../../images/integrations/bitbucket/integrations-bitbucket-env.png)
![integrations bitbucket](/images/integrations/bitbucket/integrations-bitbucket-env.png)
</Step>
<Step title="Integrate Infisical secrets into the pipeline">
Edit your Bitbucket pipeline YAML file to include the use of the Infisical CLI to fetch and inject secrets into any script or command within the pipeline.

View File

@@ -262,7 +262,7 @@ The Infisical agent supports multiple authentication methods. Below are the avai
## Quick start Infisical Agent
To install the Infisical agent, you must first install the [Infisical CLI](../cli/overview) in the desired environment where you'd like the agent to run. This is because the Infisical agent is a sub-command of the Infisical CLI.
To install the Infisical agent, you must first install the [Infisical CLI](/cli/overview) in the desired environment where you'd like the agent to run. This is because the Infisical agent is a sub-command of the Infisical CLI.
Once you have the CLI installed, you will need to provision programmatic access for the agent via [Universal Auth](/documentation/platform/identities/universal-auth). To obtain a **Client ID** and a **Client Secret**, follow the step by step guide outlined [here](/documentation/platform/identities/universal-auth).

View File

@@ -191,6 +191,7 @@
"documentation/platform/dynamic-secrets/snowflake"
]
},
"documentation/platform/project-templates",
{
"group": "Workflow Integrations",
"pages": [
@@ -644,6 +645,16 @@
"api-reference/endpoints/project-roles/list"
]
},
{
"group": "Project Templates",
"pages": [
"api-reference/endpoints/project-templates/create",
"api-reference/endpoints/project-templates/update",
"api-reference/endpoints/project-templates/delete",
"api-reference/endpoints/project-templates/get-by-id",
"api-reference/endpoints/project-templates/list"
]
},
{
"group": "Environments",
"pages": [

View File

@@ -28,7 +28,7 @@ const integrationSlugNameMapping: Mapping = {
"cloudflare-workers": "Cloudflare Workers",
codefresh: "Codefresh",
"digital-ocean-app-platform": "Digital Ocean App Platform",
bitbucket: "BitBucket",
bitbucket: "Bitbucket",
"cloud-66": "Cloud 66",
northflank: "Northflank",
windmill: "Windmill",

View File

@@ -11,7 +11,7 @@ import { SecretType } from "@app/hooks/api/types";
import Button from "../basic/buttons/Button";
import Error from "../basic/Error";
import { createNotification } from "../notifications";
import { parseDotEnv } from "../utilities/parseDotEnv";
import { parseDotEnv } from "../utilities/parseSecrets";
import guidGenerator from "../utilities/randomId";
interface DropZoneProps {

View File

@@ -6,7 +6,7 @@ const LINE =
* @param {ArrayBuffer} src - source buffer
* @returns {String} text - text of buffer
*/
export function parseDotEnv(src: ArrayBuffer) {
export function parseDotEnv(src: ArrayBuffer | string) {
const object: {
[key: string]: { value: string; comments: string[] };
} = {};
@@ -65,3 +65,15 @@ export function parseDotEnv(src: ArrayBuffer) {
return object;
}
export const parseJson = (src: ArrayBuffer | string) => {
const file = src.toString();
const formatedData: Record<string, string> = JSON.parse(file);
const env: Record<string, { value: string; comments: string[] }> = {};
Object.keys(formatedData).forEach((key) => {
if (typeof formatedData[key] === "string") {
env[key] = { value: formatedData[key], comments: [] };
}
});
return env;
};

View File

@@ -7,11 +7,11 @@ import Select, {
Props
} from "react-select";
import { faCheckCircle, faCircleXmark } from "@fortawesome/free-regular-svg-icons";
import { faChevronDown, faX } from "@fortawesome/free-solid-svg-icons";
import { faChevronDown, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
const DropdownIndicator = (props: DropdownIndicatorProps) => {
const DropdownIndicator = <T,>(props: DropdownIndicatorProps<T>) => {
return (
<components.DropdownIndicator {...props}>
<FontAwesomeIcon icon={faChevronDown} size="xs" />
@@ -19,7 +19,7 @@ const DropdownIndicator = (props: DropdownIndicatorProps) => {
);
};
const ClearIndicator = (props: ClearIndicatorProps) => {
const ClearIndicator = <T,>(props: ClearIndicatorProps<T>) => {
return (
<components.ClearIndicator {...props}>
<FontAwesomeIcon icon={faCircleXmark} />
@@ -30,12 +30,12 @@ const ClearIndicator = (props: ClearIndicatorProps) => {
const MultiValueRemove = (props: MultiValueRemoveProps) => {
return (
<components.MultiValueRemove {...props}>
<FontAwesomeIcon icon={faX} size="xs" />
<FontAwesomeIcon icon={faXmark} size="xs" />
</components.MultiValueRemove>
);
};
const Option = ({ isSelected, children, ...props }: OptionProps) => {
const Option = <T,>({ isSelected, children, ...props }: OptionProps<T>) => {
return (
<components.Option isSelected={isSelected} {...props}>
{children}
@@ -46,10 +46,10 @@ const Option = ({ isSelected, children, ...props }: OptionProps) => {
);
};
export const MultiSelect = (props: Props) => (
export const FilterableSelect = <T,>({ isMulti, closeMenuOnSelect, ...props }: Props<T>) => (
<Select
isMulti
closeMenuOnSelect={false}
isMulti={isMulti}
closeMenuOnSelect={closeMenuOnSelect ?? !isMulti}
hideSelectedOptions={false}
unstyled
styles={{
@@ -75,11 +75,11 @@ export const MultiSelect = (props: Props) => (
control: ({ isFocused }) =>
twMerge(
isFocused ? "border-primary-400/50" : "border-mineshaft-600 hover:border-gray-400",
"border w-full p-0.5 rounded-md font-inter bg-mineshaft-900 hover:cursor-pointer"
"border w-full p-0.5 rounded-md text-mineshaft-200 font-inter bg-mineshaft-900 hover:cursor-pointer"
),
placeholder: () => "text-mineshaft-400 text-sm pl-1 py-0.5",
input: () => "pl-1 py-0.5",
valueContainer: () => "p-1 max-h-[14rem] !overflow-y-scroll gap-1",
valueContainer: () => `p-1 max-h-[14rem] ${isMulti ? "!overflow-y-scroll" : ""} gap-1`,
singleValue: () => "leading-7 ml-1",
multiValue: () => "bg-mineshaft-600 rounded items-center py-0.5 px-2 gap-1.5",
multiValueLabel: () => "leading-6 text-sm",
@@ -94,7 +94,7 @@ export const MultiSelect = (props: Props) => (
option: ({ isFocused, isSelected }) =>
twMerge(
isFocused && "bg-mineshaft-700 active:bg-mineshaft-600",
isSelected && "text-mineshaft-400",
isSelected && "text-mineshaft-200",
"hover:cursor-pointer text-xs px-3 py-2"
),
noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md"

View File

@@ -0,0 +1 @@
export * from "./FilterableSelect";

View File

@@ -1,4 +1,4 @@
import { cloneElement, ReactNode } from "react";
import { cloneElement, ReactElement, ReactNode } from "react";
import { faExclamationTriangle, faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Label from "@radix-ui/react-label";
@@ -82,7 +82,8 @@ export type FormControlProps = {
children: JSX.Element;
className?: string;
icon?: ReactNode;
tooltipText?: string;
tooltipText?: ReactElement | string;
tooltipClassName?: string;
};
export const FormControl = ({
@@ -96,7 +97,8 @@ export const FormControl = ({
isError,
icon,
className,
tooltipText
tooltipText,
tooltipClassName
}: FormControlProps): JSX.Element => {
return (
<div className={twMerge("mb-4", className)}>
@@ -108,6 +110,7 @@ export const FormControl = ({
id={id}
icon={icon}
tooltipText={tooltipText}
tooltipClassName={tooltipClassName}
/>
) : (
label

View File

@@ -1 +0,0 @@
export * from "./MultiSelect";

View File

@@ -4,7 +4,7 @@ import { twMerge } from "tailwind-merge";
import { useToggle } from "@app/hooks";
const REGEX = /(\${([^}]+)})/g;
const REGEX = /(\${([a-zA-Z0-9-_.]+)})/g;
const replaceContentWithDot = (str: string) => {
let finalStr = "";
for (let i = 0; i < str.length; i += 1) {

View File

@@ -11,6 +11,7 @@ export * from "./Drawer";
export * from "./Dropdown";
export * from "./EmailServiceSetupModal";
export * from "./EmptyState";
export * from "./FilterableSelect";
export * from "./FontAwesomeSymbol";
export * from "./FormControl";
export * from "./HoverCardv2";
@@ -18,7 +19,6 @@ export * from "./IconButton";
export * from "./Input";
export * from "./Menu";
export * from "./Modal";
export * from "./MultiSelect";
export * from "./NoticeBanner";
export * from "./Pagination";
export * from "./Popoverv2";

View File

@@ -22,7 +22,8 @@ export enum OrgPermissionSubjects {
Identity = "identity",
Kms = "kms",
AdminConsole = "organization-admin-console",
AuditLogs = "audit-logs"
AuditLogs = "audit-logs",
ProjectTemplates = "project-templates"
}
export enum OrgPermissionAdminConsoleAction {
@@ -45,6 +46,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs];
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates];
export type TOrgPermission = MongoAbility<OrgPermissionSet>;

View File

@@ -1,3 +1,5 @@
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
enum OrgMembershipRole {
Admin = "admin",
Member = "member",
@@ -18,3 +20,6 @@ export const formatProjectRoleName = (name: string) => {
if (name === ProjectMemberRole.Member) return "developer";
return name;
};
export const isCustomProjectRole = (slug: string) =>
!Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole);

View File

@@ -46,9 +46,10 @@ export type TDeleteCertificateTemplateDTO = {
export type TCreateEstConfigDTO = {
certificateTemplateId: string;
caChain: string;
caChain?: string;
passphrase: string;
isEnabled: boolean;
disableBootstrapCertValidation: boolean;
};
export type TUpdateEstConfigDTO = {
@@ -56,11 +57,13 @@ export type TUpdateEstConfigDTO = {
caChain?: string;
passphrase?: string;
isEnabled?: boolean;
disableBootstrapCertValidation?: boolean;
};
export type TEstConfig = {
id: string;
certificateTemplateId: string;
caChain: string;
isEnabled: false;
isEnabled: boolean;
disableBootstrapCertValidation: boolean;
};

View File

@@ -5,6 +5,7 @@ import axios from "axios";
import { createNotification } from "@app/components/notifications";
import { apiRequest } from "@app/config/request";
import {
DashboardProjectSecretsByKeys,
DashboardProjectSecretsDetails,
DashboardProjectSecretsDetailsResponse,
DashboardProjectSecretsOverview,
@@ -12,6 +13,7 @@ import {
DashboardSecretsOrderBy,
TDashboardProjectSecretsQuickSearch,
TDashboardProjectSecretsQuickSearchResponse,
TGetDashboardProjectSecretsByKeys,
TGetDashboardProjectSecretsDetailsDTO,
TGetDashboardProjectSecretsOverviewDTO,
TGetDashboardProjectSecretsQuickSearchDTO
@@ -101,6 +103,23 @@ export const fetchProjectSecretsDetails = async ({
return data;
};
export const fetchDashboardProjectSecretsByKeys = async ({
keys,
...params
}: TGetDashboardProjectSecretsByKeys) => {
const { data } = await apiRequest.get<DashboardProjectSecretsByKeys>(
"/api/v1/dashboard/secrets-by-keys",
{
params: {
...params,
keys: encodeURIComponent(keys.join(","))
}
}
);
return data;
};
export const useGetProjectSecretsOverview = (
{
projectId,

View File

@@ -29,6 +29,10 @@ export type DashboardProjectSecretsDetailsResponse = {
totalCount: number;
};
export type DashboardProjectSecretsByKeys = {
secrets: SecretV3Raw[];
};
export type DashboardProjectSecretsOverview = Omit<
DashboardProjectSecretsOverviewResponse,
"secrets"
@@ -89,3 +93,10 @@ export type TGetDashboardProjectSecretsQuickSearchDTO = {
search: string;
environments: string[];
};
export type TGetDashboardProjectSecretsByKeys = {
projectId: string;
secretPath: string;
environment: string;
keys: string[];
};

View File

@@ -199,9 +199,11 @@ export type TDynamicSecretProvider =
binddn: string;
bindpass: string;
ca?: string | undefined;
creationLdif: string;
revocationLdif: string;
credentialType: string;
creationLdif?: string;
revocationLdif?: string;
rollbackLdif?: string;
rotationLdif?: string;
};
}
| {

View File

@@ -1,10 +1,11 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace";
import {
App,
BitBucketEnvironment,
BitBucketWorkspace,
ChecklyGroup,
Environment,
@@ -94,6 +95,17 @@ const integrationAuthKeys = {
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const,
getIntegrationAuthBitBucketWorkspaces: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthBitbucketWorkspaces"] as const,
getIntegrationAuthBitBucketEnvironments: (
integrationAuthId: string,
workspaceSlug: string,
repoSlug: string
) =>
[
{ integrationAuthId },
workspaceSlug,
repoSlug,
"integrationAuthBitbucketEnvironments"
] as const,
getIntegrationAuthNorthflankSecretGroups: ({
integrationAuthId,
appId
@@ -403,6 +415,25 @@ const fetchIntegrationAuthBitBucketWorkspaces = async (integrationAuthId: string
return workspaces;
};
const fetchIntegrationAuthBitBucketEnvironments = async (
integrationAuthId: string,
workspaceSlug: string,
repoSlug: string
) => {
const {
data: { environments }
} = await apiRequest.get<{ environments: BitBucketEnvironment[] }>(
`/api/v1/integration-auth/${integrationAuthId}/bitbucket/environments`,
{
params: {
workspaceSlug,
repoSlug
}
}
);
return environments;
};
const fetchIntegrationAuthNorthflankSecretGroups = async ({
integrationAuthId,
appId
@@ -727,6 +758,30 @@ export const useGetIntegrationAuthBitBucketWorkspaces = (integrationAuthId: stri
});
};
export const useGetIntegrationAuthBitBucketEnvironments = (
{
integrationAuthId,
workspaceSlug,
repoSlug
}: {
integrationAuthId: string;
workspaceSlug: string;
repoSlug: string;
},
options?: UseQueryOptions<BitBucketEnvironment[]>
) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthBitBucketEnvironments(
integrationAuthId,
workspaceSlug,
repoSlug
),
queryFn: () =>
fetchIntegrationAuthBitBucketEnvironments(integrationAuthId, workspaceSlug, repoSlug),
...options
});
};
export const useGetIntegrationAuthNorthflankSecretGroups = ({
integrationAuthId,
appId

View File

@@ -79,6 +79,12 @@ export type BitBucketWorkspace = {
slug: string;
};
export type BitBucketEnvironment = {
uuid: string;
name: string;
slug: string;
};
export type NorthflankSecretGroup = {
name: string;
groupId: string;

View File

@@ -0,0 +1,3 @@
export * from "./mutations";
export * from "./queries";
export * from "./types";

View File

@@ -0,0 +1,58 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { projectTemplateKeys } from "@app/hooks/api/projectTemplates/queries";
import {
TCreateProjectTemplateDTO,
TDeleteProjectTemplateDTO,
TProjectTemplateResponse,
TUpdateProjectTemplateDTO
} from "@app/hooks/api/projectTemplates/types";
export const useCreateProjectTemplate = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (payload: TCreateProjectTemplateDTO) => {
const { data } = await apiRequest.post<TProjectTemplateResponse>(
"/api/v1/project-templates",
payload
);
return data.projectTemplate;
},
onSuccess: () => queryClient.invalidateQueries(projectTemplateKeys.list())
});
};
export const useUpdateProjectTemplate = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ templateId, ...params }: TUpdateProjectTemplateDTO) => {
const { data } = await apiRequest.patch<TProjectTemplateResponse>(
`/api/v1/project-templates/${templateId}`,
params
);
return data.projectTemplate;
},
onSuccess: (_, { templateId }) => {
queryClient.invalidateQueries(projectTemplateKeys.list());
queryClient.invalidateQueries(projectTemplateKeys.byId(templateId));
}
});
};
export const useDeleteProjectTemplate = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ templateId }: TDeleteProjectTemplateDTO) => {
const { data } = await apiRequest.delete(`/api/v1/project-templates/${templateId}`);
return data;
},
onSuccess: (_, { templateId }) => {
queryClient.invalidateQueries(projectTemplateKeys.list());
queryClient.invalidateQueries(projectTemplateKeys.byId(templateId));
}
});
};

View File

@@ -0,0 +1,61 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import {
TListProjectTemplates,
TProjectTemplate,
TProjectTemplateResponse
} from "@app/hooks/api/projectTemplates/types";
export const projectTemplateKeys = {
all: ["project-template"] as const,
list: () => [...projectTemplateKeys.all, "list"] as const,
byId: (templateId: string) => [...projectTemplateKeys.all, templateId] as const
};
export const useListProjectTemplates = (
options?: Omit<
UseQueryOptions<
TProjectTemplate[],
unknown,
TProjectTemplate[],
ReturnType<typeof projectTemplateKeys.list>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: projectTemplateKeys.list(),
queryFn: async () => {
const { data } = await apiRequest.get<TListProjectTemplates>("/api/v1/project-templates");
return data.projectTemplates;
},
...options
});
};
export const useGetProjectTemplateById = (
templateId: string,
options?: Omit<
UseQueryOptions<
TProjectTemplate,
unknown,
TProjectTemplate,
ReturnType<typeof projectTemplateKeys.byId>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: projectTemplateKeys.byId(templateId),
queryFn: async () => {
const { data } = await apiRequest.get<TProjectTemplateResponse>(
`/api/v1/project-templates/${templateId}`
);
return data.projectTemplate;
},
...options
});
};

View File

@@ -0,0 +1,31 @@
import { TProjectRole } from "@app/hooks/api/roles/types";
export type TProjectTemplate = {
id: string;
name: string;
description?: string;
roles: Pick<TProjectRole, "slug" | "name" | "permissions">[];
environments: { name: string; slug: string; position: number }[];
createdAt: string;
updatedAt: string;
};
export type TListProjectTemplates = { projectTemplates: TProjectTemplate[] };
export type TProjectTemplateResponse = { projectTemplate: TProjectTemplate };
export type TCreateProjectTemplateDTO = {
name: string;
description?: string;
};
export type TUpdateProjectTemplateDTO = Partial<
Pick<TProjectTemplate, "name" | "description" | "roles" | "environments">
> & { templateId: string };
export type TDeleteProjectTemplateDTO = {
templateId: string;
};
export enum InfisicalProjectTemplate {
Default = "default"
}

View File

@@ -43,4 +43,5 @@ export type SubscriptionPlan = {
externalKms: boolean;
pkiEst: boolean;
enforceMfa: boolean;
projectTemplates: boolean;
};

View File

@@ -208,19 +208,21 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) =>
export const createWorkspace = ({
projectName,
kmsKeyId
kmsKeyId,
template
}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => {
return apiRequest.post("/api/v2/workspace", { projectName, kmsKeyId });
return apiRequest.post("/api/v2/workspace", { projectName, kmsKeyId, template });
};
export const useCreateWorkspace = () => {
const queryClient = useQueryClient();
return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({
mutationFn: async ({ projectName, kmsKeyId }) =>
mutationFn: async ({ projectName, kmsKeyId, template }) =>
createWorkspace({
projectName,
kmsKeyId
kmsKeyId,
template
}),
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);

View File

@@ -57,6 +57,7 @@ export type TGetUpgradeProjectStatusDTO = {
export type CreateWorkspaceDTO = {
projectName: string;
kmsKeyId?: string;
template?: string;
};
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };

View File

@@ -21,6 +21,7 @@ import {
faEnvelope,
faInfinity,
faInfo,
faInfoCircle,
faMobile,
faPlus,
faQuestion,
@@ -78,6 +79,7 @@ import {
useSelectOrganization
} from "@app/hooks/api";
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
import { Workspace } from "@app/hooks/api/types";
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
@@ -124,7 +126,8 @@ const formSchema = yup.object({
.trim()
.max(64, "Too long, maximum length is 64 characters"),
addMembers: yup.bool().required().label("Add Members"),
kmsKeyId: yup.string().label("KMS Key ID")
kmsKeyId: yup.string().label("KMS Key ID"),
template: yup.string().label("Project Template Name")
});
type TAddProjectFormData = yup.InferType<typeof formSchema>;
@@ -273,7 +276,16 @@ export const AppLayout = ({ children }: LayoutProps) => {
putUserInOrg();
}, [router.query.id]);
const onCreateProject = async ({ name, addMembers, kmsKeyId }: TAddProjectFormData) => {
const canReadProjectTemplates = permission.can(
OrgPermissionActions.Read,
OrgPermissionSubjects.ProjectTemplates
);
const { data: projectTemplates = [] } = useListProjectTemplates({
enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates)
});
const onCreateProject = async ({ name, addMembers, kmsKeyId, template }: TAddProjectFormData) => {
// type check
if (!currentOrg) return;
if (!user) return;
@@ -284,7 +296,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
}
} = await createWs.mutateAsync({
projectName: name,
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined,
template
});
if (addMembers) {
@@ -909,20 +922,72 @@ export const AppLayout = ({ children }: LayoutProps) => {
subTitle="This project will contain your secrets and configurations."
>
<form onSubmit={handleSubmit(onCreateProject)}>
<Controller
control={control}
name="name"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Project Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Type your project name" />
</FormControl>
)}
/>
<div className="flex gap-2">
<Controller
control={control}
name="name"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Project Name"
isError={Boolean(error)}
errorText={error?.message}
className="flex-1"
>
<Input {...field} placeholder="Type your project name" />
</FormControl>
)}
/>
<Controller
control={control}
name="template"
render={({ field: { value, onChange } }) => (
<OrgPermissionCan
I={OrgPermissionActions.Read}
a={OrgPermissionSubjects.ProjectTemplates}
>
{(isAllowed) => (
<FormControl
label="Project Template"
icon={<FontAwesomeIcon icon={faInfoCircle} size="sm" />}
tooltipText={
<>
<p>
Create this project from a template to provision it with custom
environments and roles.
</p>
{subscription && !subscription.projectTemplates && (
<p className="pt-2">Project templates are a paid feature.</p>
)}
</>
}
>
<Select
defaultValue={InfisicalProjectTemplate.Default}
placeholder={InfisicalProjectTemplate.Default}
isDisabled={!isAllowed || !subscription?.projectTemplates}
value={value}
onValueChange={onChange}
className="w-44"
>
{projectTemplates.length
? projectTemplates.map((template) => (
<SelectItem key={template.id} value={template.name}>
{template.name}
</SelectItem>
))
: Object.values(InfisicalProjectTemplate).map((template) => (
<SelectItem key={template} value={template}>
{template}
</SelectItem>
))}
</Select>
</FormControl>
)}
</OrgPermissionCan>
)}
/>
</div>
<div className="mt-4 pl-1">
<Controller
control={control}

View File

@@ -0,0 +1 @@
export * from "./slugSchema";

View File

@@ -0,0 +1,12 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
export const slugSchema = z
.string()
.trim()
.min(1)
.max(32)
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Invalid slug format"
});

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
@@ -10,9 +11,12 @@ import {
faCircleInfo
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { motion } from "framer-motion";
import queryString from "query-string";
import z from "zod";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useCreateIntegration } from "@app/hooks/api";
import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries";
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
@@ -83,10 +87,61 @@ const mappingBehaviors = [
}
];
const schema = z
.object({
awsRegion: z.string().trim().min(1, { message: "AWS region is required" }),
secretPath: z.string().trim().min(1, { message: "Secret path is required" }),
sourceEnvironment: z.string().trim().min(1, { message: "Source environment is required" }),
secretPrefix: z.string().default(""),
secretName: z.string().trim().min(1).optional(),
mappingBehavior: z.nativeEnum(IntegrationMappingBehavior),
kmsKeyId: z.string().optional(),
shouldTag: z.boolean().optional(),
tags: z
.object({
key: z.string(),
value: z.string()
})
.array()
})
.refine(
(val) =>
val.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE ||
(val.mappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE &&
val.secretName &&
val.secretName !== ""),
{
message: "Secret name must be defined for many-to-one integrations",
path: ["secretName"]
}
);
type TFormSchema = z.infer<typeof schema>;
export default function AWSSecretManagerCreateIntegrationPage() {
const router = useRouter();
const { mutateAsync } = useCreateIntegration();
const {
control,
setValue,
handleSubmit,
watch,
formState: { isSubmitting }
} = useForm<TFormSchema>({
resolver: zodResolver(schema),
defaultValues: {
shouldTag: false,
secretPath: "/",
secretPrefix: "",
mappingBehavior: IntegrationMappingBehavior.MANY_TO_ONE,
tags: []
}
});
const shouldTagState = watch("shouldTag");
const selectedSourceEnvironment = watch("sourceEnvironment");
const selectedAWSRegion = watch("awsRegion");
const selectedMappingBehavior = watch("mappingBehavior");
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
@@ -94,25 +149,6 @@ export default function AWSSecretManagerCreateIntegrationPage() {
(integrationAuthId as string) ?? ""
);
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [selectedAWSRegion, setSelectedAWSRegion] = useState("");
const [selectedMappingBehavior, setSelectedMappingBehavior] = useState(
IntegrationMappingBehavior.MANY_TO_ONE
);
const [targetSecretName, setTargetSecretName] = useState("");
const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState("");
const [tagKey, setTagKey] = useState("");
const [tagValue, setTagValue] = useState("");
const [kmsKeyId, setKmsKeyId] = useState("");
const [secretPrefix, setSecretPrefix] = useState("");
// const [path, setPath] = useState('');
// const [pathErrorText, setPathErrorText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [shouldTag, setShouldTag] = useState(false);
const { data: integrationAuthAwsKmsKeys, isLoading: isIntegrationAuthAwsKmsKeysLoading } =
useGetIntegrationAuthAwsKmsKeys({
integrationAuthId: String(integrationAuthId),
@@ -121,63 +157,46 @@ export default function AWSSecretManagerCreateIntegrationPage() {
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
setSelectedAWSRegion(awsRegions[0].slug);
setValue("sourceEnvironment", workspace.environments[0].slug);
setValue("awsRegion", awsRegions[0].slug);
}
}, [workspace]);
// const isValidAWSPath = (path: string) => {
// const pattern = /^\/[\w./]+\/$/;
// return pattern.test(path) && path.length <= 2048;
// }
const handleButtonClick = async () => {
const handleButtonClick = async ({
secretName,
sourceEnvironment,
awsRegion,
secretPath,
shouldTag,
tags,
secretPrefix,
kmsKeyId,
mappingBehavior
}: TFormSchema) => {
try {
if (!selectedMappingBehavior) {
return;
}
if (
selectedMappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE &&
targetSecretName.trim() === ""
) {
setTargetSecretName("Secret name cannot be blank");
return;
}
if (!integrationAuth?.id) return;
setIsLoading(true);
await mutateAsync({
integrationAuthId: integrationAuth?.id,
isActive: true,
app: targetSecretName.trim(),
sourceEnvironment: selectedSourceEnvironment,
region: selectedAWSRegion,
app: secretName,
sourceEnvironment,
region: awsRegion,
secretPath,
metadata: {
...(shouldTag
? {
secretAWSTag: [
{
key: tagKey,
value: tagValue
}
]
secretAWSTag: tags
}
: {}),
...(secretPrefix && { secretPrefix }),
...(kmsKeyId && { kmsKeyId }),
mappingBehavior: selectedMappingBehavior
mappingBehavior
}
});
setIsLoading(false);
setTargetSecretNameErrorText("");
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
setIsLoading(false);
console.error(err);
}
};
@@ -191,226 +210,305 @@ export default function AWSSecretManagerCreateIntegrationPage() {
<title>Set Up AWS Secrets Manager Integration</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<Card className="max-w-lg rounded-md border border-mineshaft-600">
<CardTitle
className="px-6 text-left text-xl"
subTitle="Choose which environment in Infisical you want to sync to secerts in AWS Secrets Manager."
>
<div className="flex flex-row items-center">
<div className="inline flex items-center">
<Image
src="/images/integrations/Amazon Web Services.png"
height={35}
width={35}
alt="AWS logo"
/>
</div>
<span className="ml-1.5">AWS Secrets Manager Integration </span>
<Link href="https://infisical.com/docs/integrations/cloud/aws-secret-manager" passHref>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs"
/>
</div>
</a>
</Link>
</div>
</CardTitle>
<Tabs defaultValue={TabSections.Connection} className="px-6">
<TabList>
<div className="flex w-full flex-row border-b border-mineshaft-600">
<Tab value={TabSections.Connection}>Connection</Tab>
<Tab value={TabSections.Options}>Options</Tab>
</div>
</TabList>
<TabPanel value={TabSections.Connection}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<FormControl label="Project Environment">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`flyio-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
<form onSubmit={handleSubmit(handleButtonClick)}>
<Card className="max-w-lg rounded-md border border-mineshaft-600">
<CardTitle
className="px-6 text-left text-xl"
subTitle="Choose which environment in Infisical you want to sync to secerts in AWS Secrets Manager."
>
<div className="flex flex-row items-center">
<div className="flex items-center">
<Image
src="/images/integrations/Amazon Web Services.png"
height={35}
width={35}
alt="AWS logo"
/>
</FormControl>
<FormControl label="AWS Region">
<Select
value={selectedAWSRegion}
onValueChange={(val) => {
setSelectedAWSRegion(val);
setKmsKeyId("");
}}
className="w-full border border-mineshaft-500"
>
{awsRegions.map((awsRegion) => (
<SelectItem
value={awsRegion.slug}
className="flex w-full justify-between"
key={`aws-environment-${awsRegion.slug}`}
>
{awsRegion.name} <Badge variant="success">{awsRegion.slug}</Badge>
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Mapping Behavior">
<Select
value={selectedMappingBehavior}
onValueChange={(val) => {
setSelectedMappingBehavior(val as IntegrationMappingBehavior);
}}
className="w-full border border-mineshaft-500 text-left"
>
{mappingBehaviors.map((option) => (
<SelectItem
value={option.value}
className="text-left"
key={`aws-environment-${option.value}`}
>
{option.label}
</SelectItem>
))}
</Select>
</FormControl>
{selectedMappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE && (
<FormControl
label="AWS SM Secret Name"
errorText={targetSecretNameErrorText}
isError={targetSecretNameErrorText !== "" ?? false}
>
<Input
placeholder={`${workspace.name
.toLowerCase()
.replace(/ /g, "-")}/${selectedSourceEnvironment}`}
value={targetSecretName}
onChange={(e) => setTargetSecretName(e.target.value)}
/>
</FormControl>
)}
</motion.div>
</TabPanel>
<TabPanel value={TabSections.Options}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: -30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<div className="mt-2 ml-1">
<Switch
id="tag-aws"
onCheckedChange={() => setShouldTag(!shouldTag)}
isChecked={shouldTag}
>
Tag in AWS Secrets Manager
</Switch>
</div>
{shouldTag && (
<div className="mt-4 flex justify-between">
<FormControl label="Tag Key">
<Input
placeholder="managed-by"
value={tagKey}
onChange={(e) => setTagKey(e.target.value)}
<span className="ml-1.5">AWS Secrets Manager Integration </span>
<Link
href="https://infisical.com/docs/integrations/cloud/aws-secret-manager"
passHref
>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs"
/>
</FormControl>
<FormControl label="Tag Value">
<Input
placeholder="infisical"
value={tagValue}
onChange={(e) => setTagValue(e.target.value)}
/>
</FormControl>
</div>
)}
<FormControl label="Secret Prefix" className="mt-4">
<Input
value={secretPrefix}
onChange={(e) => setSecretPrefix(e.target.value)}
placeholder="INFISICAL_"
/>
</FormControl>
<FormControl label="Encryption Key" className="mt-4">
<Select
value={kmsKeyId}
onValueChange={(e) => {
if (e === "no-keys") return;
setKmsKeyId(e);
}}
className="w-full border border-mineshaft-500"
>
{integrationAuthAwsKmsKeys?.length ? (
integrationAuthAwsKmsKeys.map((key) => {
return (
<SelectItem
value={key.id as string}
key={`repo-id-${key.id}`}
className="w-[28.4rem] text-sm"
>
{key.alias}
</SelectItem>
);
})
) : (
<SelectItem isDisabled value="no-keys" key="no-keys">
No KMS keys available
</SelectItem>
</div>
</a>
</Link>
</div>
</CardTitle>
<Tabs defaultValue={TabSections.Connection} className="px-6">
<TabList>
<div className="flex w-full flex-row border-b border-mineshaft-600">
<Tab value={TabSections.Connection}>Connection</Tab>
<Tab value={TabSections.Options}>Options</Tab>
</div>
</TabList>
<TabPanel value={TabSections.Connection}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<Controller
control={control}
name="sourceEnvironment"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Project Environment"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-full"
value={field.value}
onValueChange={(val) => {
field.onChange(val);
}}
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
</Select>
</FormControl>
</motion.div>
</TabPanel>
</Tabs>
<Button
onClick={handleButtonClick}
color="mineshaft"
variant="outline_bg"
className="mb-6 mt-2 ml-auto mr-6"
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
<div className="mt-6 w-full max-w-md border-t border-mineshaft-800" />
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
<div className="flex flex-row items-center">
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
/>
<Controller
control={control}
name="secretPath"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secrets Path"
errorText={error?.message}
isError={Boolean(error)}
>
<SecretPathInput {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="awsRegion"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="AWS region"
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"
>
{awsRegions.map((awsRegion) => (
<SelectItem
value={awsRegion.slug}
className="flex w-full justify-between"
key={`aws-environment-${awsRegion.slug}`}
>
{awsRegion.name} <Badge variant="success">{awsRegion.slug}</Badge>
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="mappingBehavior"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Mapping 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"
>
{mappingBehaviors.map((option) => (
<SelectItem
value={option.value}
className="text-left"
key={`mapping-behavior-${option.value}`}
>
{option.label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{selectedMappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE && (
<Controller
control={control}
name="secretName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="AWS SM Secret Name"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
placeholder={`${workspace.name
.toLowerCase()
.replace(/ /g, "-")}/${selectedSourceEnvironment}`}
{...field}
/>
</FormControl>
)}
/>
)}
</motion.div>
</TabPanel>
<TabPanel value={TabSections.Options}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: -30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<div className="mt-2 ml-1">
<Controller
control={control}
name="shouldTag"
render={({ field: { onChange, value } }) => (
<Switch
id="tag-aws"
onCheckedChange={(isChecked) => onChange(isChecked)}
isChecked={value}
>
Tag in AWS Secrets Manager
</Switch>
)}
/>
</div>
{shouldTagState && (
<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"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Prefix"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Input placeholder="INFISICAL_" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="kmsKeyId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Encryption Key"
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"
>
{integrationAuthAwsKmsKeys?.length ? (
integrationAuthAwsKmsKeys.map((key) => {
return (
<SelectItem
value={key.id as string}
key={`repo-id-${key.id}`}
className="w-[28.4rem] text-sm"
>
{key.alias}
</SelectItem>
);
})
) : (
<SelectItem isDisabled value="no-keys" key="no-keys">
No KMS keys available
</SelectItem>
)}
</Select>
</FormControl>
)}
/>
</motion.div>
</TabPanel>
</Tabs>
<Button
color="mineshaft"
variant="outline_bg"
type="submit"
className="mb-6 mt-2 ml-auto mr-6"
isLoading={isSubmitting}
>
Create Integration
</Button>
</Card>
<div className="mt-6 w-full max-w-md border-t border-mineshaft-800" />
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
<div className="flex flex-row items-center">
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
</div>
<span className="mt-4 text-sm text-mineshaft-300">
After creating an integration, your secrets will start syncing immediately. This might
cause an unexpected override of current secrets in AWS Secrets Manager with secrets from
Infisical.
</span>
</div>
<span className="mt-4 text-sm text-mineshaft-300">
After creating an integration, your secrets will start syncing immediately. This might
cause an unexpected override of current secrets in AWS Secrets Manager with secrets from
Infisical.
</span>
</div>
</form>
</div>
) : (
<div className="flex h-full w-full items-center justify-center">

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