Compare commits
32 Commits
misc/true-
...
scott/past
Author | SHA1 | Date | |
---|---|---|---|
|
c8109b4e84 | ||
|
1f2b0443cc | ||
|
ddcf5b576b | ||
|
7138b392f2 | ||
|
0e946f73bd | ||
|
7b8551f883 | ||
|
3b1ce86ee6 | ||
|
c649661133 | ||
|
70e44d04ef | ||
|
0dddd58be1 | ||
|
d4c911a28f | ||
|
65d642113d | ||
|
92e7e90c21 | ||
|
f9f6ec0a8d | ||
|
d80a70731d | ||
|
bd99b4e356 | ||
|
7db0bd7daa | ||
|
8bc538af93 | ||
|
d5f718c6ad | ||
|
829b399cda | ||
|
829ae7d3c0 | ||
|
19c26c680c | ||
|
027b200b1a | ||
|
e761e65322 | ||
|
370ed45abb | ||
|
61f786e8d8 | ||
|
26064e3a08 | ||
|
9b246166a1 | ||
|
ac0cb6d96f | ||
|
f71f894de8 | ||
|
66d2cc8947 | ||
|
e034aa381a |
2
backend/src/@types/fastify.d.ts
vendored
@@ -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
|
||||
|
8
backend/src/@types/knex.d.ts
vendored
@@ -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
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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");
|
||||
}
|
||||
});
|
||||
}
|
@@ -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>;
|
||||
|
@@ -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";
|
||||
|
@@ -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",
|
||||
|
@@ -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>;
|
||||
|
23
backend/src/db/schemas/project-templates.ts
Normal 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>>;
|
@@ -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" });
|
||||
};
|
||||
|
@@ -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 })
|
||||
})
|
||||
}
|
||||
},
|
||||
|
309
backend/src/ee/routes/v1/project-template-router.ts
Normal 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 };
|
||||
}
|
||||
});
|
||||
};
|
@@ -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 })
|
||||
})
|
||||
}
|
||||
},
|
||||
|
@@ -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;
|
||||
|
@@ -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({
|
||||
|
@@ -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,
|
||||
|
@@ -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 };
|
||||
};
|
||||
|
@@ -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",
|
||||
|
@@ -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 });
|
||||
|
@@ -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) => {
|
||||
|
@@ -65,6 +65,7 @@ export type TFeatureSet = {
|
||||
};
|
||||
pkiEst: boolean;
|
||||
enforceMfa: boolean;
|
||||
projectTemplates: false;
|
||||
};
|
||||
|
||||
export type TOrgPlansTableDTO = {
|
||||
|
@@ -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;
|
||||
|
@@ -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 }
|
||||
];
|
@@ -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);
|
@@ -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);
|
@@ -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
|
||||
};
|
||||
};
|
@@ -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"
|
||||
}
|
@@ -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."
|
||||
}
|
||||
};
|
||||
|
@@ -58,7 +58,7 @@ export enum OrderByDirection {
|
||||
DESC = "desc"
|
||||
}
|
||||
|
||||
export type ProjectServiceActor = {
|
||||
export type OrgServiceActor = {
|
||||
type: ActorType;
|
||||
id: string;
|
||||
authMethod: ActorAuthMethod;
|
||||
|
@@ -1,2 +1,3 @@
|
||||
export { isDisposableEmail } from "./validate-email";
|
||||
export { isValidFolderName, isValidSecretPath } from "./validate-folder-name";
|
||||
export { blockLocalAndPrivateIpAddresses } from "./validate-url";
|
||||
|
8
backend/src/lib/validator/validate-folder-name.ts
Normal 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));
|
@@ -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[] = [];
|
||||
|
@@ -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: {
|
||||
|
@@ -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 };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -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",
|
||||
|
@@ -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()
|
||||
|
@@ -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 };
|
||||
}
|
||||
});
|
||||
|
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -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 =
|
||||
|
@@ -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` });
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
};
|
||||
|
@@ -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;
|
||||
|
@@ -334,7 +334,7 @@ export const getIntegrationOptions = async () => {
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "BitBucket",
|
||||
name: "Bitbucket",
|
||||
slug: "bitbucket",
|
||||
image: "BitBucket.png",
|
||||
isAvailable: true,
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -32,6 +32,7 @@ export type TCreateProjectDTO = {
|
||||
slug?: string;
|
||||
kmsKeyId?: string;
|
||||
createDefaultEnvs?: boolean;
|
||||
template?: string;
|
||||
tx?: Knex;
|
||||
};
|
||||
|
||||
|
@@ -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(),
|
||||
|
@@ -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
|
||||
|
@@ -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 });
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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 = {
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -185,6 +185,7 @@ export type TGetSecretsRawDTO = {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
keys?: string[];
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetASecretRawDTO = {
|
||||
|
@@ -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>
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/project-templates/{templateId}"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get By ID"
|
||||
openapi: "GET /api/v1/project-templates/{templateId}"
|
||||
---
|
4
docs/api-reference/endpoints/project-templates/list.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/project-templates"
|
||||
---
|
@@ -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>
|
@@ -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">
|
||||

|
||||
</Step>
|
||||
<Step title="Select 'LDAP'">
|
||||

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

|
||||
</Step>
|
||||
<Step title="Select 'LDAP'">
|
||||

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

|
||||

|
||||

|
||||

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

|
||||

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

|
||||

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

|
||||
</Step>
|
||||
<Step title="Select 'LDAP'">
|
||||

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

|
||||

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

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

|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Active Directory Integration
|
||||
|
||||
|
@@ -35,6 +35,7 @@ These endpoints are exposed on port 8443 under the .well-known/est path e.g.
|
||||
|
||||

|
||||
|
||||
- **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.
|
||||
|
||||
|
147
docs/documentation/platform/project-templates.mdx
Normal 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.
|
||||

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

|
||||
</Step>
|
||||
<Step title="Configuring a Project Template">
|
||||
Once your template is created, you'll be directed to the configuration section.
|
||||

|
||||
|
||||
Customize the environments and roles to your needs.
|
||||

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

|
||||
|
||||
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>
|
After Width: | Height: | Size: 929 KiB |
Before Width: | Height: | Size: 612 KiB After Width: | Height: | Size: 507 KiB |
After Width: | Height: | Size: 951 KiB |
After Width: | Height: | Size: 579 KiB |
After Width: | Height: | Size: 562 KiB |
After Width: | Height: | Size: 1.0 MiB |
After Width: | Height: | Size: 1015 KiB |
@@ -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.
|
||||
|
||||

|
||||

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

|
||||

|
||||
</Step>
|
||||
<Step title='Configure integration'>
|
||||
Select which workspace, repository, and optionally, deployment environment, you'd like to sync your secrets
|
||||
to.
|
||||

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

|
||||
</Step>
|
||||
</Steps>
|
||||

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

|
||||

|
||||
</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.
|
||||
|
@@ -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).
|
||||
|
||||
|
@@ -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": [
|
||||
|
@@ -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",
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
};
|
@@ -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"
|
1
frontend/src/components/v2/FilterableSelect/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./FilterableSelect";
|
@@ -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
|
||||
|
@@ -1 +0,0 @@
|
||||
export * from "./MultiSelect";
|
@@ -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) {
|
||||
|
@@ -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";
|
||||
|
@@ -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>;
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
};
|
||||
|
@@ -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,
|
||||
|
@@ -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[];
|
||||
};
|
||||
|
@@ -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;
|
||||
};
|
||||
}
|
||||
| {
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
3
frontend/src/hooks/api/projectTemplates/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./mutations";
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
58
frontend/src/hooks/api/projectTemplates/mutations.tsx
Normal 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));
|
||||
}
|
||||
});
|
||||
};
|
61
frontend/src/hooks/api/projectTemplates/queries.tsx
Normal 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
|
||||
});
|
||||
};
|
31
frontend/src/hooks/api/projectTemplates/types.ts
Normal 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"
|
||||
}
|
@@ -43,4 +43,5 @@ export type SubscriptionPlan = {
|
||||
externalKms: boolean;
|
||||
pkiEst: boolean;
|
||||
enforceMfa: boolean;
|
||||
projectTemplates: boolean;
|
||||
};
|
||||
|
@@ -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);
|
||||
|
@@ -57,6 +57,7 @@ export type TGetUpgradeProjectStatusDTO = {
|
||||
export type CreateWorkspaceDTO = {
|
||||
projectName: string;
|
||||
kmsKeyId?: string;
|
||||
template?: string;
|
||||
};
|
||||
|
||||
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
|
||||
|
@@ -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}
|
||||
|
1
frontend/src/lib/schemas/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./slugSchema";
|
12
frontend/src/lib/schemas/slugSchema.ts
Normal 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"
|
||||
});
|
@@ -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">
|
||||
|