feat: added router and updated available service

This commit is contained in:
=
2025-10-24 12:53:44 +05:30
parent 98e2be477b
commit c63327e56e
5 changed files with 871 additions and 34 deletions

View File

@@ -33,11 +33,13 @@ export enum ApiDocsTags {
LdapAuth = "LDAP Auth",
Groups = "Groups",
Organizations = "Organizations",
OrgIdentityMembership = "Organization Identity Membership",
SubOrganizations = "Sub Organizations",
Projects = "Projects",
ProjectUsers = "Project Users",
ProjectGroups = "Project Groups",
ProjectIdentities = "Project Identities",
ProjectIdentityMembership = "Project Identity Membership",
ProjectRoles = "Project Roles",
ProjectTemplates = "Project Templates",
Environments = "Environments",
@@ -719,6 +721,49 @@ export const ORGANIZATIONS = {
}
} as const;
export const ORG_IDENTITY_MEMBERSHIP = {
CREATE_IDENTITY_MEMBERSHIP: {
identityId: "The ID of the machine identity to create the membership for.",
roles: {
description: "A list of role slugs to assign to the identity organization membership.",
role: "The role slug to assign to the newly created identity organization membership.",
isTemporary:
"Whether the assigned role is temporary. If isTemporary is set true, must provide temporaryMode, temporaryRange and temporaryAccessStartTime.",
temporaryMode: "Type of temporary expiry.",
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s, 2m, 3h, etc.",
temporaryAccessStartTime: "Time to which the temporary access starts."
}
},
UPDATE_IDENTITY_MEMBERSHIP: {
identityId: "The ID of the machine identity to update the membership for.",
roles: {
description: "A list of role slugs to assign to the identity organization membership.",
role: "The role slug to assign to the identity organization membership.",
isTemporary:
"Whether the assigned role is temporary. If isTemporary is set true, must provide temporaryMode, temporaryRange and temporaryAccessStartTime.",
temporaryMode: "Type of temporary expiry.",
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s, 2m, 3h, etc.",
temporaryAccessStartTime: "Time to which the temporary access starts."
}
},
DELETE_IDENTITY_MEMBERSHIP: {
identityId: "The ID of the machine identity to delete the membership from."
},
LIST_IDENTITY_MEMBERSHIPS: {
offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.",
limit: "The number of identity memberships to return.",
identityName: "The text string that identity membership names will be filtered by.",
roles: "The role slugs to filter identity memberships by."
},
GET_IDENTITY_MEMBERSHIP_BY_ID: {
identityId: "The ID of the machine identity to get the membership for."
},
LIST_AVAILABLE_IDENTITIES: {
offset: "The offset to start from. If you enter 10, it will start from the 10th identity.",
limit: "The number of identities to return."
}
} as const;
export const SUB_ORGANIZATIONS = {
CREATE: {
name: "The name of the sub organization to create."
@@ -907,6 +952,55 @@ export const PROJECT_IDENTITIES = {
}
};
export const PROJECT_IDENTITY_MEMBERSHIP = {
CREATE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to create the identity membership for.",
identityId: "The ID of the machine identity to create the membership for.",
roles: {
description: "A list of role slugs to assign to the identity project membership.",
role: "The role slug to assign to the newly created identity project membership.",
isTemporary:
"Whether the assigned role is temporary. If isTemporary is set true, must provide temporaryMode, temporaryRange and temporaryAccessStartTime.",
temporaryMode: "Type of temporary expiry.",
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s, 2m, 3h, etc.",
temporaryAccessStartTime: "Time to which the temporary access starts."
}
},
UPDATE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to update the identity membership for.",
identityId: "The ID of the machine identity to update the membership for.",
roles: {
description: "A list of role slugs to assign to the identity project membership.",
role: "The role slug to assign to the identity project membership.",
isTemporary:
"Whether the assigned role is temporary. If isTemporary is set true, must provide temporaryMode, temporaryRange and temporaryAccessStartTime.",
temporaryMode: "Type of temporary expiry.",
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s, 2m, 3h, etc.",
temporaryAccessStartTime: "Time to which the temporary access starts."
}
},
DELETE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to delete the identity membership from.",
identityId: "The ID of the machine identity to delete the membership from."
},
LIST_IDENTITY_MEMBERSHIPS: {
projectId: "The ID of the project to list identity memberships from.",
offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.",
limit: "The number of identity memberships to return.",
identityName: "The text string that identity membership names will be filtered by.",
roles: "The role slugs to filter identity memberships by."
},
GET_IDENTITY_MEMBERSHIP_BY_ID: {
projectId: "The ID of the project to get the identity membership for.",
identityId: "The ID of the machine identity to get the membership for."
},
LIST_AVAILABLE_IDENTITIES: {
projectId: "The ID of the project to list available identities for.",
offset: "The offset to start from. If you enter 10, it will start from the 10th identity.",
limit: "The number of identities to return."
}
} as const;
export const ENVIRONMENTS = {
CREATE: {
projectId: "The ID of the project to create the environment in.",

View File

@@ -1,9 +1,9 @@
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 { 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";
@@ -25,8 +25,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 +33,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({
@@ -92,6 +93,85 @@ export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProv
}
});
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
}
});
return {
roles: membership.roles.map((el) => ({ ...el, membershipId: membership.id }))
};
}
});
server.route({
method: "DELETE",
url: "/identity-memberships/:identityId",
@@ -101,15 +181,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({
@@ -134,4 +214,208 @@ export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProv
};
}
});
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()
}),
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
}
});
return { identities };
}
});
};

