mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 09:25:59 +00:00
Add/remove bulk users to projects upon add/remove users to/from groups
This commit is contained in:
@@ -155,6 +155,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
req.permission.authMethod,
|
req.permission.authMethod,
|
||||||
req.permission.orgId
|
req.permission.orgId
|
||||||
);
|
);
|
||||||
|
|
||||||
return { data: { permissions, membership } };
|
return { data: { permissions, membership } };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
|
||||||
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
import { OrgMembershipRole, SecretKeyEncoding, TOrgRoles } from "@app/db/schemas";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
|
import { decryptAsymmetric, encryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
|
import { TGroupProjectDALFactory } from "../../../services/group-project/group-project-dal";
|
||||||
import { TOrgDALFactory } from "../../../services/org/org-dal";
|
import { TOrgDALFactory } from "../../../services/org/org-dal";
|
||||||
|
import { TProjectDALFactory } from "../../../services/project/project-dal";
|
||||||
|
import { TProjectBotDALFactory } from "../../../services/project-bot/project-bot-dal";
|
||||||
|
import { TProjectKeyDALFactory } from "../../../services/project-key/project-key-dal";
|
||||||
import { TUserDALFactory } from "../../../services/user/user-dal";
|
import { TUserDALFactory } from "../../../services/user/user-dal";
|
||||||
import { TLicenseServiceFactory } from "../license/license-service";
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
@@ -23,10 +28,17 @@ import {
|
|||||||
import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
|
import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
|
||||||
|
|
||||||
type TGroupServiceFactoryDep = {
|
type TGroupServiceFactoryDep = {
|
||||||
userDAL: Pick<TUserDALFactory, "findOne">;
|
userDAL: Pick<TUserDALFactory, "findOne" | "findUserEncKeyByUsername">;
|
||||||
groupDAL: Pick<TGroupDALFactory, "create" | "findOne" | "update" | "delete" | "findAllGroupMembers">;
|
groupDAL: Pick<TGroupDALFactory, "create" | "findOne" | "update" | "delete" | "findAllGroupMembers">;
|
||||||
|
groupProjectDAL: TGroupProjectDALFactory;
|
||||||
orgDAL: Pick<TOrgDALFactory, "findMembership">;
|
orgDAL: Pick<TOrgDALFactory, "findMembership">;
|
||||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findOne" | "create" | "delete">;
|
userGroupMembershipDAL: Pick<
|
||||||
|
TUserGroupMembershipDALFactory,
|
||||||
|
"findOne" | "create" | "delete" | "filterProjectsByUserMembership"
|
||||||
|
>;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
||||||
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "create" | "delete" | "findLatestProjectKey">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
};
|
};
|
||||||
@@ -36,8 +48,12 @@ export type TGroupServiceFactory = ReturnType<typeof groupServiceFactory>;
|
|||||||
export const groupServiceFactory = ({
|
export const groupServiceFactory = ({
|
||||||
userDAL,
|
userDAL,
|
||||||
groupDAL,
|
groupDAL,
|
||||||
|
groupProjectDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
userGroupMembershipDAL,
|
userGroupMembershipDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
projectKeyDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService
|
licenseService
|
||||||
}: TGroupServiceFactoryDep) => {
|
}: TGroupServiceFactoryDep) => {
|
||||||
@@ -211,7 +227,7 @@ export const groupServiceFactory = ({
|
|||||||
throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" });
|
throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" });
|
||||||
|
|
||||||
// get user with username
|
// get user with username
|
||||||
const user = await userDAL.findOne({
|
const user = await userDAL.findUserEncKeyByUsername({
|
||||||
username
|
username
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -223,7 +239,7 @@ export const groupServiceFactory = ({
|
|||||||
// check if user group membership already exists
|
// check if user group membership already exists
|
||||||
const existingUserGroupMembership = await userGroupMembershipDAL.findOne({
|
const existingUserGroupMembership = await userGroupMembershipDAL.findOne({
|
||||||
groupId: group.id,
|
groupId: group.id,
|
||||||
userId: user.id
|
userId: user.userId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingUserGroupMembership)
|
if (existingUserGroupMembership)
|
||||||
@@ -233,7 +249,7 @@ export const groupServiceFactory = ({
|
|||||||
|
|
||||||
// check if user is even part of the organization
|
// check if user is even part of the organization
|
||||||
const existingUserOrgMembership = await orgDAL.findMembership({
|
const existingUserOrgMembership = await orgDAL.findMembership({
|
||||||
userId: user.id,
|
userId: user.userId,
|
||||||
orgId
|
orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -243,10 +259,77 @@ export const groupServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
await userGroupMembershipDAL.create({
|
await userGroupMembershipDAL.create({
|
||||||
userId: user.id,
|
userId: user.userId,
|
||||||
groupId: group.id
|
groupId: group.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// check which projects the group is part of
|
||||||
|
const projectIds = (
|
||||||
|
await groupProjectDAL.find({
|
||||||
|
groupId: group.id
|
||||||
|
})
|
||||||
|
).map((gp) => gp.projectId);
|
||||||
|
|
||||||
|
const keys = await projectKeyDAL.find({
|
||||||
|
receiverId: user.userId,
|
||||||
|
$in: {
|
||||||
|
projectId: projectIds
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const keysSet = new Set(keys.map((k) => k.projectId));
|
||||||
|
const projectsToAddKeyFor = projectIds.filter((p) => !keysSet.has(p));
|
||||||
|
|
||||||
|
for await (const projectId of projectsToAddKeyFor) {
|
||||||
|
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
|
||||||
|
|
||||||
|
if (!ghostUser) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find sudo user"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
|
||||||
|
|
||||||
|
if (!ghostUserLatestKey) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find sudo user latest key"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const bot = await projectBotDAL.findOne({ projectId });
|
||||||
|
|
||||||
|
if (!bot) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find bot"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const botPrivateKey = infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
||||||
|
iv: bot.iv,
|
||||||
|
tag: bot.tag,
|
||||||
|
ciphertext: bot.encryptedPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const plaintextProjectKey = decryptAsymmetric({
|
||||||
|
ciphertext: ghostUserLatestKey.encryptedKey,
|
||||||
|
nonce: ghostUserLatestKey.nonce,
|
||||||
|
publicKey: ghostUserLatestKey.sender.publicKey,
|
||||||
|
privateKey: botPrivateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ciphertext: encryptedKey, nonce } = encryptAsymmetric(plaintextProjectKey, user.publicKey, botPrivateKey);
|
||||||
|
|
||||||
|
await projectKeyDAL.create({
|
||||||
|
encryptedKey,
|
||||||
|
nonce,
|
||||||
|
senderId: ghostUser.id,
|
||||||
|
receiverId: user.userId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -300,6 +383,25 @@ export const groupServiceFactory = ({
|
|||||||
message: `User ${username} is not part of the group ${groupSlug}`
|
message: `User ${username} is not part of the group ${groupSlug}`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const projectIds = (
|
||||||
|
await groupProjectDAL.find({
|
||||||
|
groupId: group.id
|
||||||
|
})
|
||||||
|
).map((gp) => gp.projectId);
|
||||||
|
|
||||||
|
const t = await userGroupMembershipDAL.filterProjectsByUserMembership(user.id, group.id, projectIds);
|
||||||
|
|
||||||
|
const projectsToDeleteKeyFor = projectIds.filter((p) => !t.has(p));
|
||||||
|
|
||||||
|
if (projectsToDeleteKeyFor.length) {
|
||||||
|
await projectKeyDAL.delete({
|
||||||
|
receiverId: user.id,
|
||||||
|
$in: {
|
||||||
|
projectId: projectsToDeleteKeyFor
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await userGroupMembershipDAL.delete({
|
await userGroupMembershipDAL.delete({
|
||||||
groupId: group.id,
|
groupId: group.id,
|
||||||
userId: user.id
|
userId: user.id
|
||||||
|
|||||||
@@ -8,42 +8,49 @@ export type TUserGroupMembershipDALFactory = ReturnType<typeof userGroupMembersh
|
|||||||
export const userGroupMembershipDALFactory = (db: TDbClient) => {
|
export const userGroupMembershipDALFactory = (db: TDbClient) => {
|
||||||
const userGroupMembershipOrm = ormify(db, TableName.UserGroupMembership);
|
const userGroupMembershipOrm = ormify(db, TableName.UserGroupMembership);
|
||||||
|
|
||||||
// special query
|
/**
|
||||||
const findGroupMembersInProject = async (groupId: string, projectId: string) => {
|
* Returns a sub-set of projectIds fed into this function corresponding to projects where either:
|
||||||
try {
|
* - The user is a direct member of the project.
|
||||||
const members = await db(TableName.UserGroupMembership)
|
* - The user is a member of a group that is a member of the project, excluding projects that they are part of
|
||||||
.where(`${TableName.UserGroupMembership}.groupId`, groupId)
|
* through the group with id [groupId].
|
||||||
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
*/
|
||||||
.join(TableName.ProjectMembership, function () {
|
const filterProjectsByUserMembership = async (userId: string, groupId: string, projectIds: string[]) => {
|
||||||
this.on(`${TableName.Users}.id`, "=", `${TableName.ProjectMembership}.userId`).andOn(
|
const userProjectMemberships: string[] = await db(TableName.ProjectMembership)
|
||||||
`${TableName.ProjectMembership}.projectId`,
|
.where(`${TableName.ProjectMembership}.userId`, userId)
|
||||||
"=",
|
.whereIn(`${TableName.ProjectMembership}.projectId`, projectIds)
|
||||||
db.raw("?", [projectId])
|
.pluck(`${TableName.ProjectMembership}.projectId`);
|
||||||
);
|
|
||||||
})
|
|
||||||
.leftJoin<TUserEncryptionKeys>(
|
|
||||||
TableName.UserEncryptionKey,
|
|
||||||
`${TableName.UserEncryptionKey}.userId`,
|
|
||||||
`${TableName.Users}.id`
|
|
||||||
)
|
|
||||||
.select(
|
|
||||||
db.ref("id").withSchema(TableName.UserGroupMembership),
|
|
||||||
db.ref("groupId").withSchema(TableName.UserGroupMembership),
|
|
||||||
db.ref("email").withSchema(TableName.Users),
|
|
||||||
db.ref("username").withSchema(TableName.Users),
|
|
||||||
db.ref("firstName").withSchema(TableName.Users),
|
|
||||||
db.ref("lastName").withSchema(TableName.Users),
|
|
||||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
|
||||||
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
|
||||||
)
|
|
||||||
.where({ isGhost: false }); // MAKE SURE USER IS NOT A GHOST USER
|
|
||||||
|
|
||||||
return members.map(({ email, username, firstName, lastName, userId, publicKey, ...data }) => ({
|
const userGroupMemberships: string[] = await db(TableName.UserGroupMembership)
|
||||||
...data,
|
.where(`${TableName.UserGroupMembership}.userId`, userId)
|
||||||
user: { email, username, firstName, lastName, id: userId, publicKey }
|
.whereNot(`${TableName.UserGroupMembership}.groupId`, groupId)
|
||||||
}));
|
.join(
|
||||||
|
TableName.GroupProjectMembership,
|
||||||
|
`${TableName.UserGroupMembership}.groupId`,
|
||||||
|
`${TableName.GroupProjectMembership}.groupId`
|
||||||
|
)
|
||||||
|
.whereIn(`${TableName.GroupProjectMembership}.projectId`, projectIds)
|
||||||
|
.pluck(`${TableName.GroupProjectMembership}.projectId`);
|
||||||
|
|
||||||
|
return new Set(userProjectMemberships.concat(userGroupMemberships));
|
||||||
|
};
|
||||||
|
|
||||||
|
// special query
|
||||||
|
const findUserGroupMembershipsInProject = async (usernames: string[], projectId: string) => {
|
||||||
|
try {
|
||||||
|
const usernameDocs: string[] = await db(TableName.UserGroupMembership)
|
||||||
|
.join(
|
||||||
|
TableName.GroupProjectMembership,
|
||||||
|
`${TableName.UserGroupMembership}.groupId`,
|
||||||
|
`${TableName.GroupProjectMembership}.groupId`
|
||||||
|
)
|
||||||
|
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
|
||||||
|
.whereIn(`${TableName.Users}.username`, usernames) // TODO: pluck usernames
|
||||||
|
.pluck(`${TableName.Users}.id`);
|
||||||
|
|
||||||
|
return usernameDocs;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find group members in project" });
|
throw new DatabaseError({ error, name: "Find user group members in project" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,7 +58,7 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
|
|||||||
* Return list of users that are part of the group with id [groupId]
|
* Return list of users that are part of the group with id [groupId]
|
||||||
* that have not yet been added individually to project with id [projectId].
|
* that have not yet been added individually to project with id [projectId].
|
||||||
*
|
*
|
||||||
* Filters out users that are part of other groups in the project.
|
* Note: Filters out users that are part of other groups in the project.
|
||||||
* @param groupId
|
* @param groupId
|
||||||
* @param projectId
|
* @param projectId
|
||||||
* @returns
|
* @returns
|
||||||
@@ -111,7 +118,8 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...userGroupMembershipOrm,
|
...userGroupMembershipOrm,
|
||||||
findGroupMembersInProject,
|
filterProjectsByUserMembership,
|
||||||
|
findUserGroupMembershipsInProject,
|
||||||
findGroupMembersNotInProject
|
findGroupMembersNotInProject
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,6 +45,44 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const getProjectPermission = async (userId: string, projectId: string) => {
|
const getProjectPermission = async (userId: string, projectId: string) => {
|
||||||
try {
|
try {
|
||||||
|
const groups: string[] = await db(TableName.GroupProjectMembership)
|
||||||
|
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
|
||||||
|
.pluck(`${TableName.GroupProjectMembership}.groupId`);
|
||||||
|
|
||||||
|
const groupDocs = await db(TableName.UserGroupMembership)
|
||||||
|
.where(`${TableName.UserGroupMembership}.userId`, userId)
|
||||||
|
.whereIn(`${TableName.UserGroupMembership}.groupId`, groups)
|
||||||
|
.join(
|
||||||
|
TableName.GroupProjectMembership,
|
||||||
|
`${TableName.GroupProjectMembership}.groupId`,
|
||||||
|
`${TableName.UserGroupMembership}.groupId`
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
TableName.GroupProjectMembershipRole,
|
||||||
|
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
|
||||||
|
`${TableName.GroupProjectMembership}.id`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ProjectRoles,
|
||||||
|
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
||||||
|
`${TableName.ProjectRoles}.id`
|
||||||
|
)
|
||||||
|
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||||
|
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.GroupProjectMembershipRole))
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"),
|
||||||
|
// TODO(roll-forward-migration): remove this field when we drop this in next migration after a week
|
||||||
|
db.ref("role").withSchema(TableName.GroupProjectMembership).as("oldRoleField"),
|
||||||
|
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("membershipCreatedAt"),
|
||||||
|
db.ref("updatedAt").withSchema(TableName.GroupProjectMembership).as("membershipUpdatedAt"),
|
||||||
|
db.ref("projectId").withSchema(TableName.GroupProjectMembership),
|
||||||
|
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||||
|
db.ref("orgId").withSchema(TableName.Project),
|
||||||
|
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug")
|
||||||
|
)
|
||||||
|
.select("permissions");
|
||||||
|
|
||||||
const docs = await db(TableName.ProjectMembership)
|
const docs = await db(TableName.ProjectMembership)
|
||||||
.join(
|
.join(
|
||||||
TableName.ProjectUserMembershipRole,
|
TableName.ProjectUserMembershipRole,
|
||||||
@@ -67,17 +105,16 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("role").withSchema(TableName.ProjectMembership).as("oldRoleField"),
|
db.ref("role").withSchema(TableName.ProjectMembership).as("oldRoleField"),
|
||||||
db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"),
|
db.ref("createdAt").withSchema(TableName.ProjectMembership).as("membershipCreatedAt"),
|
||||||
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
|
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
|
||||||
|
db.ref("projectId").withSchema(TableName.ProjectMembership),
|
||||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||||
db.ref("orgId").withSchema(TableName.Project),
|
db.ref("orgId").withSchema(TableName.Project),
|
||||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug")
|
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug")
|
||||||
)
|
)
|
||||||
.select("permissions");
|
.select("permissions");
|
||||||
|
|
||||||
console.log("getProjectPermission docs: ", docs);
|
|
||||||
|
|
||||||
const permission = sqlNestRelationships({
|
const permission = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs.concat(groupDocs),
|
||||||
key: "membershipId",
|
key: "projectId",
|
||||||
parentMapper: ({
|
parentMapper: ({
|
||||||
orgId,
|
orgId,
|
||||||
orgAuthEnforced,
|
orgAuthEnforced,
|
||||||
@@ -107,11 +144,48 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// const permission = sqlNestRelationships({
|
||||||
|
// data: docs, // TODO: concat?
|
||||||
|
// key: "membershipId",
|
||||||
|
// parentMapper: ({
|
||||||
|
// orgId,
|
||||||
|
// orgAuthEnforced,
|
||||||
|
// membershipId,
|
||||||
|
// membershipCreatedAt,
|
||||||
|
// membershipUpdatedAt,
|
||||||
|
// oldRoleField,
|
||||||
|
// // role
|
||||||
|
// }) => ({
|
||||||
|
// orgId,
|
||||||
|
// orgAuthEnforced,
|
||||||
|
// userId,
|
||||||
|
// role: oldRoleField,
|
||||||
|
// // role,
|
||||||
|
// id: membershipId,
|
||||||
|
// projectId,
|
||||||
|
// createdAt: membershipCreatedAt,
|
||||||
|
// updatedAt: membershipUpdatedAt
|
||||||
|
// }),
|
||||||
|
// childrenMapper: [
|
||||||
|
// {
|
||||||
|
// key: "id",
|
||||||
|
// label: "roles" as const,
|
||||||
|
// mapper: (data) =>
|
||||||
|
// ProjectUserMembershipRolesSchema.extend({
|
||||||
|
// permissions: z.unknown(),
|
||||||
|
// customRoleSlug: z.string().optional().nullable()
|
||||||
|
// }).parse(data)
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// });
|
||||||
|
|
||||||
// when introducting cron mode change it here
|
// when introducting cron mode change it here
|
||||||
const activeRoles = permission?.[0]?.roles.filter(
|
const activeRoles = permission?.[0]?.roles.filter(
|
||||||
({ isTemporary, temporaryAccessEndTime }) =>
|
({ isTemporary, temporaryAccessEndTime }) =>
|
||||||
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
|
||||||
);
|
);
|
||||||
|
|
||||||
return permission?.[0] ? { ...permission[0], roles: activeRoles } : undefined;
|
return permission?.[0] ? { ...permission[0], roles: activeRoles } : undefined;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "GetProjectPermission" });
|
throw new DatabaseError({ error, name: "GetProjectPermission" });
|
||||||
|
|||||||
@@ -218,6 +218,7 @@ const buildViewerPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
||||||
|
|||||||
@@ -247,8 +247,12 @@ export const registerRoutes = async (
|
|||||||
const groupService = groupServiceFactory({
|
const groupService = groupServiceFactory({
|
||||||
userDAL,
|
userDAL,
|
||||||
groupDAL,
|
groupDAL,
|
||||||
|
groupProjectDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
userGroupMembershipDAL,
|
userGroupMembershipDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
projectKeyDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService
|
licenseService
|
||||||
});
|
});
|
||||||
@@ -362,6 +366,7 @@ export const registerRoutes = async (
|
|||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
|||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
|
|
||||||
|
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
@@ -45,6 +46,7 @@ type TProjectMembershipServiceFactoryDep = {
|
|||||||
projectMembershipDAL: TProjectMembershipDALFactory;
|
projectMembershipDAL: TProjectMembershipDALFactory;
|
||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "find" | "delete">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "find" | "delete">;
|
||||||
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
|
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
|
||||||
|
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
|
||||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||||
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
|
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">;
|
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">;
|
||||||
@@ -63,6 +65,7 @@ export const projectMembershipServiceFactory = ({
|
|||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
licenseService
|
licenseService
|
||||||
@@ -120,6 +123,13 @@ export const projectMembershipServiceFactory = ({
|
|||||||
});
|
});
|
||||||
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
|
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
|
||||||
|
|
||||||
|
const userIdsToExcludeForProjectKeyAddition = new Set(
|
||||||
|
await userGroupMembershipDAL.findUserGroupMembershipsInProject(
|
||||||
|
orgMembers.map(({ username }) => username),
|
||||||
|
projectId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
await projectMembershipDAL.transaction(async (tx) => {
|
await projectMembershipDAL.transaction(async (tx) => {
|
||||||
const projectMemberships = await projectMembershipDAL.insertMany(
|
const projectMemberships = await projectMembershipDAL.insertMany(
|
||||||
orgMembers.map(({ userId }) => ({
|
orgMembers.map(({ userId }) => ({
|
||||||
@@ -135,13 +145,15 @@ export const projectMembershipServiceFactory = ({
|
|||||||
);
|
);
|
||||||
const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId);
|
const encKeyGroupByOrgMembId = groupBy(members, (i) => i.orgMembershipId);
|
||||||
await projectKeyDAL.insertMany(
|
await projectKeyDAL.insertMany(
|
||||||
orgMembers.map(({ userId, id }) => ({
|
orgMembers
|
||||||
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
|
.filter(({ userId }) => !userIdsToExcludeForProjectKeyAddition.has(userId as string))
|
||||||
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
|
.map(({ userId, id }) => ({
|
||||||
senderId: actorId,
|
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
|
||||||
receiverId: userId as string,
|
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
|
||||||
projectId
|
senderId: actorId,
|
||||||
})),
|
receiverId: userId as string,
|
||||||
|
projectId
|
||||||
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -247,6 +259,10 @@ export const projectMembershipServiceFactory = ({
|
|||||||
|
|
||||||
const members: TProjectMemberships[] = [];
|
const members: TProjectMemberships[] = [];
|
||||||
|
|
||||||
|
const userIdsToExcludeForProjectKeyAddition = new Set(
|
||||||
|
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
|
||||||
|
);
|
||||||
|
|
||||||
await projectMembershipDAL.transaction(async (tx) => {
|
await projectMembershipDAL.transaction(async (tx) => {
|
||||||
const projectMemberships = await projectMembershipDAL.insertMany(
|
const projectMemberships = await projectMembershipDAL.insertMany(
|
||||||
orgMembers.map(({ user }) => ({
|
orgMembers.map(({ user }) => ({
|
||||||
@@ -265,13 +281,15 @@ export const projectMembershipServiceFactory = ({
|
|||||||
|
|
||||||
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
|
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
|
||||||
await projectKeyDAL.insertMany(
|
await projectKeyDAL.insertMany(
|
||||||
orgMembers.map(({ user, id }) => ({
|
orgMembers
|
||||||
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
|
.filter(({ user }) => !userIdsToExcludeForProjectKeyAddition.has(user.id))
|
||||||
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
|
.map(({ user, id }) => ({
|
||||||
senderId: ghostUser.id,
|
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
|
||||||
receiverId: user.id,
|
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
|
||||||
projectId
|
senderId: ghostUser.id,
|
||||||
})),
|
receiverId: user.id,
|
||||||
|
projectId
|
||||||
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -458,6 +476,10 @@ export const projectMembershipServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userIdsToExcludeFromProjectKeyRemoval = new Set(
|
||||||
|
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
|
||||||
|
);
|
||||||
|
|
||||||
const memberships = await projectMembershipDAL.transaction(async (tx) => {
|
const memberships = await projectMembershipDAL.transaction(async (tx) => {
|
||||||
const deletedMemberships = await projectMembershipDAL.delete(
|
const deletedMemberships = await projectMembershipDAL.delete(
|
||||||
{
|
{
|
||||||
@@ -469,11 +491,15 @@ export const projectMembershipServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// delete project keys belonging to users that are not part of any other groups in the project
|
||||||
await projectKeyDAL.delete(
|
await projectKeyDAL.delete(
|
||||||
{
|
{
|
||||||
projectId,
|
projectId,
|
||||||
$in: {
|
$in: {
|
||||||
receiverId: projectMembers.map(({ user }) => user.id).filter(Boolean)
|
receiverId: projectMembers
|
||||||
|
.filter(({ user }) => !userIdsToExcludeFromProjectKeyRemoval.has(user.id))
|
||||||
|
.map(({ user }) => user.id)
|
||||||
|
.filter(Boolean)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
|
|||||||
+10
-3
@@ -7,6 +7,7 @@ import {
|
|||||||
faMoneyBill,
|
faMoneyBill,
|
||||||
faServer,
|
faServer,
|
||||||
faSignIn,
|
faSignIn,
|
||||||
|
faUser,
|
||||||
faUserCog,
|
faUserCog,
|
||||||
faUsers
|
faUsers
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -35,11 +36,17 @@ type Props = {
|
|||||||
|
|
||||||
const SIMPLE_PERMISSION_OPTIONS = [
|
const SIMPLE_PERMISSION_OPTIONS = [
|
||||||
{
|
{
|
||||||
title: "Members management",
|
title: "User management",
|
||||||
subtitle: "Invite, view and remove members from the organization",
|
subtitle: "Invite, view and remove users from the organization",
|
||||||
icon: faUsers,
|
icon: faUser,
|
||||||
formName: "member"
|
formName: "member"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Group management",
|
||||||
|
subtitle: "Invite, view and remove user groups from the organization",
|
||||||
|
icon: faUsers,
|
||||||
|
formName: "groups"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Machine identity management",
|
title: "Machine identity management",
|
||||||
subtitle: "Create, view, update and remove (machine) identities from the organization",
|
subtitle: "Create, view, update and remove (machine) identities from the organization",
|
||||||
|
|||||||
+1
@@ -28,6 +28,7 @@ export const formSchema = z.object({
|
|||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
member: generalPermissionSchema,
|
member: generalPermissionSchema,
|
||||||
|
groups: generalPermissionSchema,
|
||||||
role: generalPermissionSchema,
|
role: generalPermissionSchema,
|
||||||
settings: generalPermissionSchema,
|
settings: generalPermissionSchema,
|
||||||
"service-account": generalPermissionSchema,
|
"service-account": generalPermissionSchema,
|
||||||
|
|||||||
+8
-2
@@ -55,11 +55,17 @@ const SINGLE_PERMISSION_LIST = [
|
|||||||
formName: "role"
|
formName: "role"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Project Members",
|
title: "User management",
|
||||||
subtitle: "Project members management control",
|
subtitle: "Add, view and remove users from the project",
|
||||||
icon: faUser,
|
icon: faUser,
|
||||||
formName: "member"
|
formName: "member"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Group management",
|
||||||
|
subtitle: "Add, view and remove user groups from the project",
|
||||||
|
icon: faUsers,
|
||||||
|
formName: "groups"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Machine identity management",
|
title: "Machine identity management",
|
||||||
subtitle: "Add, view, update and remove (machine) identities from the project",
|
subtitle: "Add, view, update and remove (machine) identities from the project",
|
||||||
|
|||||||
+1
@@ -36,6 +36,7 @@ export const formSchema = z.object({
|
|||||||
.object({
|
.object({
|
||||||
secrets: z.record(multiEnvPermissionSchema).optional(),
|
secrets: z.record(multiEnvPermissionSchema).optional(),
|
||||||
member: generalPermissionSchema,
|
member: generalPermissionSchema,
|
||||||
|
groups: generalPermissionSchema,
|
||||||
identity: generalPermissionSchema,
|
identity: generalPermissionSchema,
|
||||||
role: generalPermissionSchema,
|
role: generalPermissionSchema,
|
||||||
integrations: generalPermissionSchema,
|
integrations: generalPermissionSchema,
|
||||||
|
|||||||
Reference in New Issue
Block a user