Complete logic for user provisioning/deprovisioning to projects with groups

This commit is contained in:
Tuan Dang
2024-03-27 10:53:51 -07:00
parent 8ffbaa2f6c
commit 0da6262ead
6 changed files with 223 additions and 5 deletions

View File

@ -1,5 +1,6 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
export type TUserGroupMembershipDALFactory = ReturnType<typeof userGroupMembershipDALFactory>;
@ -7,7 +8,110 @@ export type TUserGroupMembershipDALFactory = ReturnType<typeof userGroupMembersh
export const userGroupMembershipDALFactory = (db: TDbClient) => {
const userGroupMembershipOrm = ormify(db, TableName.UserGroupMembership);
// special query
const findGroupMembersInProject = async (groupId: string, projectId: string) => {
try {
const members = await db(TableName.UserGroupMembership)
.where(`${TableName.UserGroupMembership}.groupId`, groupId)
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.join(TableName.ProjectMembership, function () {
this.on(`${TableName.Users}.id`, "=", `${TableName.ProjectMembership}.userId`).andOn(
`${TableName.ProjectMembership}.projectId`,
"=",
db.raw("?", [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 }) => ({
...data,
user: { email, username, firstName, lastName, id: userId, publicKey }
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find group members in project" });
}
};
/**
* 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].
*
* Filters out users that are part of other groups in the project.
* @param groupId
* @param projectId
* @returns
*/
const findGroupMembersNotInProject = async (groupId: string, projectId: string) => {
try {
// get list of groups in the project with id [projectId]
// that that are not the group with id [groupId]
const groups: string[] = await db(TableName.GroupProjectMembership)
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
.whereNot(`${TableName.GroupProjectMembership}.groupId`, groupId)
.pluck(`${TableName.GroupProjectMembership}.groupId`);
// main query
const members = await db(TableName.UserGroupMembership)
.where(`${TableName.UserGroupMembership}.groupId`, groupId)
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.ProjectMembership, function () {
this.on(`${TableName.Users}.id`, "=", `${TableName.ProjectMembership}.userId`).andOn(
`${TableName.ProjectMembership}.projectId`,
"=",
db.raw("?", [projectId])
);
})
.whereNull(`${TableName.ProjectMembership}.userId`)
.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
.whereNotIn(`${TableName.UserGroupMembership}.userId`, function () {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.select(`${TableName.UserGroupMembership}.userId`)
.from(TableName.UserGroupMembership)
.whereIn(`${TableName.UserGroupMembership}.groupId`, groups);
});
return members.map(({ email, username, firstName, lastName, userId, publicKey, ...data }) => ({
...data,
user: { email, username, firstName, lastName, id: userId, publicKey }
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find group members not in project" });
}
};
return {
...userGroupMembershipOrm
...userGroupMembershipOrm,
findGroupMembersInProject,
findGroupMembersNotInProject
};
};

View File

@ -73,6 +73,8 @@ export const permissionDALFactory = (db: TDbClient) => {
)
.select("permissions");
console.log("getProjectPermission docs: ", docs);
const permission = sqlNestRelationships({
data: docs,
key: "membershipId",

View File

@ -256,7 +256,10 @@ export const registerRoutes = async (
groupDAL,
groupProjectDAL,
groupProjectMembershipRoleDAL,
userGroupMembershipDAL,
projectDAL,
projectKeyDAL,
projectBotDAL,
projectRoleDAL,
permissionService
});

View File

@ -1,15 +1,20 @@
import { ForbiddenError } from "@casl/ability";
import ms from "ms";
import { ProjectMembershipRole } from "@app/db/schemas";
import { ProjectMembershipRole, SecretKeyEncoding } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { TGroupDALFactory } from "../../ee/services/group/group-dal";
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
import { TGroupProjectDALFactory } from "./group-project-dal";
@ -27,8 +32,11 @@ type TGroupProjectServiceFactoryDep = {
TGroupProjectMembershipRoleDALFactory,
"create" | "transaction" | "insertMany" | "delete"
>;
projectDAL: Pick<TProjectDALFactory, "findById">;
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany" | "transaction">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
projectBotDAL: TProjectBotDALFactory;
groupDAL: Pick<TGroupDALFactory, "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRole">;
};
@ -39,7 +47,10 @@ export const groupProjectServiceFactory = ({
groupDAL,
groupProjectDAL,
groupProjectMembershipRoleDAL,
userGroupMembershipDAL,
projectDAL,
projectKeyDAL,
projectBotDAL,
projectRoleDAL,
permissionService
}: TGroupProjectServiceFactoryDep) => {
@ -104,6 +115,66 @@ export const groupProjectServiceFactory = ({
);
return groupProjectMembership;
});
// share project key with users in group that have not
// individually been added to the project and that are not part of
// other groups that are in the project
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, projectId);
if (groupMembers.length) {
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 projectKeyData = groupMembers.map(({ user: { publicKey, id } }) => {
const { ciphertext: encryptedKey, nonce } = encryptAsymmetric(plaintextProjectKey, publicKey, botPrivateKey);
return {
encryptedKey,
nonce,
senderId: ghostUser.id,
receiverId: id,
projectId
};
});
await projectKeyDAL.insertMany(projectKeyData);
}
return projectGroup;
};
@ -215,7 +286,19 @@ export const groupProjectServiceFactory = ({
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission);
if (!hasRequiredPriviledges) throw new ForbiddenRequestError({ message: "Failed to delete more privileged group" });
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, projectId);
if (groupMembers.length) {
await projectKeyDAL.delete({
projectId,
$in: {
receiverId: groupMembers.map(({ user: { id } }) => id)
}
});
}
const [deletedGroup] = await groupProjectDAL.delete({ groupId: group.id, projectId });
return deletedGroup;
};

View File

@ -30,8 +30,33 @@ export const projectDALFactory = (db: TDbClient) => {
{ column: `${TableName.Environment}.position`, order: "asc" }
]);
const groups: string[] = await db(TableName.UserGroupMembership)
.where({ userId })
.select(selectAllTableCols(TableName.UserGroupMembership))
.pluck("groupId");
const groupWorkspaces = await db(TableName.GroupProjectMembership)
.whereIn("groupId", groups)
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
.whereNotIn(
`${TableName.Project}.id`,
workspaces.map(({ id }) => id)
)
.leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
.select(
selectAllTableCols(TableName.Project),
db.ref("id").withSchema(TableName.Project).as("_id"),
db.ref("id").withSchema(TableName.Environment).as("envId"),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("name").withSchema(TableName.Environment).as("envName")
)
.orderBy([
{ column: `${TableName.Project}.name`, order: "asc" },
{ column: `${TableName.Environment}.position`, order: "asc" }
]);
const nestedWorkspaces = sqlNestRelationships({
data: workspaces,
data: workspaces.concat(groupWorkspaces),
key: "id",
parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }),
childrenMapper: [

View File

@ -105,6 +105,7 @@ export const OrgGroupModal = ({
});
}
handlePopUpToggle("group", false);
reset();
createNotification({
text: `Successfully ${popUp?.group?.data ? "updated" : "created"} group`,