View File

@@ -0,0 +1,438 @@
import { z } from "zod";
import { AccessScope, IdentitiesSchema, MembershipRolesSchema, TemporaryPermissionMode } from "@app/db/schemas";
import { ApiDocsTags, PROJECT_IDENTITY_MEMBERSHIP } from "@app/lib/api-docs";
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";
const sanitizedProjectIdentityMembershipSchema = z.object({
id: z.string().uuid(),
projectId: z.string(),
identityId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export const registerProjectIdentityMembershipRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:projectId/identity-memberships/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectIdentityMembership],
description: "Create project identity membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.projectId),
identityId: z.string().trim().describe(PROJECT_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.identityId)
}),
body: z.object({
roles: z
.array(
z.union([
z.object({
role: z.string().describe(PROJECT_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(false)
.default(false)
.describe(PROJECT_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.isTemporary)
}),
z.object({
role: z.string().describe(PROJECT_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(true)
.describe(PROJECT_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.isTemporary),
temporaryMode: z
.nativeEnum(TemporaryPermissionMode)
.describe(PROJECT_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.temporaryMode),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(PROJECT_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(PROJECT_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.temporaryAccessStartTime)
})
])
)
.describe(PROJECT_IDENTITY_MEMBERSHIP.CREATE_IDENTITY_MEMBERSHIP.roles.description)
.min(1)
}),
response: {
200: z.object({
identityMembership: sanitizedProjectIdentityMembershipSchema
})
}
},
handler: async (req) => {
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: req.body.roles
}
});
return {
identityMembership: { ...membership, identityId: req.params.identityId, projectId: req.params.projectId }
};
}
});
server.route({
method: "PATCH",
url: "/:projectId/identity-memberships/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectIdentityMembership],
description: "Update project identity membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.projectId),
identityId: z.string().trim().describe(PROJECT_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.identityId)
}),
body: z.object({
roles: z
.array(
z.union([
z.object({
role: z.string().describe(PROJECT_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(false)
.default(false)
.describe(PROJECT_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary)
}),
z.object({
role: z.string().describe(PROJECT_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(true)
.describe(PROJECT_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary),
temporaryMode: z
.nativeEnum(TemporaryPermissionMode)
.describe(PROJECT_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryMode),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(PROJECT_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(PROJECT_IDENTITY_MEMBERSHIP.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryAccessStartTime)
})
])
)
.min(1)
.describe(PROJECT_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.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
selector: {
identityId: req.params.identityId
},
data: {
roles: req.body.roles
}
});
return {
roles: membership.roles.map((el) => ({ ...el, membershipId: membership.id }))
};
}
});
server.route({
method: "DELETE",
url: "/:projectId/identity-memberships/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectIdentityMembership],
description: "Delete project identity membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITY_MEMBERSHIP.DELETE_IDENTITY_MEMBERSHIP.projectId),
identityId: z.string().trim().describe(PROJECT_IDENTITY_MEMBERSHIP.DELETE_IDENTITY_MEMBERSHIP.identityId)
}),
response: {
200: z.object({
identityMembership: sanitizedProjectIdentityMembershipSchema
})
}
},
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
}
});
return {
identityMembership: { ...membership, identityId: req.params.identityId, projectId: req.params.projectId }
};
}
});
server.route({
method: "GET",
url: "/:projectId/identity-memberships",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectIdentityMembership],
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(100)
.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: "/:projectId/identity-memberships/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectIdentityMembership],
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()
})
),
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.Project,
orgId: req.permission.orgId,
projectId: req.params.projectId
},
selector: {
identityId: req.params.identityId
}
});
return { identityMembership };
}
});
server.route({
method: "GET",
url: "/:projectId/available-identities",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: false,
tags: [ApiDocsTags.ProjectIdentityMembership],
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(100)
.default(20)
.describe(PROJECT_IDENTITY_MEMBERSHIP.LIST_AVAILABLE_IDENTITIES.limit)
.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
}
});
return { identities };
}
});
};

