Merge pull request #1842 from akhilmhdh/feat/membership-by-id

Endpoints for retreiving membership details
This commit is contained in:
Akhil Mohan
2024-05-19 23:51:30 +05:30
committed by GitHub
22 changed files with 459 additions and 99 deletions
+64 -30
View File
@@ -148,36 +148,6 @@ export const PROJECTS = {
name: "The new name of the project.", name: "The new name of the project.",
autoCapitalization: "Disable or enable auto-capitalization for the project." autoCapitalization: "Disable or enable auto-capitalization for the project."
}, },
INVITE_MEMBER: {
projectId: "The ID of the project to invite the member to.",
emails: "A list of organization member emails to invite to the project.",
usernames: "A list of usernames to invite to the project."
},
REMOVE_MEMBER: {
projectId: "The ID of the project to remove the member from.",
emails: "A list of organization member emails to remove from the project.",
usernames: "A list of usernames to remove from the project."
},
GET_USER_MEMBERSHIPS: {
workspaceId: "The ID of the project to get memberships from."
},
UPDATE_USER_MEMBERSHIP: {
workspaceId: "The ID of the project to update the membership for.",
membershipId: "The ID of the membership to update.",
roles: "A list of roles to update the membership to."
},
LIST_IDENTITY_MEMBERSHIPS: {
projectId: "The ID of the project to get identity memberships from."
},
UPDATE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to update the identity membership for.",
identityId: "The ID of the identity to update the membership for.",
roles: "A list of roles to update the membership to."
},
DELETE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to delete the identity membership from.",
identityId: "The ID of the identity to delete the membership from."
},
GET_KEY: { GET_KEY: {
workspaceId: "The ID of the project to get the key from." workspaceId: "The ID of the project to get the key from."
}, },
@@ -216,6 +186,70 @@ export const PROJECTS = {
} }
} as const; } as const;
export const PROJECT_USERS = {
INVITE_MEMBER: {
projectId: "The ID of the project to invite the member to.",
emails: "A list of organization member emails to invite to the project.",
usernames: "A list of usernames to invite to the project."
},
REMOVE_MEMBER: {
projectId: "The ID of the project to remove the member from.",
emails: "A list of organization member emails to remove from the project.",
usernames: "A list of usernames to remove from the project."
},
GET_USER_MEMBERSHIPS: {
workspaceId: "The ID of the project to get memberships from."
},
GET_USER_MEMBERSHIP: {
workspaceId: "The ID of the project to get memberships from.",
username: "The username to get project membership of. Email is the default username."
},
UPDATE_USER_MEMBERSHIP: {
workspaceId: "The ID of the project to update the membership for.",
membershipId: "The ID of the membership to update.",
roles: "A list of roles to update the membership to."
}
};
export const PROJECT_IDENTITIES = {
LIST_IDENTITY_MEMBERSHIPS: {
projectId: "The ID of the project to get identity memberships from."
},
GET_IDENTITY_MEMBERSHIP_BY_ID: {
identityId: "The ID of the identity to get the membership for.",
projectId: "The ID of the project to get the identity membership for."
},
UPDATE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to update the identity membership for.",
identityId: "The ID of the 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 newly created identity project membership.",
isTemporary: "Whether the assigned role is temporary.",
temporaryMode: "Type of temporary expiry.",
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s,2m,3h",
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 identity to delete the membership from."
},
CREATE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to create the identity membership from.",
identityId: "The ID of the identity to create the membership from.",
role: "The role slug to assign to the newly created identity project membership.",
roles: {
description: "A list of role slugs to assign to the newly created identity project membership.",
role: "The role slug to assign to the newly created identity project membership.",
isTemporary: "Whether the assigned role is temporary.",
temporaryMode: "Type of temporary expiry.",
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s,2m,3h",
temporaryAccessStartTime: "Time to which the temporary access starts"
}
}
};
export const ENVIRONMENTS = { export const ENVIRONMENTS = {
CREATE: { CREATE: {
workspaceId: "The ID of the project to create the environment in.", workspaceId: "The ID of the project to create the environment in.",
@@ -9,7 +9,7 @@ import {
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { PROJECTS } from "@app/lib/api-docs"; import { PROJECT_USERS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -30,7 +30,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
} }
], ],
params: z.object({ params: z.object({
workspaceId: z.string().trim().describe(PROJECTS.GET_USER_MEMBERSHIPS.workspaceId) workspaceId: z.string().trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIPS.workspaceId)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -74,6 +74,66 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
} }
}); });
server.route({
method: "POST",
url: "/:workspaceId/memberships/details",
config: {
rateLimit: readLimit
},
schema: {
description: "Return project user memberships",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.workspaceId)
}),
body: z.object({
username: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.username)
}),
response: {
200: z.object({
membership: ProjectMembershipsSchema.extend({
user: UsersSchema.pick({
email: true,
firstName: true,
lastName: true,
id: true
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
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()
})
)
}).omit({ createdAt: true, updatedAt: true })
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const membership = await server.services.projectMembership.getProjectMembershipByUsername({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.workspaceId,
username: req.body.username
});
return { membership };
}
});
server.route({ server.route({
method: "POST", method: "POST",
url: "/:workspaceId/memberships", url: "/:workspaceId/memberships",
@@ -142,8 +202,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
} }
], ],
params: z.object({ params: z.object({
workspaceId: z.string().trim().describe(PROJECTS.UPDATE_USER_MEMBERSHIP.workspaceId), workspaceId: z.string().trim().describe(PROJECT_USERS.UPDATE_USER_MEMBERSHIP.workspaceId),
membershipId: z.string().trim().describe(PROJECTS.UPDATE_USER_MEMBERSHIP.membershipId) membershipId: z.string().trim().describe(PROJECT_USERS.UPDATE_USER_MEMBERSHIP.membershipId)
}), }),
body: z.object({ body: z.object({
roles: z roles: z
@@ -164,7 +224,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
) )
.min(1) .min(1)
.refine((data) => data.some(({ isTemporary }) => !isTemporary), "At least one long lived role is required") .refine((data) => data.some(({ isTemporary }) => !isTemporary), "At least one long lived role is required")
.describe(PROJECTS.UPDATE_USER_MEMBERSHIP.roles) .describe(PROJECT_USERS.UPDATE_USER_MEMBERSHIP.roles)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -7,7 +7,8 @@ import {
ProjectMembershipRole, ProjectMembershipRole,
ProjectUserMembershipRolesSchema ProjectUserMembershipRolesSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { PROJECTS } from "@app/lib/api-docs"; import { PROJECT_IDENTITIES } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -22,12 +23,48 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "Create project identity membership",
security: [
{
bearerAuth: []
}
],
params: z.object({ params: z.object({
projectId: z.string().trim(), projectId: z.string().trim(),
identityId: z.string().trim() identityId: z.string().trim()
}), }),
body: z.object({ body: z.object({
role: z.string().trim().min(1).default(ProjectMembershipRole.NoAccess) // @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(ProjectUserMembershipTemporaryMode)
.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: { response: {
200: z.object({ 200: z.object({
@@ -36,6 +73,9 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
} }
}, },
handler: async (req) => { handler: async (req) => {
const { role, roles } = req.body;
if (!role && !roles) throw new BadRequestError({ message: "You must provide either role or roles field" });
const identityMembership = await server.services.identityProject.createProjectIdentity({ const identityMembership = await server.services.identityProject.createProjectIdentity({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
@@ -43,7 +83,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
identityId: req.params.identityId, identityId: req.params.identityId,
projectId: req.params.projectId, projectId: req.params.projectId,
role: req.body.role roles: roles || [{ role }]
}); });
return { identityMembership }; return { identityMembership };
} }
@@ -64,28 +104,39 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
} }
], ],
params: z.object({ params: z.object({
projectId: z.string().trim().describe(PROJECTS.UPDATE_IDENTITY_MEMBERSHIP.projectId), projectId: z.string().trim().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.projectId),
identityId: z.string().trim().describe(PROJECTS.UPDATE_IDENTITY_MEMBERSHIP.identityId) identityId: z.string().trim().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.identityId)
}), }),
body: z.object({ body: z.object({
roles: z roles: z
.array( .array(
z.union([ z.union([
z.object({ z.object({
role: z.string(), role: z.string().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z.literal(false).default(false) isTemporary: z
.literal(false)
.default(false)
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary)
}), }),
z.object({ z.object({
role: z.string(), role: z.string().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z.literal(true), isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary),
temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), temporaryMode: z
temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), .nativeEnum(ProjectUserMembershipTemporaryMode)
temporaryAccessStartTime: z.string().datetime() .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) .min(1)
.describe(PROJECTS.UPDATE_IDENTITY_MEMBERSHIP.roles) .describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.description)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -122,8 +173,8 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
} }
], ],
params: z.object({ params: z.object({
projectId: z.string().trim().describe(PROJECTS.DELETE_IDENTITY_MEMBERSHIP.projectId), projectId: z.string().trim().describe(PROJECT_IDENTITIES.DELETE_IDENTITY_MEMBERSHIP.projectId),
identityId: z.string().trim().describe(PROJECTS.DELETE_IDENTITY_MEMBERSHIP.identityId) identityId: z.string().trim().describe(PROJECT_IDENTITIES.DELETE_IDENTITY_MEMBERSHIP.identityId)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -159,7 +210,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
} }
], ],
params: z.object({ params: z.object({
projectId: z.string().trim().describe(PROJECTS.LIST_IDENTITY_MEMBERSHIPS.projectId) projectId: z.string().trim().describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.projectId)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -200,4 +251,61 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
return { identityMemberships }; return { identityMemberships };
} }
}); });
server.route({
method: "GET",
url: "/:projectId/identity-memberships/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Return project identity membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITIES.GET_IDENTITY_MEMBERSHIP_BY_ID.projectId),
identityId: z.string().trim().describe(PROJECT_IDENTITIES.GET_IDENTITY_MEMBERSHIP_BY_ID.identityId)
}),
response: {
200: z.object({
identityMembership: 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, authMethod: true })
})
})
}
},
handler: async (req) => {
const identityMembership = await server.services.identityProject.getProjectIdentityByIdentityId({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.params.projectId,
identityId: req.params.identityId
});
return { identityMembership };
}
});
}; };
@@ -2,7 +2,7 @@ import { z } from "zod";
import { ProjectMembershipsSchema } from "@app/db/schemas"; import { ProjectMembershipsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { PROJECTS } from "@app/lib/api-docs"; import { PROJECT_USERS } from "@app/lib/api-docs";
import { writeLimit } from "@app/server/config/rateLimiter"; import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -22,11 +22,11 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
} }
], ],
params: z.object({ params: z.object({
projectId: z.string().describe(PROJECTS.INVITE_MEMBER.projectId) projectId: z.string().describe(PROJECT_USERS.INVITE_MEMBER.projectId)
}), }),
body: z.object({ body: z.object({
emails: z.string().email().array().default([]).describe(PROJECTS.INVITE_MEMBER.emails), emails: z.string().email().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.emails),
usernames: z.string().array().default([]).describe(PROJECTS.INVITE_MEMBER.usernames) usernames: z.string().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.usernames)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -77,11 +77,11 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
} }
], ],
params: z.object({ params: z.object({
projectId: z.string().describe(PROJECTS.REMOVE_MEMBER.projectId) projectId: z.string().describe(PROJECT_USERS.REMOVE_MEMBER.projectId)
}), }),
body: z.object({ body: z.object({
emails: z.string().email().array().default([]).describe(PROJECTS.REMOVE_MEMBER.emails), emails: z.string().email().array().default([]).describe(PROJECT_USERS.REMOVE_MEMBER.emails),
usernames: z.string().array().default([]).describe(PROJECTS.REMOVE_MEMBER.usernames) usernames: z.string().array().default([]).describe(PROJECT_USERS.REMOVE_MEMBER.usernames)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -10,11 +10,16 @@ export type TIdentityProjectDALFactory = ReturnType<typeof identityProjectDALFac
export const identityProjectDALFactory = (db: TDbClient) => { export const identityProjectDALFactory = (db: TDbClient) => {
const identityProjectOrm = ormify(db, TableName.IdentityProjectMembership); const identityProjectOrm = ormify(db, TableName.IdentityProjectMembership);
const findByProjectId = async (projectId: string, tx?: Knex) => { const findByProjectId = async (projectId: string, filter: { identityId?: string } = {}, tx?: Knex) => {
try { try {
const docs = await (tx || db)(TableName.IdentityProjectMembership) const docs = await (tx || db)(TableName.IdentityProjectMembership)
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId) .where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
.join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`) .join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`)
.where((qb) => {
if (filter.identityId) {
void qb.where("identityId", filter.identityId);
}
})
.join( .join(
TableName.IdentityProjectMembershipRole, TableName.IdentityProjectMembershipRole,
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`, `${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
@@ -18,6 +18,7 @@ import { TIdentityProjectMembershipRoleDALFactory } from "./identity-project-mem
import { import {
TCreateProjectIdentityDTO, TCreateProjectIdentityDTO,
TDeleteProjectIdentityDTO, TDeleteProjectIdentityDTO,
TGetProjectIdentityByIdentityIdDTO,
TListProjectIdentityDTO, TListProjectIdentityDTO,
TUpdateProjectIdentityDTO TUpdateProjectIdentityDTO
} from "./identity-project-types"; } from "./identity-project-types";
@@ -51,7 +52,7 @@ export const identityProjectServiceFactory = ({
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
projectId, projectId,
role roles
}: TCreateProjectIdentityDTO) => { }: TCreateProjectIdentityDTO) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -78,18 +79,33 @@ export const identityProjectServiceFactory = ({
message: `Failed to find identity with id ${identityId}` message: `Failed to find identity with id ${identityId}`
}); });
const { permission: rolePermission, role: customRole } = await permissionService.getProjectPermissionByRole( for await (const { role: requestedRoleChange } of roles) {
role, const { permission: rolePermission } = await permissionService.getProjectPermissionByRole(
project.id requestedRoleChange,
projectId
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredPriviledges) {
throw new ForbiddenRequestError({ message: "Failed to change to a more privileged role" });
}
}
// validate custom roles input
const customInputRoles = roles.filter(
({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
); );
const hasCustomRole = Boolean(customInputRoles.length);
const customRoles = hasCustomRole
? await projectRoleDAL.find({
projectId,
$in: { slug: customInputRoles.map(({ role }) => role) }
})
: [];
if (customRoles.length !== customInputRoles.length) throw new BadRequestError({ message: "Custom role not found" });
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
if (!hasPriviledge)
throw new ForbiddenRequestError({
message: "Failed to add identity to project with more privileged role"
});
const isCustomRole = Boolean(customRole);
const projectIdentity = await identityProjectDAL.transaction(async (tx) => { const projectIdentity = await identityProjectDAL.transaction(async (tx) => {
const identityProjectMembership = await identityProjectDAL.create( const identityProjectMembership = await identityProjectDAL.create(
{ {
@@ -98,16 +114,32 @@ export const identityProjectServiceFactory = ({
}, },
tx tx
); );
const sanitizedProjectMembershipRoles = roles.map((inputRole) => {
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
if (!inputRole.isTemporary) {
return {
projectMembershipId: identityProjectMembership.id,
role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null
};
}
await identityProjectMembershipRoleDAL.create( // check cron or relative here later for now its just relative
{ const relativeTimeInMs = ms(inputRole.temporaryRange);
return {
projectMembershipId: identityProjectMembership.id, projectMembershipId: identityProjectMembership.id,
role: isCustomRole ? ProjectMembershipRole.Custom : role, role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
customRoleId: customRole?.id customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null,
}, isTemporary: true,
tx temporaryMode: ProjectUserMembershipTemporaryMode.Relative,
); temporaryRange: inputRole.temporaryRange,
return identityProjectMembership; temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs)
};
});
const identityRoles = await identityProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
return { ...identityProjectMembership, roles: identityRoles };
}); });
return projectIdentity; return projectIdentity;
}; };
@@ -251,10 +283,33 @@ export const identityProjectServiceFactory = ({
return identityMemberships; return identityMemberships;
}; };
const getProjectIdentityByIdentityId = async ({
projectId,
actor,
actorId,
actorAuthMethod,
actorOrgId,
identityId
}: TGetProjectIdentityByIdentityIdDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
const [identityMembership] = await identityProjectDAL.findByProjectId(projectId, { identityId });
if (!identityMembership) throw new BadRequestError({ message: `Membership not found for identity ${identityId}` });
return identityMembership;
};
return { return {
createProjectIdentity, createProjectIdentity,
updateProjectIdentity, updateProjectIdentity,
deleteProjectIdentity, deleteProjectIdentity,
listProjectIdentities listProjectIdentities,
getProjectIdentityByIdentityId
}; };
}; };
@@ -4,7 +4,19 @@ import { ProjectUserMembershipTemporaryMode } from "../project-membership/projec
export type TCreateProjectIdentityDTO = { export type TCreateProjectIdentityDTO = {
identityId: string; identityId: string;
role: string; roles: (
| {
role: string;
isTemporary?: false;
}
| {
role: string;
isTemporary: true;
temporaryMode: ProjectUserMembershipTemporaryMode.Relative;
temporaryRange: string;
temporaryAccessStartTime: string;
}
)[];
} & TProjectPermission; } & TProjectPermission;
export type TUpdateProjectIdentityDTO = { export type TUpdateProjectIdentityDTO = {
@@ -29,3 +41,7 @@ export type TDeleteProjectIdentityDTO = {
} & TProjectPermission; } & TProjectPermission;
export type TListProjectIdentityDTO = TProjectPermission; export type TListProjectIdentityDTO = TProjectPermission;
export type TGetProjectIdentityByIdentityIdDTO = {
identityId: string;
} & TProjectPermission;
@@ -9,11 +9,19 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
const projectMemberOrm = ormify(db, TableName.ProjectMembership); const projectMemberOrm = ormify(db, TableName.ProjectMembership);
// special query // special query
const findAllProjectMembers = async (projectId: string) => { const findAllProjectMembers = async (projectId: string, filter: { usernames?: string[]; username?: string } = {}) => {
try { try {
const docs = await db(TableName.ProjectMembership) const docs = await db(TableName.ProjectMembership)
.where({ [`${TableName.ProjectMembership}.projectId` as "projectId"]: projectId }) .where({ [`${TableName.ProjectMembership}.projectId` as "projectId"]: projectId })
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.where((qb) => {
if (filter.usernames) {
void qb.whereIn("username", filter.usernames);
}
if (filter.username) {
void qb.where("username", filter.username);
}
})
.join<TUserEncryptionKeys>( .join<TUserEncryptionKeys>(
TableName.UserEncryptionKey, TableName.UserEncryptionKey,
`${TableName.UserEncryptionKey}.userId`, `${TableName.UserEncryptionKey}.userId`,
@@ -34,6 +34,7 @@ import {
TAddUsersToWorkspaceNonE2EEDTO, TAddUsersToWorkspaceNonE2EEDTO,
TDeleteProjectMembershipOldDTO, TDeleteProjectMembershipOldDTO,
TDeleteProjectMembershipsDTO, TDeleteProjectMembershipsDTO,
TGetProjectMembershipByUsernameDTO,
TGetProjectMembershipDTO, TGetProjectMembershipDTO,
TUpdateProjectMembershipDTO TUpdateProjectMembershipDTO
} from "./project-membership-types"; } from "./project-membership-types";
@@ -89,6 +90,28 @@ export const projectMembershipServiceFactory = ({
return projectMembershipDAL.findAllProjectMembers(projectId); return projectMembershipDAL.findAllProjectMembers(projectId);
}; };
const getProjectMembershipByUsername = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectId,
username
}: TGetProjectMembershipByUsernameDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
const [membership] = await projectMembershipDAL.findAllProjectMembers(projectId, { username });
if (!membership) throw new BadRequestError({ message: `Project membership not found for user ${username}` });
return membership;
};
const addUsersToProject = async ({ const addUsersToProject = async ({
projectId, projectId,
actorId, actorId,
@@ -510,6 +533,7 @@ export const projectMembershipServiceFactory = ({
return { return {
getProjectMemberships, getProjectMemberships,
getProjectMembershipByUsername,
updateProjectMembership, updateProjectMembership,
addUsersToProjectNonE2EE, addUsersToProjectNonE2EE,
deleteProjectMemberships, deleteProjectMemberships,
@@ -9,6 +9,10 @@ export type TInviteUserToProjectDTO = {
emails: string[]; emails: string[];
} & TProjectPermission; } & TProjectPermission;
export type TGetProjectMembershipByUsernameDTO = {
username: string;
} & TProjectPermission;
export type TUpdateProjectMembershipDTO = { export type TUpdateProjectMembershipDTO = {
membershipId: string; membershipId: string;
roles: ( roles: (
@@ -0,0 +1,4 @@
---
title: "Create Identity Membership"
openapi: "POST /api/v2/workspace/{projectId}/identity-memberships/{identityId}"
---
@@ -0,0 +1,4 @@
---
title: "Get Identity by ID"
openapi: "GET /api/v2/workspace/{projectId}/identity-memberships/{identityId}"
---
@@ -0,0 +1,4 @@
---
title: "Get By Username"
openapi: "POST /api/v1/workspace/{workspaceId}/memberships/details"
---
@@ -1,4 +1,4 @@
--- ---
title: "Invite Member" title: "Invite Member"
openapi: "POST /api/v2/workspace/{projectId}/memberships" openapi: "POST /api/v2/workspace/{projectId}/memberships"
--- ---
+51 -17
View File
@@ -32,7 +32,10 @@
"thumbsRating": true "thumbsRating": true
}, },
"api": { "api": {
"baseUrl": ["https://app.infisical.com", "http://localhost:8080"] "baseUrl": [
"https://app.infisical.com",
"http://localhost:8080"
]
}, },
"topbarLinks": [ "topbarLinks": [
{ {
@@ -73,7 +76,9 @@
"documentation/getting-started/introduction", "documentation/getting-started/introduction",
{ {
"group": "Quickstart", "group": "Quickstart",
"pages": ["documentation/guides/local-development"] "pages": [
"documentation/guides/local-development"
]
}, },
{ {
"group": "Guides", "group": "Guides",
@@ -214,7 +219,9 @@
}, },
{ {
"group": "Reference architectures", "group": "Reference architectures",
"pages": ["self-hosting/reference-architectures/aws-ecs"] "pages": [
"self-hosting/reference-architectures/aws-ecs"
]
}, },
"self-hosting/ee", "self-hosting/ee",
"self-hosting/faq" "self-hosting/faq"
@@ -370,11 +377,15 @@
}, },
{ {
"group": "Build Tool Integrations", "group": "Build Tool Integrations",
"pages": ["integrations/build-tools/gradle"] "pages": [
"integrations/build-tools/gradle"
]
}, },
{ {
"group": "", "group": "",
"pages": ["sdks/overview"] "pages": [
"sdks/overview"
]
}, },
{ {
"group": "SDK's", "group": "SDK's",
@@ -392,7 +403,9 @@
"api-reference/overview/authentication", "api-reference/overview/authentication",
{ {
"group": "Examples", "group": "Examples",
"pages": ["api-reference/overview/examples/integration"] "pages": [
"api-reference/overview/examples/integration"
]
} }
] ]
}, },
@@ -438,17 +451,30 @@
"api-reference/endpoints/workspaces/delete-workspace", "api-reference/endpoints/workspaces/delete-workspace",
"api-reference/endpoints/workspaces/get-workspace", "api-reference/endpoints/workspaces/get-workspace",
"api-reference/endpoints/workspaces/update-workspace", "api-reference/endpoints/workspaces/update-workspace",
"api-reference/endpoints/workspaces/invite-member-to-workspace",
"api-reference/endpoints/workspaces/remove-member-from-workspace",
"api-reference/endpoints/workspaces/memberships",
"api-reference/endpoints/workspaces/update-membership",
"api-reference/endpoints/workspaces/list-identity-memberships",
"api-reference/endpoints/workspaces/update-identity-membership",
"api-reference/endpoints/workspaces/delete-identity-membership",
"api-reference/endpoints/workspaces/secret-snapshots", "api-reference/endpoints/workspaces/secret-snapshots",
"api-reference/endpoints/workspaces/rollback-snapshot" "api-reference/endpoints/workspaces/rollback-snapshot"
] ]
}, },
{
"group": "Project Users",
"pages": [
"api-reference/endpoints/project-users/invite-member-to-workspace",
"api-reference/endpoints/project-users/remove-member-from-workspace",
"api-reference/endpoints/project-users/memberships",
"api-reference/endpoints/project-users/get-by-username",
"api-reference/endpoints/project-users/update-membership"
]
},
{
"group": "Project Identities",
"pages": [
"api-reference/endpoints/project-identities/add-identity-membership",
"api-reference/endpoints/project-identities/list-identity-memberships",
"api-reference/endpoints/project-identities/get-by-id",
"api-reference/endpoints/project-identities/update-identity-membership",
"api-reference/endpoints/project-identities/delete-identity-membership"
]
},
{ {
"group": "Environments", "group": "Environments",
"pages": [ "pages": [
@@ -525,11 +551,15 @@
}, },
{ {
"group": "Service Tokens", "group": "Service Tokens",
"pages": ["api-reference/endpoints/service-tokens/get"] "pages": [
"api-reference/endpoints/service-tokens/get"
]
}, },
{ {
"group": "Audit Logs", "group": "Audit Logs",
"pages": ["api-reference/endpoints/audit-logs/export-audit-log"] "pages": [
"api-reference/endpoints/audit-logs/export-audit-log"
]
} }
] ]
}, },
@@ -545,7 +575,9 @@
}, },
{ {
"group": "", "group": "",
"pages": ["changelog/overview"] "pages": [
"changelog/overview"
]
}, },
{ {
"group": "Contributing", "group": "Contributing",
@@ -569,7 +601,9 @@
}, },
{ {
"group": "Contributing to SDK", "group": "Contributing to SDK",
"pages": ["contributing/sdk/developing"] "pages": [
"contributing/sdk/developing"
]
} }
] ]
} }