mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Complete logic for user provisioning/deprovisioning to projects with groups
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { TDbClient } from "@app/db";
|
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";
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TUserGroupMembershipDALFactory = ReturnType<typeof userGroupMembershipDALFactory>;
|
export type TUserGroupMembershipDALFactory = ReturnType<typeof userGroupMembershipDALFactory>;
|
||||||
@@ -7,7 +8,110 @@ 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) => {
|
||||||
|
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 {
|
return {
|
||||||
...userGroupMembershipOrm
|
...userGroupMembershipOrm,
|
||||||
|
findGroupMembersInProject,
|
||||||
|
findGroupMembersNotInProject
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
)
|
)
|
||||||
.select("permissions");
|
.select("permissions");
|
||||||
|
|
||||||
|
console.log("getProjectPermission docs: ", docs);
|
||||||
|
|
||||||
const permission = sqlNestRelationships({
|
const permission = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs,
|
||||||
key: "membershipId",
|
key: "membershipId",
|
||||||
|
|||||||
@@ -256,7 +256,10 @@ export const registerRoutes = async (
|
|||||||
groupDAL,
|
groupDAL,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
groupProjectMembershipRoleDAL,
|
groupProjectMembershipRoleDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectBotDAL,
|
||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
permissionService
|
permissionService
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import ms from "ms";
|
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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
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 { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
|
|
||||||
import { TGroupDALFactory } from "../../ee/services/group/group-dal";
|
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 { 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 { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
|
||||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||||
import { TGroupProjectDALFactory } from "./group-project-dal";
|
import { TGroupProjectDALFactory } from "./group-project-dal";
|
||||||
@@ -27,8 +32,11 @@ type TGroupProjectServiceFactoryDep = {
|
|||||||
TGroupProjectMembershipRoleDALFactory,
|
TGroupProjectMembershipRoleDALFactory,
|
||||||
"create" | "transaction" | "insertMany" | "delete"
|
"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">;
|
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||||
|
projectBotDAL: TProjectBotDALFactory;
|
||||||
groupDAL: Pick<TGroupDALFactory, "findOne">;
|
groupDAL: Pick<TGroupDALFactory, "findOne">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRole">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getProjectPermissionByRole">;
|
||||||
};
|
};
|
||||||
@@ -39,7 +47,10 @@ export const groupProjectServiceFactory = ({
|
|||||||
groupDAL,
|
groupDAL,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
groupProjectMembershipRoleDAL,
|
groupProjectMembershipRoleDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectBotDAL,
|
||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
permissionService
|
permissionService
|
||||||
}: TGroupProjectServiceFactoryDep) => {
|
}: TGroupProjectServiceFactoryDep) => {
|
||||||
@@ -104,6 +115,66 @@ export const groupProjectServiceFactory = ({
|
|||||||
);
|
);
|
||||||
return groupProjectMembership;
|
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;
|
return projectGroup;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -215,7 +286,19 @@ export const groupProjectServiceFactory = ({
|
|||||||
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission);
|
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission);
|
||||||
if (!hasRequiredPriviledges) throw new ForbiddenRequestError({ message: "Failed to delete more privileged group" });
|
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 });
|
const [deletedGroup] = await groupProjectDAL.delete({ groupId: group.id, projectId });
|
||||||
|
|
||||||
return deletedGroup;
|
return deletedGroup;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,33 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
{ column: `${TableName.Environment}.position`, order: "asc" }
|
{ 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({
|
const nestedWorkspaces = sqlNestRelationships({
|
||||||
data: workspaces,
|
data: workspaces.concat(groupWorkspaces),
|
||||||
key: "id",
|
key: "id",
|
||||||
parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }),
|
parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ export const OrgGroupModal = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
handlePopUpToggle("group", false);
|
handlePopUpToggle("group", false);
|
||||||
|
reset();
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: `Successfully ${popUp?.group?.data ? "updated" : "created"} group`,
|
text: `Successfully ${popUp?.group?.data ? "updated" : "created"} group`,
|
||||||
|
|||||||
Reference in New Issue
Block a user