View File

@@ -92,6 +92,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("id").withSchema(TableName.Identity).as("identityId"),
db.ref("orgId").withSchema(TableName.Identity).as("identityOrgId"),
db.ref("projectId").withSchema(TableName.Identity).as("identityProjectId"),
db.ref("hasDeleteProtection").withSchema(TableName.Identity).as("identityHasDeleteProtection"),
db.ref("slug").withSchema(TableName.Role).as("roleSlug"),
@@ -134,6 +135,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
const {
identityId: actorIdentityId,
identityOrgId,
identityProjectId,
identityHasDeleteProtection,
identityName,
uaId,
@@ -155,7 +157,8 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
name: identityName,
id: actorIdentityId,
hasDeleteProtection: identityHasDeleteProtection,
identityOrgId,
orgId: identityOrgId,
projectId: identityProjectId,
authMethods: buildAuthMethods({
uaId,
awsId,
@@ -281,6 +284,8 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
.select(
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("id").withSchema(TableName.Identity).as("identityId"),
db.ref("orgId").withSchema(TableName.Identity).as("identityOrgId"),
db.ref("projectId").withSchema(TableName.Identity).as("identityProjectId"),
db.ref("hasDeleteProtection").withSchema(TableName.Identity).as("identityHasDeleteProtection"),
db.ref("slug").withSchema(TableName.Role).as("roleSlug"),
@@ -310,13 +315,22 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
data: docs,
key: "id",
parentMapper: (el) => {
const { identityId: actorIdentityId, identityHasDeleteProtection, identityName } = el;
const {
identityId: actorIdentityId,
identityHasDeleteProtection,
identityName,
identityProjectId,
identityOrgId
} = el;
return {
...MembershipsSchema.parse(el),
identityId: actorIdentityId,
identity: {
name: identityName,
id: actorIdentityId,
hasDeleteProtection: identityHasDeleteProtection
hasDeleteProtection: identityHasDeleteProtection,
orgId: identityOrgId,
projectId: identityProjectId
}
};
},
@@ -356,14 +370,18 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
}
};
// this right now only support sub organization
const listAvailableIdentities = async (orgId: string, rootOrgId: string) => {
const listAvailableIdentities = async (scopeData: AccessScopeData, rootOrgId: string) => {
try {
const usersConnectedToOrg = db
const identitesConnectedToOrg = db
.replicaNode()(TableName.Membership)
.whereNotNull(`${TableName.Membership}.actorIdentityId`)
.where(`${TableName.Membership}.scope`, AccessScope.Organization)
.where(`${TableName.Membership}.scopeOrgId`, orgId)
.where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId)
.where(`${TableName.Membership}.scope`, scopeData.scope)
.where((qb) => {
if (scopeData.scope === AccessScope.Project) {
void qb.where(`${TableName.Membership}.scopeProjectId`, scopeData.projectId);
}
})
.select("actorIdentityId");
const docs = await db
@@ -371,8 +389,15 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Membership}.actorIdentityId`)
.where(`${TableName.Membership}.scope`, AccessScope.Organization)
.whereNotNull(`${TableName.Membership}.actorIdentityId`)
.where(`${TableName.Membership}.scopeOrgId`, rootOrgId)
.whereNotIn(`${TableName.Membership}.actorIdentityId`, usersConnectedToOrg)
.where((qb) => {
// if sub org pick from root and if project pick from org of project
if (scopeData.scope === AccessScope.Organization) {
void qb.where(`${TableName.Membership}.scopeOrgId`, rootOrgId);
} else {
void qb.where(`${TableName.Membership}.scopeOrgId`, scopeData.orgId);
}
})
.whereNotIn(`${TableName.Membership}.actorIdentityId`, identitesConnectedToOrg)
.select(
db.ref("id").withSchema(TableName.Identity),
db.ref("name").withSchema(TableName.Identity),

View File

@@ -340,13 +340,9 @@ export const membershipIdentityServiceFactory = ({
await factory.onListMembershipIdentityGuard(dto);
const organizationDetails = await orgDAL.findById(dto.scopeData.orgId);
if (!organizationDetails.rootOrgId) return { identities: [] };
if (dto.permission.rootOrgId === dto.permission.orgId) return { identities: [] };
const identities = await membershipIdentityDAL.listAvailableIdentities(
organizationDetails.id,
organizationDetails.rootOrgId
);
const identities = await membershipIdentityDAL.listAvailableIdentities(dto.scopeData, dto.permission.rootOrgId);
return { identities };
};