mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 23:28:25 +00:00
General improvements on auth templates
This commit is contained in:
@@ -1352,6 +1352,7 @@ interface AddIdentityLdapAuthEvent {
|
|||||||
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||||
allowedFields?: TAllowedFields[];
|
allowedFields?: TAllowedFields[];
|
||||||
url: string;
|
url: string;
|
||||||
|
templateId?: string | null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1365,6 +1366,7 @@ interface UpdateIdentityLdapAuthEvent {
|
|||||||
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||||
allowedFields?: TAllowedFields[];
|
allowedFields?: TAllowedFields[];
|
||||||
url?: string;
|
url?: string;
|
||||||
|
templateId?: string | null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ export enum OrgPermissionMachineIdentityAuthTemplateActions {
|
|||||||
EditTemplates = "edit-templates",
|
EditTemplates = "edit-templates",
|
||||||
CreateTemplates = "create-templates",
|
CreateTemplates = "create-templates",
|
||||||
DeleteTemplates = "delete-templates",
|
DeleteTemplates = "delete-templates",
|
||||||
UnlinkTemplates = "unlink-templates"
|
UnlinkTemplates = "unlink-templates",
|
||||||
|
UseTemplates = "use-templates"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum OrgPermissionAdminConsoleAction {
|
export enum OrgPermissionAdminConsoleAction {
|
||||||
@@ -382,6 +383,7 @@ const buildAdminPermission = () => {
|
|||||||
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
|
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
|
||||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||||
);
|
);
|
||||||
|
can(OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
|
||||||
|
|
||||||
can(OrgPermissionSecretShareAction.ManageSettings, OrgPermissionSubjects.SecretShare);
|
can(OrgPermissionSecretShareAction.ManageSettings, OrgPermissionSubjects.SecretShare);
|
||||||
|
|
||||||
@@ -423,6 +425,7 @@ const buildMemberPermission = () => {
|
|||||||
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
|
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
|
||||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||||
);
|
);
|
||||||
|
can(OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -148,42 +148,6 @@ export const IDENTITIES = {
|
|||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const IDENTITY_TEMPLATES = {
|
|
||||||
CREATE: {
|
|
||||||
name: "The name of the identity template to create.",
|
|
||||||
organizationId: "The organization ID to which the identity template belongs.",
|
|
||||||
authMethod: "The auth method of the template.",
|
|
||||||
credentials: "Set of credentials to reuse by this template"
|
|
||||||
},
|
|
||||||
UPDATE: {
|
|
||||||
identityTemplateId: "The ID of the identity to update.",
|
|
||||||
name: "The new name of the identity template.",
|
|
||||||
authMethod: "The auth method of the template."
|
|
||||||
},
|
|
||||||
DELETE: {
|
|
||||||
identityTemplateId: "The ID of the identity template to delete."
|
|
||||||
},
|
|
||||||
GET_BY_ID: {
|
|
||||||
identityTemplateId: "The ID of the identity template to get details.",
|
|
||||||
organizationId: "The organization ID to which the identity template belongs.",
|
|
||||||
authMethod: "The auth method of the template.",
|
|
||||||
credentials: "Set of credentials used by this template"
|
|
||||||
},
|
|
||||||
LIST: {
|
|
||||||
orgId: "The ID of the organization to list identity templates."
|
|
||||||
},
|
|
||||||
SEARCH: {
|
|
||||||
search: {
|
|
||||||
desc: "The filters to apply to the search.",
|
|
||||||
name: "The name of the identity template to filter by."
|
|
||||||
},
|
|
||||||
offset: "The offset to start from. If you enter 10, it will start from the 10th identity.",
|
|
||||||
limit: "The number of identity templates to return.",
|
|
||||||
orderBy: "The column to order identity templates by.",
|
|
||||||
orderDirection: "The direction to order identity templates in."
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UNIVERSAL_AUTH = {
|
export const UNIVERSAL_AUTH = {
|
||||||
LOGIN: {
|
LOGIN: {
|
||||||
clientId: "Your Machine Identity Client ID.",
|
clientId: "Your Machine Identity Client ID.",
|
||||||
@@ -251,7 +215,7 @@ export const LDAP_AUTH = {
|
|||||||
password: "The password of the LDAP user to login."
|
password: "The password of the LDAP user to login."
|
||||||
},
|
},
|
||||||
ATTACH: {
|
ATTACH: {
|
||||||
templateId: "The ID of the template to attach the configuration onto.",
|
templateId: "The ID of the identity auth template to attach the configuration onto.",
|
||||||
identityId: "The ID of the identity to attach the configuration onto.",
|
identityId: "The ID of the identity to attach the configuration onto.",
|
||||||
url: "The URL of the LDAP server.",
|
url: "The URL of the LDAP server.",
|
||||||
allowedFields:
|
allowedFields:
|
||||||
@@ -279,7 +243,7 @@ export const LDAP_AUTH = {
|
|||||||
accessTokenMaxTTL: "The new maximum 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.",
|
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 template to update the configuration for."
|
templateId: "The ID of the identity auth template to update the configuration to."
|
||||||
},
|
},
|
||||||
RETRIEVE: {
|
RETRIEVE: {
|
||||||
identityId: "The ID of the identity to retrieve the configuration for."
|
identityId: "The ID of the identity to retrieve the configuration for."
|
||||||
|
|||||||
@@ -1469,7 +1469,8 @@ export const registerRoutes = async (
|
|||||||
identityLdapAuthDAL,
|
identityLdapAuthDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
kmsService,
|
kmsService,
|
||||||
licenseService
|
licenseService,
|
||||||
|
auditLogService
|
||||||
});
|
});
|
||||||
|
|
||||||
const identityAccessTokenService = identityAccessTokenServiceFactory({
|
const identityAccessTokenService = identityAccessTokenServiceFactory({
|
||||||
|
|||||||
@@ -200,55 +200,104 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string().trim().describe(LDAP_AUTH.ATTACH.identityId)
|
identityId: z.string().trim().describe(LDAP_AUTH.ATTACH.identityId)
|
||||||
}),
|
}),
|
||||||
body: z
|
body: z.union([
|
||||||
.object({
|
// Template-based configuration
|
||||||
templateId: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.templateId),
|
z
|
||||||
url: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.url),
|
.object({
|
||||||
bindDN: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.bindDN),
|
templateId: z.string().trim().describe(LDAP_AUTH.ATTACH.templateId),
|
||||||
bindPass: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.bindPass),
|
searchFilter: z
|
||||||
searchBase: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.searchBase),
|
.string()
|
||||||
searchFilter: z
|
.trim()
|
||||||
.string()
|
.min(1)
|
||||||
.trim()
|
.default("(uid={{username}})")
|
||||||
.min(1)
|
.refine(isValidLdapFilter, "Invalid LDAP search filter")
|
||||||
.default("(uid={{username}})")
|
.describe(LDAP_AUTH.ATTACH.searchFilter),
|
||||||
.refine(isValidLdapFilter, "Invalid LDAP search filter")
|
allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields),
|
||||||
.describe(LDAP_AUTH.ATTACH.searchFilter),
|
ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate),
|
||||||
allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields),
|
accessTokenTrustedIps: z
|
||||||
ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate),
|
.object({
|
||||||
accessTokenTrustedIps: z
|
ipAddress: z.string().trim()
|
||||||
.object({
|
})
|
||||||
ipAddress: z.string().trim()
|
.array()
|
||||||
})
|
.min(1)
|
||||||
.array()
|
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
||||||
.min(1)
|
.describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps),
|
||||||
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
accessTokenTTL: z
|
||||||
.describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps),
|
.number()
|
||||||
accessTokenTTL: z
|
.int()
|
||||||
.number()
|
.min(0)
|
||||||
.int()
|
.max(315360000)
|
||||||
.min(0)
|
.default(2592000)
|
||||||
.max(315360000)
|
.describe(LDAP_AUTH.ATTACH.accessTokenTTL),
|
||||||
.default(2592000)
|
accessTokenMaxTTL: z
|
||||||
.describe(LDAP_AUTH.ATTACH.accessTokenTTL),
|
.number()
|
||||||
accessTokenMaxTTL: z
|
.int()
|
||||||
.number()
|
.min(1)
|
||||||
.int()
|
.max(315360000)
|
||||||
.min(1)
|
.default(2592000)
|
||||||
.max(315360000)
|
.describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL),
|
||||||
.default(2592000)
|
accessTokenNumUsesLimit: z
|
||||||
.describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL),
|
.number()
|
||||||
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit)
|
.int()
|
||||||
})
|
.min(0)
|
||||||
.refine(
|
.default(0)
|
||||||
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
.describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit)
|
||||||
"Access Token TTL cannot be greater than Access Token Max TTL."
|
})
|
||||||
)
|
.refine(
|
||||||
.refine((val) => {
|
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||||
const hasTemplateId = !!val.templateId;
|
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||||
const hasManualConfig = !!(val.url && val.bindDN && val.bindPass && val.searchBase);
|
),
|
||||||
return hasTemplateId || hasManualConfig;
|
|
||||||
}, "Either templateId must be provided, or all of url, bindDN, bindPass, and searchBase must be provided."),
|
// 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: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityLdapAuth: IdentityLdapAuthsSchema.omit({
|
identityLdapAuth: IdentityLdapAuthsSchema.omit({
|
||||||
@@ -281,7 +330,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
|
|||||||
accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL,
|
accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL,
|
||||||
accessTokenTTL: identityLdapAuth.accessTokenTTL,
|
accessTokenTTL: identityLdapAuth.accessTokenTTL,
|
||||||
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
|
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
|
||||||
allowedFields: req.body.allowedFields
|
allowedFields: req.body.allowedFields,
|
||||||
|
templateId: identityLdapAuth.templateId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -383,7 +433,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
|
|||||||
accessTokenTTL: identityLdapAuth.accessTokenTTL,
|
accessTokenTTL: identityLdapAuth.accessTokenTTL,
|
||||||
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
|
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
|
||||||
accessTokenTrustedIps: identityLdapAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
accessTokenTrustedIps: identityLdapAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||||
allowedFields: req.body.allowedFields
|
allowedFields: req.body.allowedFields,
|
||||||
|
templateId: identityLdapAuth.templateId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ import {
|
|||||||
} from "@app/services/identity-auth-template/identity-auth-template-enums";
|
} from "@app/services/identity-auth-template/identity-auth-template-enums";
|
||||||
|
|
||||||
const ldapTemplateFieldsSchema = z.object({
|
const ldapTemplateFieldsSchema = z.object({
|
||||||
url: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP_URL_REQUIRED),
|
url: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.URL_REQUIRED),
|
||||||
bindDN: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.BIND_DN_REQUIRED),
|
bindDN: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.BIND_DN_REQUIRED),
|
||||||
bindPass: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.BIND_PASSWORD_REQUIRED),
|
bindPass: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.BIND_PASSWORD_REQUIRED),
|
||||||
searchBase: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.SEARCH_BASE_REQUIRED)
|
searchBase: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.SEARCH_BASE_REQUIRED),
|
||||||
|
ldapCaCertificate: z.string().trim().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) => {
|
export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) => {
|
||||||
@@ -44,8 +45,8 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider)
|
|||||||
templateFields: ldapTemplateFieldsSchema
|
templateFields: ldapTemplateFieldsSchema
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: IdentityAuthTemplatesSchema.extend({
|
||||||
message: z.string()
|
templateFields: z.record(z.string(), z.unknown())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -72,7 +73,7 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return { message: TEMPLATE_SUCCESS_MESSAGES.CREATED };
|
return template;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,8 +105,8 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider)
|
|||||||
templateFields: ldapTemplateFieldsSchema.partial().optional()
|
templateFields: ldapTemplateFieldsSchema.partial().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: IdentityAuthTemplatesSchema.extend({
|
||||||
message: z.string()
|
templateFields: z.record(z.string(), z.unknown())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -132,7 +133,7 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return { message: TEMPLATE_SUCCESS_MESSAGES.UPDATED };
|
return template;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -204,10 +205,8 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider)
|
|||||||
templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED)
|
templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: IdentityAuthTemplatesSchema.extend({
|
||||||
template: IdentityAuthTemplatesSchema.extend({
|
templateFields: ldapTemplateFieldsSchema
|
||||||
templateFields: ldapTemplateFieldsSchema
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -220,7 +219,7 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider)
|
|||||||
actorOrgId: req.permission.orgId
|
actorOrgId: req.permission.orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
return { template };
|
return template;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -347,7 +346,7 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider)
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/:templateId/usage",
|
url: "/:templateId/delete-usage",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -135,13 +135,8 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerIntegrationRouter, { prefix: "/integration" });
|
await server.register(registerIntegrationRouter, { prefix: "/integration" });
|
||||||
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });
|
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });
|
||||||
await server.register(registerWebhookRouter, { prefix: "/webhooks" });
|
await server.register(registerWebhookRouter, { prefix: "/webhooks" });
|
||||||
await server.register(
|
await server.register(registerIdentityRouter, { prefix: "/identities" });
|
||||||
async (identitiesRouter) => {
|
await server.register(registerIdentityTemplateRouter, { prefix: "/identity-templates" });
|
||||||
await identitiesRouter.register(registerIdentityRouter);
|
|
||||||
await identitiesRouter.register(registerIdentityTemplateRouter, { prefix: "/templates" });
|
|
||||||
},
|
|
||||||
{ prefix: "/identities" }
|
|
||||||
);
|
|
||||||
|
|
||||||
await server.register(
|
await server.register(
|
||||||
async (secretSharingRouter) => {
|
async (secretSharingRouter) => {
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ export const TEMPLATE_VALIDATION_MESSAGES = {
|
|||||||
TEMPLATE_NAME_MAX_LENGTH: "Template name must be at most 64 characters long",
|
TEMPLATE_NAME_MAX_LENGTH: "Template name must be at most 64 characters long",
|
||||||
AUTH_METHOD_REQUIRED: "Auth method is required",
|
AUTH_METHOD_REQUIRED: "Auth method is required",
|
||||||
TEMPLATE_ID_REQUIRED: "Template ID is required",
|
TEMPLATE_ID_REQUIRED: "Template ID is required",
|
||||||
LDAP_URL_REQUIRED: "LDAP URL is required",
|
LDAP: {
|
||||||
BIND_DN_REQUIRED: "Bind DN is required",
|
URL_REQUIRED: "LDAP URL is required",
|
||||||
BIND_PASSWORD_REQUIRED: "Bind password is required",
|
BIND_DN_REQUIRED: "Bind DN is required",
|
||||||
SEARCH_BASE_REQUIRED: "Search base is required"
|
BIND_PASSWORD_REQUIRED: "Bind password is required",
|
||||||
|
SEARCH_BASE_REQUIRED: "Search base is required"
|
||||||
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const TEMPLATE_SUCCESS_MESSAGES = {
|
export const TEMPLATE_SUCCESS_MESSAGES = {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
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 { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import {
|
import {
|
||||||
OrgPermissionMachineIdentityAuthTemplateActions,
|
OrgPermissionMachineIdentityAuthTemplateActions,
|
||||||
@@ -9,6 +10,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
|||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { TOrgPermission } from "@app/lib/types";
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TIdentityLdapAuthDALFactory } from "../identity-ldap-auth/identity-ldap-auth-dal";
|
import { TIdentityLdapAuthDALFactory } from "../identity-ldap-auth/identity-ldap-auth-dal";
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
@@ -30,6 +32,7 @@ type TIdentityAuthTemplateServiceFactoryDep = {
|
|||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TIdentityAuthTemplateServiceFactory = ReturnType<typeof identityAuthTemplateServiceFactory>;
|
export type TIdentityAuthTemplateServiceFactory = ReturnType<typeof identityAuthTemplateServiceFactory>;
|
||||||
@@ -39,7 +42,8 @@ export const identityAuthTemplateServiceFactory = ({
|
|||||||
identityLdapAuthDAL,
|
identityLdapAuthDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
kmsService,
|
kmsService,
|
||||||
licenseService
|
licenseService,
|
||||||
|
auditLogService
|
||||||
}: TIdentityAuthTemplateServiceFactoryDep) => {
|
}: TIdentityAuthTemplateServiceFactoryDep) => {
|
||||||
// Plan check
|
// Plan check
|
||||||
const $checkPlan = async (orgId: string) => {
|
const $checkPlan = async (orgId: string) => {
|
||||||
@@ -87,7 +91,7 @@ export const identityAuthTemplateServiceFactory = ({
|
|||||||
orgId: actorOrgId
|
orgId: actorOrgId
|
||||||
});
|
});
|
||||||
|
|
||||||
return template;
|
return { ...template, templateFields };
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateTemplate = async ({
|
const updateTemplate = async ({
|
||||||
@@ -104,7 +108,7 @@ export const identityAuthTemplateServiceFactory = ({
|
|||||||
templateFields?: Record<string, unknown>;
|
templateFields?: Record<string, unknown>;
|
||||||
} & Omit<TOrgPermission, "orgId">) => {
|
} & Omit<TOrgPermission, "orgId">) => {
|
||||||
await $checkPlan(actorOrgId);
|
await $checkPlan(actorOrgId);
|
||||||
const template = await identityAuthTemplateDAL.findById(templateId);
|
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
|
||||||
if (!template) {
|
if (!template) {
|
||||||
throw new NotFoundError({ message: "Template not found" });
|
throw new NotFoundError({ message: "Template not found" });
|
||||||
}
|
}
|
||||||
@@ -126,6 +130,8 @@ export const identityAuthTemplateServiceFactory = ({
|
|||||||
orgId: template.orgId
|
orgId: template.orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let finalTemplateFields: Record<string, unknown> = {};
|
||||||
|
|
||||||
const updatedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => {
|
const updatedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => {
|
||||||
const authTemplate = await identityAuthTemplateDAL.updateById(
|
const authTemplate = await identityAuthTemplateDAL.updateById(
|
||||||
templateId,
|
templateId,
|
||||||
@@ -149,12 +155,13 @@ export const identityAuthTemplateServiceFactory = ({
|
|||||||
) as TLdapTemplateFields;
|
) as TLdapTemplateFields;
|
||||||
|
|
||||||
const mergedTemplateFields: TLdapTemplateFields = { ...currentTemplateFields, ...templateFields };
|
const mergedTemplateFields: TLdapTemplateFields = { ...currentTemplateFields, ...templateFields };
|
||||||
|
finalTemplateFields = mergedTemplateFields;
|
||||||
const ldapUpdateData: {
|
const ldapUpdateData: {
|
||||||
url?: string;
|
url?: string;
|
||||||
searchBase?: string;
|
searchBase?: string;
|
||||||
encryptedBindDN?: Buffer;
|
encryptedBindDN?: Buffer;
|
||||||
encryptedBindPass?: Buffer;
|
encryptedBindPass?: Buffer;
|
||||||
|
encryptedLdapCaCertificate?: Buffer;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
if ("url" in templateFields) {
|
if ("url" in templateFields) {
|
||||||
@@ -173,16 +180,38 @@ export const identityAuthTemplateServiceFactory = ({
|
|||||||
plainText: Buffer.from(mergedTemplateFields.bindPass)
|
plainText: Buffer.from(mergedTemplateFields.bindPass)
|
||||||
}).cipherTextBlob;
|
}).cipherTextBlob;
|
||||||
}
|
}
|
||||||
|
if ("ldapCaCertificate" in templateFields) {
|
||||||
|
ldapUpdateData.encryptedLdapCaCertificate = encryptor({
|
||||||
|
plainText: Buffer.from(mergedTemplateFields.ldapCaCertificate || "")
|
||||||
|
}).cipherTextBlob;
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(ldapUpdateData).length > 0) {
|
if (Object.keys(ldapUpdateData).length > 0) {
|
||||||
await identityLdapAuthDAL.update({ templateId }, ldapUpdateData, tx);
|
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 authTemplate;
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatedTemplate;
|
return { ...updatedTemplate, templateFields: finalTemplateFields };
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteTemplate = async ({
|
const deleteTemplate = async ({
|
||||||
@@ -193,7 +222,7 @@ export const identityAuthTemplateServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
}: TDeleteIdentityAuthTemplateDTO) => {
|
}: TDeleteIdentityAuthTemplateDTO) => {
|
||||||
await $checkPlan(actorOrgId);
|
await $checkPlan(actorOrgId);
|
||||||
const template = await identityAuthTemplateDAL.findById(templateId);
|
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
|
||||||
if (!template) {
|
if (!template) {
|
||||||
throw new NotFoundError({ message: "Template not found" });
|
throw new NotFoundError({ message: "Template not found" });
|
||||||
}
|
}
|
||||||
@@ -212,7 +241,25 @@ export const identityAuthTemplateServiceFactory = ({
|
|||||||
|
|
||||||
const deletedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => {
|
const deletedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => {
|
||||||
// Remove template reference from identityLdapAuth records
|
// Remove template reference from identityLdapAuth records
|
||||||
await identityLdapAuthDAL.update({ templateId }, { templateId: null }, tx);
|
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
|
// Delete the template
|
||||||
const [deletedTpl] = await identityAuthTemplateDAL.delete({ id: templateId }, tx);
|
const [deletedTpl] = await identityAuthTemplateDAL.delete({ id: templateId }, tx);
|
||||||
@@ -230,7 +277,7 @@ export const identityAuthTemplateServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
}: TGetIdentityAuthTemplateDTO) => {
|
}: TGetIdentityAuthTemplateDTO) => {
|
||||||
await $checkPlan(actorOrgId);
|
await $checkPlan(actorOrgId);
|
||||||
const template = await identityAuthTemplateDAL.findById(templateId);
|
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
|
||||||
if (!template) {
|
if (!template) {
|
||||||
throw new NotFoundError({ message: "Template not found" });
|
throw new NotFoundError({ message: "Template not found" });
|
||||||
}
|
}
|
||||||
@@ -313,7 +360,7 @@ export const identityAuthTemplateServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
|
OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates,
|
||||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -350,7 +397,7 @@ export const identityAuthTemplateServiceFactory = ({
|
|||||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||||
);
|
);
|
||||||
|
|
||||||
const template = await identityAuthTemplateDAL.findById(templateId);
|
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
|
||||||
if (!template) {
|
if (!template) {
|
||||||
throw new NotFoundError({ message: "Template not found" });
|
throw new NotFoundError({ message: "Template not found" });
|
||||||
}
|
}
|
||||||
@@ -376,11 +423,11 @@ export const identityAuthTemplateServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
|
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
|
||||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||||
);
|
);
|
||||||
|
|
||||||
const template = await identityAuthTemplateDAL.findById(templateId);
|
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
|
||||||
if (!template) {
|
if (!template) {
|
||||||
throw new NotFoundError({ message: "Template not found" });
|
throw new NotFoundError({ message: "Template not found" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type TLdapTemplateFields = {
|
|||||||
bindDN: string;
|
bindDN: string;
|
||||||
bindPass: string;
|
bindPass: string;
|
||||||
searchBase: string;
|
searchBase: string;
|
||||||
|
ldapCaCertificate?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Union type for all template field types
|
// Union type for all template field types
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import { IdentityAuthMethod } from "@app/db/schemas";
|
import { IdentityAuthMethod } from "@app/db/schemas";
|
||||||
import { testLDAPConfig } from "@app/ee/services/ldap-config/ldap-fns";
|
import { testLDAPConfig } from "@app/ee/services/ldap-config/ldap-fns";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
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 {
|
import {
|
||||||
constructPermissionErrorMessage,
|
constructPermissionErrorMessage,
|
||||||
validatePrivilegeChangeOperation
|
validatePrivilegeChangeOperation
|
||||||
@@ -217,6 +221,14 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
|
if (templateId) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates,
|
||||||
|
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||||
|
|
||||||
if (!plan.ldap) {
|
if (!plan.ldap) {
|
||||||
@@ -254,7 +266,7 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.orgId)
|
? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.orgId)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
let ldapConfig: { bindDN: string; bindPass: string; searchBase: string; url: string };
|
let ldapConfig: { bindDN: string; bindPass: string; searchBase: string; url: string; ldapCaCertificate?: string };
|
||||||
if (template) {
|
if (template) {
|
||||||
ldapConfig = JSON.parse(decryptor({ cipherTextBlob: template.templateFields }).toString());
|
ldapConfig = JSON.parse(decryptor({ cipherTextBlob: template.templateFields }).toString());
|
||||||
} else {
|
} else {
|
||||||
@@ -267,7 +279,8 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
bindDN,
|
bindDN,
|
||||||
bindPass,
|
bindPass,
|
||||||
searchBase,
|
searchBase,
|
||||||
url
|
url,
|
||||||
|
ldapCaCertificate
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,9 +293,9 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
let encryptedLdapCaCertificate: Buffer | undefined;
|
let encryptedLdapCaCertificate: Buffer | undefined;
|
||||||
if (ldapCaCertificate) {
|
if (ldapConfig.ldapCaCertificate) {
|
||||||
const { cipherTextBlob: encryptedCertificate } = encryptor({
|
const { cipherTextBlob: encryptedCertificate } = encryptor({
|
||||||
plainText: Buffer.from(ldapCaCertificate)
|
plainText: Buffer.from(ldapConfig.ldapCaCertificate)
|
||||||
});
|
});
|
||||||
|
|
||||||
encryptedLdapCaCertificate = encryptedCertificate;
|
encryptedLdapCaCertificate = encryptedCertificate;
|
||||||
@@ -291,7 +304,7 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
const isConnected = await testLDAPConfig({
|
const isConnected = await testLDAPConfig({
|
||||||
bindDN: ldapConfig.bindDN,
|
bindDN: ldapConfig.bindDN,
|
||||||
bindPass: ldapConfig.bindPass,
|
bindPass: ldapConfig.bindPass,
|
||||||
caCert: ldapCaCertificate || "",
|
caCert: ldapConfig.ldapCaCertificate || "",
|
||||||
url: ldapConfig.url
|
url: ldapConfig.url
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -371,6 +384,13 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
|
if (templateId) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates,
|
||||||
|
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||||
|
|
||||||
if (!plan.ldap) {
|
if (!plan.ldap) {
|
||||||
@@ -411,6 +431,7 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
bindPass?: string;
|
bindPass?: string;
|
||||||
searchBase?: string;
|
searchBase?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
|
ldapCaCertificate?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (template) {
|
if (template) {
|
||||||
@@ -420,7 +441,8 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
bindDN,
|
bindDN,
|
||||||
bindPass,
|
bindPass,
|
||||||
searchBase,
|
searchBase,
|
||||||
url
|
url,
|
||||||
|
ldapCaCertificate
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,9 +456,9 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
let encryptedLdapCaCertificate: Buffer | undefined;
|
let encryptedLdapCaCertificate: Buffer | undefined;
|
||||||
if (ldapCaCertificate) {
|
if (config.ldapCaCertificate) {
|
||||||
const { cipherTextBlob: ldapCaCertificateCiphertext } = encryptor({
|
const { cipherTextBlob: ldapCaCertificateCiphertext } = encryptor({
|
||||||
plainText: Buffer.from(ldapCaCertificate)
|
plainText: Buffer.from(config.ldapCaCertificate)
|
||||||
});
|
});
|
||||||
|
|
||||||
encryptedLdapCaCertificate = ldapCaCertificateCiphertext;
|
encryptedLdapCaCertificate = ldapCaCertificateCiphertext;
|
||||||
@@ -456,7 +478,7 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
const isConnected = await testLDAPConfig({
|
const isConnected = await testLDAPConfig({
|
||||||
bindDN: config.bindDN || ldapConfig.bindDN,
|
bindDN: config.bindDN || ldapConfig.bindDN,
|
||||||
bindPass: config.bindPass || ldapConfig.bindPass,
|
bindPass: config.bindPass || ldapConfig.bindPass,
|
||||||
caCert: ldapCaCertificate || ldapConfig.caCert,
|
caCert: config.ldapCaCertificate || ldapConfig.caCert,
|
||||||
url: config.url || ldapConfig.url
|
url: config.url || ldapConfig.url
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -291,7 +291,6 @@
|
|||||||
{
|
{
|
||||||
"group": "Machine Identities",
|
"group": "Machine Identities",
|
||||||
"pages": [
|
"pages": [
|
||||||
"documentation/platform/identities/auth-templates",
|
|
||||||
"documentation/platform/identities/alicloud-auth",
|
"documentation/platform/identities/alicloud-auth",
|
||||||
"documentation/platform/identities/aws-auth",
|
"documentation/platform/identities/aws-auth",
|
||||||
"documentation/platform/identities/azure-auth",
|
"documentation/platform/identities/azure-auth",
|
||||||
@@ -323,6 +322,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"documentation/platform/identities/auth-templates",
|
||||||
"documentation/platform/token",
|
"documentation/platform/token",
|
||||||
"documentation/platform/mfa",
|
"documentation/platform/mfa",
|
||||||
"documentation/platform/github-org-sync"
|
"documentation/platform/github-org-sync"
|
||||||
|
|||||||
@@ -67,16 +67,16 @@ Auth templates are managed in **Organization Settings > Access Control > Identit
|
|||||||
|
|
||||||
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.
|
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
|
### Managing Template Usage
|
||||||
|
|
||||||
You can view which identities are using a specific template by clicking **View Usages** in the template's dropdown menu. This allows you to:
|
You can view which identities are using a specific template by clicking **View Usages** in the template's dropdown menu.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
- See all identities currently using the template
|
|
||||||
- Unlink identities from the template
|
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
<AccordionGroup>
|
<AccordionGroup>
|
||||||
@@ -85,11 +85,11 @@ You can view which identities are using a specific template by clicking **View U
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="What happens if I delete a template that's in use?">
|
<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.
|
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>
|
||||||
|
|
||||||
<Accordion title="Can I see which identities are using a specific template?">
|
<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. You can also unlink identities from templates from this view.
|
Yes, click **View Usages** in the template's dropdown menu to see all identities currently using that template.
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="Do templates support all authentication methods?">
|
<Accordion title="Do templates support all authentication methods?">
|
||||||
|
|||||||
@@ -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.
|
**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
|
## Guide
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Creating an identity">
|
<Step title="Creating an identity">
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 491 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 660 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 461 KiB After Width: | Height: | Size: 192 KiB |
@@ -217,3 +217,14 @@ Supports conditions and permission inversion
|
|||||||
| `edit-gateways` | Modify existing gateway settings |
|
| `edit-gateways` | Modify existing gateway settings |
|
||||||
| `delete-gateways` | Remove gateways from organization |
|
| `delete-gateways` | Remove gateways from organization |
|
||||||
| `attach-gateways` | Attach gateways to resources |
|
| `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 |
|
||||||
|
| `use-templates` | Attach identity auth templates to identities |
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ export enum OrgPermissionMachineIdentityAuthTemplateActions {
|
|||||||
CreateTemplates = "create-templates",
|
CreateTemplates = "create-templates",
|
||||||
EditTemplates = "edit-templates",
|
EditTemplates = "edit-templates",
|
||||||
DeleteTemplates = "delete-templates",
|
DeleteTemplates = "delete-templates",
|
||||||
UnlinkTemplates = "unlink-templates"
|
UnlinkTemplates = "unlink-templates",
|
||||||
|
UseTemplates = "use-templates"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum OrgPermissionSubjects {
|
export enum OrgPermissionSubjects {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const useCreateIdentityAuthTemplate = () => {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (dto: CreateIdentityAuthTemplateDTO) => {
|
mutationFn: async (dto: CreateIdentityAuthTemplateDTO) => {
|
||||||
const { data } = await apiRequest.post<{ template: IdentityAuthTemplate }>(
|
const { data } = await apiRequest.post<{ template: IdentityAuthTemplate }>(
|
||||||
"/api/v1/identities/templates",
|
"/api/v1/identity-templates",
|
||||||
dto
|
dto
|
||||||
);
|
);
|
||||||
return data.template;
|
return data.template;
|
||||||
@@ -37,7 +37,7 @@ export const useUpdateIdentityAuthTemplate = () => {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (dto: UpdateIdentityAuthTemplateDTO) => {
|
mutationFn: async (dto: UpdateIdentityAuthTemplateDTO) => {
|
||||||
const { data } = await apiRequest.patch<{ template: IdentityAuthTemplate }>(
|
const { data } = await apiRequest.patch<{ template: IdentityAuthTemplate }>(
|
||||||
`/api/v1/identities/templates/${dto.templateId}`,
|
`/api/v1/identity-templates/${dto.templateId}`,
|
||||||
dto
|
dto
|
||||||
);
|
);
|
||||||
return data.template;
|
return data.template;
|
||||||
@@ -58,7 +58,7 @@ export const useDeleteIdentityAuthTemplate = () => {
|
|||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (dto: DeleteIdentityAuthTemplateDTO) => {
|
mutationFn: async (dto: DeleteIdentityAuthTemplateDTO) => {
|
||||||
await apiRequest.delete(`/api/v1/identities/templates/${dto.templateId}`, {
|
await apiRequest.delete(`/api/v1/identity-templates/${dto.templateId}`, {
|
||||||
params: { organizationId: dto.organizationId }
|
params: { organizationId: dto.organizationId }
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -79,7 +79,7 @@ export const useUnlinkTemplateUsage = () => {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (dto: UnlinkTemplateUsageDTO) => {
|
mutationFn: async (dto: UnlinkTemplateUsageDTO) => {
|
||||||
const { data } = await apiRequest.post<MachineAuthTemplateUsage[]>(
|
const { data } = await apiRequest.post<MachineAuthTemplateUsage[]>(
|
||||||
`/api/v1/identities/templates/${dto.templateId}/usage`,
|
`/api/v1/identity-templates/${dto.templateId}/delete-usage`,
|
||||||
{ identityIds: dto.identityIds },
|
{ identityIds: dto.identityIds },
|
||||||
{ params: { organizationId: dto.organizationId } }
|
{ params: { organizationId: dto.organizationId } }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const useGetIdentityAuthTemplates = (dto: GetIdentityAuthTemplatesDTO) =>
|
|||||||
const { data } = await apiRequest.get<{
|
const { data } = await apiRequest.get<{
|
||||||
templates: IdentityAuthTemplate[];
|
templates: IdentityAuthTemplate[];
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
}>("/api/v1/identities/templates/search", {
|
}>("/api/v1/identity-templates/search", {
|
||||||
params: {
|
params: {
|
||||||
organizationId: dto.organizationId,
|
organizationId: dto.organizationId,
|
||||||
limit: dto.limit || 50,
|
limit: dto.limit || 50,
|
||||||
@@ -47,13 +47,13 @@ export const useGetIdentityAuthTemplate = (templateId: string, organizationId: s
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: identityAuthTemplatesKeys.getTemplate(templateId),
|
queryKey: identityAuthTemplatesKeys.getTemplate(templateId),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<{ template: IdentityAuthTemplate }>(
|
const { data } = await apiRequest.get<IdentityAuthTemplate>(
|
||||||
`/api/v1/identities/templates/${templateId}`,
|
`/api/v1/identity-templates/${templateId}`,
|
||||||
{
|
{
|
||||||
params: { organizationId }
|
params: { organizationId }
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return data.template;
|
return data;
|
||||||
},
|
},
|
||||||
enabled: Boolean(templateId) && Boolean(organizationId)
|
enabled: Boolean(templateId) && Boolean(organizationId)
|
||||||
});
|
});
|
||||||
@@ -63,12 +63,9 @@ export const useGetAvailableTemplates = (authMethod: MachineIdentityAuthMethod)
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: identityAuthTemplatesKeys.getAvailableTemplates(authMethod),
|
queryKey: identityAuthTemplatesKeys.getAvailableTemplates(authMethod),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<IdentityAuthTemplate[]>(
|
const { data } = await apiRequest.get<IdentityAuthTemplate[]>("/api/v1/identity-templates", {
|
||||||
"/api/v1/identities/templates",
|
params: { authMethod }
|
||||||
{
|
});
|
||||||
params: { authMethod }
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
enabled: Boolean(authMethod)
|
enabled: Boolean(authMethod)
|
||||||
@@ -80,7 +77,7 @@ export const useGetTemplateUsages = (dto: GetTemplateUsagesDTO) => {
|
|||||||
queryKey: identityAuthTemplatesKeys.getTemplateUsages(dto.templateId),
|
queryKey: identityAuthTemplatesKeys.getTemplateUsages(dto.templateId),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<MachineAuthTemplateUsage[]>(
|
const { data } = await apiRequest.get<MachineAuthTemplateUsage[]>(
|
||||||
`/api/v1/identities/templates/${dto.templateId}/usage`,
|
`/api/v1/identity-templates/${dto.templateId}/usage`,
|
||||||
{
|
{
|
||||||
params: { organizationId: dto.organizationId }
|
params: { organizationId: dto.organizationId }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface LdapTemplateFields {
|
|||||||
bindDN: string;
|
bindDN: string;
|
||||||
bindPass: string;
|
bindPass: string;
|
||||||
searchBase: string;
|
searchBase: string;
|
||||||
|
ldapCaCertificate?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IdentityAuthTemplate {
|
export interface IdentityAuthTemplate {
|
||||||
|
|||||||
+33
-7
@@ -11,7 +11,8 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Select,
|
Select,
|
||||||
SelectItem
|
SelectItem,
|
||||||
|
TextArea
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import {
|
import {
|
||||||
@@ -29,7 +30,11 @@ const schema = z.object({
|
|||||||
url: z.string().min(1, "LDAP URL is required"),
|
url: z.string().min(1, "LDAP URL is required"),
|
||||||
bindDN: z.string().min(1, "Bind DN is required"),
|
bindDN: z.string().min(1, "Bind DN is required"),
|
||||||
bindPass: z.string().min(1, "Bind Pass is required"),
|
bindPass: z.string().min(1, "Bind Pass is required"),
|
||||||
searchBase: z.string().min(1, "Search Base / DN 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>;
|
export type FormData = z.infer<typeof schema>;
|
||||||
@@ -66,7 +71,8 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) =
|
|||||||
url: "",
|
url: "",
|
||||||
bindDN: "",
|
bindDN: "",
|
||||||
bindPass: "",
|
bindPass: "",
|
||||||
searchBase: ""
|
searchBase: "",
|
||||||
|
ldapCaCertificate: ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -78,7 +84,8 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) =
|
|||||||
url: template.templateFields?.url || "",
|
url: template.templateFields?.url || "",
|
||||||
bindDN: template.templateFields?.bindDN || "",
|
bindDN: template.templateFields?.bindDN || "",
|
||||||
bindPass: template.templateFields?.bindPass || "",
|
bindPass: template.templateFields?.bindPass || "",
|
||||||
searchBase: template.templateFields?.searchBase || ""
|
searchBase: template.templateFields?.searchBase || "",
|
||||||
|
ldapCaCertificate: template.templateFields?.ldapCaCertificate || ""
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reset({
|
reset({
|
||||||
@@ -87,7 +94,8 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) =
|
|||||||
url: "",
|
url: "",
|
||||||
bindDN: "",
|
bindDN: "",
|
||||||
bindPass: "",
|
bindPass: "",
|
||||||
searchBase: ""
|
searchBase: "",
|
||||||
|
ldapCaCertificate: ""
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isEdit, template, reset]);
|
}, [isEdit, template, reset]);
|
||||||
@@ -105,7 +113,8 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) =
|
|||||||
url: data.url,
|
url: data.url,
|
||||||
bindDN: data.bindDN,
|
bindDN: data.bindDN,
|
||||||
bindPass: data.bindPass,
|
bindPass: data.bindPass,
|
||||||
searchBase: data.searchBase
|
searchBase: data.searchBase,
|
||||||
|
ldapCaCertificate: data.ldapCaCertificate
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -121,7 +130,8 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) =
|
|||||||
url: data.url,
|
url: data.url,
|
||||||
bindDN: data.bindDN,
|
bindDN: data.bindDN,
|
||||||
bindPass: data.bindPass,
|
bindPass: data.bindPass,
|
||||||
searchBase: data.searchBase
|
searchBase: data.searchBase,
|
||||||
|
ldapCaCertificate: data.ldapCaCertificate
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -267,6 +277,22 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) =
|
|||||||
</FormControl>
|
</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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -182,7 +182,7 @@ export const IdentityAuthTemplatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
<Td>{template.name}</Td>
|
<Td>{template.name}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="capitalize">{template.authMethod}</span>
|
<span className="uppercase">{template.authMethod}</span>
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
|
|||||||
+163
-94
@@ -20,7 +20,11 @@ import {
|
|||||||
TextArea,
|
TextArea,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} 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 {
|
import {
|
||||||
MachineIdentityAuthMethod,
|
MachineIdentityAuthMethod,
|
||||||
useAddIdentityLdapAuth,
|
useAddIdentityLdapAuth,
|
||||||
@@ -143,6 +147,12 @@ export const IdentityLdapAuthForm = ({
|
|||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityLdapAuth();
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentityLdapAuth();
|
||||||
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
||||||
const { data: templates } = useGetAvailableTemplates(MachineIdentityAuthMethod.LDAP);
|
const { data: templates } = useGetAvailableTemplates(MachineIdentityAuthMethod.LDAP);
|
||||||
|
const { permission } = useOrgPermission();
|
||||||
|
|
||||||
|
const canUseTemplates = permission.can(
|
||||||
|
OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates,
|
||||||
|
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||||
|
);
|
||||||
|
|
||||||
const { data } = useGetIdentityLdapAuth(identityId ?? "", {
|
const { data } = useGetIdentityLdapAuth(identityId ?? "", {
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
@@ -153,11 +163,12 @@ export const IdentityLdapAuthForm = ({
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
watch,
|
watch,
|
||||||
|
setValue,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
scope: "template",
|
scope: "custom",
|
||||||
templateId: "",
|
templateId: "",
|
||||||
url: "",
|
url: "",
|
||||||
bindDN: "",
|
bindDN: "",
|
||||||
@@ -188,15 +199,11 @@ export const IdentityLdapAuthForm = ({
|
|||||||
// Helper function to determine scope based on existing data
|
// Helper function to determine scope based on existing data
|
||||||
const determineScope = (authData: any) => {
|
const determineScope = (authData: any) => {
|
||||||
// If templateId exists in the data, it's template scope
|
// If templateId exists in the data, it's template scope
|
||||||
// If url, bindDN, bindPass, searchBase exist, it's custom scope
|
|
||||||
if (authData.templateId) {
|
if (authData.templateId) {
|
||||||
return "template";
|
return "template";
|
||||||
}
|
}
|
||||||
if (authData.url || authData.bindDN || authData.bindPass || authData.searchBase) {
|
// Default to custom if we can't determine
|
||||||
return "custom";
|
return "custom";
|
||||||
}
|
|
||||||
// Default to template if we can't determine
|
|
||||||
return "template";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -228,7 +235,7 @@ export const IdentityLdapAuthForm = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
reset({
|
reset({
|
||||||
scope: "template",
|
scope: "custom",
|
||||||
templateId: "",
|
templateId: "",
|
||||||
url: "",
|
url: "",
|
||||||
bindDN: "",
|
bindDN: "",
|
||||||
@@ -345,30 +352,38 @@ export const IdentityLdapAuthForm = ({
|
|||||||
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel value={IdentityFormTab.Configuration}>
|
<TabPanel value={IdentityFormTab.Configuration}>
|
||||||
<Controller
|
{canUseTemplates && (
|
||||||
control={control}
|
<Controller
|
||||||
name="scope"
|
control={control}
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
name="scope"
|
||||||
<FormControl
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
label="Configuration Type"
|
<FormControl
|
||||||
isError={Boolean(error)}
|
label="Configuration Type"
|
||||||
errorText={error?.message}
|
isError={Boolean(error)}
|
||||||
>
|
errorText={error?.message}
|
||||||
<Select
|
|
||||||
value={value}
|
|
||||||
onValueChange={(val) => {
|
|
||||||
onChange(val);
|
|
||||||
}}
|
|
||||||
className="w-full"
|
|
||||||
position="popper"
|
|
||||||
dropdownContainerClassName="max-w-none"
|
|
||||||
>
|
>
|
||||||
<SelectItem value="template">Use Template</SelectItem>
|
<Select
|
||||||
<SelectItem value="custom">Custom Configuration</SelectItem>
|
value={value}
|
||||||
</Select>
|
onValueChange={(val) => {
|
||||||
</FormControl>
|
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" && (
|
{scope === "template" && (
|
||||||
<Controller
|
<Controller
|
||||||
@@ -385,6 +400,13 @@ export const IdentityLdapAuthForm = ({
|
|||||||
value={value}
|
value={value}
|
||||||
onValueChange={(val) => {
|
onValueChange={(val) => {
|
||||||
onChange(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"
|
className="w-full"
|
||||||
position="popper"
|
position="popper"
|
||||||
@@ -404,66 +426,104 @@ export const IdentityLdapAuthForm = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{scope === "custom" && (
|
<Controller
|
||||||
<>
|
control={control}
|
||||||
<Controller
|
name="url"
|
||||||
control={control}
|
render={({ field, fieldState: { error } }) => (
|
||||||
name="url"
|
<FormControl
|
||||||
render={({ field, fieldState: { error } }) => (
|
label="LDAP URL"
|
||||||
<FormControl
|
isError={Boolean(error)}
|
||||||
label="LDAP URL"
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
tooltipText={
|
||||||
errorText={error?.message}
|
scope === "template"
|
||||||
isRequired
|
? "This field cannot be modified when using a template"
|
||||||
>
|
: undefined
|
||||||
<Input {...field} placeholder="ldaps://domain-or-ip:636" type="text" />
|
}
|
||||||
</FormControl>
|
isRequired
|
||||||
)}
|
>
|
||||||
/>
|
<Input
|
||||||
<Controller
|
{...field}
|
||||||
control={control}
|
placeholder="ldaps://domain-or-ip:636"
|
||||||
name="bindDN"
|
type="text"
|
||||||
render={({ field, fieldState: { error } }) => (
|
isDisabled={scope === "template"}
|
||||||
<FormControl
|
containerClassName={scope === "template" ? "opacity-55" : ""}
|
||||||
isRequired
|
/>
|
||||||
label="Bind DN"
|
</FormControl>
|
||||||
isError={Boolean(error)}
|
)}
|
||||||
errorText={error?.message}
|
/>
|
||||||
>
|
<Controller
|
||||||
<Input {...field} placeholder="cn=infisical,ou=Users,dc=example,dc=com" />
|
control={control}
|
||||||
</FormControl>
|
name="bindDN"
|
||||||
)}
|
render={({ field, fieldState: { error } }) => (
|
||||||
/>
|
<FormControl
|
||||||
<Controller
|
isRequired
|
||||||
control={control}
|
label="Bind DN"
|
||||||
name="bindPass"
|
isError={Boolean(error)}
|
||||||
render={({ field, fieldState: { error } }) => (
|
errorText={error?.message}
|
||||||
<FormControl
|
tooltipText={
|
||||||
isRequired
|
scope === "template"
|
||||||
label="Bind Pass"
|
? "This field cannot be modified when using a template"
|
||||||
isError={Boolean(error)}
|
: undefined
|
||||||
errorText={error?.message}
|
}
|
||||||
>
|
>
|
||||||
<Input {...field} placeholder="********" type="password" />
|
<Input
|
||||||
</FormControl>
|
{...field}
|
||||||
)}
|
containerClassName={scope === "template" ? "opacity-55" : ""}
|
||||||
/>
|
placeholder="cn=infisical,ou=Users,dc=example,dc=com"
|
||||||
<Controller
|
isDisabled={scope === "template"}
|
||||||
control={control}
|
/>
|
||||||
name="searchBase"
|
</FormControl>
|
||||||
render={({ field, fieldState: { error } }) => (
|
)}
|
||||||
<FormControl
|
/>
|
||||||
isRequired
|
<Controller
|
||||||
label="Search Base / DN"
|
control={control}
|
||||||
isError={Boolean(error)}
|
name="bindPass"
|
||||||
errorText={error?.message}
|
render={({ field, fieldState: { error } }) => (
|
||||||
>
|
<FormControl
|
||||||
<Input {...field} placeholder="ou=machines,dc=acme,dc=com" />
|
isRequired
|
||||||
</FormControl>
|
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"
|
||||||
|
containerClassName={scope === "template" ? "opacity-55" : ""}
|
||||||
|
isDisabled={scope === "template"}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="searchBase"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isRequired
|
||||||
|
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"
|
||||||
|
containerClassName={scope === "template" ? "opacity-55" : ""}
|
||||||
|
isDisabled={scope === "template"}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@@ -641,9 +701,18 @@ export const IdentityLdapAuthForm = ({
|
|||||||
isOptional
|
isOptional
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
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>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+84
-84
@@ -103,93 +103,93 @@ export const IdentitySection = withPermission(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div>
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="flex items-center gap-1">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
<div className="flex items-center gap-1">
|
||||||
<a
|
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
||||||
href="https://infisical.com/docs/documentation/platform/identities/overview"
|
<a
|
||||||
target="_blank"
|
href="https://infisical.com/docs/documentation/platform/identities/overview"
|
||||||
rel="noopener noreferrer"
|
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}
|
|
||||||
>
|
>
|
||||||
Create Identity
|
<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">
|
||||||
</Button>
|
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||||
)}
|
<span>Docs</span>
|
||||||
</OrgPermissionCan>
|
<FontAwesomeIcon
|
||||||
</div>
|
icon={faArrowUpRightFromSquare}
|
||||||
<IdentityTable handlePopUpOpen={handlePopUpOpen} />
|
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||||
|
/>
|
||||||
{/* Identity Auth Templates Section */}
|
</div>
|
||||||
{subscription.machineIdentityAuthTemplates && (
|
</a>
|
||||||
<div className="mt-8">
|
|
||||||
<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>
|
</div>
|
||||||
<IdentityAuthTemplatesTable handlePopUpOpen={handlePopUpOpen} />
|
<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>
|
</div>
|
||||||
)}
|
<IdentityTable handlePopUpOpen={handlePopUpOpen} />
|
||||||
|
</div>
|
||||||
|
{/* Identity Auth Templates Section */}
|
||||||
|
<div className="mb-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>
|
||||||
<IdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<IdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
<IdentityAuthTemplateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<IdentityAuthTemplateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
<MachineAuthTemplateUsagesModal
|
<MachineAuthTemplateUsagesModal
|
||||||
|
|||||||
+37
-202
@@ -1,14 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { faCertificate } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { faCertificate, faTrash } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
DeleteActionModal,
|
|
||||||
EmptyState,
|
EmptyState,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
@@ -22,14 +14,7 @@ import {
|
|||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import {
|
import { useGetTemplateUsages } from "@app/hooks/api/identityAuthTemplates";
|
||||||
MachineAuthTemplateUsage,
|
|
||||||
TEMPLATE_ERROR_MESSAGES,
|
|
||||||
TEMPLATE_UI_LABELS,
|
|
||||||
useGetTemplateUsages,
|
|
||||||
useUnlinkTemplateUsage
|
|
||||||
} from "@app/hooks/api/identityAuthTemplates";
|
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -45,201 +30,51 @@ export const MachineAuthTemplateUsagesModal = ({
|
|||||||
templateName
|
templateName
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const [selectedUsageIds, setSelectedUsageIds] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
|
||||||
"unlinkUsages"
|
|
||||||
] as const);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen) {
|
|
||||||
setSelectedUsageIds([]);
|
|
||||||
handlePopUpClose("unlinkUsages");
|
|
||||||
}
|
|
||||||
}, [isOpen, handlePopUpClose]);
|
|
||||||
|
|
||||||
const organizationId = currentOrg?.id || "";
|
const organizationId = currentOrg?.id || "";
|
||||||
|
|
||||||
const {
|
const { data: usages = [], isPending } = useGetTemplateUsages({
|
||||||
data: usages = [],
|
|
||||||
isPending,
|
|
||||||
refetch
|
|
||||||
} = useGetTemplateUsages({
|
|
||||||
templateId,
|
templateId,
|
||||||
organizationId
|
organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: unlinkUsage } = useUnlinkTemplateUsage();
|
|
||||||
|
|
||||||
const handleUnlinkUsages = async (selectedUsages: MachineAuthTemplateUsage[]) => {
|
|
||||||
try {
|
|
||||||
await unlinkUsage({
|
|
||||||
templateId,
|
|
||||||
identityIds: selectedUsages.map((usage) => usage.identityId),
|
|
||||||
organizationId
|
|
||||||
});
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: TEMPLATE_ERROR_MESSAGES.UNLINK_SUCCESS,
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
|
|
||||||
setSelectedUsageIds([]);
|
|
||||||
handlePopUpClose("unlinkUsages");
|
|
||||||
refetch();
|
|
||||||
} catch {
|
|
||||||
createNotification({
|
|
||||||
text: TEMPLATE_ERROR_MESSAGES.UNLINK_FAILED,
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUsageToggle = (usageId: string) => {
|
|
||||||
setSelectedUsageIds((prev) =>
|
|
||||||
prev.includes(usageId) ? prev.filter((id) => id !== usageId) : [...prev, usageId]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectAll = () => {
|
|
||||||
if (selectedUsageIds.length === usages.length) {
|
|
||||||
setSelectedUsageIds([]);
|
|
||||||
} else {
|
|
||||||
setSelectedUsageIds(usages.map((usage) => usage.identityId));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Modal isOpen={isOpen} onOpenChange={onClose}>
|
||||||
<Modal isOpen={isOpen} onOpenChange={onClose}>
|
<ModalContent title={`Usages for Identity Auth Template: ${templateName}`}>
|
||||||
<ModalContent
|
<div>
|
||||||
title={`Auth Template Usages: ${templateName}`}
|
<TableContainer>
|
||||||
subTitle="Manage identities using this template"
|
<Table>
|
||||||
className="max-w-4xl"
|
<THead>
|
||||||
>
|
<Tr className="h-14">
|
||||||
<div>
|
<Th>Identity Name</Th>
|
||||||
<div
|
<Th>Identity ID</Th>
|
||||||
className={twMerge(
|
</Tr>
|
||||||
"h-0 flex-shrink-0 overflow-hidden transition-all",
|
</THead>
|
||||||
selectedUsageIds.length > 0 && "h-16"
|
<TBody>
|
||||||
)}
|
{isPending && <TableSkeleton columns={3} innerKey="template-usages" />}
|
||||||
>
|
{!isPending &&
|
||||||
<div className="flex items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 px-4 py-2 text-bunker-300">
|
usages.map((usage) => (
|
||||||
<div className="mr-2 text-sm">{selectedUsageIds.length} Selected</div>
|
<Tr
|
||||||
<button
|
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||||
type="button"
|
key={`usage-${usage.identityId}`}
|
||||||
className="mr-auto text-xs text-mineshaft-400 underline-offset-2 hover:text-mineshaft-200 hover:underline"
|
|
||||||
onClick={() => setSelectedUsageIds([])}
|
|
||||||
>
|
|
||||||
{TEMPLATE_UI_LABELS.UNSELECT_ALL}
|
|
||||||
</button>
|
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
|
||||||
colorSchema="danger"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
|
||||||
className="ml-2"
|
|
||||||
onClick={() => {
|
|
||||||
const selectedUsagesList = usages.filter((usage) =>
|
|
||||||
selectedUsageIds.includes(usage.identityId)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!selectedUsagesList.length) return;
|
|
||||||
|
|
||||||
handlePopUpOpen("unlinkUsages", { selectedUsagesList });
|
|
||||||
}}
|
|
||||||
size="xs"
|
|
||||||
>
|
|
||||||
{TEMPLATE_UI_LABELS.UNLINK}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TableContainer>
|
|
||||||
<Table>
|
|
||||||
<THead>
|
|
||||||
<Tr className="h-14">
|
|
||||||
<Th className="w-12">
|
|
||||||
<Checkbox
|
|
||||||
id="select-all"
|
|
||||||
className="mr-2"
|
|
||||||
isChecked={usages.length > 0 && selectedUsageIds.length === usages.length}
|
|
||||||
onCheckedChange={handleSelectAll}
|
|
||||||
/>
|
|
||||||
</Th>
|
|
||||||
<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}`}
|
|
||||||
>
|
|
||||||
<Td>
|
|
||||||
<Checkbox
|
|
||||||
id={`select-usage-${usage.identityId}`}
|
|
||||||
isChecked={selectedUsageIds.includes(usage.identityId)}
|
|
||||||
onCheckedChange={() => handleUsageToggle(usage.identityId)}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<DeleteActionModal
|
|
||||||
isOpen={popUp.unlinkUsages.isOpen}
|
|
||||||
title="Are you sure you want to unlink the following template usages?"
|
|
||||||
onChange={(isDeleteOpen) => handlePopUpToggle("unlinkUsages", isDeleteOpen)}
|
|
||||||
deleteKey="confirm"
|
|
||||||
onDeleteApproved={() =>
|
|
||||||
handleUnlinkUsages(
|
|
||||||
popUp.unlinkUsages.data.selectedUsagesList as MachineAuthTemplateUsage[]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
buttonText={TEMPLATE_UI_LABELS.UNLINK}
|
|
||||||
>
|
|
||||||
<div className="mt-4 text-sm text-mineshaft-400">
|
|
||||||
This template will no longer be used by the following{" "}
|
|
||||||
{popUp.unlinkUsages.data?.selectedUsagesList?.length > 1 ? "identities" : "identity"}:
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 max-h-[20rem] overflow-y-auto rounded border border-mineshaft-600 bg-red/10 p-4 pl-8 text-sm text-red-200">
|
|
||||||
<ul className="list-disc">
|
|
||||||
{(popUp.unlinkUsages.data?.selectedUsagesList as MachineAuthTemplateUsage[])?.map(
|
|
||||||
(usage) => (
|
|
||||||
<li key={usage.identityId}>
|
|
||||||
<div className="mb-1 flex items-center">
|
|
||||||
<span className="break-all">{usage.identityName}</span>
|
|
||||||
<Badge
|
|
||||||
variant="danger"
|
|
||||||
className="ml-2 inline-flex w-min items-center gap-1.5 whitespace-nowrap"
|
|
||||||
>
|
>
|
||||||
{usage.identityId}
|
<Td>{usage.identityName}</Td>
|
||||||
</Badge>
|
<Td>
|
||||||
</div>
|
<span className="text-sm text-mineshaft-400">{usage.identityId}</span>
|
||||||
</li>
|
</Td>
|
||||||
)
|
</Tr>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isPending && usages.length === 0 && (
|
||||||
|
<EmptyState
|
||||||
|
title="This template is not currently being used by any identities"
|
||||||
|
icon={faCertificate}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</TableContainer>
|
||||||
</div>
|
</div>
|
||||||
</DeleteActionModal>
|
</ModalContent>
|
||||||
</>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+2
-1
@@ -89,7 +89,8 @@ const machineIdentityAuthTemplatePermissionSchema = z
|
|||||||
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: z.boolean().optional(),
|
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: z.boolean().optional(),
|
||||||
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: z.boolean().optional(),
|
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: z.boolean().optional(),
|
||||||
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: z.boolean().optional(),
|
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: z.boolean().optional(),
|
||||||
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: z.boolean().optional()
|
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: z.boolean().optional(),
|
||||||
|
[OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates]: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
|
|||||||
+10
-3
@@ -43,6 +43,10 @@ const PERMISSION_ACTIONS = [
|
|||||||
{
|
{
|
||||||
action: OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
|
action: OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
|
||||||
label: "Unlink Templates"
|
label: "Unlink Templates"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates,
|
||||||
|
label: "Attach Templates"
|
||||||
}
|
}
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@@ -103,7 +107,8 @@ export const OrgPermissionMachineIdentityAuthTemplateRow = ({
|
|||||||
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: true,
|
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: true,
|
||||||
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: true,
|
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: true,
|
||||||
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: true,
|
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: true,
|
||||||
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: true
|
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: true,
|
||||||
|
[OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates]: true
|
||||||
},
|
},
|
||||||
{ shouldDirty: true }
|
{ shouldDirty: true }
|
||||||
);
|
);
|
||||||
@@ -116,7 +121,8 @@ export const OrgPermissionMachineIdentityAuthTemplateRow = ({
|
|||||||
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: false,
|
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: false,
|
||||||
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: false,
|
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: false,
|
||||||
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: false,
|
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: false,
|
||||||
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false
|
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false,
|
||||||
|
[OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates]: true
|
||||||
},
|
},
|
||||||
{ shouldDirty: true }
|
{ shouldDirty: true }
|
||||||
);
|
);
|
||||||
@@ -131,7 +137,8 @@ export const OrgPermissionMachineIdentityAuthTemplateRow = ({
|
|||||||
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: false,
|
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: false,
|
||||||
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: false,
|
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: false,
|
||||||
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: false,
|
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: false,
|
||||||
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false
|
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false,
|
||||||
|
[OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates]: false
|
||||||
},
|
},
|
||||||
{ shouldDirty: true }
|
{ shouldDirty: true }
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user