Merge branch 'main' of https://github.com/Infisical/infisical into feat/adds-GETtokenAuthTokenById-api-endpoint

This commit is contained in:
Piyush Gupta
2025-11-17 20:22:09 +05:30
422 changed files with 15873 additions and 4805 deletions
@@ -0,0 +1,87 @@
// import { z } from "zod";
// import { getConfig } from "@app/lib/config/env";
// import { ForbiddenRequestError } from "@app/lib/errors";
// import { logger } from "@app/lib/logger";
// import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
// import { AuthMode } from "@app/services/auth/auth-type";
// export const registerBddNockRouter = async (server: FastifyZodProvider) => {
// const checkIfBddNockApiEnabled = () => {
// const appCfg = getConfig();
// // Note: Please note that this API is only available in development mode and only for BDD tests.
// // This endpoint should NEVER BE ENABLED IN PRODUCTION!
// if (appCfg.NODE_ENV !== "development" || !appCfg.isBddNockApiEnabled) {
// throw new ForbiddenRequestError({ message: "BDD Nock API is not enabled" });
// }
// };
// server.route({
// method: "POST",
// url: "/define",
// schema: {
// body: z.object({ definitions: z.unknown().array() }),
// response: {
// 200: z.object({ status: z.string() })
// }
// },
// onRequest: verifyAuth([AuthMode.JWT]),
// handler: async (req) => {
// checkIfBddNockApiEnabled();
// const { body } = req;
// const { definitions } = body;
// logger.info(definitions, "Defining nock");
// const processedDefinitions = definitions.map((definition: unknown) => {
// const { path, ...rest } = definition as Definition;
// return {
// ...rest,
// path:
// path !== undefined && typeof path === "string"
// ? path
// : new RegExp((path as unknown as { regex: string }).regex ?? "")
// } as Definition;
// });
// nock.define(processedDefinitions);
// // Ensure we are activating the nocks, because we could have called `nock.restore()` before this call.
// if (!nock.isActive()) {
// nock.activate();
// }
// return { status: "ok" };
// }
// });
// server.route({
// method: "POST",
// url: "/clean-all",
// schema: {
// response: {
// 200: z.object({ status: z.string() })
// }
// },
// onRequest: verifyAuth([AuthMode.JWT]),
// handler: async () => {
// checkIfBddNockApiEnabled();
// logger.info("Cleaning all nocks");
// nock.cleanAll();
// return { status: "ok" };
// }
// });
// server.route({
// method: "POST",
// url: "/restore",
// schema: {
// response: {
// 200: z.object({ status: z.string() })
// }
// },
// onRequest: verifyAuth([AuthMode.JWT]),
// handler: async () => {
// checkIfBddNockApiEnabled();
// logger.info("Restore network requests from nock");
// nock.restore();
// return { status: "ok" };
// }
// });
// };
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import RE2 from "re2";
import { z } from "zod";
import { CertificatesSchema } from "@app/db/schemas";
@@ -616,4 +617,64 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
};
}
});
server.route({
method: "POST",
url: "/:serialNumber/pkcs12",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: true,
tags: [ApiDocsTags.PkiCertificates],
description: "Download certificate in PKCS12 format",
params: z.object({
serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber)
}),
body: z.object({
password: z
.string()
.min(6, "Password must be at least 6 characters long")
.describe("Password for the keystore (minimum 6 characters)"),
alias: z.string().min(1, "Alias is required").describe("Alias for the certificate in the keystore")
}),
response: {
200: z.any().describe("PKCS12 keystore as binary data")
}
},
handler: async (req, reply) => {
const { pkcs12Data, cert } = await server.services.certificate.getCertPkcs12({
serialNumber: req.params.serialNumber,
password: req.body.password,
alias: req.body.alias,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: cert.projectId,
event: {
type: EventType.EXPORT_CERT_PKCS12,
metadata: {
certId: cert.id,
cn: cert.commonName,
serialNumber: cert.serialNumber
}
}
});
addNoCacheHeaders(reply);
reply.header("Content-Type", "application/octet-stream");
reply.header(
"Content-Disposition",
`attachment; filename="certificate-${req.params.serialNumber.replace(new RE2("[^\\w.-]", "g"), "_")}.p12"`
);
return pkcs12Data;
}
});
};
@@ -19,7 +19,7 @@ import { ProjectIdentityOrderBy } from "@app/services/identity-project/identity-
import { SanitizedProjectSchema } from "../sanitizedSchemas";
export const registerIdentityProjectRouter = async (server: FastifyZodProvider) => {
export const registerDeprecatedIdentityProjectMembershipRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:projectId/identity-memberships/:identityId",
@@ -293,7 +293,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
temporaryAccessEndTime: z.date().nullable().optional()
})
),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
identity: IdentitiesSchema.pick({ name: true, id: true, projectId: true, orgId: true }).extend({
authMethods: z.array(z.string())
}),
project: SanitizedProjectSchema.pick({ name: true, id: true })
@@ -362,7 +362,9 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
temporaryAccessEndTime: z.date().nullable().optional()
})
),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
lastLoginAuthMethod: z.string().nullable().optional(),
lastLoginTime: z.date().nullable().optional(),
identity: IdentitiesSchema.pick({ name: true, id: true, projectId: true, orgId: true }).extend({
authMethods: z.array(z.string())
}),
project: SanitizedProjectSchema.pick({ name: true, id: true })
@@ -1,9 +1,10 @@
import { z } from "zod";
import { AccessScope, TemporaryPermissionMode } from "@app/db/schemas";
import { ApiDocsTags, PROJECT_IDENTITIES } from "@app/lib/api-docs";
import { AccessScope, IdentitiesSchema, MembershipRolesSchema, TemporaryPermissionMode } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, ORG_IDENTITY_MEMBERSHIP } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { writeLimit } from "@app/server/config/rateLimiter";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -15,7 +16,7 @@ const sanitizedOrgIdentityMembershipSchema = z.object({
updatedAt: z.date()
});
export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProvider) => {
export const registerIdentityOrgMembershipRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/identity-memberships/:identityId",
@@ -25,8 +26,7 @@ export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProv
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: true,
// this is hidden so not updating tags
tags: [ApiDocsTags.ProjectIdentities],
tags: [ApiDocsTags.OrgIdentityMembership],
description: "Create org identity membership",
security: [
{
@@ -34,38 +34,40 @@ export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProv
}
],
params: z.object({
identityId: z.string().trim()
identityId: z.string().trim().describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.identityId)
}),
body: z.object({
roles: z
.array(
z.union([
z.object({
role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
role: z.string().describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(false)
.default(false)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role)
.describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.isTemporary)
}),
z.object({
role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
role: z.string().describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(true)
.describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.isTemporary),
temporaryMode: z
.nativeEnum(TemporaryPermissionMode)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
.describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.temporaryMode),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
.describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role)
.describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.temporaryAccessStartTime)
})
])
)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.description)
.max(1)
.describe(ORG_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.description)
.min(1)
}),
response: {
200: z.object({
@@ -86,12 +88,115 @@ export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProv
}
});
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.CREATE_IDENTITY_ORG_MEMBERSHIP,
metadata: {
identityId: req.params.identityId,
roles: req.body.roles
}
}
});
return {
identityMembership: { ...membership, identityId: req.params.identityId, orgId: req.permission.orgId }
};
}
});
server.route({
method: "PATCH",
url: "/identity-memberships/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: true,
tags: [ApiDocsTags.OrgIdentityMembership],
description: "Update org identity membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.identityId)
}),
body: z.object({
roles: z
.array(
z.union([
z.object({
role: z.string().describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(false)
.default(false)
.describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary)
}),
z.object({
role: z.string().describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(true)
.describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary),
temporaryMode: z
.nativeEnum(TemporaryPermissionMode)
.describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryMode),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryAccessStartTime)
})
])
)
.min(1)
.describe(ORG_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.description)
}),
response: {
200: z.object({
roles: MembershipRolesSchema.array()
})
}
},
handler: async (req) => {
const { membership } = await server.services.membershipIdentity.updateMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
selector: {
identityId: req.params.identityId
},
data: {
roles: req.body.roles
}
});
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.UPDATE_IDENTITY_ORG_MEMBERSHIP,
metadata: {
identityId: req.params.identityId,
roles: req.body.roles
}
}
});
return {
roles: membership.roles.map((el) => ({ ...el, membershipId: membership.id }))
};
}
});
server.route({
method: "DELETE",
url: "/identity-memberships/:identityId",
@@ -101,15 +206,15 @@ export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProv
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: true,
tags: [ApiDocsTags.ProjectIdentities],
description: "Delete org identity memberships",
tags: [ApiDocsTags.OrgIdentityMembership],
description: "Delete org identity membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(PROJECT_IDENTITIES.DELETE_IDENTITY_MEMBERSHIP.identityId)
identityId: z.string().trim().describe(ORG_IDENTITY_MEMBERSHIP.DELETE_IDENTITY_MEMBERSHIP.identityId)
}),
response: {
200: z.object({
@@ -129,9 +234,226 @@ export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProv
}
});
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.DELETE_IDENTITY_ORG_MEMBERSHIP,
metadata: {
identityId: req.params.identityId
}
}
});
return {
identityMembership: { ...membership, identityId: req.params.identityId, orgId: req.permission.orgId }
};
}
});
server.route({
method: "GET",
url: "/identity-memberships",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: true,
tags: [ApiDocsTags.OrgIdentityMembership],
description: "List org identity memberships",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
offset: z.coerce
.number()
.min(0)
.default(0)
.describe(ORG_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.offset)
.optional(),
limit: z.coerce
.number()
.min(1)
.max(100)
.default(20)
.describe(ORG_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.limit)
.optional(),
identityName: z
.string()
.trim()
.describe(ORG_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.identityName)
.optional(),
roles: z
.string()
.transform((val) => val.split(",").map((role) => role.trim()))
.describe(ORG_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.roles)
.optional()
}),
response: {
200: z.object({
identityMemberships: z
.object({
id: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
identity: IdentitiesSchema.pick({ name: true, id: true, orgId: true, projectId: true })
})
.array(),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { data: identityMemberships, totalCount } = await server.services.membershipIdentity.listMemberships({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
data: {
offset: req.query.offset,
limit: req.query.limit,
identityName: req.query.identityName,
roles: req.query.roles
}
});
return { identityMemberships, totalCount };
}
});
server.route({
method: "GET",
url: "/identity-memberships/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: true,
tags: [ApiDocsTags.OrgIdentityMembership],
description: "Get org identity membership by identity ID",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(ORG_IDENTITY_MEMBERSHIP.GET_IDENTITY_MEMBERSHIP_BY_ID.identityId)
}),
response: {
200: z.object({
identityMembership: z.object({
id: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
identity: IdentitiesSchema.pick({ name: true, id: true, orgId: true, projectId: true }).extend({
authMethods: z.array(z.string())
})
})
})
}
},
handler: async (req) => {
const identityMembership = await server.services.membershipIdentity.getMembershipByIdentityId({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
selector: {
identityId: req.params.identityId
}
});
return { identityMembership };
}
});
server.route({
method: "GET",
url: "/available-identities",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: false,
tags: [ApiDocsTags.OrgIdentityMembership],
description: "List available identities for org membership",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
offset: z.coerce
.number()
.min(0)
.default(0)
.describe(ORG_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.offset)
.optional(),
limit: z.coerce
.number()
.min(1)
.max(100)
.default(20)
.describe(ORG_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.limit)
.optional(),
identityName: z.string().describe(ORG_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.identityName).optional()
}),
response: {
200: z.object({
identities: IdentitiesSchema.pick({ id: true, name: true }).array()
})
}
},
handler: async (req) => {
const { identities } = await server.services.membershipIdentity.listAvailableIdentities({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
data: {
offset: req.query.offset,
limit: req.query.limit,
identityName: req.query.identityName
}
});
return { identities };
}
});
};
@@ -0,0 +1,493 @@
import { z } from "zod";
import {
AccessScope,
IdentitiesSchema,
IdentityProjectMembershipsSchema,
ProjectMembershipRole,
TemporaryPermissionMode
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, PROJECT_IDENTITIES, PROJECT_IDENTITY_MEMBERSHIP } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerIdentityProjectMembershipRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectIdentities],
description: "Create project identity membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim(),
identityId: z.string().trim()
}),
body: z.object({
// @depreciated
role: z.string().trim().optional().default(ProjectMembershipRole.NoAccess),
roles: z
.array(
z.union([
z.object({
role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(false)
.default(false)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role)
}),
z.object({
role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
temporaryMode: z
.nativeEnum(TemporaryPermissionMode)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role)
})
])
)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.description)
.optional()
}),
response: {
200: z.object({
identityMembership: IdentityProjectMembershipsSchema
})
}
},
handler: async (req) => {
const { role, roles } = req.body;
if (!role && !roles) throw new BadRequestError({ message: "You must provide either role or roles field" });
const { membership } = await server.services.membershipIdentity.createMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
data: {
identityId: req.params.identityId,
roles: roles || [{ role, isTemporary: false }]
}
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: req.params.projectId,
event: {
type: EventType.CREATE_IDENTITY_PROJECT_MEMBERSHIP,
metadata: {
identityId: req.params.identityId,
roles: req.body.roles
}
}
});
return {
identityMembership: { ...membership, identityId: req.params.identityId, projectId: req.params.projectId }
};
}
});
server.route({
method: "PATCH",
url: "/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectIdentities],
description: "Update project identity memberships",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.projectId),
identityId: z.string().trim().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.identityId)
}),
body: z.object({
roles: z
.array(
z.union([
z.object({
role: z.string().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(false)
.default(false)
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary)
}),
z.object({
role: z.string().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary),
temporaryMode: z
.nativeEnum(TemporaryPermissionMode)
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryMode),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryAccessStartTime)
})
])
)
.min(1)
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.description)
}),
response: {
200: z.object({
identityMembership: IdentityProjectMembershipsSchema
})
}
},
handler: async (req) => {
const { membership } = await server.services.membershipIdentity.updateMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
selector: {
identityId: req.params.identityId
},
data: {
roles: req.body.roles
}
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: req.params.projectId,
event: {
type: EventType.UPDATE_IDENTITY_PROJECT_MEMBERSHIP,
metadata: {
identityId: req.params.identityId,
roles: req.body.roles
}
}
});
return {
identityMembership: { ...membership, identityId: req.params.identityId, projectId: req.params.projectId }
};
}
});
server.route({
method: "DELETE",
url: "/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectIdentities],
description: "Delete project identity memberships",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITIES.DELETE_IDENTITY_MEMBERSHIP.projectId),
identityId: z.string().trim().describe(PROJECT_IDENTITIES.DELETE_IDENTITY_MEMBERSHIP.identityId)
}),
response: {
200: z.object({
identityMembership: IdentityProjectMembershipsSchema
})
}
},
handler: async (req) => {
const { membership } = await server.services.membershipIdentity.deleteMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
selector: {
identityId: req.params.identityId
}
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: req.params.projectId,
event: {
type: EventType.DELETE_IDENTITY_PROJECT_MEMBERSHIP,
metadata: {
identityId: req.params.identityId
}
}
});
return {
identityMembership: { ...membership, identityId: req.params.identityId, projectId: req.params.projectId }
};
}
});
server.route({
method: "GET",
url: "/identities",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.IdentityProjectMembership],
description: "List project identity memberships",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.projectId)
}),
querystring: z.object({
offset: z.coerce
.number()
.min(0)
.default(0)
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.offset)
.optional(),
limit: z.coerce
.number()
.min(1)
.max(1000)
.default(20)
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.limit)
.optional(),
identityName: z
.string()
.trim()
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.identityName)
.optional(),
roles: z
.string()
.transform((val) => val.split(",").map((role) => role.trim()))
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_IDENTITY_MEMBERSHIPS.roles)
.optional()
}),
response: {
200: z.object({
identityMemberships: z
.object({
id: z.string(),
identityId: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
identity: IdentitiesSchema.pick({ name: true, id: true, orgId: true, projectId: true })
})
.array(),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { data: identityMemberships, totalCount } = await server.services.membershipIdentity.listMemberships({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
data: {
offset: req.query.offset,
limit: req.query.limit,
identityName: req.query.identityName,
roles: req.query.roles
}
});
return { identityMemberships, totalCount };
}
});
server.route({
method: "GET",
url: "/identities/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.IdentityProjectMembership],
description: "Get project identity membership by identity ID",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITY_MEMBERSHIP.GET_IDENTITY_MEMBERSHIP_BY_ID.projectId),
identityId: z.string().trim().describe(PROJECT_IDENTITY_MEMBERSHIP.GET_IDENTITY_MEMBERSHIP_BY_ID.identityId)
}),
response: {
200: z.object({
identityMembership: z.object({
id: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
lastLoginAuthMethod: z.string().nullable().optional(),
lastLoginTime: z.date().nullable().optional(),
identity: IdentitiesSchema.pick({ name: true, id: true, orgId: true, projectId: true }).extend({
authMethods: z.array(z.string()),
metadata: z
.object({
id: z.string().trim().min(1),
key: z.string().trim().min(1),
value: z.string().trim().min(1)
})
.array()
.optional()
})
})
})
}
},
handler: async (req) => {
const identityMembership = await server.services.membershipIdentity.getMembershipByIdentityId({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
selector: {
identityId: req.params.identityId
}
});
return { identityMembership };
}
});
server.route({
method: "GET",
url: "/available-identities",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: false,
tags: [ApiDocsTags.IdentityProjectMembership],
description: "List available identities for project membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.projectId)
}),
querystring: z.object({
offset: z.coerce
.number()
.min(0)
.default(0)
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.offset)
.optional(),
limit: z.coerce
.number()
.min(1)
.max(1000)
.default(20)
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.limit)
.optional(),
identityName: z
.string()
.trim()
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.identityName)
.optional()
}),
response: {
200: z.object({
identities: IdentitiesSchema.pick({ id: true, name: true }).array()
})
}
},
handler: async (req) => {
const { identities } = await server.services.membershipIdentity.listAvailableIdentities({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
data: {
offset: req.query.offset,
limit: req.query.limit,
identityName: req.query.identityName
}
});
return { identities };
}
});
};
@@ -60,7 +60,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const identity = await server.services.identity.createIdentity({
const identity = await server.services.identityV1.createIdentity({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -136,7 +136,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const identity = await server.services.identity.updateIdentity({
const identity = await server.services.identityV1.updateIdentity({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -189,7 +189,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const identity = await server.services.identity.deleteIdentity({
const identity = await server.services.identityV1.deleteIdentity({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -258,7 +258,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const identity = await server.services.identity.getIdentityById({
const identity = await server.services.identityV1.getIdentityById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -308,7 +308,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const { identityMemberships, totalCount } = await server.services.identity.listOrgIdentities({
const { identityMemberships, totalCount } = await server.services.identityV1.listOrgIdentities({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -402,7 +402,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const { identityMemberships, totalCount } = await server.services.identity.searchOrgIdentities({
const { identityMemberships, totalCount } = await server.services.identityV1.searchOrgIdentities({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -468,7 +468,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const identityMemberships = await server.services.identity.listProjectIdentitiesByIdentityId({
const identityMemberships = await server.services.identityV1.listProjectIdentitiesByIdentityId({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
+26 -5
View File
@@ -8,12 +8,14 @@ import { registerSecretSyncRouter, SECRET_SYNC_REGISTER_ROUTER_MAP } from "@app/
import { registerAdminRouter } from "./admin-router";
import { registerAuthRoutes } from "./auth-router";
// import { registerBddNockRouter } from "./bdd-nock-router";
import { registerProjectBotRouter } from "./bot-router";
import { registerCaRouter } from "./certificate-authority-router";
import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers";
import { registerCertificateProfilesRouter } from "./certificate-profiles-router";
import { registerCertRouter } from "./certificate-router";
import { registerCertificateTemplateRouter } from "./certificate-template-router";
import { registerDeprecatedIdentityProjectMembershipRouter } from "./deprecated-identity-project-membership-router";
import { registerDeprecatedProjectEnvRouter } from "./deprecated-project-env-router";
import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router";
import { registerDeprecatedProjectRouter } from "./deprecated-project-router";
@@ -33,8 +35,8 @@ import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-rou
import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router";
import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router";
import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
import { registerOrgIdentityMembershipRouter } from "./identity-org-membership-router";
import { registerIdentityProjectRouter } from "./identity-project-router";
import { registerIdentityOrgMembershipRouter } from "./identity-org-membership-router";
import { registerIdentityProjectMembershipRouter } from "./identity-project-membership-router";
import { registerIdentityRouter } from "./identity-router";
import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-router";
import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router";
@@ -45,6 +47,7 @@ import { registerInviteOrgRouter } from "./invite-org-router";
import { registerMicrosoftTeamsRouter } from "./microsoft-teams-router";
import { registerNotificationRouter } from "./notification-router";
import { registerOrgAdminRouter } from "./org-admin-router";
import { registerOrgIdentityRouter } from "./org-identity-router";
import { registerOrgRouter } from "./organization-router";
import { registerPasswordRouter } from "./password-router";
import { registerPkiAlertRouter } from "./pki-alert-router";
@@ -52,6 +55,7 @@ import { registerPkiCollectionRouter } from "./pki-collection-router";
import { registerPkiSubscriberRouter } from "./pki-subscriber-router";
import { PKI_SYNC_REGISTER_ROUTER_MAP, registerPkiSyncRouter } from "./pki-sync-routers";
import { registerProjectEnvRouter } from "./project-env-router";
import { registerProjectIdentityRouter } from "./project-identity-router";
import { registerProjectKeyRouter } from "./project-key-router";
import { registerProjectMembershipRouter } from "./project-membership-router";
import { registerProjectRouter } from "./project-router";
@@ -90,8 +94,14 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
{ prefix: "/auth" }
);
await server.register(registerPasswordRouter, { prefix: "/password" });
await server.register(registerOrgRouter, { prefix: "/organization" });
await server.register(registerOrgIdentityMembershipRouter, { prefix: "/organization" });
await server.register(
async (orgRouter) => {
await orgRouter.register(registerOrgRouter);
await orgRouter.register(registerOrgIdentityRouter);
await orgRouter.register(registerIdentityOrgMembershipRouter);
},
{ prefix: "/organization" }
);
await server.register(registerAdminRouter, { prefix: "/admin" });
await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" });
await server.register(registerUserRouter, { prefix: "/user" });
@@ -126,14 +136,19 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
async (projectRouter) => {
await projectRouter.register(registerProjectRouter);
await projectRouter.register(registerProjectMembershipRouter);
await projectRouter.register(registerProjectIdentityRouter);
await projectRouter.register(registerProjectEnvRouter);
await projectRouter.register(registerSecretTagRouter);
await projectRouter.register(registerGroupProjectRouter);
await projectRouter.register(registerIdentityProjectRouter);
await projectRouter.register(registerDeprecatedIdentityProjectMembershipRouter);
},
{ prefix: "/projects" }
);
await server.register(registerIdentityProjectMembershipRouter, {
prefix: "/projects/:projectId/memberships"
});
await server.register(
async (pkiRouter) => {
await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
@@ -223,4 +238,10 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerEventRouter, { prefix: "/events" });
await server.register(registerUpgradePathRouter, { prefix: "/upgrade-path" });
// Note: This is a special route for BDD tests. It's only available in development mode and only for BDD tests.
// This route should NEVER BE ENABLED IN PRODUCTION!
// if (getConfig().isBddNockApiEnabled) {
// await server.register(registerBddNockRouter, { prefix: "/bdd-nock" });
// }
};
@@ -0,0 +1,286 @@
import { z } from "zod";
import { AccessScope, IdentitiesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, IDENTITIES } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const metadataSchema = z.object({
key: z.string().trim().min(1, "Metadata key cannot be empty"),
value: z.string().trim().min(1, "Metadata value cannot be empty")
});
const sanitizedIdentitySchema = IdentitiesSchema.pick({
id: true,
name: true,
orgId: true,
projectId: true,
createdAt: true,
updatedAt: true,
hasDeleteProtection: true
}).extend({
authMethods: z.array(z.string()).optional(),
metadata: z.array(metadataSchema).optional()
});
export const registerOrgIdentityRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/identities",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
tags: [ApiDocsTags.Identities],
description: "Create an identity",
security: [
{
bearerAuth: []
}
],
body: z.object({
name: z.string().trim().min(1).describe(IDENTITIES.CREATE.name),
hasDeleteProtection: z.boolean().default(false).describe(IDENTITIES.CREATE.hasDeleteProtection),
metadata: z.array(metadataSchema).optional().describe(IDENTITIES.CREATE.metadata)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.createIdentity({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
data: {
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
});
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.CREATE_IDENTITY,
metadata: {
identityId: identity.id,
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
}
});
return { identity };
}
});
server.route({
method: "PATCH",
url: "/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
tags: [ApiDocsTags.Identities],
description: "Update an identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(IDENTITIES.UPDATE.identityId)
}),
body: z.object({
name: z.string().trim().min(1).optional().describe(IDENTITIES.UPDATE.name),
hasDeleteProtection: z.boolean().optional().describe(IDENTITIES.UPDATE.hasDeleteProtection),
metadata: z.array(metadataSchema).optional().describe(IDENTITIES.UPDATE.metadata)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.updateIdentity({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
selector: {
identityId: req.params.identityId
},
data: {
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
});
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.UPDATE_IDENTITY,
metadata: {
identityId: req.params.identityId,
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
}
});
return { identity };
}
});
server.route({
method: "DELETE",
url: "/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
tags: [ApiDocsTags.Identities],
description: "Delete an identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(IDENTITIES.DELETE.identityId)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.deleteIdentity({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
selector: {
identityId: req.params.identityId
}
});
await server.services.auditLog.createAuditLog({
orgId: req.permission.orgId,
...req.auditLogInfo,
event: {
type: EventType.DELETE_IDENTITY,
metadata: {
identityId: req.params.identityId
}
}
});
return { identity };
}
});
server.route({
method: "GET",
url: "/identities/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
tags: [ApiDocsTags.Identities],
description: "Get an identity by ID",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(IDENTITIES.GET_BY_ID.identityId)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.getIdentityById({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
selector: {
identityId: req.params.identityId
}
});
return { identity };
}
});
server.route({
method: "GET",
url: "/identities",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
tags: [ApiDocsTags.Identities],
description: "List identities",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
offset: z.coerce.number().min(0).default(0).describe(IDENTITIES.LIST.offset).optional(),
limit: z.coerce.number().min(1).max(1000).default(20).describe(IDENTITIES.LIST.limit).optional(),
search: z.string().trim().describe(IDENTITIES.LIST.search).optional()
}),
response: {
200: z.object({
identities: z.array(sanitizedIdentitySchema),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { docs: identities, count: totalCount } = await server.services.identityV2.listIdentities({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
data: {
offset: req.query.offset,
limit: req.query.limit,
search: req.query.search
}
});
return { identities, totalCount };
}
});
};
@@ -0,0 +1,313 @@
import { z } from "zod";
import { AccessScope, IdentitiesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, IDENTITIES } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const metadataSchema = z.object({
key: z.string().trim().min(1, "Metadata key cannot be empty"),
value: z.string().trim().min(1, "Metadata value cannot be empty")
});
const sanitizedIdentitySchema = IdentitiesSchema.pick({
id: true,
name: true,
orgId: true,
projectId: true,
createdAt: true,
updatedAt: true,
hasDeleteProtection: true
}).extend({
activeLockoutAuthMethods: z.string().array().optional(),
authMethods: z.string().array().optional(),
metadata: z
.object({
key: z.string(),
value: z.string(),
id: z.string()
})
.array()
.optional()
});
export const registerProjectIdentityRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:projectId/identities",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.Identities],
description: "Create an identity in a project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe("The ID of the project to create the identity in")
}),
body: z.object({
name: z.string().trim().min(1).describe(IDENTITIES.CREATE.name),
hasDeleteProtection: z.boolean().default(false).describe(IDENTITIES.CREATE.hasDeleteProtection),
metadata: z.array(metadataSchema).optional().describe(IDENTITIES.CREATE.metadata)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.createIdentity({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
data: {
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
});
await server.services.auditLog.createAuditLog({
projectId: req.params.projectId,
...req.auditLogInfo,
event: {
type: EventType.CREATE_IDENTITY,
metadata: {
identityId: identity.id,
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
}
});
return { identity };
}
});
server.route({
method: "PATCH",
url: "/:projectId/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.Identities],
description: "Update an identity in a project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe("The ID of the project"),
identityId: z.string().trim().describe(IDENTITIES.UPDATE.identityId)
}),
body: z.object({
name: z.string().trim().min(1).optional().describe(IDENTITIES.UPDATE.name),
hasDeleteProtection: z.boolean().optional().describe(IDENTITIES.UPDATE.hasDeleteProtection),
metadata: z.array(metadataSchema).optional().describe(IDENTITIES.UPDATE.metadata)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.updateIdentity({
permission: req.permission,
scopeData: {
scope: AccessScope.Project,
projectId: req.params.projectId,
orgId: req.permission.orgId
},
selector: {
identityId: req.params.identityId
},
data: {
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
});
await server.services.auditLog.createAuditLog({
projectId: req.params.projectId,
...req.auditLogInfo,
event: {
type: EventType.UPDATE_IDENTITY,
metadata: {
identityId: req.params.identityId,
name: req.body.name,
hasDeleteProtection: req.body.hasDeleteProtection,
metadata: req.body.metadata
}
}
});
return { identity };
}
});
server.route({
method: "DELETE",
url: "/:projectId/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.Identities],
description: "Delete an identity from a project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe("The ID of the project"),
identityId: z.string().trim().describe(IDENTITIES.DELETE.identityId)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.deleteIdentity({
permission: req.permission,
scopeData: {
orgId: req.permission.orgId,
scope: AccessScope.Project,
projectId: req.params.projectId
},
selector: {
identityId: req.params.identityId
}
});
await server.services.auditLog.createAuditLog({
projectId: req.params.projectId,
...req.auditLogInfo,
event: {
type: EventType.DELETE_IDENTITY,
metadata: {
identityId: req.params.identityId
}
}
});
return { identity };
}
});
server.route({
method: "GET",
url: "/:projectId/identities/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.Identities],
description: "Get an identity by ID in a project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe("The ID of the project"),
identityId: z.string().trim().describe(IDENTITIES.GET_BY_ID.identityId)
}),
response: {
200: z.object({
identity: sanitizedIdentitySchema
})
}
},
handler: async (req) => {
const { identity } = await server.services.identityV2.getIdentityById({
permission: req.permission,
scopeData: {
orgId: req.permission.orgId,
scope: AccessScope.Project,
projectId: req.params.projectId
},
selector: {
identityId: req.params.identityId
}
});
return { identity };
}
});
server.route({
method: "GET",
url: "/:projectId/identities",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.Identities],
description: "List identities in a project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe("The ID of the project")
}),
querystring: z.object({
offset: z.coerce.number().min(0).default(0).describe(IDENTITIES.LIST.offset).optional(),
limit: z.coerce.number().min(1).max(1000).default(20).describe(IDENTITIES.LIST.limit).optional(),
search: z.string().trim().describe(IDENTITIES.LIST.search).optional()
}),
response: {
200: z.object({
identities: z.array(sanitizedIdentitySchema),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { docs: identities, count: totalCount } = await server.services.identityV2.listIdentities({
permission: req.permission,
scopeData: {
orgId: req.permission.orgId,
scope: AccessScope.Project,
projectId: req.params.projectId
},
data: {
offset: req.query.offset,
limit: req.query.limit,
search: req.query.search
}
});
return { identities, totalCount };
}
});
};