Addressed pr comments

This commit is contained in:
Carlos Monastyrski
2025-08-03 13:02:20 -03:00
parent 4f0007faa5
commit ebe05661d3
14 changed files with 76 additions and 52 deletions

View File

@@ -25,9 +25,9 @@ export async function up(knex: Knex): Promise<void> {
}
export async function down(knex: Knex): Promise<void> {
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");
});
}

View File

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

View File

@@ -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: {

View File

@@ -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
};
};

View File

@@ -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",

View File

@@ -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,

View File

@@ -16,16 +16,16 @@ export type TTemplateFieldsByMethod = {
};
// Generic base types that use conditional types for type safety
export type TCreateIdentityAuthTemplateDTO<T extends IdentityAuthTemplateMethod = IdentityAuthTemplateMethod> = {
export type TCreateIdentityAuthTemplateDTO = {
name: string;
authMethod: T;
templateFields: TTemplateFieldsByMethod[T];
authMethod: IdentityAuthTemplateMethod;
templateFields: TTemplateFieldsByMethod[IdentityAuthTemplateMethod];
} & Omit<TProjectPermission, "projectId">;
export type TUpdateIdentityAuthTemplateDTO<T extends IdentityAuthTemplateMethod = IdentityAuthTemplateMethod> = {
export type TUpdateIdentityAuthTemplateDTO = {
templateId: string;
name?: string;
templateFields?: Partial<TTemplateFieldsByMethod[T]>;
templateFields?: Partial<TTemplateFieldsByMethod[IdentityAuthTemplateMethod]>;
} & Omit<TProjectPermission, "projectId">;
export type TDeleteIdentityAuthTemplateDTO = {
@@ -39,6 +39,7 @@ export type TGetIdentityAuthTemplateDTO = {
export type TListIdentityAuthTemplatesDTO = {
limit?: number;
offset?: number;
search?: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetTemplatesByAuthMethodDTO = {
@@ -55,5 +56,5 @@ export type TUnlinkTemplateUsageDTO = {
} & Omit<TProjectPermission, "projectId">;
// Specific LDAP types for convenience
export type TCreateLdapTemplateDTO = TCreateIdentityAuthTemplateDTO<IdentityAuthTemplateMethod.LDAP>;
export type TUpdateLdapTemplateDTO = TUpdateIdentityAuthTemplateDTO<IdentityAuthTemplateMethod.LDAP>;
export type TCreateLdapTemplateDTO = TCreateIdentityAuthTemplateDTO;
export type TUpdateLdapTemplateDTO = TUpdateIdentityAuthTemplateDTO;

View File

@@ -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;

View File

@@ -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<IdentityAuthTemplate[]>(
"/api/v1/identities/templates",

View File

@@ -42,6 +42,7 @@ export interface GetIdentityAuthTemplatesDTO {
organizationId: string;
limit?: number;
offset?: number;
search?: string;
}
export interface MachineAuthTemplateUsage {

View File

@@ -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) => {
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="identity-auth-templates" />}
{!isPending &&
filteredTemplates?.map((template) => (
templates?.map((template) => (
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`template-${template.id}`}
@@ -268,7 +265,7 @@ export const IdentityAuthTemplatesTable = ({ handlePopUpOpen }: Props) => {
onChangePerPage={handlePerPageChange}
/>
)}
{!isPending && data && filteredTemplates.length === 0 && (
{!isPending && data && templates.length === 0 && (
<EmptyState
title={
debouncedSearch.trim().length > 0

View File

@@ -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>(IdentityFormTab.Configuration);
const { data: templates } = useGetIdentityAuthTemplatesByOrgId("ldap");
const { data: templates } = useGetAvailableTemplates(MachineIdentityAuthMethod.LDAP);
const { data } = useGetIdentityLdapAuth(identityId ?? "", {
enabled: isUpdate

View File

@@ -156,7 +156,7 @@ export const IdentitySection = withPermission(
<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/overview"
href="https://infisical.com/docs/documentation/platform/identities/auth-templates"
target="_blank"
rel="noopener noreferrer"
>

View File

@@ -171,7 +171,7 @@ export const MachineAuthTemplateUsagesModal = ({
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="template-usages" />}
{isPending && <TableSkeleton columns={3} innerKey="template-usages" />}
{!isPending &&
usages.map((usage) => (
<Tr
@@ -180,7 +180,7 @@ export const MachineAuthTemplateUsagesModal = ({
>
<Td>
<Checkbox
id="select-usage"
id={`select-usage-${usage.identityId}`}
isChecked={selectedUsageIds.includes(usage.identityId)}
onCheckedChange={() => handleUsageToggle(usage.identityId)}
/>