Compare commits
10 Commits
log-availa
...
fix/bulkCo
Author | SHA1 | Date | |
---|---|---|---|
|
3e803debb4 | ||
|
e8eb1b5f8b | ||
|
6e37b9f969 | ||
|
098a8b81be | ||
|
830a2f9581 | ||
|
dc4db40936 | ||
|
0beff3cc1c | ||
|
3dde786621 | ||
|
ebe05661d3 | ||
|
4f0007faa5 |
@@ -99,6 +99,7 @@ const main = async () => {
|
||||
(el) =>
|
||||
!el.tableName.includes("_migrations") &&
|
||||
!el.tableName.includes("audit_logs_") &&
|
||||
!el.tableName.includes("active_locks") &&
|
||||
el.tableName !== "intermediate_audit_logs"
|
||||
);
|
||||
|
||||
|
2
backend/src/@types/fastify.d.ts
vendored
@@ -18,6 +18,7 @@ import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/extern
|
||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";
|
||||
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
||||
import { TIdentityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template";
|
||||
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||
import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
|
||||
import { TKmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal";
|
||||
@@ -300,6 +301,7 @@ declare module "fastify" {
|
||||
reminder: TReminderServiceFactory;
|
||||
bus: TEventBusService;
|
||||
sse: TServerSentEventsService;
|
||||
identityAuthTemplate: TIdentityAuthTemplateServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
10
backend/src/@types/knex.d.ts
vendored
@@ -494,6 +494,11 @@ import {
|
||||
TAccessApprovalPoliciesEnvironmentsInsert,
|
||||
TAccessApprovalPoliciesEnvironmentsUpdate
|
||||
} from "@app/db/schemas/access-approval-policies-environments";
|
||||
import {
|
||||
TIdentityAuthTemplates,
|
||||
TIdentityAuthTemplatesInsert,
|
||||
TIdentityAuthTemplatesUpdate
|
||||
} from "@app/db/schemas/identity-auth-templates";
|
||||
import {
|
||||
TIdentityLdapAuths,
|
||||
TIdentityLdapAuthsInsert,
|
||||
@@ -878,6 +883,11 @@ declare module "knex/types/tables" {
|
||||
TIdentityProjectAdditionalPrivilegeInsert,
|
||||
TIdentityProjectAdditionalPrivilegeUpdate
|
||||
>;
|
||||
[TableName.IdentityAuthTemplate]: KnexOriginal.CompositeTableType<
|
||||
TIdentityAuthTemplates,
|
||||
TIdentityAuthTemplatesInsert,
|
||||
TIdentityAuthTemplatesUpdate
|
||||
>;
|
||||
|
||||
[TableName.AccessApprovalPolicy]: KnexOriginal.CompositeTableType<
|
||||
TAccessApprovalPolicies,
|
||||
|
@@ -0,0 +1,36 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.IdentityAuthTemplate))) {
|
||||
await knex.schema.createTable(TableName.IdentityAuthTemplate, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.binary("templateFields").notNullable();
|
||||
t.uuid("orgId").notNullable();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
t.string("name", 64).notNullable();
|
||||
t.string("authMethod").notNullable();
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.IdentityAuthTemplate);
|
||||
}
|
||||
if (!(await knex.schema.hasColumn(TableName.IdentityLdapAuth, "templateId"))) {
|
||||
await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => {
|
||||
t.uuid("templateId").nullable();
|
||||
t.foreign("templateId").references("id").inTable(TableName.IdentityAuthTemplate).onDelete("SET NULL");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.IdentityLdapAuth, "templateId")) {
|
||||
await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => {
|
||||
t.dropForeign(["templateId"]);
|
||||
t.dropColumn("templateId");
|
||||
});
|
||||
}
|
||||
await knex.schema.dropTableIfExists(TableName.IdentityAuthTemplate);
|
||||
await dropOnUpdateTrigger(knex, TableName.IdentityAuthTemplate);
|
||||
}
|
24
backend/src/db/schemas/identity-auth-templates.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const IdentityAuthTemplatesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
templateFields: zodBuffer,
|
||||
orgId: z.string().uuid(),
|
||||
name: z.string(),
|
||||
authMethod: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TIdentityAuthTemplates = z.infer<typeof IdentityAuthTemplatesSchema>;
|
||||
export type TIdentityAuthTemplatesInsert = Omit<z.input<typeof IdentityAuthTemplatesSchema>, TImmutableDBKeys>;
|
||||
export type TIdentityAuthTemplatesUpdate = Partial<Omit<z.input<typeof IdentityAuthTemplatesSchema>, TImmutableDBKeys>>;
|
@@ -25,7 +25,8 @@ export const IdentityLdapAuthsSchema = z.object({
|
||||
allowedFields: z.unknown().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
accessTokenPeriod: z.coerce.number().default(0)
|
||||
accessTokenPeriod: z.coerce.number().default(0),
|
||||
templateId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TIdentityLdapAuths = z.infer<typeof IdentityLdapAuthsSchema>;
|
||||
|
@@ -91,6 +91,7 @@ export enum TableName {
|
||||
IdentityProjectMembership = "identity_project_memberships",
|
||||
IdentityProjectMembershipRole = "identity_project_membership_role",
|
||||
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
|
||||
IdentityAuthTemplate = "identity_auth_templates",
|
||||
// used by both identity and users
|
||||
IdentityMetadata = "identity_metadata",
|
||||
ResourceMetadata = "resource_metadata",
|
||||
|
391
backend/src/ee/routes/v1/identity-template-router.ts
Normal file
@@ -0,0 +1,391 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { IdentityAuthTemplatesSchema } from "@app/db/schemas/identity-auth-templates";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import {
|
||||
IdentityAuthTemplateMethod,
|
||||
TEMPLATE_SUCCESS_MESSAGES,
|
||||
TEMPLATE_VALIDATION_MESSAGES
|
||||
} from "@app/ee/services/identity-auth-template/identity-auth-template-enums";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const ldapTemplateFieldsSchema = z.object({
|
||||
url: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.URL_REQUIRED),
|
||||
bindDN: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.BIND_DN_REQUIRED),
|
||||
bindPass: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.BIND_PASSWORD_REQUIRED),
|
||||
searchBase: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.SEARCH_BASE_REQUIRED),
|
||||
ldapCaCertificate: z.string().trim().optional()
|
||||
});
|
||||
|
||||
export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "Create identity auth template",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
body: z.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_REQUIRED)
|
||||
.max(64, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_MAX_LENGTH),
|
||||
authMethod: z.nativeEnum(IdentityAuthTemplateMethod),
|
||||
templateFields: ldapTemplateFieldsSchema
|
||||
}),
|
||||
response: {
|
||||
200: IdentityAuthTemplatesSchema.extend({
|
||||
templateFields: z.record(z.string(), z.unknown())
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const template = await server.services.identityAuthTemplate.createTemplate({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
name: req.body.name,
|
||||
authMethod: req.body.authMethod,
|
||||
templateFields: req.body.templateFields
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_CREATE,
|
||||
metadata: {
|
||||
templateId: template.id,
|
||||
name: template.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return template;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:templateId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "Update identity auth template",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED)
|
||||
}),
|
||||
body: z.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_REQUIRED)
|
||||
.max(64, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_MAX_LENGTH)
|
||||
.optional(),
|
||||
templateFields: ldapTemplateFieldsSchema.partial().optional()
|
||||
}),
|
||||
response: {
|
||||
200: IdentityAuthTemplatesSchema.extend({
|
||||
templateFields: z.record(z.string(), z.unknown())
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const template = await server.services.identityAuthTemplate.updateTemplate({
|
||||
templateId: req.params.templateId,
|
||||
name: req.body.name,
|
||||
templateFields: req.body.templateFields,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_UPDATE,
|
||||
metadata: {
|
||||
templateId: template.id,
|
||||
name: template.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return template;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:templateId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "Delete identity auth template",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const template = await server.services.identityAuthTemplate.deleteTemplate({
|
||||
templateId: req.params.templateId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_DELETE,
|
||||
metadata: {
|
||||
templateId: template.id,
|
||||
name: template.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { message: TEMPLATE_SUCCESS_MESSAGES.DELETED };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:templateId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "Get identity auth template by ID",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED)
|
||||
}),
|
||||
response: {
|
||||
200: IdentityAuthTemplatesSchema.extend({
|
||||
templateFields: ldapTemplateFieldsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const template = await server.services.identityAuthTemplate.getTemplate({
|
||||
templateId: req.params.templateId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return template;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/search",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "List identity auth templates",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
querystring: z.object({
|
||||
limit: z.coerce.number().positive().max(100).default(5).optional(),
|
||||
offset: z.coerce.number().min(0).default(0).optional(),
|
||||
search: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
templates: IdentityAuthTemplatesSchema.extend({
|
||||
templateFields: ldapTemplateFieldsSchema
|
||||
}).array(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { templates, totalCount } = await server.services.identityAuthTemplate.listTemplates({
|
||||
limit: req.query.limit,
|
||||
offset: req.query.offset,
|
||||
search: req.query.search,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return { templates, totalCount };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "Get identity auth templates by authentication method",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
querystring: z.object({
|
||||
authMethod: z.nativeEnum(IdentityAuthTemplateMethod)
|
||||
}),
|
||||
response: {
|
||||
200: IdentityAuthTemplatesSchema.extend({
|
||||
templateFields: ldapTemplateFieldsSchema
|
||||
}).array()
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const templates = await server.services.identityAuthTemplate.getTemplatesByAuthMethod({
|
||||
authMethod: req.query.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return templates;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:templateId/usage",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "Get template usage by template ID",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
templateId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
identityId: z.string(),
|
||||
identityName: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const templates = await server.services.identityAuthTemplate.findTemplateUsages({
|
||||
templateId: req.params.templateId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return templates;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:templateId/delete-usage",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "Unlink identity auth template usage",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
templateId: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
identityIds: z.string().array()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
authId: z.string(),
|
||||
identityId: z.string(),
|
||||
identityName: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const templates = await server.services.identityAuthTemplate.unlinkTemplateUsage({
|
||||
templateId: req.params.templateId,
|
||||
identityIds: req.body.identityIds,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return templates;
|
||||
}
|
||||
});
|
||||
};
|
@@ -13,6 +13,7 @@ import { registerGatewayRouter } from "./gateway-router";
|
||||
import { registerGithubOrgSyncRouter } from "./github-org-sync-router";
|
||||
import { registerGroupRouter } from "./group-router";
|
||||
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
||||
import { registerIdentityTemplateRouter } from "./identity-template-router";
|
||||
import { registerKmipRouter } from "./kmip-router";
|
||||
import { registerKmipSpecRouter } from "./kmip-spec-router";
|
||||
import { registerLdapRouter } from "./ldap-router";
|
||||
@@ -125,6 +126,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerExternalKmsRouter, {
|
||||
prefix: "/external-kms"
|
||||
});
|
||||
await server.register(registerIdentityTemplateRouter, { prefix: "/identity-templates" });
|
||||
|
||||
await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" });
|
||||
|
||||
|
@@ -161,6 +161,9 @@ export enum EventType {
|
||||
CREATE_IDENTITY = "create-identity",
|
||||
UPDATE_IDENTITY = "update-identity",
|
||||
DELETE_IDENTITY = "delete-identity",
|
||||
MACHINE_IDENTITY_AUTH_TEMPLATE_CREATE = "machine-identity-auth-template-create",
|
||||
MACHINE_IDENTITY_AUTH_TEMPLATE_UPDATE = "machine-identity-auth-template-update",
|
||||
MACHINE_IDENTITY_AUTH_TEMPLATE_DELETE = "machine-identity-auth-template-delete",
|
||||
LOGIN_IDENTITY_UNIVERSAL_AUTH = "login-identity-universal-auth",
|
||||
ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth",
|
||||
UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth",
|
||||
@@ -830,6 +833,30 @@ interface LoginIdentityUniversalAuthEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface MachineIdentityAuthTemplateCreateEvent {
|
||||
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_CREATE;
|
||||
metadata: {
|
||||
templateId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface MachineIdentityAuthTemplateUpdateEvent {
|
||||
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_UPDATE;
|
||||
metadata: {
|
||||
templateId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface MachineIdentityAuthTemplateDeleteEvent {
|
||||
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_DELETE;
|
||||
metadata: {
|
||||
templateId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddIdentityUniversalAuthEvent {
|
||||
type: EventType.ADD_IDENTITY_UNIVERSAL_AUTH;
|
||||
metadata: {
|
||||
@@ -1325,6 +1352,7 @@ interface AddIdentityLdapAuthEvent {
|
||||
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||
allowedFields?: TAllowedFields[];
|
||||
url: string;
|
||||
templateId?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1338,6 +1366,7 @@ interface UpdateIdentityLdapAuthEvent {
|
||||
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||
allowedFields?: TAllowedFields[];
|
||||
url?: string;
|
||||
templateId?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3439,6 +3468,9 @@ export type Event =
|
||||
| UpdateIdentityEvent
|
||||
| DeleteIdentityEvent
|
||||
| LoginIdentityUniversalAuthEvent
|
||||
| MachineIdentityAuthTemplateCreateEvent
|
||||
| MachineIdentityAuthTemplateUpdateEvent
|
||||
| MachineIdentityAuthTemplateDeleteEvent
|
||||
| AddIdentityUniversalAuthEvent
|
||||
| UpdateIdentityUniversalAuthEvent
|
||||
| DeleteIdentityUniversalAuthEvent
|
||||
|
@@ -0,0 +1,83 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { buildFindFilter, ormify } from "@app/lib/knex";
|
||||
|
||||
import { IdentityAuthTemplateMethod } from "./identity-auth-template-enums";
|
||||
|
||||
export type TIdentityAuthTemplateDALFactory = ReturnType<typeof identityAuthTemplateDALFactory>;
|
||||
|
||||
export const identityAuthTemplateDALFactory = (db: TDbClient) => {
|
||||
const identityAuthTemplateOrm = ormify(db, TableName.IdentityAuthTemplate);
|
||||
|
||||
const findByOrgId = async (
|
||||
orgId: string,
|
||||
{ limit, offset, search, tx }: { limit?: number; offset?: number; search?: string; tx?: Knex } = {}
|
||||
) => {
|
||||
let query = (tx || db.replicaNode())(TableName.IdentityAuthTemplate).where({ orgId });
|
||||
let countQuery = (tx || db.replicaNode())(TableName.IdentityAuthTemplate).where({ orgId });
|
||||
|
||||
if (search) {
|
||||
const searchFilter = `%${search.toLowerCase()}%`;
|
||||
query = query.whereRaw("LOWER(name) LIKE ?", [searchFilter]);
|
||||
countQuery = countQuery.whereRaw("LOWER(name) LIKE ?", [searchFilter]);
|
||||
}
|
||||
|
||||
query = query.orderBy("createdAt", "desc");
|
||||
|
||||
if (limit !== undefined) {
|
||||
query = query.limit(limit);
|
||||
}
|
||||
if (offset !== undefined) {
|
||||
query = query.offset(offset);
|
||||
}
|
||||
|
||||
const docs = await query;
|
||||
|
||||
const [{ count }] = (await countQuery.count("* as count")) as [{ count: string | number }];
|
||||
|
||||
return { docs, totalCount: Number(count) };
|
||||
};
|
||||
|
||||
const findByAuthMethod = async (authMethod: string, orgId: string, tx?: Knex) => {
|
||||
const query = (tx || db.replicaNode())(TableName.IdentityAuthTemplate)
|
||||
.where({ authMethod, orgId })
|
||||
.orderBy("createdAt", "desc");
|
||||
const docs = await query;
|
||||
return docs;
|
||||
};
|
||||
|
||||
const findTemplateUsages = async (templateId: string, authMethod: string, tx?: Knex) => {
|
||||
switch (authMethod) {
|
||||
case IdentityAuthTemplateMethod.LDAP:
|
||||
const query = (tx || db.replicaNode())(TableName.IdentityLdapAuth)
|
||||
.join(TableName.Identity, `${TableName.IdentityLdapAuth}.identityId`, `${TableName.Identity}.id`)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.where(buildFindFilter({ templateId }, TableName.IdentityLdapAuth))
|
||||
.select(
|
||||
db.ref("identityId").withSchema(TableName.IdentityLdapAuth),
|
||||
db.ref("name").withSchema(TableName.Identity).as("identityName")
|
||||
);
|
||||
const docs = await query;
|
||||
return docs;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const findByIdAndOrgId = async (id: string, orgId: string, tx?: Knex) => {
|
||||
const query = (tx || db.replicaNode())(TableName.IdentityAuthTemplate).where({ id, orgId });
|
||||
const doc = await query;
|
||||
return doc?.[0];
|
||||
};
|
||||
|
||||
return {
|
||||
...identityAuthTemplateOrm,
|
||||
findByOrgId,
|
||||
findByAuthMethod,
|
||||
findTemplateUsages,
|
||||
findByIdAndOrgId
|
||||
};
|
||||
};
|
@@ -0,0 +1,22 @@
|
||||
export enum IdentityAuthTemplateMethod {
|
||||
LDAP = "ldap"
|
||||
}
|
||||
|
||||
export const TEMPLATE_VALIDATION_MESSAGES = {
|
||||
TEMPLATE_NAME_REQUIRED: "Template name is required",
|
||||
TEMPLATE_NAME_MAX_LENGTH: "Template name must be at most 64 characters long",
|
||||
AUTH_METHOD_REQUIRED: "Auth method is required",
|
||||
TEMPLATE_ID_REQUIRED: "Template ID is required",
|
||||
LDAP: {
|
||||
URL_REQUIRED: "LDAP URL is required",
|
||||
BIND_DN_REQUIRED: "Bind DN is required",
|
||||
BIND_PASSWORD_REQUIRED: "Bind password is required",
|
||||
SEARCH_BASE_REQUIRED: "Search base is required"
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const TEMPLATE_SUCCESS_MESSAGES = {
|
||||
CREATED: "Template created successfully",
|
||||
UPDATED: "Template updated successfully",
|
||||
DELETED: "Template deleted successfully"
|
||||
} as const;
|
@@ -0,0 +1,454 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import {
|
||||
OrgPermissionMachineIdentityAuthTemplateActions,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TIdentityLdapAuthDALFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { TIdentityAuthTemplateDALFactory } from "./identity-auth-template-dal";
|
||||
import { IdentityAuthTemplateMethod } from "./identity-auth-template-enums";
|
||||
import {
|
||||
TDeleteIdentityAuthTemplateDTO,
|
||||
TFindTemplateUsagesDTO,
|
||||
TGetIdentityAuthTemplateDTO,
|
||||
TGetTemplatesByAuthMethodDTO,
|
||||
TLdapTemplateFields,
|
||||
TListIdentityAuthTemplatesDTO,
|
||||
TUnlinkTemplateUsageDTO
|
||||
} from "./identity-auth-template-types";
|
||||
|
||||
type TIdentityAuthTemplateServiceFactoryDep = {
|
||||
identityAuthTemplateDAL: TIdentityAuthTemplateDALFactory;
|
||||
identityLdapAuthDAL: TIdentityLdapAuthDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
};
|
||||
|
||||
export type TIdentityAuthTemplateServiceFactory = ReturnType<typeof identityAuthTemplateServiceFactory>;
|
||||
|
||||
export const identityAuthTemplateServiceFactory = ({
|
||||
identityAuthTemplateDAL,
|
||||
identityLdapAuthDAL,
|
||||
permissionService,
|
||||
kmsService,
|
||||
licenseService,
|
||||
auditLogService
|
||||
}: TIdentityAuthTemplateServiceFactoryDep) => {
|
||||
// Plan check
|
||||
const $checkPlan = async (orgId: string) => {
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.machineIdentityAuthTemplates)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to use identity auth template due to plan restriction. Upgrade plan to access machine identity auth templates."
|
||||
});
|
||||
};
|
||||
const createTemplate = async ({
|
||||
name,
|
||||
authMethod,
|
||||
templateFields,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: {
|
||||
name: string;
|
||||
authMethod: string;
|
||||
templateFields: Record<string, unknown>;
|
||||
} & Omit<TOrgPermission, "orgId">) => {
|
||||
await $checkPlan(actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
const template = await identityAuthTemplateDAL.create({
|
||||
name,
|
||||
authMethod,
|
||||
templateFields: encryptor({ plainText: Buffer.from(JSON.stringify(templateFields)) }).cipherTextBlob,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
return { ...template, templateFields };
|
||||
};
|
||||
|
||||
const updateTemplate = async ({
|
||||
templateId,
|
||||
name,
|
||||
templateFields,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: {
|
||||
templateId: string;
|
||||
name?: string;
|
||||
templateFields?: Record<string, unknown>;
|
||||
} & Omit<TOrgPermission, "orgId">) => {
|
||||
await $checkPlan(actorOrgId);
|
||||
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
|
||||
if (!template) {
|
||||
throw new NotFoundError({ message: "Template not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
template.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: template.orgId
|
||||
});
|
||||
|
||||
let finalTemplateFields: Record<string, unknown> = {};
|
||||
|
||||
const updatedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => {
|
||||
const authTemplate = await identityAuthTemplateDAL.updateById(
|
||||
templateId,
|
||||
{
|
||||
name,
|
||||
...(templateFields && {
|
||||
templateFields: encryptor({ plainText: Buffer.from(JSON.stringify(templateFields)) }).cipherTextBlob
|
||||
})
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (templateFields && template.authMethod === IdentityAuthTemplateMethod.LDAP) {
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: template.orgId
|
||||
});
|
||||
|
||||
const currentTemplateFields = JSON.parse(
|
||||
decryptor({ cipherTextBlob: template.templateFields }).toString()
|
||||
) as TLdapTemplateFields;
|
||||
|
||||
const mergedTemplateFields: TLdapTemplateFields = { ...currentTemplateFields, ...templateFields };
|
||||
finalTemplateFields = mergedTemplateFields;
|
||||
const ldapUpdateData: {
|
||||
url?: string;
|
||||
searchBase?: string;
|
||||
encryptedBindDN?: Buffer;
|
||||
encryptedBindPass?: Buffer;
|
||||
encryptedLdapCaCertificate?: Buffer;
|
||||
} = {};
|
||||
|
||||
if ("url" in templateFields) {
|
||||
ldapUpdateData.url = mergedTemplateFields.url;
|
||||
}
|
||||
if ("searchBase" in templateFields) {
|
||||
ldapUpdateData.searchBase = mergedTemplateFields.searchBase;
|
||||
}
|
||||
if ("bindDN" in templateFields) {
|
||||
ldapUpdateData.encryptedBindDN = encryptor({
|
||||
plainText: Buffer.from(mergedTemplateFields.bindDN)
|
||||
}).cipherTextBlob;
|
||||
}
|
||||
if ("bindPass" in templateFields) {
|
||||
ldapUpdateData.encryptedBindPass = encryptor({
|
||||
plainText: Buffer.from(mergedTemplateFields.bindPass)
|
||||
}).cipherTextBlob;
|
||||
}
|
||||
if ("ldapCaCertificate" in templateFields) {
|
||||
ldapUpdateData.encryptedLdapCaCertificate = encryptor({
|
||||
plainText: Buffer.from(mergedTemplateFields.ldapCaCertificate || "")
|
||||
}).cipherTextBlob;
|
||||
}
|
||||
|
||||
if (Object.keys(ldapUpdateData).length > 0) {
|
||||
const updatedLdapAuths = await identityLdapAuthDAL.update({ templateId }, ldapUpdateData, tx);
|
||||
await Promise.all(
|
||||
updatedLdapAuths.map(async (updatedLdapAuth) => {
|
||||
await auditLogService.createAuditLog({
|
||||
actor: {
|
||||
type: ActorType.PLATFORM,
|
||||
metadata: {}
|
||||
},
|
||||
orgId: actorOrgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_IDENTITY_LDAP_AUTH,
|
||||
metadata: {
|
||||
identityId: updatedLdapAuth.identityId,
|
||||
templateId: template.id
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
return authTemplate;
|
||||
});
|
||||
|
||||
return { ...updatedTemplate, templateFields: finalTemplateFields };
|
||||
};
|
||||
|
||||
const deleteTemplate = async ({
|
||||
templateId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TDeleteIdentityAuthTemplateDTO) => {
|
||||
await $checkPlan(actorOrgId);
|
||||
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
|
||||
if (!template) {
|
||||
throw new NotFoundError({ message: "Template not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
template.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
|
||||
const deletedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => {
|
||||
// Remove template reference from identityLdapAuth records
|
||||
const updatedLdapAuths = await identityLdapAuthDAL.update({ templateId }, { templateId: null }, tx);
|
||||
await Promise.all(
|
||||
updatedLdapAuths.map(async (updatedLdapAuth) => {
|
||||
await auditLogService.createAuditLog({
|
||||
actor: {
|
||||
type: ActorType.PLATFORM,
|
||||
metadata: {}
|
||||
},
|
||||
orgId: actorOrgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_IDENTITY_LDAP_AUTH,
|
||||
metadata: {
|
||||
identityId: updatedLdapAuth.identityId,
|
||||
templateId: template.id
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// Delete the template
|
||||
const [deletedTpl] = await identityAuthTemplateDAL.delete({ id: templateId }, tx);
|
||||
return deletedTpl;
|
||||
});
|
||||
|
||||
return deletedTemplate;
|
||||
};
|
||||
|
||||
const getTemplate = async ({
|
||||
templateId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TGetIdentityAuthTemplateDTO) => {
|
||||
await $checkPlan(actorOrgId);
|
||||
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
|
||||
if (!template) {
|
||||
throw new NotFoundError({ message: "Template not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
template.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: template.orgId
|
||||
});
|
||||
const decryptedTemplateFields = decryptor({ cipherTextBlob: template.templateFields }).toString();
|
||||
return {
|
||||
...template,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
templateFields: JSON.parse(decryptedTemplateFields)
|
||||
};
|
||||
};
|
||||
|
||||
const listTemplates = async ({
|
||||
limit,
|
||||
offset,
|
||||
search,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TListIdentityAuthTemplatesDTO) => {
|
||||
await $checkPlan(actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
|
||||
const { docs, totalCount } = await identityAuthTemplateDAL.findByOrgId(actorOrgId, { limit, offset, search });
|
||||
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
return {
|
||||
totalCount,
|
||||
templates: docs.map((doc) => ({
|
||||
...doc,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
templateFields: JSON.parse(decryptor({ cipherTextBlob: doc.templateFields }).toString())
|
||||
}))
|
||||
};
|
||||
};
|
||||
|
||||
const getTemplatesByAuthMethod = async ({
|
||||
authMethod,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TGetTemplatesByAuthMethodDTO) => {
|
||||
await $checkPlan(actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
|
||||
const docs = await identityAuthTemplateDAL.findByAuthMethod(authMethod, actorOrgId);
|
||||
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
return docs.map((doc) => ({
|
||||
...doc,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
templateFields: JSON.parse(decryptor({ cipherTextBlob: doc.templateFields }).toString())
|
||||
}));
|
||||
};
|
||||
|
||||
const findTemplateUsages = async ({
|
||||
templateId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TFindTemplateUsagesDTO) => {
|
||||
await $checkPlan(actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
|
||||
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
|
||||
if (!template) {
|
||||
throw new NotFoundError({ message: "Template not found" });
|
||||
}
|
||||
|
||||
const docs = await identityAuthTemplateDAL.findTemplateUsages(templateId, template.authMethod);
|
||||
return docs;
|
||||
};
|
||||
|
||||
const unlinkTemplateUsage = async ({
|
||||
templateId,
|
||||
identityIds,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TUnlinkTemplateUsageDTO) => {
|
||||
await $checkPlan(actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
|
||||
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
|
||||
if (!template) {
|
||||
throw new NotFoundError({ message: "Template not found" });
|
||||
}
|
||||
|
||||
switch (template.authMethod) {
|
||||
case IdentityAuthTemplateMethod.LDAP:
|
||||
await identityLdapAuthDAL.update({ $in: { identityId: identityIds }, templateId }, { templateId: null });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
createTemplate,
|
||||
updateTemplate,
|
||||
deleteTemplate,
|
||||
getTemplate,
|
||||
listTemplates,
|
||||
getTemplatesByAuthMethod,
|
||||
findTemplateUsages,
|
||||
unlinkTemplateUsage
|
||||
};
|
||||
};
|
@@ -0,0 +1,61 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { IdentityAuthTemplateMethod } from "./identity-auth-template-enums";
|
||||
|
||||
// Method-specific template field types
|
||||
export type TLdapTemplateFields = {
|
||||
url: string;
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
searchBase: string;
|
||||
ldapCaCertificate?: string;
|
||||
};
|
||||
|
||||
// Union type for all template field types
|
||||
export type TTemplateFieldsByMethod = {
|
||||
[IdentityAuthTemplateMethod.LDAP]: TLdapTemplateFields;
|
||||
};
|
||||
|
||||
// Generic base types that use conditional types for type safety
|
||||
export type TCreateIdentityAuthTemplateDTO = {
|
||||
name: string;
|
||||
authMethod: IdentityAuthTemplateMethod;
|
||||
templateFields: TTemplateFieldsByMethod[IdentityAuthTemplateMethod];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateIdentityAuthTemplateDTO = {
|
||||
templateId: string;
|
||||
name?: string;
|
||||
templateFields?: Partial<TTemplateFieldsByMethod[IdentityAuthTemplateMethod]>;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteIdentityAuthTemplateDTO = {
|
||||
templateId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetIdentityAuthTemplateDTO = {
|
||||
templateId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListIdentityAuthTemplatesDTO = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetTemplatesByAuthMethodDTO = {
|
||||
authMethod: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TFindTemplateUsagesDTO = {
|
||||
templateId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUnlinkTemplateUsageDTO = {
|
||||
templateId: string;
|
||||
identityIds: string[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
// Specific LDAP types for convenience
|
||||
export type TCreateLdapTemplateDTO = TCreateIdentityAuthTemplateDTO;
|
||||
export type TUpdateLdapTemplateDTO = TUpdateIdentityAuthTemplateDTO;
|
6
backend/src/ee/services/identity-auth-template/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type { TIdentityAuthTemplateDALFactory } from "./identity-auth-template-dal";
|
||||
export { identityAuthTemplateDALFactory } from "./identity-auth-template-dal";
|
||||
export * from "./identity-auth-template-enums";
|
||||
export type { TIdentityAuthTemplateServiceFactory } from "./identity-auth-template-service";
|
||||
export { identityAuthTemplateServiceFactory } from "./identity-auth-template-service";
|
||||
export type * from "./identity-auth-template-types";
|
@@ -31,7 +31,8 @@ export const getDefaultOnPremFeatures = () => {
|
||||
caCrl: false,
|
||||
sshHostGroups: false,
|
||||
enterpriseSecretSyncs: false,
|
||||
enterpriseAppConnections: false
|
||||
enterpriseAppConnections: false,
|
||||
machineIdentityAuthTemplates: false
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -60,7 +60,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
enterpriseSecretSyncs: false,
|
||||
enterpriseAppConnections: false,
|
||||
fips: false,
|
||||
eventSubscriptions: false
|
||||
eventSubscriptions: false,
|
||||
machineIdentityAuthTemplates: false
|
||||
});
|
||||
|
||||
export const setupLicenseRequestWithStore = (
|
||||
|
@@ -75,6 +75,7 @@ export type TFeatureSet = {
|
||||
secretScanning: false;
|
||||
enterpriseSecretSyncs: false;
|
||||
enterpriseAppConnections: false;
|
||||
machineIdentityAuthTemplates: false;
|
||||
fips: false;
|
||||
eventSubscriptions: false;
|
||||
};
|
||||
|
@@ -28,6 +28,15 @@ export enum OrgPermissionKmipActions {
|
||||
Setup = "setup"
|
||||
}
|
||||
|
||||
export enum OrgPermissionMachineIdentityAuthTemplateActions {
|
||||
ListTemplates = "list-templates",
|
||||
EditTemplates = "edit-templates",
|
||||
CreateTemplates = "create-templates",
|
||||
DeleteTemplates = "delete-templates",
|
||||
UnlinkTemplates = "unlink-templates",
|
||||
AttachTemplates = "attach-templates"
|
||||
}
|
||||
|
||||
export enum OrgPermissionAdminConsoleAction {
|
||||
AccessAllProjects = "access-all-projects"
|
||||
}
|
||||
@@ -88,6 +97,7 @@ export enum OrgPermissionSubjects {
|
||||
Identity = "identity",
|
||||
Kms = "kms",
|
||||
AdminConsole = "organization-admin-console",
|
||||
MachineIdentityAuthTemplate = "machine-identity-auth-template",
|
||||
AuditLogs = "audit-logs",
|
||||
ProjectTemplates = "project-templates",
|
||||
AppConnections = "app-connections",
|
||||
@@ -126,6 +136,7 @@ export type OrgPermissionSet =
|
||||
)
|
||||
]
|
||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
|
||||
| [OrgPermissionMachineIdentityAuthTemplateActions, OrgPermissionSubjects.MachineIdentityAuthTemplate]
|
||||
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
|
||||
| [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare];
|
||||
|
||||
@@ -237,6 +248,14 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z
|
||||
.literal(OrgPermissionSubjects.MachineIdentityAuthTemplate)
|
||||
.describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionMachineIdentityAuthTemplateActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(OrgPermissionSubjects.Gateway).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe(
|
||||
@@ -350,6 +369,25 @@ const buildAdminPermission = () => {
|
||||
// the proxy assignment is temporary in order to prevent "more privilege" error during role assignment to MI
|
||||
can(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||
|
||||
can(OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
|
||||
can(OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
|
||||
can(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
can(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
can(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
can(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
|
||||
can(OrgPermissionSecretShareAction.ManageSettings, OrgPermissionSubjects.SecretShare);
|
||||
|
||||
return rules;
|
||||
@@ -385,6 +423,16 @@ const buildMemberPermission = () => {
|
||||
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
|
||||
|
||||
can(OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
|
||||
can(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
can(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
|
@@ -18,6 +18,7 @@ import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "@app/services/
|
||||
|
||||
export enum ApiDocsTags {
|
||||
Identities = "Identities",
|
||||
IdentityTemplates = "Identity Templates",
|
||||
TokenAuth = "Token Auth",
|
||||
UniversalAuth = "Universal Auth",
|
||||
GcpAuth = "GCP Auth",
|
||||
@@ -214,6 +215,7 @@ export const LDAP_AUTH = {
|
||||
password: "The password of the LDAP user to login."
|
||||
},
|
||||
ATTACH: {
|
||||
templateId: "The ID of the identity auth template to attach the configuration onto.",
|
||||
identityId: "The ID of the identity to attach the configuration onto.",
|
||||
url: "The URL of the LDAP server.",
|
||||
allowedFields:
|
||||
@@ -240,7 +242,8 @@ export const LDAP_AUTH = {
|
||||
accessTokenTTL: "The new lifetime for an access token in seconds.",
|
||||
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
|
||||
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.",
|
||||
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
|
||||
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
|
||||
templateId: "The ID of the identity auth template to update the configuration to."
|
||||
},
|
||||
RETRIEVE: {
|
||||
identityId: "The ID of the identity to retrieve the configuration for."
|
||||
|
@@ -179,6 +179,8 @@ import { identityAccessTokenDALFactory } from "@app/services/identity-access-tok
|
||||
import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||
import { identityAliCloudAuthDALFactory } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-dal";
|
||||
import { identityAliCloudAuthServiceFactory } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-service";
|
||||
import { identityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-dal";
|
||||
import { identityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-service";
|
||||
import { identityAwsAuthDALFactory } from "@app/services/identity-aws-auth/identity-aws-auth-dal";
|
||||
import { identityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
|
||||
import { identityAzureAuthDALFactory } from "@app/services/identity-azure-auth/identity-azure-auth-dal";
|
||||
@@ -394,6 +396,7 @@ export const registerRoutes = async (
|
||||
const identityProjectDAL = identityProjectDALFactory(db);
|
||||
const identityProjectMembershipRoleDAL = identityProjectMembershipRoleDALFactory(db);
|
||||
const identityProjectAdditionalPrivilegeDAL = identityProjectAdditionalPrivilegeDALFactory(db);
|
||||
const identityAuthTemplateDAL = identityAuthTemplateDALFactory(db);
|
||||
|
||||
const identityTokenAuthDAL = identityTokenAuthDALFactory(db);
|
||||
const identityUaDAL = identityUaDALFactory(db);
|
||||
@@ -1461,6 +1464,15 @@ export const registerRoutes = async (
|
||||
identityMetadataDAL
|
||||
});
|
||||
|
||||
const identityAuthTemplateService = identityAuthTemplateServiceFactory({
|
||||
identityAuthTemplateDAL,
|
||||
identityLdapAuthDAL,
|
||||
permissionService,
|
||||
kmsService,
|
||||
licenseService,
|
||||
auditLogService
|
||||
});
|
||||
|
||||
const identityAccessTokenService = identityAccessTokenServiceFactory({
|
||||
identityAccessTokenDAL,
|
||||
identityOrgMembershipDAL,
|
||||
@@ -1604,7 +1616,8 @@ export const registerRoutes = async (
|
||||
identityAccessTokenDAL,
|
||||
identityOrgMembershipDAL,
|
||||
licenseService,
|
||||
identityDAL
|
||||
identityDAL,
|
||||
identityAuthTemplateDAL
|
||||
});
|
||||
|
||||
const dynamicSecretProviders = buildDynamicSecretProviders({
|
||||
@@ -2008,6 +2021,7 @@ export const registerRoutes = async (
|
||||
webhook: webhookService,
|
||||
serviceToken: serviceTokenService,
|
||||
identity: identityService,
|
||||
identityAuthTemplate: identityAuthTemplateService,
|
||||
identityAccessToken: identityAccessTokenService,
|
||||
identityProject: identityProjectService,
|
||||
identityTokenAuth: identityTokenAuthService,
|
||||
|
@@ -200,49 +200,104 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
|
||||
params: z.object({
|
||||
identityId: z.string().trim().describe(LDAP_AUTH.ATTACH.identityId)
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
url: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.url),
|
||||
bindDN: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.bindDN),
|
||||
bindPass: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.bindPass),
|
||||
searchBase: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.searchBase),
|
||||
searchFilter: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.default("(uid={{username}})")
|
||||
.refine(isValidLdapFilter, "Invalid LDAP search filter")
|
||||
.describe(LDAP_AUTH.ATTACH.searchFilter),
|
||||
allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields),
|
||||
ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate),
|
||||
accessTokenTrustedIps: z
|
||||
.object({
|
||||
ipAddress: z.string().trim()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
||||
.describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps),
|
||||
accessTokenTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(315360000)
|
||||
.default(2592000)
|
||||
.describe(LDAP_AUTH.ATTACH.accessTokenTTL),
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(315360000)
|
||||
.default(2592000)
|
||||
.describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit)
|
||||
})
|
||||
.refine(
|
||||
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||
),
|
||||
body: z.union([
|
||||
// Template-based configuration
|
||||
z
|
||||
.object({
|
||||
templateId: z.string().trim().describe(LDAP_AUTH.ATTACH.templateId),
|
||||
searchFilter: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.default("(uid={{username}})")
|
||||
.refine(isValidLdapFilter, "Invalid LDAP search filter")
|
||||
.describe(LDAP_AUTH.ATTACH.searchFilter),
|
||||
allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields),
|
||||
ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate),
|
||||
accessTokenTrustedIps: z
|
||||
.object({
|
||||
ipAddress: z.string().trim()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
||||
.describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps),
|
||||
accessTokenTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(315360000)
|
||||
.default(2592000)
|
||||
.describe(LDAP_AUTH.ATTACH.accessTokenTTL),
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(315360000)
|
||||
.default(2592000)
|
||||
.describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.default(0)
|
||||
.describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit)
|
||||
})
|
||||
.refine(
|
||||
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||
),
|
||||
|
||||
// Manual configuration
|
||||
z
|
||||
.object({
|
||||
url: z.string().trim().describe(LDAP_AUTH.ATTACH.url),
|
||||
bindDN: z.string().trim().describe(LDAP_AUTH.ATTACH.bindDN),
|
||||
bindPass: z.string().trim().describe(LDAP_AUTH.ATTACH.bindPass),
|
||||
searchBase: z.string().trim().describe(LDAP_AUTH.ATTACH.searchBase),
|
||||
searchFilter: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.default("(uid={{username}})")
|
||||
.refine(isValidLdapFilter, "Invalid LDAP search filter")
|
||||
.describe(LDAP_AUTH.ATTACH.searchFilter),
|
||||
allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields),
|
||||
ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate),
|
||||
accessTokenTrustedIps: z
|
||||
.object({
|
||||
ipAddress: z.string().trim()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
||||
.describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps),
|
||||
accessTokenTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(315360000)
|
||||
.default(2592000)
|
||||
.describe(LDAP_AUTH.ATTACH.accessTokenTTL),
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(315360000)
|
||||
.default(2592000)
|
||||
.describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.default(0)
|
||||
.describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit)
|
||||
})
|
||||
.refine(
|
||||
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||
)
|
||||
]),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityLdapAuth: IdentityLdapAuthsSchema.omit({
|
||||
@@ -275,7 +330,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
|
||||
accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL,
|
||||
accessTokenTTL: identityLdapAuth.accessTokenTTL,
|
||||
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
|
||||
allowedFields: req.body.allowedFields
|
||||
allowedFields: req.body.allowedFields,
|
||||
templateId: identityLdapAuth.templateId
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -309,6 +365,7 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
|
||||
bindDN: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.bindDN),
|
||||
bindPass: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.bindPass),
|
||||
searchBase: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.searchBase),
|
||||
templateId: z.string().trim().optional().describe(LDAP_AUTH.UPDATE.templateId),
|
||||
searchFilter: z
|
||||
.string()
|
||||
.trim()
|
||||
@@ -376,7 +433,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
|
||||
accessTokenTTL: identityLdapAuth.accessTokenTTL,
|
||||
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: identityLdapAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||
allowedFields: req.body.allowedFields
|
||||
allowedFields: req.body.allowedFields,
|
||||
templateId: identityLdapAuth.templateId
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -413,7 +471,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
|
||||
}).extend({
|
||||
bindDN: z.string(),
|
||||
bindPass: z.string(),
|
||||
ldapCaCertificate: z.string().optional()
|
||||
ldapCaCertificate: z.string().optional(),
|
||||
templateId: z.string().optional().nullable()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@@ -2,9 +2,14 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { IdentityAuthMethod } from "@app/db/schemas";
|
||||
import { TIdentityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template";
|
||||
import { testLDAPConfig } from "@app/ee/services/ldap-config/ldap-fns";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import {
|
||||
OrgPermissionIdentityActions,
|
||||
OrgPermissionMachineIdentityAuthTemplateActions,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/ee/services/permission/org-permission";
|
||||
import {
|
||||
constructPermissionErrorMessage,
|
||||
validatePrivilegeChangeOperation
|
||||
@@ -44,6 +49,7 @@ type TIdentityLdapAuthServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
kmsService: TKmsServiceFactory;
|
||||
identityDAL: TIdentityDALFactory;
|
||||
identityAuthTemplateDAL: TIdentityAuthTemplateDALFactory;
|
||||
};
|
||||
|
||||
export type TIdentityLdapAuthServiceFactory = ReturnType<typeof identityLdapAuthServiceFactory>;
|
||||
@@ -55,7 +61,8 @@ export const identityLdapAuthServiceFactory = ({
|
||||
identityOrgMembershipDAL,
|
||||
licenseService,
|
||||
permissionService,
|
||||
kmsService
|
||||
kmsService,
|
||||
identityAuthTemplateDAL
|
||||
}: TIdentityLdapAuthServiceFactoryDep) => {
|
||||
const getLdapConfig = async (identityId: string) => {
|
||||
const identity = await identityDAL.findOne({ id: identityId });
|
||||
@@ -173,6 +180,7 @@ export const identityLdapAuthServiceFactory = ({
|
||||
|
||||
const attachLdapAuth = async ({
|
||||
identityId,
|
||||
templateId,
|
||||
url,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
@@ -213,6 +221,14 @@ export const identityLdapAuthServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
|
||||
|
||||
if (templateId) {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
}
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
|
||||
if (!plan.ldap) {
|
||||
@@ -241,33 +257,55 @@ export const identityLdapAuthServiceFactory = ({
|
||||
if (allowedFields) AllowedFieldsSchema.array().parse(allowedFields);
|
||||
|
||||
const identityLdapAuth = await identityLdapAuthDAL.transaction(async (tx) => {
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
|
||||
const template = templateId
|
||||
? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.orgId)
|
||||
: undefined;
|
||||
|
||||
let ldapConfig: { bindDN: string; bindPass: string; searchBase: string; url: string; ldapCaCertificate?: string };
|
||||
if (template) {
|
||||
ldapConfig = JSON.parse(decryptor({ cipherTextBlob: template.templateFields }).toString());
|
||||
} else {
|
||||
if (!bindDN || !bindPass || !searchBase || !url) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid request. Missing bind DN, bind pass, search base, or URL."
|
||||
});
|
||||
}
|
||||
ldapConfig = {
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
url,
|
||||
ldapCaCertificate
|
||||
};
|
||||
}
|
||||
|
||||
const { cipherTextBlob: encryptedBindPass } = encryptor({
|
||||
plainText: Buffer.from(bindPass)
|
||||
plainText: Buffer.from(ldapConfig.bindPass)
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedBindDN } = encryptor({
|
||||
plainText: Buffer.from(ldapConfig.bindDN)
|
||||
});
|
||||
|
||||
let encryptedLdapCaCertificate: Buffer | undefined;
|
||||
if (ldapCaCertificate) {
|
||||
if (ldapConfig.ldapCaCertificate) {
|
||||
const { cipherTextBlob: encryptedCertificate } = encryptor({
|
||||
plainText: Buffer.from(ldapCaCertificate)
|
||||
plainText: Buffer.from(ldapConfig.ldapCaCertificate)
|
||||
});
|
||||
|
||||
encryptedLdapCaCertificate = encryptedCertificate;
|
||||
}
|
||||
|
||||
const { cipherTextBlob: encryptedBindDN } = encryptor({
|
||||
plainText: Buffer.from(bindDN)
|
||||
});
|
||||
|
||||
const isConnected = await testLDAPConfig({
|
||||
bindDN,
|
||||
bindPass,
|
||||
caCert: ldapCaCertificate || "",
|
||||
url
|
||||
bindDN: ldapConfig.bindDN,
|
||||
bindPass: ldapConfig.bindPass,
|
||||
caCert: ldapConfig.ldapCaCertificate || "",
|
||||
url: ldapConfig.url
|
||||
});
|
||||
|
||||
if (!isConnected) {
|
||||
@@ -282,15 +320,16 @@ export const identityLdapAuthServiceFactory = ({
|
||||
identityId: identityMembershipOrg.identityId,
|
||||
encryptedBindDN,
|
||||
encryptedBindPass,
|
||||
searchBase,
|
||||
searchBase: ldapConfig.searchBase,
|
||||
searchFilter,
|
||||
url,
|
||||
url: ldapConfig.url,
|
||||
encryptedLdapCaCertificate,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps),
|
||||
allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined
|
||||
allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined,
|
||||
templateId
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -301,6 +340,7 @@ export const identityLdapAuthServiceFactory = ({
|
||||
|
||||
const updateLdapAuth = async ({
|
||||
identityId,
|
||||
templateId,
|
||||
url,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
@@ -344,6 +384,13 @@ export const identityLdapAuthServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
if (templateId) {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
}
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
|
||||
if (!plan.ldap) {
|
||||
@@ -371,33 +418,56 @@ export const identityLdapAuthServiceFactory = ({
|
||||
|
||||
if (allowedFields) AllowedFieldsSchema.array().parse(allowedFields);
|
||||
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
|
||||
const template = templateId
|
||||
? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.orgId)
|
||||
: undefined;
|
||||
let config: {
|
||||
bindDN?: string;
|
||||
bindPass?: string;
|
||||
searchBase?: string;
|
||||
url?: string;
|
||||
ldapCaCertificate?: string;
|
||||
};
|
||||
|
||||
if (template) {
|
||||
config = JSON.parse(decryptor({ cipherTextBlob: template.templateFields }).toString());
|
||||
} else {
|
||||
config = {
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
url,
|
||||
ldapCaCertificate
|
||||
};
|
||||
}
|
||||
|
||||
let encryptedBindPass: Buffer | undefined;
|
||||
if (bindPass) {
|
||||
if (config.bindPass) {
|
||||
const { cipherTextBlob: bindPassCiphertext } = encryptor({
|
||||
plainText: Buffer.from(bindPass)
|
||||
plainText: Buffer.from(config.bindPass)
|
||||
});
|
||||
|
||||
encryptedBindPass = bindPassCiphertext;
|
||||
}
|
||||
|
||||
let encryptedLdapCaCertificate: Buffer | undefined;
|
||||
if (ldapCaCertificate) {
|
||||
if (config.ldapCaCertificate) {
|
||||
const { cipherTextBlob: ldapCaCertificateCiphertext } = encryptor({
|
||||
plainText: Buffer.from(ldapCaCertificate)
|
||||
plainText: Buffer.from(config.ldapCaCertificate)
|
||||
});
|
||||
|
||||
encryptedLdapCaCertificate = ldapCaCertificateCiphertext;
|
||||
}
|
||||
|
||||
let encryptedBindDN: Buffer | undefined;
|
||||
if (bindDN) {
|
||||
if (config.bindDN) {
|
||||
const { cipherTextBlob: bindDNCiphertext } = encryptor({
|
||||
plainText: Buffer.from(bindDN)
|
||||
plainText: Buffer.from(config.bindDN)
|
||||
});
|
||||
|
||||
encryptedBindDN = bindDNCiphertext;
|
||||
@@ -406,10 +476,10 @@ export const identityLdapAuthServiceFactory = ({
|
||||
const { ldapConfig } = await getLdapConfig(identityId);
|
||||
|
||||
const isConnected = await testLDAPConfig({
|
||||
bindDN: bindDN || ldapConfig.bindDN,
|
||||
bindPass: bindPass || ldapConfig.bindPass,
|
||||
caCert: ldapCaCertificate || ldapConfig.caCert,
|
||||
url: url || ldapConfig.url
|
||||
bindDN: config.bindDN || ldapConfig.bindDN,
|
||||
bindPass: config.bindPass || ldapConfig.bindPass,
|
||||
caCert: config.ldapCaCertificate || ldapConfig.caCert,
|
||||
url: config.url || ldapConfig.url
|
||||
});
|
||||
|
||||
if (!isConnected) {
|
||||
@@ -420,14 +490,15 @@ export const identityLdapAuthServiceFactory = ({
|
||||
}
|
||||
|
||||
const updatedLdapAuth = await identityLdapAuthDAL.updateById(identityLdapAuth.id, {
|
||||
url,
|
||||
searchBase,
|
||||
url: config.url,
|
||||
searchBase: config.searchBase,
|
||||
searchFilter,
|
||||
encryptedBindDN,
|
||||
encryptedBindPass,
|
||||
encryptedLdapCaCertificate,
|
||||
allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined,
|
||||
accessTokenMaxTTL,
|
||||
templateId: template?.id || null,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
||||
|
@@ -14,11 +14,12 @@ export type TAllowedFields = z.infer<typeof AllowedFieldsSchema>;
|
||||
|
||||
export type TAttachLdapAuthDTO = {
|
||||
identityId: string;
|
||||
url: string;
|
||||
searchBase: string;
|
||||
templateId?: string;
|
||||
url?: string;
|
||||
searchBase?: string;
|
||||
searchFilter: string;
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
bindDN?: string;
|
||||
bindPass?: string;
|
||||
ldapCaCertificate?: string;
|
||||
allowedFields?: TAllowedFields[];
|
||||
accessTokenTTL: number;
|
||||
@@ -30,6 +31,7 @@ export type TAttachLdapAuthDTO = {
|
||||
|
||||
export type TUpdateLdapAuthDTO = {
|
||||
identityId: string;
|
||||
templateId?: string;
|
||||
url?: string;
|
||||
searchBase?: string;
|
||||
searchFilter?: string;
|
||||
|
@@ -322,6 +322,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"documentation/platform/identities/auth-templates",
|
||||
"documentation/platform/token",
|
||||
"documentation/platform/mfa",
|
||||
"documentation/platform/github-org-sync"
|
||||
|
96
docs/documentation/platform/identities/auth-templates.mdx
Normal file
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: "Machine Identity Auth Templates"
|
||||
description: "Learn how to use auth templates to standardize authentication configurations for machine identities."
|
||||
---
|
||||
|
||||
## Concept
|
||||
|
||||
Machine Identity Auth Templates allow you to create reusable authentication configurations that can be applied across multiple machine identities. This feature helps standardize authentication setups, reduces configuration drift, and simplifies identity management at scale.
|
||||
|
||||
Instead of manually configuring authentication settings for each identity, you can create templates with predefined authentication parameters and apply them to multiple identities. This ensures consistency and reduces the likelihood of configuration errors.
|
||||
|
||||
Key Benefits:
|
||||
|
||||
- **Standardization**: Ensure consistent authentication configurations across identities
|
||||
- **Efficiency**: Reduce time spent configuring individual identities
|
||||
- **Governance**: Centrally manage and update authentication parameters
|
||||
- **Scalability**: Easily apply proven configurations to new identities
|
||||
|
||||
## Managing Auth Templates
|
||||
|
||||
Auth templates are managed in **Organization Settings > Access Control > Identities** under the **Identity Auth Templates** section.
|
||||
|
||||

|
||||
|
||||
### Creating a Template
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to Auth Templates">
|
||||
In your organization settings, go to **Access Control > Identities** and scroll down to the **Identity Auth Templates** section.
|
||||
</Step>
|
||||
|
||||
<Step title="Create a new template">
|
||||
Click **Create Template** to open the template creation modal.
|
||||
|
||||

|
||||
|
||||
Select the authentication method you want to create a template for (currently supports LDAP Auth).
|
||||
</Step>
|
||||
|
||||
<Step title="Configure template settings">
|
||||
Fill in the template configuration based on your chosen authentication method.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="LDAP Auth Template">
|
||||
**For LDAP Auth templates**, configure the following fields:
|
||||
|
||||

|
||||
|
||||
- **Template Name**: A descriptive name for your template
|
||||
- **URL**: The LDAP server to connect to such as `ldap://ldap.your-org.com`, `ldaps://ldap.myorg.com:636` _(for connection over SSL/TLS)_, etc.
|
||||
- **Bind DN**: The DN to bind to the LDAP server with.
|
||||
- **Bind Pass**: The password to bind to the LDAP server with.
|
||||
- **Search Base / DN**: Base DN under which to perform user search such as `ou=Users,dc=acme,dc=com`.
|
||||
- **CA Certificate**: The CA certificate to use when verifying the LDAP server certificate. This field is optional but recommended.
|
||||
|
||||
<Note>
|
||||
You can read more about LDAP Auth configuration in the [LDAP Auth documentation](/documentation/platform/identities/ldap-auth/general).
|
||||
</Note>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Using Templates
|
||||
|
||||
Once created, templates can be applied when configuring authentication methods for machine identities. When adding an auth method to an identity, you'll have the option to select from available templates or configure manually.
|
||||
|
||||

|
||||

|
||||
|
||||
### Managing Template Usage
|
||||
|
||||
You can view which identities are using a specific template by clicking **View Usages** in the template's dropdown menu.
|
||||
|
||||

|
||||

|
||||
|
||||
## FAQ
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Can I modify a template after it's been applied to identities?">
|
||||
Yes, you can edit existing templates. After editing a template, changes to templates will automatically update identities that are already using them.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="What happens if I delete a template that's in use?">
|
||||
If you delete a template that's currently being used by identities, those identities will continue to function with their existing configuration. However, the link to the template will be broken, and you won't be able to use the template for new identities.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Can I see which identities are using a specific template?">
|
||||
Yes, click **View Usages** in the template's dropdown menu to see all identities currently using that template.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Do templates support all authentication methods?">
|
||||
Currently, auth templates support LDAP Auth. Support for additional authentication methods will be added in future releases.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
@@ -5,6 +5,12 @@ description: "Learn how to authenticate with Infisical using LDAP."
|
||||
|
||||
**LDAP Auth** is an LDAP based authentication method that allows you to authenticate with Infisical using a machine identity configured with an [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol) directory.
|
||||
|
||||
## Templates
|
||||
|
||||
You can create reusable LDAP authentication templates to standardize configurations across multiple machine identities. Templates help ensure consistency, reduce configuration errors, and simplify identity management at scale.
|
||||
|
||||
To create and manage LDAP auth templates, see our [Machine Identity Auth Templates documentation](/documentation/platform/identities/auth-templates). Once you've created a template, you can apply it when configuring LDAP auth for your identities in the guide below.
|
||||
|
||||
## Guide
|
||||
<Steps>
|
||||
<Step title="Creating an identity">
|
||||
|
After Width: | Height: | Size: 491 KiB |
After Width: | Height: | Size: 680 KiB |
BIN
docs/images/platform/identities/auth-templates/ldap-template.png
Normal file
After Width: | Height: | Size: 487 KiB |
After Width: | Height: | Size: 660 KiB |
After Width: | Height: | Size: 192 KiB |
After Width: | Height: | Size: 688 KiB |
After Width: | Height: | Size: 680 KiB |
@@ -217,3 +217,14 @@ Supports conditions and permission inversion
|
||||
| `edit-gateways` | Modify existing gateway settings |
|
||||
| `delete-gateways` | Remove gateways from organization |
|
||||
| `attach-gateways` | Attach gateways to resources |
|
||||
|
||||
#### Subject: `machine-identity-auth-template`
|
||||
|
||||
| Action | Description |
|
||||
| ------------------ | ---------------------------------------------- |
|
||||
| `list-templates` | View identity auth templates |
|
||||
| `create-templates` | Create new identity auth templates |
|
||||
| `edit-templates` | Modify existing identity auth templates |
|
||||
| `delete-templates` | Remove identity auth templates |
|
||||
| `unlink-templates` | Unlink identity auth templates from identities |
|
||||
| `attach-templates` | Attach identity auth templates to identities |
|
||||
|
@@ -21,6 +21,15 @@ export enum OrgGatewayPermissionActions {
|
||||
AttachGateways = "attach-gateways"
|
||||
}
|
||||
|
||||
export enum OrgPermissionMachineIdentityAuthTemplateActions {
|
||||
ListTemplates = "list-templates",
|
||||
CreateTemplates = "create-templates",
|
||||
EditTemplates = "edit-templates",
|
||||
DeleteTemplates = "delete-templates",
|
||||
UnlinkTemplates = "unlink-templates",
|
||||
AttachTemplates = "attach-templates"
|
||||
}
|
||||
|
||||
export enum OrgPermissionSubjects {
|
||||
Workspace = "workspace",
|
||||
Role = "role",
|
||||
@@ -42,7 +51,8 @@ export enum OrgPermissionSubjects {
|
||||
Kmip = "kmip",
|
||||
Gateway = "gateway",
|
||||
SecretShare = "secret-share",
|
||||
GithubOrgSync = "github-org-sync"
|
||||
GithubOrgSync = "github-org-sync",
|
||||
MachineIdentityAuthTemplate = "machine-identity-auth-template"
|
||||
}
|
||||
|
||||
export enum OrgPermissionAdminConsoleAction {
|
||||
@@ -113,6 +123,10 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections]
|
||||
| [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity]
|
||||
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
|
||||
| [
|
||||
OrgPermissionMachineIdentityAuthTemplateActions,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
]
|
||||
| [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway]
|
||||
| [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare];
|
||||
// TODO(scott): add back once org UI refactored
|
||||
|
@@ -1385,6 +1385,7 @@ export const useAddIdentityLdapAuth = () => {
|
||||
return useMutation<IdentityLdapAuth, object, AddIdentityLdapAuthDTO>({
|
||||
mutationFn: async ({
|
||||
identityId,
|
||||
templateId,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
@@ -1400,6 +1401,7 @@ export const useAddIdentityLdapAuth = () => {
|
||||
const { data } = await apiRequest.post<{ identityLdapAuth: IdentityLdapAuth }>(
|
||||
`/api/v1/auth/ldap-auth/identities/${identityId}`,
|
||||
{
|
||||
templateId,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
@@ -1432,6 +1434,7 @@ export const useUpdateIdentityLdapAuth = () => {
|
||||
return useMutation<IdentityLdapAuth, object, UpdateIdentityLdapAuthDTO>({
|
||||
mutationFn: async ({
|
||||
identityId,
|
||||
templateId,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
@@ -1447,6 +1450,7 @@ export const useUpdateIdentityLdapAuth = () => {
|
||||
const { data } = await apiRequest.patch<{ identityLdapAuth: IdentityLdapAuth }>(
|
||||
`/api/v1/auth/ldap-auth/identities/${identityId}`,
|
||||
{
|
||||
templateId,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
|
@@ -567,10 +567,11 @@ export type IdentityTokenAuth = {
|
||||
export type AddIdentityLdapAuthDTO = {
|
||||
organizationId: string;
|
||||
identityId: string;
|
||||
url: string;
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
searchBase: string;
|
||||
templateId?: string;
|
||||
url?: string;
|
||||
bindDN?: string;
|
||||
bindPass?: string;
|
||||
searchBase?: string;
|
||||
searchFilter: string;
|
||||
ldapCaCertificate?: string;
|
||||
allowedFields?: {
|
||||
@@ -588,6 +589,7 @@ export type AddIdentityLdapAuthDTO = {
|
||||
export type UpdateIdentityLdapAuthDTO = {
|
||||
identityId: string;
|
||||
organizationId: string;
|
||||
templateId?: string;
|
||||
url?: string;
|
||||
bindDN?: string;
|
||||
bindPass?: string;
|
||||
@@ -612,10 +614,11 @@ export type DeleteIdentityLdapAuthDTO = {
|
||||
};
|
||||
|
||||
export type IdentityLdapAuth = {
|
||||
url: string;
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
searchBase: string;
|
||||
url?: string;
|
||||
bindDN?: string;
|
||||
templateId?: string;
|
||||
bindPass?: string;
|
||||
searchBase?: string;
|
||||
searchFilter: string;
|
||||
ldapCaCertificate?: string;
|
||||
allowedFields?: {
|
||||
|
3
frontend/src/hooks/api/identityAuthTemplates/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./mutations";
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
97
frontend/src/hooks/api/identityAuthTemplates/mutations.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { identityAuthTemplatesKeys } from "./queries";
|
||||
import {
|
||||
CreateIdentityAuthTemplateDTO,
|
||||
DeleteIdentityAuthTemplateDTO,
|
||||
IdentityAuthTemplate,
|
||||
MachineAuthTemplateUsage,
|
||||
UnlinkTemplateUsageDTO,
|
||||
UpdateIdentityAuthTemplateDTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreateIdentityAuthTemplate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (dto: CreateIdentityAuthTemplateDTO) => {
|
||||
const { data } = await apiRequest.post<{ template: IdentityAuthTemplate }>(
|
||||
"/api/v1/identity-templates",
|
||||
dto
|
||||
);
|
||||
return data.template;
|
||||
},
|
||||
onSuccess: (_, { organizationId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identityAuthTemplatesKeys.getTemplates({ organizationId })
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateIdentityAuthTemplate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (dto: UpdateIdentityAuthTemplateDTO) => {
|
||||
const { data } = await apiRequest.patch<{ template: IdentityAuthTemplate }>(
|
||||
`/api/v1/identity-templates/${dto.templateId}`,
|
||||
dto
|
||||
);
|
||||
return data.template;
|
||||
},
|
||||
onSuccess: (_, { organizationId, templateId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identityAuthTemplatesKeys.getTemplates({ organizationId })
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identityAuthTemplatesKeys.getTemplate(templateId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteIdentityAuthTemplate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (dto: DeleteIdentityAuthTemplateDTO) => {
|
||||
await apiRequest.delete(`/api/v1/identity-templates/${dto.templateId}`, {
|
||||
params: { organizationId: dto.organizationId }
|
||||
});
|
||||
},
|
||||
onSuccess: (_, { organizationId, templateId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identityAuthTemplatesKeys.getTemplates({ organizationId })
|
||||
});
|
||||
queryClient.removeQueries({
|
||||
queryKey: identityAuthTemplatesKeys.getTemplate(templateId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUnlinkTemplateUsage = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (dto: UnlinkTemplateUsageDTO) => {
|
||||
const { data } = await apiRequest.post<MachineAuthTemplateUsage[]>(
|
||||
`/api/v1/identity-templates/${dto.templateId}/delete-usage`,
|
||||
{ identityIds: dto.identityIds },
|
||||
{ params: { organizationId: dto.organizationId } }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { templateId, organizationId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identityAuthTemplatesKeys.getTemplateUsages(templateId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identityAuthTemplatesKeys.getTemplates({ organizationId })
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
89
frontend/src/hooks/api/identityAuthTemplates/queries.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import {
|
||||
GetIdentityAuthTemplatesDTO,
|
||||
GetTemplateUsagesDTO,
|
||||
IdentityAuthTemplate,
|
||||
MachineAuthTemplateUsage,
|
||||
MachineIdentityAuthMethod
|
||||
} from "./types";
|
||||
|
||||
export const identityAuthTemplatesKeys = {
|
||||
all: ["identity-auth-templates"] as const,
|
||||
getTemplates: (dto: GetIdentityAuthTemplatesDTO) =>
|
||||
[...identityAuthTemplatesKeys.all, "list", dto] as const,
|
||||
getTemplate: (templateId: string) =>
|
||||
[...identityAuthTemplatesKeys.all, "single", templateId] as const,
|
||||
getAvailableTemplates: (authMethod: MachineIdentityAuthMethod) =>
|
||||
[...identityAuthTemplatesKeys.all, "available", authMethod] as const,
|
||||
getTemplateUsages: (templateId: string) =>
|
||||
[...identityAuthTemplatesKeys.all, "usages", templateId] as const
|
||||
};
|
||||
|
||||
export const useGetIdentityAuthTemplates = (dto: GetIdentityAuthTemplatesDTO) => {
|
||||
return useQuery({
|
||||
queryKey: identityAuthTemplatesKeys.getTemplates(dto),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
templates: IdentityAuthTemplate[];
|
||||
totalCount: number;
|
||||
}>("/api/v1/identity-templates/search", {
|
||||
params: {
|
||||
organizationId: dto.organizationId,
|
||||
limit: dto.limit || 50,
|
||||
offset: dto.offset || 0,
|
||||
...(dto.search && { search: dto.search })
|
||||
}
|
||||
});
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(dto.organizationId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIdentityAuthTemplate = (templateId: string, organizationId: string) => {
|
||||
return useQuery({
|
||||
queryKey: identityAuthTemplatesKeys.getTemplate(templateId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<IdentityAuthTemplate>(
|
||||
`/api/v1/identity-templates/${templateId}`,
|
||||
{
|
||||
params: { organizationId }
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(templateId) && Boolean(organizationId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetAvailableTemplates = (authMethod: MachineIdentityAuthMethod) => {
|
||||
return useQuery({
|
||||
queryKey: identityAuthTemplatesKeys.getAvailableTemplates(authMethod),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<IdentityAuthTemplate[]>("/api/v1/identity-templates", {
|
||||
params: { authMethod }
|
||||
});
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(authMethod)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetTemplateUsages = (dto: GetTemplateUsagesDTO) => {
|
||||
return useQuery({
|
||||
queryKey: identityAuthTemplatesKeys.getTemplateUsages(dto.templateId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<MachineAuthTemplateUsage[]>(
|
||||
`/api/v1/identity-templates/${dto.templateId}/usage`,
|
||||
{
|
||||
params: { organizationId: dto.organizationId }
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(dto.templateId) && Boolean(dto.organizationId)
|
||||
});
|
||||
};
|
78
frontend/src/hooks/api/identityAuthTemplates/types.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
export enum MachineIdentityAuthMethod {
|
||||
LDAP = "ldap"
|
||||
}
|
||||
|
||||
export interface LdapTemplateFields {
|
||||
url: string;
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
searchBase: string;
|
||||
ldapCaCertificate?: string;
|
||||
}
|
||||
|
||||
export interface IdentityAuthTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
authMethod: MachineIdentityAuthMethod;
|
||||
organizationId: string;
|
||||
templateFields: LdapTemplateFields;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateIdentityAuthTemplateDTO {
|
||||
organizationId: string;
|
||||
name: string;
|
||||
authMethod: MachineIdentityAuthMethod;
|
||||
templateFields: LdapTemplateFields;
|
||||
}
|
||||
|
||||
export interface UpdateIdentityAuthTemplateDTO {
|
||||
templateId: string;
|
||||
organizationId: string;
|
||||
name?: string;
|
||||
templateFields?: Partial<LdapTemplateFields>;
|
||||
}
|
||||
|
||||
export interface DeleteIdentityAuthTemplateDTO {
|
||||
templateId: string;
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
export interface GetIdentityAuthTemplatesDTO {
|
||||
organizationId: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface MachineAuthTemplateUsage {
|
||||
identityId: string;
|
||||
identityName: string;
|
||||
}
|
||||
|
||||
export interface GetTemplateUsagesDTO {
|
||||
templateId: string;
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
export interface UnlinkTemplateUsageDTO {
|
||||
templateId: string;
|
||||
identityIds: string[];
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
export const TEMPLATE_ERROR_MESSAGES = {
|
||||
UNLINK_SUCCESS: "Successfully unlinked template usages",
|
||||
UNLINK_FAILED: "Failed to unlink template usages",
|
||||
SINGLE_UNLINK_SUCCESS: "Successfully unlinked template usage",
|
||||
SINGLE_UNLINK_FAILED: "Failed to unlink template usage"
|
||||
} as const;
|
||||
|
||||
export const TEMPLATE_UI_LABELS = {
|
||||
VIEW_USAGES: "View Usages",
|
||||
EDIT_TEMPLATE: "Edit Template",
|
||||
DELETE_TEMPLATE: "Delete Template",
|
||||
UNLINK: "Unlink",
|
||||
UNSELECT_ALL: "Unselect All"
|
||||
} as const;
|
@@ -15,6 +15,7 @@ export * from "./gateways";
|
||||
export * from "./githubOrgSyncConfig";
|
||||
export * from "./groups";
|
||||
export * from "./identities";
|
||||
export * from "./identityAuthTemplates";
|
||||
export * from "./identityProjectAdditionalPrivilege";
|
||||
export * from "./incidentContacts";
|
||||
export * from "./integrationAuth";
|
||||
|
@@ -53,4 +53,5 @@ export type SubscriptionPlan = {
|
||||
secretScanning: boolean;
|
||||
enterpriseSecretSyncs: boolean;
|
||||
enterpriseAppConnections: boolean;
|
||||
machineIdentityAuthTemplates: boolean;
|
||||
};
|
||||
|
@@ -0,0 +1,317 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import {
|
||||
MachineIdentityAuthMethod,
|
||||
useCreateIdentityAuthTemplate,
|
||||
useUpdateIdentityAuthTemplate
|
||||
} from "@app/hooks/api/identityAuthTemplates";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const authMethods = [{ label: "LDAP Auth", value: MachineIdentityAuthMethod.LDAP }];
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(1, "Template name is required"),
|
||||
method: z.nativeEnum(MachineIdentityAuthMethod),
|
||||
url: z.string().min(1, "LDAP URL is required"),
|
||||
bindDN: z.string().min(1, "Bind DN is required"),
|
||||
bindPass: z.string().min(1, "Bind Pass is required"),
|
||||
searchBase: z.string().min(1, "Search Base / DN is required"),
|
||||
ldapCaCertificate: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => val || undefined)
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["createTemplate", "editTemplate"]>;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["createTemplate", "editTemplate"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
|
||||
const { mutateAsync: createTemplate } = useCreateIdentityAuthTemplate();
|
||||
const { mutateAsync: updateTemplate } = useUpdateIdentityAuthTemplate();
|
||||
|
||||
const isEdit = popUp.editTemplate.isOpen;
|
||||
const template = popUp.editTemplate?.data?.template;
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
method: MachineIdentityAuthMethod.LDAP,
|
||||
url: "",
|
||||
bindDN: "",
|
||||
bindPass: "",
|
||||
searchBase: "",
|
||||
ldapCaCertificate: ""
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit && template) {
|
||||
reset({
|
||||
name: template.name || "",
|
||||
method: MachineIdentityAuthMethod.LDAP,
|
||||
url: template.templateFields?.url || "",
|
||||
bindDN: template.templateFields?.bindDN || "",
|
||||
bindPass: template.templateFields?.bindPass || "",
|
||||
searchBase: template.templateFields?.searchBase || "",
|
||||
ldapCaCertificate: template.templateFields?.ldapCaCertificate || ""
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
name: "",
|
||||
method: MachineIdentityAuthMethod.LDAP,
|
||||
url: "",
|
||||
bindDN: "",
|
||||
bindPass: "",
|
||||
searchBase: "",
|
||||
ldapCaCertificate: ""
|
||||
});
|
||||
}
|
||||
}, [isEdit, template, reset]);
|
||||
|
||||
const selectedMethod = watch("method");
|
||||
|
||||
const onFormSubmit = async (data: FormData) => {
|
||||
try {
|
||||
if (isEdit && template) {
|
||||
await updateTemplate({
|
||||
templateId: template.id,
|
||||
organizationId: orgId,
|
||||
name: data.name,
|
||||
templateFields: {
|
||||
url: data.url,
|
||||
bindDN: data.bindDN,
|
||||
bindPass: data.bindPass,
|
||||
searchBase: data.searchBase,
|
||||
ldapCaCertificate: data.ldapCaCertificate
|
||||
}
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully updated auth template",
|
||||
type: "success"
|
||||
});
|
||||
} else {
|
||||
await createTemplate({
|
||||
organizationId: orgId,
|
||||
name: data.name,
|
||||
authMethod: data.method,
|
||||
templateFields: {
|
||||
url: data.url,
|
||||
bindDN: data.bindDN,
|
||||
bindPass: data.bindPass,
|
||||
searchBase: data.searchBase,
|
||||
ldapCaCertificate: data.ldapCaCertificate
|
||||
}
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully created auth template",
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
|
||||
handlePopUpToggle(isEdit ? "editTemplate" : "createTemplate", false);
|
||||
reset();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = err as any;
|
||||
const text =
|
||||
error?.response?.data?.message ?? `Failed to ${isEdit ? "update" : "create"} auth template`;
|
||||
|
||||
createNotification({
|
||||
text,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
handlePopUpToggle(isEdit ? "editTemplate" : "createTemplate", false);
|
||||
reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp.createTemplate.isOpen || popUp.editTemplate.isOpen}
|
||||
onOpenChange={handleClose}
|
||||
>
|
||||
<ModalContent
|
||||
title={isEdit ? "Edit Identity Auth Template" : "Create Identity Auth Template"}
|
||||
subTitle={
|
||||
isEdit ? "Update the authentication template" : "Create a new authentication template"
|
||||
}
|
||||
>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Template Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="My Template" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="method"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Authentication Method"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
{...field}
|
||||
className="w-full"
|
||||
position="popper"
|
||||
placeholder="Select auth method..."
|
||||
dropdownContainerClassName="max-w-none"
|
||||
onValueChange={(value) => field.onChange(value)}
|
||||
>
|
||||
{authMethods.map(({ label, value }) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* LDAP Configuration Fields */}
|
||||
{selectedMethod === "ldap" && (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name="url"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="LDAP URL"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="ldaps://domain-or-ip:636" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="bindDN"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Bind DN"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="cn=infisical,ou=Users,dc=example,dc=com" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="bindPass"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Bind Pass"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="********" type="password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="searchBase"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Search Base / DN"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="ou=machines,dc=acme,dc=com" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="ldapCaCertificate"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="CA Certificate"
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
tooltipText="An optional PEM-encoded CA cert for the LDAP server. This is used by the TLS client for secure communication with the LDAP server."
|
||||
>
|
||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{isEdit ? "Update Template" : "Create Template"}
|
||||
</Button>
|
||||
<Button colorSchema="secondary" variant="plain" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@@ -0,0 +1,281 @@
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faEye,
|
||||
faMagnifyingGlass,
|
||||
faServer,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Spinner,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { OrgPermissionMachineIdentityAuthTemplateActions } from "@app/context/OrgPermissionContext/types";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import {
|
||||
TEMPLATE_UI_LABELS,
|
||||
useGetIdentityAuthTemplates
|
||||
} from "@app/hooks/api/identityAuthTemplates";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
enum TemplatesOrderBy {
|
||||
Name = "name",
|
||||
AuthMethod = "authMethod"
|
||||
}
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<
|
||||
["deleteTemplate", "createTemplate", "editTemplate", "viewUsages"]
|
||||
>,
|
||||
data?: any
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const IdentityAuthTemplatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const {
|
||||
offset,
|
||||
limit,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
orderDirection,
|
||||
setOrderDirection,
|
||||
search,
|
||||
debouncedSearch,
|
||||
setPage,
|
||||
setSearch,
|
||||
perPage,
|
||||
page,
|
||||
setPerPage
|
||||
} = usePagination<TemplatesOrderBy>(TemplatesOrderBy.Name, {
|
||||
initPerPage: getUserTablePreference("templatesTable", PreferenceKey.PerPage, 20)
|
||||
});
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
setPerPage(newPerPage);
|
||||
setUserTablePreference("templatesTable", PreferenceKey.PerPage, newPerPage);
|
||||
};
|
||||
|
||||
const organizationId = currentOrg?.id || "";
|
||||
|
||||
const { data, isPending, isFetching } = useGetIdentityAuthTemplates({
|
||||
organizationId,
|
||||
limit,
|
||||
offset,
|
||||
search: debouncedSearch
|
||||
});
|
||||
|
||||
const { templates = [], totalCount = 0 } = data ?? {};
|
||||
useResetPageHelper({
|
||||
totalCount,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
const handleSort = (column: TemplatesOrderBy) => {
|
||||
if (column === orderBy) {
|
||||
setOrderDirection((prev) =>
|
||||
prev === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setOrderBy(column);
|
||||
setOrderDirection(OrderByDirection.ASC);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4 flex items-center space-x-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search templates by name..."
|
||||
/>
|
||||
</div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr className="h-14">
|
||||
<Th className="w-1/6">
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={`ml-2 ${orderBy === TemplatesOrderBy.Name ? "" : "opacity-30"}`}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(TemplatesOrderBy.Name)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
orderDirection === OrderByDirection.DESC &&
|
||||
orderBy === TemplatesOrderBy.Name
|
||||
? faArrowUp
|
||||
: faArrowDown
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/6">
|
||||
<div className="flex items-center">
|
||||
Method
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={`ml-2 ${orderBy === TemplatesOrderBy.AuthMethod ? "" : "opacity-30"}`}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(TemplatesOrderBy.AuthMethod)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
orderDirection === OrderByDirection.DESC &&
|
||||
orderBy === TemplatesOrderBy.AuthMethod
|
||||
? faArrowUp
|
||||
: faArrowDown
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-2/3">URL</Th>
|
||||
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={4} innerKey="identity-auth-templates" />}
|
||||
{!isPending &&
|
||||
templates?.map((template) => (
|
||||
<Tr
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
key={`template-${template.id}`}
|
||||
>
|
||||
<Td>{template.name}</Td>
|
||||
<Td>
|
||||
<div className="flex items-center">
|
||||
<span className="uppercase">{template.authMethod}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<span className="text-sm text-mineshaft-400">
|
||||
{template.templateFields.url}
|
||||
</span>
|
||||
</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
className="w-6"
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faEye} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("viewUsages", { template });
|
||||
}}
|
||||
>
|
||||
{TEMPLATE_UI_LABELS.VIEW_USAGES}
|
||||
</DropdownMenuItem>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates}
|
||||
a={OrgPermissionSubjects.MachineIdentityAuthTemplate}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("editTemplate", { template });
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
{TEMPLATE_UI_LABELS.EDIT_TEMPLATE}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates}
|
||||
a={OrgPermissionSubjects.MachineIdentityAuthTemplate}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteTemplate", {
|
||||
templateId: template.id,
|
||||
name: template.name
|
||||
});
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
{TEMPLATE_UI_LABELS.DELETE_TEMPLATE}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isPending && data && totalCount > 0 && (
|
||||
<Pagination
|
||||
count={totalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
{!isPending && data && templates.length === 0 && (
|
||||
<EmptyState
|
||||
title={
|
||||
debouncedSearch.trim().length > 0
|
||||
? "No templates match search filter"
|
||||
: "No identity auth templates have been created"
|
||||
}
|
||||
icon={faServer}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -11,6 +11,8 @@ import {
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
@@ -18,23 +20,31 @@ import {
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import { useOrganization, useOrgPermission, useSubscription } from "@app/context";
|
||||
import {
|
||||
OrgPermissionMachineIdentityAuthTemplateActions,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import {
|
||||
MachineIdentityAuthMethod,
|
||||
useAddIdentityLdapAuth,
|
||||
useGetIdentityLdapAuth,
|
||||
useUpdateIdentityLdapAuth
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||
import { useGetAvailableTemplates } from "@app/hooks/api/identityAuthTemplates/queries";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { IdentityFormTab } from "./types";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
url: z.string().min(1),
|
||||
bindDN: z.string(),
|
||||
bindPass: z.string(),
|
||||
searchBase: z.string(),
|
||||
scope: z.enum(["template", "custom"]),
|
||||
templateId: z.string().optional(),
|
||||
url: z.string().optional(),
|
||||
bindDN: z.string().optional(),
|
||||
bindPass: z.string().optional(),
|
||||
searchBase: z.string().optional(),
|
||||
searchFilter: z.string(), // defaults to (uid={{username}})
|
||||
ldapCaCertificate: z
|
||||
.string()
|
||||
@@ -66,7 +76,50 @@ const schema = z
|
||||
)
|
||||
.min(1)
|
||||
})
|
||||
.required();
|
||||
.superRefine((data, ctx) => {
|
||||
// Validation based on scope
|
||||
if (data.scope === "template") {
|
||||
if (!data.templateId) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Template is required when using template scope",
|
||||
path: ["templateId"]
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.scope === "custom") {
|
||||
if (!data.url) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "LDAP URL is required when using custom scope",
|
||||
path: ["url"]
|
||||
});
|
||||
}
|
||||
if (!data.bindDN) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Bind DN is required when using custom scope",
|
||||
path: ["bindDN"]
|
||||
});
|
||||
}
|
||||
if (!data.bindPass) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Bind Pass is required when using custom scope",
|
||||
path: ["bindPass"]
|
||||
});
|
||||
}
|
||||
if (!data.searchBase) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Search Base is required when using custom scope",
|
||||
path: ["searchBase"]
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
@@ -93,6 +146,13 @@ export const IdentityLdapAuthForm = ({
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityLdapAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityLdapAuth();
|
||||
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
||||
const { data: templates } = useGetAvailableTemplates(MachineIdentityAuthMethod.LDAP);
|
||||
const { permission } = useOrgPermission();
|
||||
|
||||
const canAttachTemplates = permission.can(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
|
||||
const { data } = useGetIdentityLdapAuth(identityId ?? "", {
|
||||
enabled: isUpdate
|
||||
@@ -102,11 +162,14 @@ export const IdentityLdapAuthForm = ({
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
|
||||
watch,
|
||||
setValue,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
scope: "custom",
|
||||
templateId: "",
|
||||
url: "",
|
||||
bindDN: "",
|
||||
bindPass: "",
|
||||
@@ -119,6 +182,8 @@ export const IdentityLdapAuthForm = ({
|
||||
}
|
||||
});
|
||||
|
||||
const scope = watch("scope");
|
||||
|
||||
const {
|
||||
fields: accessTokenTrustedIpsFields,
|
||||
append: appendAccessTokenTrustedIp,
|
||||
@@ -131,16 +196,30 @@ export const IdentityLdapAuthForm = ({
|
||||
remove: removeAllowedField
|
||||
} = useFieldArray({ control, name: "allowedFields" });
|
||||
|
||||
// Helper function to determine scope based on existing data
|
||||
const determineScope = (authData: any) => {
|
||||
// If templateId exists in the data, it's template scope
|
||||
if (authData.templateId) {
|
||||
return "template";
|
||||
}
|
||||
// Default to custom if we can't determine
|
||||
return "custom";
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const detectedScope = determineScope(data);
|
||||
|
||||
reset({
|
||||
url: data.url,
|
||||
bindDN: data.bindDN,
|
||||
bindPass: data.bindPass,
|
||||
searchBase: data.searchBase,
|
||||
scope: detectedScope,
|
||||
templateId: data.templateId || "",
|
||||
url: data.url || "",
|
||||
bindDN: data.bindDN || "",
|
||||
bindPass: data.bindPass || "",
|
||||
searchBase: data.searchBase || "",
|
||||
searchFilter: data.searchFilter,
|
||||
ldapCaCertificate: data.ldapCaCertificate || undefined,
|
||||
allowedFields: data.allowedFields,
|
||||
allowedFields: data.allowedFields || [],
|
||||
accessTokenTTL: String(data.accessTokenTTL),
|
||||
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
|
||||
@@ -152,78 +231,81 @@ export const IdentityLdapAuthForm = ({
|
||||
}
|
||||
)
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
url: "",
|
||||
bindDN: "",
|
||||
bindPass: "",
|
||||
searchBase: "",
|
||||
searchFilter: "(uid={{username}})",
|
||||
ldapCaCertificate: undefined,
|
||||
allowedFields: [],
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
});
|
||||
return;
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
reset({
|
||||
scope: "custom",
|
||||
templateId: "",
|
||||
url: "",
|
||||
bindDN: "",
|
||||
bindPass: "",
|
||||
searchBase: "",
|
||||
searchFilter: "(uid={{username}})",
|
||||
ldapCaCertificate: undefined,
|
||||
allowedFields: [],
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
});
|
||||
}, [data, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!subscription?.ldap) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
}
|
||||
}, [subscription]);
|
||||
}, [subscription, handlePopUpOpen, handlePopUpToggle]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
ldapCaCertificate,
|
||||
allowedFields,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}: FormData) => {
|
||||
const onFormSubmit = async (formData: FormData) => {
|
||||
try {
|
||||
if (!identityId) return;
|
||||
|
||||
const {
|
||||
scope: submissionScope,
|
||||
templateId: submissionTemplateId,
|
||||
url: submissionUrl,
|
||||
bindDN: submissionBindDN,
|
||||
bindPass: submissionBindPass,
|
||||
searchBase: submissionSearchBase,
|
||||
searchFilter,
|
||||
ldapCaCertificate,
|
||||
allowedFields,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
} = formData;
|
||||
|
||||
const basePayload = {
|
||||
organizationId: orgId,
|
||||
identityId,
|
||||
searchFilter,
|
||||
ldapCaCertificate,
|
||||
allowedFields,
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps
|
||||
};
|
||||
|
||||
// Add scope-specific fields
|
||||
const payload =
|
||||
submissionScope === "template"
|
||||
? { ...basePayload, templateId: submissionTemplateId }
|
||||
: {
|
||||
...basePayload,
|
||||
url: submissionUrl,
|
||||
bindDN: submissionBindDN,
|
||||
bindPass: submissionBindPass,
|
||||
searchBase: submissionSearchBase
|
||||
};
|
||||
|
||||
if (data) {
|
||||
await updateMutateAsync({
|
||||
organizationId: orgId,
|
||||
identityId,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
ldapCaCertificate,
|
||||
allowedFields,
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps
|
||||
});
|
||||
await updateMutateAsync(payload);
|
||||
} else {
|
||||
await addMutateAsync({
|
||||
organizationId: orgId,
|
||||
identityId,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
ldapCaCertificate,
|
||||
allowedFields,
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps
|
||||
});
|
||||
await addMutateAsync(payload);
|
||||
}
|
||||
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
@@ -247,6 +329,8 @@ export const IdentityLdapAuthForm = ({
|
||||
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
||||
setTabValue(
|
||||
[
|
||||
"scope",
|
||||
"templateId",
|
||||
"url",
|
||||
"bindDN",
|
||||
"bindPass",
|
||||
@@ -268,18 +352,102 @@ export const IdentityLdapAuthForm = ({
|
||||
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={IdentityFormTab.Configuration}>
|
||||
{canAttachTemplates && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="scope"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Configuration Type"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => {
|
||||
onChange(val);
|
||||
setValue("templateId", data?.templateId || "");
|
||||
setValue("url", data?.url || "");
|
||||
setValue("bindDN", data?.bindDN || "");
|
||||
setValue("bindPass", data?.bindPass || "");
|
||||
setValue("searchBase", data?.searchBase || "");
|
||||
setValue("ldapCaCertificate", data?.ldapCaCertificate || "");
|
||||
}}
|
||||
className="w-full"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
<SelectItem value="template">Use Template</SelectItem>
|
||||
<SelectItem value="custom">Custom Configuration</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{scope === "template" && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="templateId"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Template"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => {
|
||||
onChange(val);
|
||||
const tmp = templates?.find((t) => t.id === val);
|
||||
if (!tmp) return;
|
||||
setValue("url", tmp.templateFields.url);
|
||||
setValue("bindDN", tmp.templateFields.bindDN);
|
||||
setValue("bindPass", tmp.templateFields.bindPass);
|
||||
setValue("searchBase", tmp.templateFields.searchBase);
|
||||
setValue("ldapCaCertificate", tmp.templateFields.ldapCaCertificate);
|
||||
}}
|
||||
className="w-full"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
placeholder="Select a template"
|
||||
>
|
||||
{templates?.map((template) => {
|
||||
return (
|
||||
<SelectItem value={template.id} key={template.id}>
|
||||
{template.name}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="url"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="LDAP URL"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText={
|
||||
scope === "template"
|
||||
? "This field cannot be modified when using a template"
|
||||
: undefined
|
||||
}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="ldaps://domain-or-ip:636" type="text" />
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="ldaps://domain-or-ip:636"
|
||||
type="text"
|
||||
isDisabled={scope === "template"}
|
||||
containerClassName={scope === "template" ? "opacity-55" : ""}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -292,14 +460,23 @@ export const IdentityLdapAuthForm = ({
|
||||
label="Bind DN"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText={
|
||||
scope === "template"
|
||||
? "This field cannot be modified when using a template"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Input {...field} placeholder="cn=infisical,ou=Users,dc=example,dc=com" />
|
||||
<Input
|
||||
{...field}
|
||||
containerClassName={scope === "template" ? "opacity-55" : ""}
|
||||
placeholder="cn=infisical,ou=Users,dc=example,dc=com"
|
||||
isDisabled={scope === "template"}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="bindPass"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
@@ -307,8 +484,19 @@ export const IdentityLdapAuthForm = ({
|
||||
label="Bind Pass"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText={
|
||||
scope === "template"
|
||||
? "This field cannot be modified when using a template"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Input {...field} placeholder="********" type="password" />
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="********"
|
||||
type="password"
|
||||
containerClassName={scope === "template" ? "opacity-55" : ""}
|
||||
isDisabled={scope === "template"}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -321,8 +509,18 @@ export const IdentityLdapAuthForm = ({
|
||||
label="Search Base / DN"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText={
|
||||
scope === "template"
|
||||
? "This field cannot be modified when using a template"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Input {...field} placeholder="ou=machines,dc=acme,dc=com" />
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="ou=machines,dc=acme,dc=com"
|
||||
containerClassName={scope === "template" ? "opacity-55" : ""}
|
||||
isDisabled={scope === "template"}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -452,7 +650,6 @@ export const IdentityLdapAuthForm = ({
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
@@ -467,7 +664,6 @@ export const IdentityLdapAuthForm = ({
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
@@ -482,7 +678,6 @@ export const IdentityLdapAuthForm = ({
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
@@ -506,9 +701,18 @@ export const IdentityLdapAuthForm = ({
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
tooltipText="An optional PEM-encoded CA cert for the LDAP server. This is used by the TLS client for secure communication with the LDAP server."
|
||||
tooltipText={
|
||||
scope === "template"
|
||||
? "This field cannot be modified when using a template"
|
||||
: "An optional PEM-encoded CA cert for the LDAP server. This is used by the TLS client for secure communication with the LDAP server."
|
||||
}
|
||||
>
|
||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||
<TextArea
|
||||
{...field}
|
||||
placeholder="-----BEGIN CERTIFICATE----- ..."
|
||||
className={scope === "template" ? "opacity-55" : ""}
|
||||
isDisabled={scope === "template"}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
@@ -11,15 +11,18 @@ import {
|
||||
useOrganization,
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import { OrgPermissionMachineIdentityAuthTemplateActions } from "@app/context/OrgPermissionContext/types";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { useDeleteIdentity } from "@app/hooks/api";
|
||||
import { useDeleteIdentityAuthTemplate } from "@app/hooks/api/identityAuthTemplates";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
// import { IdentityAuthMethodModal } from "./IdentityAuthMethodModal";
|
||||
import { IdentityAuthTemplateModal } from "./IdentityAuthTemplateModal";
|
||||
import { IdentityAuthTemplatesTable } from "./IdentityAuthTemplatesTable";
|
||||
import { IdentityModal } from "./IdentityModal";
|
||||
import { IdentityTable } from "./IdentityTable";
|
||||
import { IdentityTokenAuthTokenModal } from "./IdentityTokenAuthTokenModal";
|
||||
// import { IdentityUniversalAuthClientSecretModal } from "./IdentityUniversalAuthClientSecretModal";
|
||||
import { MachineAuthTemplateUsagesModal } from "./MachineAuthTemplateUsagesModal";
|
||||
|
||||
export const IdentitySection = withPermission(
|
||||
() => {
|
||||
@@ -28,6 +31,7 @@ export const IdentitySection = withPermission(
|
||||
const orgId = currentOrg?.id || "";
|
||||
|
||||
const { mutateAsync: deleteMutateAsync } = useDeleteIdentity();
|
||||
const { mutateAsync: deleteTemplateMutateAsync } = useDeleteIdentityAuthTemplate();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"identity",
|
||||
"identityAuthMethod",
|
||||
@@ -35,7 +39,11 @@ export const IdentitySection = withPermission(
|
||||
"universalAuthClientSecret",
|
||||
"deleteUniversalAuthClientSecret",
|
||||
"upgradePlan",
|
||||
"tokenAuthToken"
|
||||
"tokenAuthToken",
|
||||
"createTemplate",
|
||||
"editTemplate",
|
||||
"deleteTemplate",
|
||||
"viewUsages"
|
||||
] as const);
|
||||
|
||||
const isMoreIdentitiesAllowed = subscription?.identityLimit
|
||||
@@ -69,53 +77,133 @@ export const IdentitySection = withPermission(
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteTemplateSubmit = async (templateId: string) => {
|
||||
try {
|
||||
await deleteTemplateMutateAsync({
|
||||
templateId,
|
||||
organizationId: orgId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted template",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteTemplate");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = err as any;
|
||||
const text = error?.response?.data?.message ?? "Failed to delete template";
|
||||
|
||||
createNotification({
|
||||
text,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/identities/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionIdentityActions.Create}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (!isMoreIdentitiesAllowed && !isEnterprise) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
description: "You can add more identities if you upgrade your Infisical plan."
|
||||
});
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("identity");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
<div>
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/identities/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Create Identity
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionIdentityActions.Create}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (!isMoreIdentitiesAllowed && !isEnterprise) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
description:
|
||||
"You can add more identities if you upgrade your Infisical plan."
|
||||
});
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("identity");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create Identity
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<IdentityTable handlePopUpOpen={handlePopUpOpen} />
|
||||
</div>
|
||||
{/* Identity Auth Templates Section */}
|
||||
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Identity Auth Templates</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/identities/auth-templates"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates}
|
||||
a={OrgPermissionSubjects.MachineIdentityAuthTemplate}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("createTemplate")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create Template
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<IdentityAuthTemplatesTable handlePopUpOpen={handlePopUpOpen} />
|
||||
</div>
|
||||
<IdentityTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<IdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<IdentityAuthTemplateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<MachineAuthTemplateUsagesModal
|
||||
isOpen={popUp.viewUsages.isOpen}
|
||||
onClose={() => handlePopUpClose("viewUsages")}
|
||||
templateId={
|
||||
(popUp?.viewUsages?.data as { template: { id: string; name: string } })?.template?.id ||
|
||||
""
|
||||
}
|
||||
templateName={
|
||||
(popUp?.viewUsages?.data as { template: { id: string; name: string } })?.template
|
||||
?.name || ""
|
||||
}
|
||||
/>
|
||||
{/* <IdentityAuthMethodModal
|
||||
popUp={popUp}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
@@ -140,6 +228,19 @@ export const IdentitySection = withPermission(
|
||||
)
|
||||
}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteTemplate.isOpen}
|
||||
title={`Are you sure you want to delete ${
|
||||
(popUp?.deleteTemplate?.data as { name: string })?.name || ""
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteTemplate", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onDeleteTemplateSubmit(
|
||||
(popUp?.deleteTemplate?.data as { templateId: string })?.templateId
|
||||
)
|
||||
}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
|
@@ -0,0 +1,90 @@
|
||||
import { faCertificate } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import {
|
||||
EmptyState,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useGetTemplateUsages } from "@app/hooks/api/identityAuthTemplates";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
templateId: string;
|
||||
templateName: string;
|
||||
};
|
||||
|
||||
export const MachineAuthTemplateUsagesModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
templateId,
|
||||
templateName
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const organizationId = currentOrg?.id || "";
|
||||
|
||||
const { data: usages = [], isPending } = useGetTemplateUsages({
|
||||
templateId,
|
||||
organizationId
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onClose}>
|
||||
<ModalContent title={`Usages for Identity Auth Template: ${templateName}`}>
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr className="h-14">
|
||||
<Th>Identity Name</Th>
|
||||
<Th>Identity ID</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={3} innerKey="template-usages" />}
|
||||
{!isPending &&
|
||||
usages.map((usage) => (
|
||||
<Tr
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
key={`usage-${usage.identityId}`}
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: "/organization/identities/$identityId",
|
||||
params: {
|
||||
identityId: usage.identityId
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Td>{usage.identityName}</Td>
|
||||
<Td>
|
||||
<span className="text-sm text-mineshaft-400">{usage.identityId}</span>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isPending && usages.length === 0 && (
|
||||
<EmptyState
|
||||
title="This template is not currently being used by any identities"
|
||||
icon={faCertificate}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@@ -9,6 +9,7 @@ import {
|
||||
OrgPermissionGroupActions,
|
||||
OrgPermissionIdentityActions,
|
||||
OrgPermissionKmipActions,
|
||||
OrgPermissionMachineIdentityAuthTemplateActions,
|
||||
OrgPermissionSecretShareAction
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { TPermission } from "@app/hooks/api/roles/types";
|
||||
@@ -82,6 +83,17 @@ const orgGatewayPermissionSchema = z
|
||||
})
|
||||
.optional();
|
||||
|
||||
const machineIdentityAuthTemplatePermissionSchema = z
|
||||
.object({
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates]: z.boolean().optional(),
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: z.boolean().optional(),
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: z.boolean().optional(),
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: z.boolean().optional(),
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: z.boolean().optional(),
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates]: z.boolean().optional()
|
||||
})
|
||||
.optional();
|
||||
|
||||
const adminConsolePermissionSchmea = z
|
||||
.object({
|
||||
"access-all-projects": z.boolean().optional()
|
||||
@@ -129,6 +141,7 @@ export const formSchema = z.object({
|
||||
"app-connections": appConnectionsPermissionSchema,
|
||||
kmip: kmipPermissionSchema,
|
||||
gateway: orgGatewayPermissionSchema,
|
||||
"machine-identity-auth-template": machineIdentityAuthTemplatePermissionSchema,
|
||||
"secret-share": secretSharingPermissionSchema
|
||||
})
|
||||
.optional()
|
||||
|
@@ -0,0 +1,211 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
|
||||
import { OrgPermissionMachineIdentityAuthTemplateActions } from "@app/context/OrgPermissionContext/types";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
import { TFormSchema } from "../OrgRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
isEditable: boolean;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
};
|
||||
|
||||
enum Permission {
|
||||
NoAccess = "no-access",
|
||||
ReadOnly = "read-only",
|
||||
FullAccess = "full-access",
|
||||
Custom = "custom"
|
||||
}
|
||||
|
||||
const PERMISSION_ACTIONS = [
|
||||
{
|
||||
action: OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
|
||||
label: "List Templates"
|
||||
},
|
||||
{
|
||||
action: OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates,
|
||||
label: "Create Templates"
|
||||
},
|
||||
{
|
||||
action: OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates,
|
||||
label: "Edit Templates"
|
||||
},
|
||||
{
|
||||
action: OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates,
|
||||
label: "Delete Templates"
|
||||
},
|
||||
{
|
||||
action: OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
|
||||
label: "Unlink Templates"
|
||||
},
|
||||
{
|
||||
action: OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
|
||||
label: "Attach Templates"
|
||||
}
|
||||
] as const;
|
||||
|
||||
export const OrgPermissionMachineIdentityAuthTemplateRow = ({
|
||||
isEditable,
|
||||
control,
|
||||
setValue
|
||||
}: Props) => {
|
||||
const [isRowExpanded, setIsRowExpanded] = useToggle();
|
||||
const [isCustom, setIsCustom] = useToggle();
|
||||
|
||||
const rule = useWatch({
|
||||
control,
|
||||
name: "permissions.machine-identity-auth-template"
|
||||
});
|
||||
|
||||
const selectedPermissionCategory = useMemo(() => {
|
||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||
const totalActions = PERMISSION_ACTIONS.length;
|
||||
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
|
||||
|
||||
if (isCustom) return Permission.Custom;
|
||||
if (score === 0) return Permission.NoAccess;
|
||||
if (score === totalActions) return Permission.FullAccess;
|
||||
if (score === 1 && rule?.[OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates])
|
||||
return Permission.ReadOnly;
|
||||
|
||||
return Permission.Custom;
|
||||
}, [rule, isCustom]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
}, [selectedPermissionCategory]);
|
||||
|
||||
useEffect(() => {
|
||||
const isRowCustom = selectedPermissionCategory === Permission.Custom;
|
||||
if (isRowCustom) {
|
||||
setIsRowExpanded.on();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
if (!val) return;
|
||||
if (val === Permission.Custom) {
|
||||
setIsRowExpanded.on();
|
||||
setIsCustom.on();
|
||||
return;
|
||||
}
|
||||
setIsCustom.off();
|
||||
|
||||
switch (val) {
|
||||
case Permission.FullAccess:
|
||||
setValue(
|
||||
"permissions.machine-identity-auth-template",
|
||||
{
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates]: true,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: true,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: true,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: true,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: true,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates]: true
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
case Permission.ReadOnly:
|
||||
setValue(
|
||||
"permissions.machine-identity-auth-template",
|
||||
{
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates]: true,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates]: true
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
|
||||
case Permission.NoAccess:
|
||||
default:
|
||||
setValue(
|
||||
"permissions.machine-identity-auth-template",
|
||||
{
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates]: false
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tr
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
onClick={() => setIsRowExpanded.toggle()}
|
||||
>
|
||||
<Td className="w-4">
|
||||
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
|
||||
</Td>
|
||||
<Td className="w-full select-none">Machine Identity Auth Templates</Td>
|
||||
<Td>
|
||||
<Select
|
||||
value={selectedPermissionCategory}
|
||||
className="h-8 w-40 bg-mineshaft-700"
|
||||
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={handlePermissionChange}
|
||||
isDisabled={!isEditable}
|
||||
position="popper"
|
||||
>
|
||||
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
||||
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
||||
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||
</Select>
|
||||
</Td>
|
||||
</Tr>
|
||||
{isRowExpanded && (
|
||||
<Tr>
|
||||
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
|
||||
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
|
||||
{PERMISSION_ACTIONS.map(({ action, label }) => {
|
||||
return (
|
||||
<Controller
|
||||
name={`permissions.machine-identity-auth-template.${action}`}
|
||||
key={`permissions.machine-identity-auth-template.${action}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
isChecked={Boolean(field.value)}
|
||||
onCheckedChange={(e) => {
|
||||
if (!isEditable) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to update default role"
|
||||
});
|
||||
return;
|
||||
}
|
||||
field.onChange(e);
|
||||
}}
|
||||
id={`permissions.machine-identity-auth-template.${action}`}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@@ -65,7 +65,13 @@ type Props = {
|
||||
title: string;
|
||||
formName: keyof Omit<
|
||||
Exclude<TFormSchema["permissions"], undefined>,
|
||||
"workspace" | "organization-admin-console" | "kmip" | "gateway" | "secret-share" | "billing"
|
||||
| "workspace"
|
||||
| "organization-admin-console"
|
||||
| "kmip"
|
||||
| "gateway"
|
||||
| "secret-share"
|
||||
| "billing"
|
||||
| "machine-identity-auth-template"
|
||||
>;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
|
@@ -21,6 +21,7 @@ import { OrgGatewayPermissionRow } from "./OrgPermissionGatewayRow";
|
||||
import { OrgPermissionGroupRow } from "./OrgPermissionGroupRow";
|
||||
import { OrgPermissionIdentityRow } from "./OrgPermissionIdentityRow";
|
||||
import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow";
|
||||
import { OrgPermissionMachineIdentityAuthTemplateRow } from "./OrgPermissionMachineIdentityAuthTemplateRow";
|
||||
import { OrgPermissionSecretShareRow } from "./OrgPermissionSecretShareRow";
|
||||
import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow";
|
||||
import { RolePermissionRow } from "./RolePermissionRow";
|
||||
@@ -205,6 +206,11 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
|
||||
setValue={setValue}
|
||||
isEditable={isCustomRole}
|
||||
/>
|
||||
<OrgPermissionMachineIdentityAuthTemplateRow
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
isEditable={isCustomRole}
|
||||
/>
|
||||
<OrgPermissionKmipRow
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
|
@@ -222,7 +222,7 @@ export const SecretItem = memo(
|
||||
}
|
||||
|
||||
if (isDirty && !isSubmitting && !isAutoSavingRef.current) {
|
||||
const debounceTime = 600;
|
||||
const debounceTime = 200;
|
||||
|
||||
autoSaveTimeoutRef.current = setTimeout(() => {
|
||||
autoSaveChanges(formValues);
|
||||
|
@@ -343,31 +343,18 @@ export const SecretListView = ({
|
||||
id: orgSecret.id,
|
||||
type: PendingAction.Update,
|
||||
secretKey: trueOriginalSecret.key,
|
||||
...(key !== trueOriginalSecret.key && { newSecretName: key }),
|
||||
...(value !== trueOriginalSecret.value && {
|
||||
originalValue: trueOriginalSecret.value,
|
||||
secretValue: value
|
||||
}),
|
||||
...(comment !== trueOriginalSecret.comment && {
|
||||
originalComment: trueOriginalSecret.comment,
|
||||
secretComment: comment
|
||||
}),
|
||||
...(modSecret.skipMultilineEncoding !==
|
||||
trueOriginalSecret.skipMultilineEncoding && {
|
||||
originalSkipMultilineEncoding: trueOriginalSecret.skipMultilineEncoding,
|
||||
skipMultilineEncoding: modSecret.skipMultilineEncoding
|
||||
}),
|
||||
...(!isSameTags && {
|
||||
originalTags:
|
||||
trueOriginalSecret.tags?.map((tag) => ({ id: tag.id, slug: tag.slug })) || [],
|
||||
tags: tags?.map((tag) => ({ id: tag.id, slug: tag.name || tag.slug || "" })) || []
|
||||
}),
|
||||
...(JSON.stringify(secretMetadata) !==
|
||||
JSON.stringify(trueOriginalSecret.secretMetadata) && {
|
||||
originalSecretMetadata: trueOriginalSecret.secretMetadata || [],
|
||||
secretMetadata: secretMetadata || []
|
||||
}),
|
||||
|
||||
newSecretName: key,
|
||||
originalValue: trueOriginalSecret.value,
|
||||
secretValue: value,
|
||||
originalComment: trueOriginalSecret.comment,
|
||||
secretComment: comment,
|
||||
originalSkipMultilineEncoding: trueOriginalSecret.skipMultilineEncoding,
|
||||
skipMultilineEncoding: modSecret.skipMultilineEncoding,
|
||||
originalTags:
|
||||
trueOriginalSecret.tags?.map((tag) => ({ id: tag.id, slug: tag.slug })) || [],
|
||||
tags: tags?.map((tag) => ({ id: tag.id, slug: tag.name || tag.slug || "" })) || [],
|
||||
originalSecretMetadata: trueOriginalSecret.secretMetadata || [],
|
||||
secretMetadata: secretMetadata || [],
|
||||
timestamp: Date.now(),
|
||||
resourceType: "secret",
|
||||
existingSecret: orgSecret
|
||||
|