diff --git a/backend/src/db/migrations/20250801170240_add-identity-auth-template.ts b/backend/src/db/migrations/20250801170240_add-identity-auth-template.ts index fff5bc40b..60974a661 100644 --- a/backend/src/db/migrations/20250801170240_add-identity-auth-template.ts +++ b/backend/src/db/migrations/20250801170240_add-identity-auth-template.ts @@ -25,9 +25,9 @@ export async function up(knex: Knex): Promise { } export async function down(knex: Knex): Promise { - if (await knex.schema.hasColumn(TableName.IdentityLdapAuth, "template")) { + if (await knex.schema.hasColumn(TableName.IdentityLdapAuth, "templateId")) { await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => { - t.dropForeign("templateId"); + t.dropForeign(["templateId"]); t.dropColumn("templateId"); }); } diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index 5c818f1f1..adc240358 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -419,15 +419,6 @@ const buildMemberPermission = () => { can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway); 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 diff --git a/backend/src/server/routes/v1/identity-template-router.ts b/backend/src/server/routes/v1/identity-template-router.ts index 1846e1f53..ec1823967 100644 --- a/backend/src/server/routes/v1/identity-template-router.ts +++ b/backend/src/server/routes/v1/identity-template-router.ts @@ -35,7 +35,11 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) } ], body: z.object({ - name: z.string().trim().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_REQUIRED), + 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 }), @@ -91,7 +95,12 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED) }), body: z.object({ - name: z.string().trim().optional(), + 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: { @@ -232,7 +241,8 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) ], querystring: z.object({ limit: z.coerce.number().positive().max(100).default(5).optional(), - offset: z.coerce.number().min(0).default(0).optional() + offset: z.coerce.number().min(0).default(0).optional(), + search: z.string().optional() }), response: { 200: z.object({ @@ -247,6 +257,7 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) 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, @@ -303,7 +314,7 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { hide: false, - description: "Get identity auth templates by authentication method", + description: "Get template usage by template ID", security: [ { bearerAuth: [] @@ -338,7 +349,7 @@ export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) method: "POST", url: "/:templateId/usage", config: { - rateLimit: readLimit + rateLimit: writeLimit }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { diff --git a/backend/src/services/identity-auth-template/identity-auth-template-dal.ts b/backend/src/services/identity-auth-template/identity-auth-template-dal.ts index cbcd1581b..736f0f033 100644 --- a/backend/src/services/identity-auth-template/identity-auth-template-dal.ts +++ b/backend/src/services/identity-auth-template/identity-auth-template-dal.ts @@ -14,9 +14,18 @@ export const identityAuthTemplateDALFactory = (db: TDbClient) => { const findByOrgId = async ( orgId: string, - { limit, offset, tx }: { limit?: number; offset?: number; tx?: Knex } = {} + { limit, offset, search, tx }: { limit?: number; offset?: number; search?: string; tx?: Knex } = {} ) => { - let query = (tx || db.replicaNode())(TableName.IdentityAuthTemplate).where({ orgId }).orderBy("createdAt", "desc"); + 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); @@ -27,9 +36,7 @@ export const identityAuthTemplateDALFactory = (db: TDbClient) => { const docs = await query; - const [{ count }] = (await (tx || db.replicaNode())(TableName.IdentityAuthTemplate) - .where({ orgId }) - .count("* as count")) as [{ count: string | number }]; + const [{ count }] = (await countQuery.count("* as count")) as [{ count: string | number }]; return { docs, totalCount: Number(count) }; }; @@ -60,10 +67,17 @@ export const identityAuthTemplateDALFactory = (db: TDbClient) => { } }; + 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 + findTemplateUsages, + findByIdAndOrgId }; }; diff --git a/backend/src/services/identity-auth-template/identity-auth-template-enums.ts b/backend/src/services/identity-auth-template/identity-auth-template-enums.ts index 163cd8fd1..36cdefb5d 100644 --- a/backend/src/services/identity-auth-template/identity-auth-template-enums.ts +++ b/backend/src/services/identity-auth-template/identity-auth-template-enums.ts @@ -4,6 +4,7 @@ export enum IdentityAuthTemplateMethod { 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", diff --git a/backend/src/services/identity-auth-template/identity-auth-template-service.ts b/backend/src/services/identity-auth-template/identity-auth-template-service.ts index 0873efc96..e55c59a4a 100644 --- a/backend/src/services/identity-auth-template/identity-auth-template-service.ts +++ b/backend/src/services/identity-auth-template/identity-auth-template-service.ts @@ -249,19 +249,20 @@ export const identityAuthTemplateServiceFactory = ({ const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: actorOrgId + 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.toString()) + templateFields: JSON.parse(decryptedTemplateFields) }; }; const listTemplates = async ({ limit, offset, + search, actorId, actorAuthMethod, actor, @@ -280,7 +281,7 @@ export const identityAuthTemplateServiceFactory = ({ OrgPermissionSubjects.MachineIdentityAuthTemplate ); - const { docs, totalCount } = await identityAuthTemplateDAL.findByOrgId(actorOrgId, { limit, offset }); + const { docs, totalCount } = await identityAuthTemplateDAL.findByOrgId(actorOrgId, { limit, offset, search }); const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, diff --git a/backend/src/services/identity-auth-template/identity-auth-template-types.ts b/backend/src/services/identity-auth-template/identity-auth-template-types.ts index 2472edec6..5c4b4a73f 100644 --- a/backend/src/services/identity-auth-template/identity-auth-template-types.ts +++ b/backend/src/services/identity-auth-template/identity-auth-template-types.ts @@ -16,16 +16,16 @@ export type TTemplateFieldsByMethod = { }; // Generic base types that use conditional types for type safety -export type TCreateIdentityAuthTemplateDTO = { +export type TCreateIdentityAuthTemplateDTO = { name: string; - authMethod: T; - templateFields: TTemplateFieldsByMethod[T]; + authMethod: IdentityAuthTemplateMethod; + templateFields: TTemplateFieldsByMethod[IdentityAuthTemplateMethod]; } & Omit; -export type TUpdateIdentityAuthTemplateDTO = { +export type TUpdateIdentityAuthTemplateDTO = { templateId: string; name?: string; - templateFields?: Partial; + templateFields?: Partial; } & Omit; export type TDeleteIdentityAuthTemplateDTO = { @@ -39,6 +39,7 @@ export type TGetIdentityAuthTemplateDTO = { export type TListIdentityAuthTemplatesDTO = { limit?: number; offset?: number; + search?: string; } & Omit; export type TGetTemplatesByAuthMethodDTO = { @@ -55,5 +56,5 @@ export type TUnlinkTemplateUsageDTO = { } & Omit; // Specific LDAP types for convenience -export type TCreateLdapTemplateDTO = TCreateIdentityAuthTemplateDTO; -export type TUpdateLdapTemplateDTO = TUpdateIdentityAuthTemplateDTO; +export type TCreateLdapTemplateDTO = TCreateIdentityAuthTemplateDTO; +export type TUpdateLdapTemplateDTO = TUpdateIdentityAuthTemplateDTO; diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts index ddb4a0e66..e108554a7 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts @@ -250,7 +250,9 @@ export const identityLdapAuthServiceFactory = ({ orgId: identityMembershipOrg.orgId }); - const template = templateId ? await identityAuthTemplateDAL.findById(templateId) : undefined; + const template = templateId + ? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.orgId) + : undefined; let ldapConfig: { bindDN: string; bindPass: string; searchBase: string; url: string }; if (template) { @@ -401,7 +403,9 @@ export const identityLdapAuthServiceFactory = ({ orgId: identityMembershipOrg.orgId }); - const template = templateId ? await identityAuthTemplateDAL.findById(templateId) : undefined; + const template = templateId + ? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.orgId) + : undefined; let config: { bindDN?: string; bindPass?: string; diff --git a/frontend/src/hooks/api/identityAuthTemplates/queries.tsx b/frontend/src/hooks/api/identityAuthTemplates/queries.tsx index b63dce79d..de6f9b22e 100644 --- a/frontend/src/hooks/api/identityAuthTemplates/queries.tsx +++ b/frontend/src/hooks/api/identityAuthTemplates/queries.tsx @@ -6,7 +6,8 @@ import { GetIdentityAuthTemplatesDTO, GetTemplateUsagesDTO, IdentityAuthTemplate, - MachineAuthTemplateUsage + MachineAuthTemplateUsage, + MachineIdentityAuthMethod } from "./types"; export const identityAuthTemplatesKeys = { @@ -15,8 +16,8 @@ export const identityAuthTemplatesKeys = { [...identityAuthTemplatesKeys.all, "list", dto] as const, getTemplate: (templateId: string) => [...identityAuthTemplatesKeys.all, "single", templateId] as const, - getTemplatesByOrgId: (authMethod: string) => - [...identityAuthTemplatesKeys.all, "list", authMethod] as const, + getAvailableTemplates: (authMethod: MachineIdentityAuthMethod) => + [...identityAuthTemplatesKeys.all, "available", authMethod] as const, getTemplateUsages: (templateId: string) => [...identityAuthTemplatesKeys.all, "usages", templateId] as const }; @@ -32,7 +33,8 @@ export const useGetIdentityAuthTemplates = (dto: GetIdentityAuthTemplatesDTO) => params: { organizationId: dto.organizationId, limit: dto.limit || 50, - offset: dto.offset || 0 + offset: dto.offset || 0, + ...(dto.search && { search: dto.search }) } }); return data; @@ -57,9 +59,9 @@ export const useGetIdentityAuthTemplate = (templateId: string, organizationId: s }); }; -export const useGetIdentityAuthTemplatesByOrgId = (authMethod: string) => { +export const useGetAvailableTemplates = (authMethod: MachineIdentityAuthMethod) => { return useQuery({ - queryKey: identityAuthTemplatesKeys.getTemplatesByOrgId(authMethod), + queryKey: identityAuthTemplatesKeys.getAvailableTemplates(authMethod), queryFn: async () => { const { data } = await apiRequest.get( "/api/v1/identities/templates", diff --git a/frontend/src/hooks/api/identityAuthTemplates/types.ts b/frontend/src/hooks/api/identityAuthTemplates/types.ts index 4b52187a9..07a1eb018 100644 --- a/frontend/src/hooks/api/identityAuthTemplates/types.ts +++ b/frontend/src/hooks/api/identityAuthTemplates/types.ts @@ -42,6 +42,7 @@ export interface GetIdentityAuthTemplatesDTO { organizationId: string; limit?: number; offset?: number; + search?: string; } export interface MachineAuthTemplateUsage { diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplatesTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplatesTable.tsx index d556424a2..623b731d8 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplatesTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplatesTable.tsx @@ -90,7 +90,8 @@ export const IdentityAuthTemplatesTable = ({ handlePopUpOpen }: Props) => { const { data, isPending, isFetching } = useGetIdentityAuthTemplates({ organizationId, limit, - offset + offset, + search: debouncedSearch }); const { templates = [], totalCount = 0 } = data ?? {}; @@ -100,10 +101,6 @@ export const IdentityAuthTemplatesTable = ({ handlePopUpOpen }: Props) => { setPage }); - const filteredTemplates = templates.filter((template) => - template.name.toLowerCase().includes(debouncedSearch.toLowerCase()) - ); - const handleSort = (column: TemplatesOrderBy) => { if (column === orderBy) { setOrderDirection((prev) => @@ -177,7 +174,7 @@ export const IdentityAuthTemplatesTable = ({ handlePopUpOpen }: Props) => { {isPending && } {!isPending && - filteredTemplates?.map((template) => ( + templates?.map((template) => ( { onChangePerPage={handlePerPageChange} /> )} - {!isPending && data && filteredTemplates.length === 0 && ( + {!isPending && data && templates.length === 0 && ( 0 diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx index 48c0e00b0..5763d2279 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm.tsx @@ -22,12 +22,13 @@ import { } from "@app/components/v2"; import { useOrganization, useSubscription } from "@app/context"; import { + MachineIdentityAuthMethod, useAddIdentityLdapAuth, useGetIdentityLdapAuth, useUpdateIdentityLdapAuth } from "@app/hooks/api"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; -import { useGetIdentityAuthTemplatesByOrgId } from "@app/hooks/api/identityAuthTemplates/queries"; +import { useGetAvailableTemplates } from "@app/hooks/api/identityAuthTemplates/queries"; import { UsePopUpState } from "@app/hooks/usePopUp"; import { IdentityFormTab } from "./types"; @@ -141,7 +142,7 @@ export const IdentityLdapAuthForm = ({ const { mutateAsync: addMutateAsync } = useAddIdentityLdapAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityLdapAuth(); const [tabValue, setTabValue] = useState(IdentityFormTab.Configuration); - const { data: templates } = useGetIdentityAuthTemplatesByOrgId("ldap"); + const { data: templates } = useGetAvailableTemplates(MachineIdentityAuthMethod.LDAP); const { data } = useGetIdentityLdapAuth(identityId ?? "", { enabled: isUpdate diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx index ceb19e5d0..0d8e333e9 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx @@ -156,7 +156,7 @@ export const IdentitySection = withPermission(

Identity Auth Templates

diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/MachineAuthTemplateUsagesModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/MachineAuthTemplateUsagesModal.tsx index 8fdd2239f..f5ab15902 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/MachineAuthTemplateUsagesModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/MachineAuthTemplateUsagesModal.tsx @@ -171,7 +171,7 @@ export const MachineAuthTemplateUsagesModal = ({ - {isPending && } + {isPending && } {!isPending && usages.map((usage) => ( handleUsageToggle(usage.identityId)} />