Finish preliminary capability for adding incomplete users to groups

This commit is contained in:
Tuan Dang
2024-04-18 10:57:25 -07:00
parent d23b39abba
commit fa7587900e
21 changed files with 1510 additions and 391 deletions
+8
View File
@@ -86,6 +86,9 @@ import {
TOrgRoles, TOrgRoles,
TOrgRolesInsert, TOrgRolesInsert,
TOrgRolesUpdate, TOrgRolesUpdate,
TPendingGroupAdditions,
TPendingGroupAdditionsInsert,
TPendingGroupAdditionsUpdate,
TProjectBots, TProjectBots,
TProjectBotsInsert, TProjectBotsInsert,
TProjectBotsUpdate, TProjectBotsUpdate,
@@ -212,6 +215,11 @@ declare module "knex/types/tables" {
interface Tables { interface Tables {
[TableName.Users]: Knex.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>; [TableName.Users]: Knex.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
[TableName.Groups]: Knex.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>; [TableName.Groups]: Knex.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
[TableName.PendingGroupAddition]: Knex.CompositeTableType<
TPendingGroupAdditions,
TPendingGroupAdditionsInsert,
TPendingGroupAdditionsUpdate
>;
[TableName.UserGroupMembership]: Knex.CompositeTableType< [TableName.UserGroupMembership]: Knex.CompositeTableType<
TUserGroupMembership, TUserGroupMembership,
TUserGroupMembershipInsert, TUserGroupMembershipInsert,
@@ -0,0 +1,25 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.PendingGroupAddition))) {
await knex.schema.createTable(TableName.PendingGroupAddition, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("userId").notNullable();
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
t.uuid("groupId").notNullable();
t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
t.unique(["userId", "groupId"]);
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.PendingGroupAddition);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.PendingGroupAddition);
await dropOnUpdateTrigger(knex, TableName.PendingGroupAddition);
}
+1
View File
@@ -27,6 +27,7 @@ export * from "./org-bots";
export * from "./org-memberships"; export * from "./org-memberships";
export * from "./org-roles"; export * from "./org-roles";
export * from "./organizations"; export * from "./organizations";
export * from "./pending-group-additions";
export * from "./project-bots"; export * from "./project-bots";
export * from "./project-environments"; export * from "./project-environments";
export * from "./project-keys"; export * from "./project-keys";
+1
View File
@@ -3,6 +3,7 @@ import { z } from "zod";
export enum TableName { export enum TableName {
Users = "users", Users = "users",
Groups = "groups", Groups = "groups",
PendingGroupAddition = "pending_group_additions",
GroupProjectMembership = "group_project_memberships", GroupProjectMembership = "group_project_memberships",
GroupProjectMembershipRole = "group_project_membership_roles", GroupProjectMembershipRole = "group_project_membership_roles",
UserGroupMembership = "user_group_membership", UserGroupMembership = "user_group_membership",
@@ -0,0 +1,20 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const PendingGroupAdditionsSchema = z.object({
id: z.string().uuid(),
userId: z.string().uuid(),
groupId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TPendingGroupAdditions = z.infer<typeof PendingGroupAdditionsSchema>;
export type TPendingGroupAdditionsInsert = Omit<z.input<typeof PendingGroupAdditionsSchema>, TImmutableDBKeys>;
export type TPendingGroupAdditionsUpdate = Partial<Omit<z.input<typeof PendingGroupAdditionsSchema>, TImmutableDBKeys>>;
+35 -8
View File
@@ -192,6 +192,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => { handler: async (req) => {
console.log(`GET /Users/${req.params.userId}`);
const user = await req.server.services.scim.getScimUser({ const user = await req.server.services.scim.getScimUser({
userId: req.params.userId, userId: req.params.userId,
orgId: req.permission.orgId orgId: req.permission.orgId
@@ -246,6 +247,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => { handler: async (req) => {
console.log(`POST /Users req.body: `, req.body);
const primaryEmail = req.body.emails?.find((email) => email.primary)?.value; const primaryEmail = req.body.emails?.find((email) => email.primary)?.value;
const user = await req.server.services.scim.createScimUser({ const user = await req.server.services.scim.createScimUser({
@@ -273,6 +275,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => { handler: async (req) => {
console.log(`DELETE /Users/${req.params.userId}`);
const user = await req.server.services.scim.deleteScimUser({ const user = await req.server.services.scim.deleteScimUser({
userId: req.params.userId, userId: req.params.userId,
orgId: req.permission.orgId orgId: req.permission.orgId
@@ -289,14 +292,28 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
body: z.object({ body: z.object({
schemas: z.array(z.string()), schemas: z.array(z.string()),
displayName: z.string().trim(), displayName: z.string().trim(),
members: z.array(z.any()).length(0).optional() // okta-specific members: z
.array(
z.object({
value: z.string(),
display: z.string()
})
)
.optional() // okta-specific
}), }),
response: { response: {
200: z.object({ 200: z.object({
schemas: z.array(z.string()), schemas: z.array(z.string()),
id: z.string().trim(), id: z.string().trim(),
displayName: z.string().trim(), displayName: z.string().trim(),
members: z.array(z.any()).length(0), members: z
.array(
z.object({
value: z.string(),
display: z.string()
})
)
.optional(),
meta: z.object({ meta: z.object({
resourceType: z.string().trim() resourceType: z.string().trim()
}) })
@@ -305,9 +322,10 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => { handler: async (req) => {
console.log(`POST /Groups req.body: `, req.body);
const group = await req.server.services.scim.createScimGroup({ const group = await req.server.services.scim.createScimGroup({
displayName: req.body.displayName, orgId: req.permission.orgId,
orgId: req.permission.orgId ...req.body
}); });
return group; return group;
@@ -345,6 +363,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => { handler: async (req) => {
console.log(`GET /Groups req.query: `, req.query);
const groups = await req.server.services.scim.listScimGroups({ const groups = await req.server.services.scim.listScimGroups({
orgId: req.permission.orgId, orgId: req.permission.orgId,
offset: req.query.startIndex, offset: req.query.startIndex,
@@ -381,6 +400,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => { handler: async (req) => {
console.log(`GET /Groups/${req.params.groupId}`);
const group = await req.server.services.scim.getScimGroup({ const group = await req.server.services.scim.getScimGroup({
groupId: req.params.groupId, groupId: req.params.groupId,
orgId: req.permission.orgId orgId: req.permission.orgId
@@ -400,7 +420,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
schemas: z.array(z.string()), schemas: z.array(z.string()),
id: z.string().trim(), id: z.string().trim(),
displayName: z.string().trim(), displayName: z.string().trim(),
members: z.array(z.any()).length(0) members: z.array(
z.object({
value: z.string(), // infisical userId
display: z.string()
})
) // note: is this where members are added to group?
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -421,10 +446,11 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => { handler: async (req) => {
console.log(`PUT /Groups/${req.params.groupId} req.body: `, req.body);
const group = await req.server.services.scim.updateScimGroupNamePut({ const group = await req.server.services.scim.updateScimGroupNamePut({
groupId: req.params.groupId, groupId: req.params.groupId,
orgId: req.permission.orgId, orgId: req.permission.orgId,
displayName: req.body.displayName ...req.body
}); });
return group; return group;
@@ -482,8 +508,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => { handler: async (req) => {
// console.log("PATCH /Groups/:groupId req.body: ", req.body); console.log(`PATCH /Groups/:${req.params.groupId} req.body: `, req.body);
// console.log("PATCH /Groups/:groupId req.body: ", req.body.Operations[0]);
const group = await req.server.services.scim.updateScimGroupNamePatch({ const group = await req.server.services.scim.updateScimGroupNamePatch({
groupId: req.params.groupId, groupId: req.params.groupId,
orgId: req.permission.orgId, orgId: req.permission.orgId,
@@ -507,6 +532,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => { handler: async (req) => {
console.log(`DELETE /Groups/:${req.params.groupId}`);
const group = await req.server.services.scim.deleteScimGroup({ const group = await req.server.services.scim.deleteScimGroup({
groupId: req.params.groupId, groupId: req.params.groupId,
orgId: req.permission.orgId orgId: req.permission.orgId
@@ -557,6 +583,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => { handler: async (req) => {
console.log(`PUT /Users/:${req.params.userId} req.body: `, req.body);
const user = await req.server.services.scim.replaceScimUser({ const user = await req.server.services.scim.replaceScimUser({
userId: req.params.userId, userId: req.params.userId,
orgId: req.permission.orgId, orgId: req.permission.orgId,
+44 -18
View File
@@ -59,13 +59,13 @@ export const groupDALFactory = (db: TDbClient) => {
} }
}; };
const countAllGroupMembers = async ({ orgId, groupId }: { orgId: string; groupId: string }) => { const countGroupMembers = async ({ orgId, groupId }: { orgId: string; groupId: string }) => {
try { try {
interface CountResult { interface CountResult {
count: string; count: string;
} }
const doc = await db<CountResult>(TableName.OrgMembership) const directCount = await db<CountResult>(TableName.OrgMembership)
.where(`${TableName.OrgMembership}.orgId`, orgId) .where(`${TableName.OrgMembership}.orgId`, orgId)
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.UserGroupMembership, function () { .leftJoin(TableName.UserGroupMembership, function () {
@@ -75,13 +75,18 @@ export const groupDALFactory = (db: TDbClient) => {
db.raw("?", [groupId]) db.raw("?", [groupId])
); );
}) })
.where({ isGhost: false }) .where({ isGhost: false, isAccepted: true })
.count(`${TableName.Users}.id`) .count(`${TableName.Users}.id`)
.first(); .first();
return parseInt((doc?.count as string) || "0", 10); const pendingCount = await db<CountResult>(TableName.PendingGroupAddition)
.where(`${TableName.PendingGroupAddition}.groupId`, groupId)
.count("*")
.first();
return parseInt((directCount?.count as string) || "0", 10) + parseInt((pendingCount?.count as string) || "0", 10);
} catch (err) { } catch (err) {
throw new DatabaseError({ error: err, name: "Count all group members" }); throw new DatabaseError({ error: err, name: "Count all direct group members" });
} }
}; };
@@ -110,14 +115,36 @@ export const groupDALFactory = (db: TDbClient) => {
db.raw("?", [groupId]) db.raw("?", [groupId])
); );
}) })
.select( .leftJoin(TableName.PendingGroupAddition, function () {
this.on(`${TableName.PendingGroupAddition}.userId`, "=", `${TableName.Users}.id`).andOn(
`${TableName.PendingGroupAddition}.groupId`,
"=",
db.raw("?", [groupId])
);
})
.select<
{
id: string;
groupId: string;
email: string;
username: string;
firstName: string;
lastName: string;
userId: string;
isPartOfGroup: boolean;
}[]
>(
db.ref("id").withSchema(TableName.OrgMembership), db.ref("id").withSchema(TableName.OrgMembership),
db.ref("groupId").withSchema(TableName.UserGroupMembership), db.ref("groupId").withSchema(TableName.UserGroupMembership),
db.ref("email").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users), db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId") db.ref("id").withSchema(TableName.Users).as("userId"),
db.raw('CASE WHEN ?? IS NOT NULL OR ?? IS NOT NULL THEN TRUE ELSE FALSE END AS "isPartOfGroup"', [
`${TableName.UserGroupMembership}.groupId`,
`${TableName.PendingGroupAddition}.groupId`
])
) )
.where({ isGhost: false }) .where({ isGhost: false })
.offset(offset); .offset(offset);
@@ -132,16 +159,15 @@ export const groupDALFactory = (db: TDbClient) => {
const members = await query; const members = await query;
return members.map( return members.map(({ email, username: memberUsername, firstName, lastName, userId, isPartOfGroup }) => ({
({ email, username: memberUsername, firstName, lastName, userId, groupId: memberGroupId }) => ({ // TODO: fix type
id: userId, id: userId,
email, email,
username: memberUsername, username: memberUsername,
firstName, firstName,
lastName, lastName,
isPartOfGroup: !!memberGroupId isPartOfGroup
}) }));
);
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find all org members" }); throw new DatabaseError({ error, name: "Find all org members" });
} }
@@ -150,7 +176,7 @@ export const groupDALFactory = (db: TDbClient) => {
return { return {
findGroups, findGroups,
findByOrgId, findByOrgId,
countAllGroupMembers, countGroupMembers,
findAllGroupMembers, findAllGroupMembers,
...groupOrm ...groupOrm
}; };
+701
View File
@@ -0,0 +1,701 @@
import { Knex } from "knex";
import { SecretKeyEncoding, TUsers } from "@app/db/schemas";
import { decryptAsymmetric, encryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ScimRequestError } from "@app/lib/errors";
import {
TAddUsersToGroupByUserIds,
TAddUsersToGroupDirectly,
TAddUsersToPendingGroupAdditions,
TConvertPendingGroupAdditionsToGroupMemberships,
TRemoveUsersFromGroupByUserIds,
TRemoveUsersFromGroupDirectly,
TRemoveUsersFromPendingGroupAdditions
} from "./group-types";
// TODO: write docstrings
/**
* Add users with usernames [usernames] to group [group]
* @param {group} group - group to add user to
* @param {string[]} usernames - username(s) of user(s) to add to group
* @returns {Promise<TUsers>} - user that was added to group
*/
export const addUsersToGroupDirectly = async ({
group,
usernames,
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx: outerTx
}: TAddUsersToGroupDirectly) => {
const processAddition = async (tx: Knex) => {
const users = await userDAL.findUserEncKeyByUsernameBatch(
{
usernames
},
tx
);
const usersUsernamesSet = new Set(users.map((u) => u.username));
usernames.forEach((username) => {
if (!usersUsernamesSet.has(username)) {
throw new BadRequestError({
message: `Failed to find user with username ${username}`
});
}
});
const userIds = users.map((u) => {
if (!u.isAccepted) {
throw new BadRequestError({
message: `User ${u.username} cannot be added to group because they have not confirmed their account`
});
}
return u.userId;
});
// check if user(s) group membership(s) already exists
const existingUserGroupMemberships = await userGroupMembershipDAL.find(
{
groupId: group.id,
$in: {
userId: userIds
}
},
{ tx }
);
if (existingUserGroupMemberships.length) {
throw new BadRequestError({
message: `User(s) are already part of the group ${group.slug}`
});
}
// check if all user(s) are part of the organization
const existingUserOrgMemberships = await orgDAL.findMembership(
{
orgId: group.orgId,
$in: {
userId: userIds
}
},
{ tx }
);
const existingUserOrgMembershipsUsernamesSet = new Set(existingUserOrgMemberships.map((u) => u.username));
usernames.forEach((username) => {
if (!existingUserOrgMembershipsUsernamesSet.has(username))
throw new BadRequestError({
message: `User ${username} is not part of the organization`
});
});
await userGroupMembershipDAL.insertMany(
userIds.map((userId) => ({
userId,
groupId: group.id
})),
tx
);
// check which projects the group is part of
const projectIds = Array.from(
new Set(
(
await groupProjectDAL.find(
{
groupId: group.id
},
{ tx }
)
).map((gp) => gp.projectId)
)
);
const keys = await projectKeyDAL.find(
{
$in: {
projectId: projectIds,
receiverId: userIds
}
},
{ tx }
);
const userKeysSet = new Set(keys.map((k) => `${k.projectId}-${k.receiverId}`));
for await (const projectId of projectIds) {
const usersToAddProjectKeyFor = users.filter((u) => !userKeysSet.has(`${projectId}-${u.userId}`));
if (usersToAddProjectKeyFor.length) {
// there are users who need to be shared keys
// process adding bulk users to projects for each project individually
const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx);
if (!ghostUser) {
throw new BadRequestError({
message: "Failed to find sudo user"
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, tx);
if (!ghostUserLatestKey) {
throw new BadRequestError({
message: "Failed to find sudo user latest key"
});
}
const bot = await projectBotDAL.findOne({ projectId }, tx);
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 projectKeysToAdd = usersToAddProjectKeyFor.map((user) => {
const { ciphertext: encryptedKey, nonce } = encryptAsymmetric(
plaintextProjectKey,
user.publicKey,
botPrivateKey
);
return {
encryptedKey,
nonce,
senderId: ghostUser.id,
receiverId: user.userId,
projectId
};
});
await projectKeyDAL.insertMany(projectKeysToAdd, tx);
}
}
return users;
};
if (outerTx) {
return processAddition(outerTx);
}
return userDAL.transaction(async (tx) => {
return processAddition(tx);
});
};
export const addUsersToPendingGroupAdditions = async ({
group,
userIds,
pendingGroupAdditionDAL,
userDAL,
orgDAL,
tx: outerTx
}: TAddUsersToPendingGroupAdditions) => {
const processAddition = async (tx: Knex) => {
const users = await userDAL.find(
{
$in: {
id: userIds
}
},
{ tx }
);
const usersUserIdsSet = new Set(users.map((u) => u.id));
userIds.forEach((userId) => {
if (!usersUserIdsSet.has(userId)) {
throw new BadRequestError({
message: `Failed to find user with id ${userId}`
});
}
});
users.map((u) => {
if (u.isAccepted) {
throw new BadRequestError({
message: `User ${u.username} cannot be added to a pending group addition because they have confirmed their account`
});
}
return u.id;
});
// check if user(s) pending group addition(s) already exist
const existingPendingGroupAdditions = await pendingGroupAdditionDAL.find(
{
groupId: group.id,
$in: {
userId: userIds
}
},
{ tx }
);
if (existingPendingGroupAdditions.length) {
throw new BadRequestError({
message: `User(s) are already part of the group ${group.slug}`
});
}
// check if all user(s) are part of the organization
const existingUserOrgMemberships = await orgDAL.findMembership(
{
orgId: group.orgId,
$in: {
userId: userIds
}
},
{ tx }
);
const existingUserOrgMembershipsUserIdsSet = new Set(existingUserOrgMemberships.map((u) => u.userId));
userIds.forEach((userId) => {
if (!existingUserOrgMembershipsUserIdsSet.has(userId))
throw new BadRequestError({
message: `User with id ${userId} is not part of the organization`
});
});
await pendingGroupAdditionDAL.insertMany(
users.map((user) => ({
userId: user.id,
groupId: group.id
})),
tx
);
return users;
};
if (outerTx) {
return processAddition(outerTx);
}
return userDAL.transaction(async (tx) => {
return processAddition(tx);
});
};
export const addUsersToGroupByUserIds = async ({
group,
userIds,
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
pendingGroupAdditionDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx: outerTx
}: TAddUsersToGroupByUserIds) => {
const processAddition = async (tx: Knex) => {
const foundMembers = await userDAL.find({
$in: {
id: userIds
}
});
const foundMembersIdsSet = new Set(foundMembers.map((member) => member.id));
const isCompleteMatch = userIds.every((userId) => foundMembersIdsSet.has(userId));
if (!isCompleteMatch) {
throw new ScimRequestError({
detail: "Members not found",
status: 404
});
}
const membersToAddToGroupDirectly: TUsers[] = [];
const membersToAddToGroupPending: TUsers[] = [];
foundMembers.forEach((member) => {
if (member.isAccepted) {
// add accepted member to group
membersToAddToGroupDirectly.push(member);
} else {
// add incomplete member to pending group addition
membersToAddToGroupPending.push(member);
}
});
let addedUsers: TUsers[] = [];
if (membersToAddToGroupDirectly.length) {
addedUsers = addedUsers.concat(
await addUsersToGroupDirectly({
group,
usernames: membersToAddToGroupDirectly.map((member) => member.username),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx
})
);
}
if (membersToAddToGroupPending.length) {
addedUsers = addedUsers.concat(
await addUsersToPendingGroupAdditions({
group,
userIds: membersToAddToGroupPending.map((member) => member.id),
pendingGroupAdditionDAL,
userDAL,
orgDAL,
tx
})
);
}
return addedUsers;
};
if (outerTx) {
return processAddition(outerTx);
}
return userDAL.transaction(async (tx) => {
return processAddition(tx);
});
};
export const removeUsersFromGroupDirectly = async ({
group,
userIds,
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL,
tx: outerTx
}: TRemoveUsersFromGroupDirectly) => {
const processRemoval = async (tx: Knex) => {
const users = await userDAL.find(
{
$in: {
id: userIds
}
},
{ tx }
);
const usersUserIdsSet = new Set(users.map((u) => u.id));
userIds.forEach((userId) => {
if (!usersUserIdsSet.has(userId)) {
throw new BadRequestError({
message: `Failed to find user with id ${userId}`
});
}
});
// check if user group membership already exists
const existingUserGroupMemberships = await userGroupMembershipDAL.find(
{
groupId: group.id,
$in: {
userId: userIds
}
},
{ tx }
);
const existingUserGroupMembershipsUserIdsSet = new Set(existingUserGroupMemberships.map((u) => u.userId));
userIds.forEach((userId) => {
if (!existingUserGroupMembershipsUserIdsSet.has(userId))
throw new BadRequestError({
message: `User(s) are not part of the group ${group.slug}`
});
});
// check which projects the group is part of
const projectIds = Array.from(
new Set(
(
await groupProjectDAL.find(
{
groupId: group.id
},
{ tx }
)
).map((gp) => gp.projectId)
)
);
// TODO: this part can be optimized
for await (const userId of userIds) {
const t = await userGroupMembershipDAL.filterProjectsByUserMembership(userId, group.id, projectIds, tx);
const projectsToDeleteKeyFor = projectIds.filter((p) => !t.has(p));
if (projectsToDeleteKeyFor.length) {
await projectKeyDAL.delete(
{
receiverId: userId,
$in: {
projectId: projectsToDeleteKeyFor
}
},
tx
);
}
await userGroupMembershipDAL.delete(
{
groupId: group.id,
userId
},
tx
);
}
return users;
};
if (outerTx) {
return processRemoval(outerTx);
}
return userDAL.transaction(async (tx) => {
return processRemoval(tx);
});
};
export const removeUsersFromPendingGroupAdditions = async ({
group,
userIds,
userDAL,
pendingGroupAdditionDAL,
tx: outerTx
}: TRemoveUsersFromPendingGroupAdditions) => {
const processRemoval = async (tx: Knex) => {
const users = await userDAL.find(
{
$in: {
id: userIds
}
},
{ tx }
);
const usersUserIdsSet = new Set(users.map((u) => u.id));
userIds.forEach((userId) => {
if (!usersUserIdsSet.has(userId)) {
throw new BadRequestError({
message: `Failed to find user with id ${userId}`
});
}
});
// check if user pending group addition already exists
const existingPendingGroupAdditions = await pendingGroupAdditionDAL.find(
{
groupId: group.id,
$in: {
userId: userIds
}
},
{ tx }
);
const existingPendingGroupAdditionsUserIdsSet = new Set(existingPendingGroupAdditions.map((u) => u.userId));
userIds.forEach((userId) => {
if (!existingPendingGroupAdditionsUserIdsSet.has(userId))
throw new BadRequestError({
message: `User(s) are not part of the group ${group.slug}`
});
});
await pendingGroupAdditionDAL.delete(
{
groupId: group.id,
$in: {
userId: userIds
}
},
tx
);
return users;
};
if (outerTx) {
return processRemoval(outerTx);
}
return userDAL.transaction(async (tx) => {
return processRemoval(tx);
});
};
export const convertPendingGroupAdditionsToGroupMemberships = async ({
userIds,
userDAL,
pendingGroupAdditionDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx: outerTx
}: TConvertPendingGroupAdditionsToGroupMemberships) => {
const processConversion = async (tx: Knex) => {
const users = await userDAL.find(
{
$in: {
id: userIds
}
},
{ tx }
);
const usersUserIdsSet = new Set(users.map((u) => u.id));
userIds.forEach((userId) => {
if (!usersUserIdsSet.has(userId)) {
throw new BadRequestError({
message: `Failed to find user with id ${userId}`
});
}
});
users.forEach((user) => {
if (!user.isAccepted) {
throw new BadRequestError({
message: `Failed to convert pending group additions to group memberships for user ${user.username} because they have not confirmed their account`
});
}
});
const pendingGroupAdditions = await pendingGroupAdditionDAL.deletePendingGroupAdditionsByUserIds(userIds, tx);
for await (const pendingGroupAddition of pendingGroupAdditions) {
await addUsersToGroupDirectly({
group: pendingGroupAddition.group,
usernames: [pendingGroupAddition.user.username],
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx
});
}
};
if (outerTx) {
return processConversion(outerTx);
}
return userDAL.transaction(async (tx) => {
await processConversion(tx);
});
};
export const removeUsersFromGroupByUserIds = async ({
group,
userIds,
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
pendingGroupAdditionDAL,
projectKeyDAL,
tx: outerTx
}: TRemoveUsersFromGroupByUserIds) => {
const processRemoval = async (tx: Knex) => {
const foundMembers = await userDAL.find({
$in: {
id: userIds
}
});
const foundMembersIdsSet = new Set(foundMembers.map((member) => member.id));
const isCompleteMatch = userIds.every((userId) => foundMembersIdsSet.has(userId));
if (!isCompleteMatch) {
throw new ScimRequestError({
detail: "Members not found",
status: 404
});
}
const membersToRemoveFromGroupDirectly: TUsers[] = [];
const membersToRemoveFromGroupPending: TUsers[] = [];
foundMembers.forEach((member) => {
if (member.isAccepted) {
// remove accepted member from group
membersToRemoveFromGroupDirectly.push(member);
} else {
// remove incomplete member from pending group addition
membersToRemoveFromGroupPending.push(member);
}
});
console.log("removeUsersFromGroupByUserIds membersToRemoveFromGroupDirectly: ", membersToRemoveFromGroupDirectly);
console.log("removeUsersFromGroupByUserIds membersToRemoveFromGroupPending: ", membersToRemoveFromGroupPending);
let removedUsers: TUsers[] = [];
if (membersToRemoveFromGroupDirectly.length) {
removedUsers = removedUsers.concat(
await removeUsersFromGroupDirectly({
group,
userIds: membersToRemoveFromGroupDirectly.map((member) => member.id),
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL,
tx
})
);
}
if (membersToRemoveFromGroupPending.length) {
removedUsers = removedUsers.concat(
await removeUsersFromPendingGroupAdditions({
group,
userIds: membersToRemoveFromGroupPending.map((member) => member.id),
pendingGroupAdditionDAL,
userDAL,
tx
})
);
}
return removedUsers;
};
if (outerTx) {
return processRemoval(outerTx);
}
return userDAL.transaction(async (tx) => {
return processRemoval(tx);
});
};
+43 -159
View File
@@ -1,22 +1,23 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import { OrgMembershipRole, SecretKeyEncoding, TOrgRoles } from "@app/db/schemas"; import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
import { TPendingGroupAdditionDALFactory } from "@app/ee/services/group/pending-group-addition-dal";
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 "@app/services/group-project/group-project-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TGroupProjectDALFactory } from "../../../services/group-project/group-project-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 { 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";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { TGroupDALFactory } from "./group-dal"; import { TGroupDALFactory } from "./group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "./group-fns";
import { import {
TAddUserToGroupDTO, TAddUserToGroupDTO,
TCreateGroupDTO, TCreateGroupDTO,
@@ -28,20 +29,21 @@ import {
import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
type TGroupServiceFactoryDep = { type TGroupServiceFactoryDep = {
userDAL: Pick<TUserDALFactory, "findOne" | "findUserEncKeyByUsername">; userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUsernameBatch" | "transaction" | "findOne">;
groupDAL: Pick< groupDAL: Pick<
TGroupDALFactory, TGroupDALFactory,
"create" | "findOne" | "update" | "delete" | "findAllGroupMembers" | "countAllGroupMembers" "create" | "findOne" | "update" | "delete" | "findAllGroupMembers" | "countGroupMembers"
>; >;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
orgDAL: Pick<TOrgDALFactory, "findMembership">; orgDAL: Pick<TOrgDALFactory, "findMembership">;
userGroupMembershipDAL: Pick< userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory, TUserGroupMembershipDALFactory,
"findOne" | "create" | "delete" | "filterProjectsByUserMembership" "findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find"
>; >;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "create" | "delete" | "findLatestProjectKey">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
pendingGroupAdditionDAL: TPendingGroupAdditionDALFactory; // remove?
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
@@ -57,6 +59,7 @@ export const groupServiceFactory = ({
projectDAL, projectDAL,
projectBotDAL, projectBotDAL,
projectKeyDAL, projectKeyDAL,
pendingGroupAdditionDAL,
permissionService, permissionService,
licenseService licenseService
}: TGroupServiceFactoryDep) => { }: TGroupServiceFactoryDep) => {
@@ -227,7 +230,7 @@ export const groupServiceFactory = ({
username username
}); });
const totalCount = await groupDAL.countAllGroupMembers({ const totalCount = await groupDAL.countGroupMembers({
orgId: group.orgId, orgId: group.orgId,
groupId: group.id groupId: group.id
}); });
@@ -272,111 +275,23 @@ export const groupServiceFactory = ({
if (!hasRequiredPriviledges) if (!hasRequiredPriviledges)
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 const user = await userDAL.findOne({ username });
const user = await userDAL.findUserEncKeyByUsername({ if (!user) throw new BadRequestError({ message: `Failed to find user with username ${username}` });
username
const users = await addUsersToGroupByUserIds({
group,
userIds: [user.id],
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
pendingGroupAdditionDAL,
projectKeyDAL,
projectDAL,
projectBotDAL
}); });
if (!user) return users[0];
throw new BadRequestError({
message: `Failed to find user with username ${username}`
});
// check if user group membership already exists
const existingUserGroupMembership = await userGroupMembershipDAL.findOne({
groupId: group.id,
userId: user.userId
});
if (existingUserGroupMembership)
throw new BadRequestError({
message: `User ${username} is already part of the group ${groupSlug}`
});
// check if user is even part of the organization
const existingUserOrgMembership = await orgDAL.findMembership({
userId: user.userId,
orgId: actorOrgId
});
if (!existingUserOrgMembership)
throw new BadRequestError({
message: `User ${username} is not part of the organization`
});
await userGroupMembershipDAL.create({
userId: user.userId,
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;
}; };
const removeUserFromGroup = async ({ const removeUserFromGroup = async ({
@@ -416,51 +331,20 @@ export const groupServiceFactory = ({
if (!hasRequiredPriviledges) if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" }); throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" });
const user = await userDAL.findOne({ const user = await userDAL.findOne({ username });
username if (!user) throw new BadRequestError({ message: `Failed to find user with username ${username}` });
const users = await removeUsersFromGroupByUserIds({
group,
userIds: [user.id],
userDAL,
userGroupMembershipDAL,
pendingGroupAdditionDAL,
groupProjectDAL,
projectKeyDAL
}); });
if (!user) return users[0];
throw new BadRequestError({
message: `Failed to find user with username ${username}`
});
// check if user group membership already exists
const existingUserGroupMembership = await userGroupMembershipDAL.findOne({
groupId: group.id,
userId: user.id
});
if (!existingUserGroupMembership)
throw new BadRequestError({
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({
groupId: group.id,
userId: user.id
});
return user;
}; };
return { return {
@@ -1,4 +1,15 @@
import { Knex } from "knex";
import { TGroups } from "@app/db/schemas";
import { TPendingGroupAdditionDALFactory } from "@app/ee/services/group/pending-group-addition-dal";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TGenericPermission } from "@app/lib/types"; import { TGenericPermission } from "@app/lib/types";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { TUserDALFactory } from "@app/services/user/user-dal";
export type TCreateGroupDTO = { export type TCreateGroupDTO = {
name: string; name: string;
@@ -35,3 +46,83 @@ export type TRemoveUserFromGroupDTO = {
groupSlug: string; groupSlug: string;
username: string; username: string;
} & TGenericPermission; } & TGenericPermission;
// group fns types
export type TAddUsersToGroupByUserIds = {
group: TGroups;
userIds: string[];
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUsernameBatch" | "transaction">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">;
orgDAL: Pick<TOrgDALFactory, "findMembership">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
pendingGroupAdditionDAL: Pick<TPendingGroupAdditionDALFactory, "insertMany" | "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx?: Knex;
};
export type TAddUsersToGroupDirectly = {
group: TGroups;
usernames: string[];
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUsernameBatch" | "transaction">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">;
orgDAL: Pick<TOrgDALFactory, "findMembership">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx?: Knex;
};
export type TAddUsersToPendingGroupAdditions = {
userIds: string[];
group: TGroups;
pendingGroupAdditionDAL: Pick<TPendingGroupAdditionDALFactory, "find" | "insertMany">;
userDAL: Pick<TUserDALFactory, "find" | "transaction">;
orgDAL: Pick<TOrgDALFactory, "findMembership">;
tx?: Knex;
};
export type TRemoveUsersFromGroupByUserIds = {
group: TGroups;
userIds: string[];
userDAL: Pick<TUserDALFactory, "find" | "transaction">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "filterProjectsByUserMembership" | "delete">;
pendingGroupAdditionDAL: Pick<TPendingGroupAdditionDALFactory, "find" | "delete">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "delete">;
tx?: Knex;
};
export type TRemoveUsersFromGroupDirectly = {
group: TGroups;
userIds: string[];
userDAL: Pick<TUserDALFactory, "find" | "transaction">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "filterProjectsByUserMembership" | "delete">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "delete">;
tx?: Knex;
};
export type TRemoveUsersFromPendingGroupAdditions = {
group: TGroups;
userIds: string[];
pendingGroupAdditionDAL: Pick<TPendingGroupAdditionDALFactory, "find" | "delete">;
userDAL: Pick<TUserDALFactory, "find" | "transaction">;
tx?: Knex;
};
export type TConvertPendingGroupAdditionsToGroupMemberships = {
userIds: string[];
pendingGroupAdditionDAL: Pick<TPendingGroupAdditionDALFactory, "deletePendingGroupAdditionsByUserIds">;
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUsernameBatch" | "transaction" | "find" | "findById">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">;
orgDAL: Pick<TOrgDALFactory, "findMembership">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx?: Knex;
};
@@ -0,0 +1,55 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
export type TPendingGroupAdditionDALFactory = ReturnType<typeof pendingGroupAdditionDALFactory>;
export const pendingGroupAdditionDALFactory = (db: TDbClient) => {
const pendingGroupAdditionOrm = ormify(db, TableName.PendingGroupAddition);
// special query
const deletePendingGroupAdditionsByUserIds = async (userIds: string[], tx?: Knex) => {
try {
const pendingGroupAdditions = await (tx || db)(TableName.PendingGroupAddition)
.whereIn(`${TableName.PendingGroupAddition}.userId`, userIds)
.join(TableName.Groups, `${TableName.PendingGroupAddition}.groupId`, `${TableName.Groups}.id`)
.join(TableName.Users, `${TableName.PendingGroupAddition}.userId`, `${TableName.Users}.id`);
await pendingGroupAdditionOrm.delete(
{
$in: {
userId: userIds
}
},
tx
);
return pendingGroupAdditions.map(({ userId, username, groupId, orgId, name, slug, role, roleId }) => ({
user: {
id: userId,
username
},
group: {
id: groupId,
orgId,
name,
slug,
role,
roleId,
createdAt: new Date(),
updatedAt: new Date()
}
}));
} catch (error) {
throw new DatabaseError({ error, name: "Filter projects by user membership" });
}
};
return {
...pendingGroupAdditionOrm,
deletePendingGroupAdditionsByUserIds
};
};
@@ -1,3 +1,5 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TUserEncryptionKeys } from "@app/db/schemas"; import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
@@ -14,24 +16,28 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
* - The user is a member of a group that is a member of the project, excluding projects that they are part of * - The user is a member of a group that is a member of the project, excluding projects that they are part of
* through the group with id [groupId]. * through the group with id [groupId].
*/ */
const filterProjectsByUserMembership = async (userId: string, groupId: string, projectIds: string[]) => { const filterProjectsByUserMembership = async (userId: string, groupId: string, projectIds: string[], tx?: Knex) => {
const userProjectMemberships: string[] = await db(TableName.ProjectMembership) try {
.where(`${TableName.ProjectMembership}.userId`, userId) const userProjectMemberships: string[] = await (tx || db)(TableName.ProjectMembership)
.whereIn(`${TableName.ProjectMembership}.projectId`, projectIds) .where(`${TableName.ProjectMembership}.userId`, userId)
.pluck(`${TableName.ProjectMembership}.projectId`); .whereIn(`${TableName.ProjectMembership}.projectId`, projectIds)
.pluck(`${TableName.ProjectMembership}.projectId`);
const userGroupMemberships: string[] = await db(TableName.UserGroupMembership) const userGroupMemberships: string[] = await (tx || db)(TableName.UserGroupMembership)
.where(`${TableName.UserGroupMembership}.userId`, userId) .where(`${TableName.UserGroupMembership}.userId`, userId)
.whereNot(`${TableName.UserGroupMembership}.groupId`, groupId) .whereNot(`${TableName.UserGroupMembership}.groupId`, groupId)
.join( .join(
TableName.GroupProjectMembership, TableName.GroupProjectMembership,
`${TableName.UserGroupMembership}.groupId`, `${TableName.UserGroupMembership}.groupId`,
`${TableName.GroupProjectMembership}.groupId` `${TableName.GroupProjectMembership}.groupId`
) )
.whereIn(`${TableName.GroupProjectMembership}.projectId`, projectIds) .whereIn(`${TableName.GroupProjectMembership}.projectId`, projectIds)
.pluck(`${TableName.GroupProjectMembership}.projectId`); .pluck(`${TableName.GroupProjectMembership}.projectId`);
return new Set(userProjectMemberships.concat(userGroupMemberships)); return new Set(userProjectMemberships.concat(userGroupMemberships));
} catch (error) {
throw new DatabaseError({ error, name: "Filter projects by user membership" });
}
}; };
// special query // special query
@@ -45,7 +51,7 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
) )
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) .join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.where(`${TableName.GroupProjectMembership}.projectId`, projectId) .where(`${TableName.GroupProjectMembership}.projectId`, projectId)
.whereIn(`${TableName.Users}.username`, usernames) // TODO: pluck usernames .whereIn(`${TableName.Users}.username`, usernames)
.pluck(`${TableName.Users}.id`); .pluck(`${TableName.Users}.id`);
return usernameDocs; return usernameDocs;
@@ -24,10 +24,10 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
customAlerts: false, customAlerts: false,
auditLogs: false, auditLogs: false,
auditLogsRetentionDays: 0, auditLogsRetentionDays: 0,
samlSSO: false, samlSSO: true,
scim: false, scim: true,
ldap: false, ldap: false,
groups: false, groups: true,
status: null, status: null,
trial_end: null, trial_end: null,
has_used_trial: true, has_used_trial: true,
@@ -40,10 +40,10 @@ export type TFeatureSet = {
customAlerts: false; customAlerts: false;
auditLogs: false; auditLogs: false;
auditLogsRetentionDays: 0; auditLogsRetentionDays: 0;
samlSSO: false; samlSSO: true;
scim: false; scim: true;
ldap: false; ldap: false;
groups: false; groups: true;
status: null; status: null;
trial_end: null; trial_end: null;
has_used_trial: true; has_used_trial: true;
+221 -27
View File
@@ -4,15 +4,21 @@ import jwt from "jsonwebtoken";
import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups } from "@app/db/schemas"; import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups } from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
import { TPendingGroupAdditionDALFactory } from "@app/ee/services/group/pending-group-addition-dal";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TScimDALFactory } from "@app/ee/services/scim/scim-dal"; import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TOrgPermission } from "@app/lib/types"; import { TOrgPermission } from "@app/lib/types";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { deleteOrgMembership } from "@app/services/org/org-fns"; import { deleteOrgMembership } from "@app/services/org/org-fns";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
@@ -42,14 +48,22 @@ import {
type TScimServiceFactoryDep = { type TScimServiceFactoryDep = {
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">; scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">;
userDAL: Pick<TUserDALFactory, "findOne" | "create" | "transaction">; userDAL: Pick<TUserDALFactory, "find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUsernameBatch">;
orgDAL: Pick< orgDAL: Pick<
TOrgDALFactory, TOrgDALFactory,
"createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction" "createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction"
>; >;
projectDAL: Pick<TProjectDALFactory, "find">; projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">;
groupDAL: Pick<TGroupDALFactory, "create" | "findOne" | "findAllGroupMembers" | "update" | "delete" | "findGroups">; groupDAL: Pick<
TGroupDALFactory,
"create" | "findOne" | "findAllGroupMembers" | "update" | "delete" | "findGroups" | "transaction"
>;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
userGroupMembershipDAL: TUserGroupMembershipDALFactory; // TODO: Pick
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
pendingGroupAdditionDAL: TPendingGroupAdditionDALFactory; // TODO: Pick
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
smtpService: TSmtpService; smtpService: TSmtpService;
@@ -65,6 +79,11 @@ export const scimServiceFactory = ({
projectDAL, projectDAL,
projectMembershipDAL, projectMembershipDAL,
groupDAL, groupDAL,
groupProjectDAL,
userGroupMembershipDAL,
projectKeyDAL,
projectBotDAL,
pendingGroupAdditionDAL,
permissionService, permissionService,
smtpService smtpService
}: TScimServiceFactoryDep) => { }: TScimServiceFactoryDep) => {
@@ -473,7 +492,19 @@ export const scimServiceFactory = ({
}; };
const listScimGroups = async ({ orgId, offset, limit }: TListScimGroupsDTO) => { const listScimGroups = async ({ orgId, offset, limit }: TListScimGroupsDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to list SCIM groups due to plan restriction. Upgrade plan to list SCIM groups."
});
const org = await orgDAL.findById(orgId); const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled) if (!org.scimEnabled)
throw new ScimRequestError({ throw new ScimRequestError({
@@ -500,30 +531,77 @@ export const scimServiceFactory = ({
}); });
}; };
const createScimGroup = async ({ displayName, orgId }: TCreateScimGroupDTO) => { const createScimGroup = async ({ displayName, orgId, members }: TCreateScimGroupDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to create a SCIM group due to plan restriction. Upgrade plan to create a SCIM group."
});
const org = await orgDAL.findById(orgId); const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled) if (!org.scimEnabled)
throw new ScimRequestError({ throw new ScimRequestError({
detail: "SCIM is disabled for the organization", detail: "SCIM is disabled for the organization",
status: 403 status: 403
}); });
const group = await groupDAL.create({ const newGroup = await groupDAL.transaction(async (tx) => {
name: displayName, const group = await groupDAL.create(
slug: slugify(`${displayName}-${alphaNumericNanoId(4)}`), {
orgId, name: displayName,
role: OrgMembershipRole.NoAccess slug: slugify(`${displayName}-${alphaNumericNanoId(4)}`),
orgId,
role: OrgMembershipRole.NoAccess
},
tx
);
if (members && members.length) {
const newMembers = await addUsersToGroupByUserIds({
group,
userIds: members.map((member) => member.value),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
pendingGroupAdditionDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx
});
return { group, newMembers };
}
return { group, newMembers: [] };
}); });
return buildScimGroup({ return buildScimGroup({
groupId: group.id, groupId: newGroup.group.id,
name: group.name, name: newGroup.group.name,
members: [] members: newGroup.newMembers.map((member) => ({
value: member.id,
display: `${member.firstName} ${member.lastName}`
}))
}); });
}; };
const getScimGroup = async ({ groupId, orgId }: TGetScimGroupDTO) => { const getScimGroup = async ({ groupId, orgId }: TGetScimGroupDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to get SCIM group due to plan restriction. Upgrade plan to get SCIM group."
});
const group = await groupDAL.findOne({ const group = await groupDAL.findOne({
id: groupId, id: groupId,
orgId orgId
@@ -536,6 +614,7 @@ export const scimServiceFactory = ({
}); });
} }
// TODO: update to include pending group additions
const users = await groupDAL.findAllGroupMembers({ const users = await groupDAL.findAllGroupMembers({
orgId: group.orgId, orgId: group.orgId,
groupId: group.id groupId: group.id
@@ -553,35 +632,130 @@ export const scimServiceFactory = ({
}); });
}; };
const updateScimGroupNamePut = async ({ groupId, orgId, displayName }: TUpdateScimGroupNamePutDTO) => { const updateScimGroupNamePut = async ({ groupId, orgId, displayName, members }: TUpdateScimGroupNamePutDTO) => {
const [group] = await groupDAL.update( console.log("updateScimGroupNamePut args: ", {
{ groupId,
id: groupId, orgId,
orgId displayName,
}, members
{ });
name: displayName
}
);
if (!group) { const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to update SCIM group due to plan restriction. Upgrade plan to update SCIM group."
});
const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({ throw new ScimRequestError({
detail: "Group Not Found", detail: "Organization Not Found",
status: 404 status: 404
}); });
} }
if (!org.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
const updatedGroup = await groupDAL.transaction(async (tx) => {
const [group] = await groupDAL.update(
{
id: groupId,
orgId
},
{
name: displayName
}
);
if (!group) {
throw new ScimRequestError({
detail: "Group Not Found",
status: 404
});
}
if (members) {
const membersIdsSet = new Set(members.map((member) => member.value));
const directMemberUserIds = (
await userGroupMembershipDAL.find({
groupId: group.id
})
).map((membership) => membership.userId);
const pendingGroupAdditionsUserIds = (
await pendingGroupAdditionDAL.find({
groupId: group.id
})
).map((pendingGroupAddition) => pendingGroupAddition.userId);
const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds);
const allMembersUserIdsSet = new Set(allMembersUserIds);
const toAddUserIds = members.filter((member) => !allMembersUserIdsSet.has(member.value));
const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId));
if (toAddUserIds.length) {
await addUsersToGroupByUserIds({
group,
userIds: toAddUserIds.map((member) => member.value),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
pendingGroupAdditionDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx
});
}
if (toRemoveUserIds.length) {
await removeUsersFromGroupByUserIds({
group,
userIds: toRemoveUserIds,
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
pendingGroupAdditionDAL,
projectKeyDAL,
tx
});
}
}
return group;
});
return buildScimGroup({ return buildScimGroup({
groupId: group.id, groupId: updatedGroup.id,
name: group.name, name: updatedGroup.name,
members: [] members
}); });
}; };
// TODO: add support for add/remove op // TODO: add support for add/remove op
const updateScimGroupNamePatch = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => { const updateScimGroupNamePatch = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to update SCIM group due to plan restriction. Upgrade plan to update SCIM group."
});
const org = await orgDAL.findById(orgId); const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled) if (!org.scimEnabled)
throw new ScimRequestError({ throw new ScimRequestError({
detail: "SCIM is disabled for the organization", detail: "SCIM is disabled for the organization",
@@ -635,6 +809,26 @@ export const scimServiceFactory = ({
}; };
const deleteScimGroup = async ({ groupId, orgId }: TDeleteScimGroupDTO) => { const deleteScimGroup = async ({ groupId, orgId }: TDeleteScimGroupDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to delete SCIM group due to plan restriction. Upgrade plan to delete SCIM group."
});
const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
const [group] = await groupDAL.delete({ const [group] = await groupDAL.delete({
id: groupId, id: groupId,
orgId orgId
@@ -81,6 +81,11 @@ export type TListScimGroups = {
export type TCreateScimGroupDTO = { export type TCreateScimGroupDTO = {
displayName: string; displayName: string;
orgId: string; orgId: string;
members?: {
// TODO: account for members with value and display (is this optional?)
value: string;
display: string;
}[];
}; };
export type TGetScimGroupDTO = { export type TGetScimGroupDTO = {
@@ -92,6 +97,10 @@ export type TUpdateScimGroupNamePutDTO = {
groupId: string; groupId: string;
orgId: string; orgId: string;
displayName: string; displayName: string;
members: {
value: string;
display: string;
}[];
}; };
export type TUpdateScimGroupNamePatchDTO = { export type TUpdateScimGroupNamePatchDTO = {
+14
View File
@@ -13,6 +13,7 @@ import { dynamicSecretLeaseQueueServiceFactory } from "@app/ee/services/dynamic-
import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service"; import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
import { groupDALFactory } from "@app/ee/services/group/group-dal"; import { groupDALFactory } from "@app/ee/services/group/group-dal";
import { groupServiceFactory } from "@app/ee/services/group/group-service"; import { groupServiceFactory } from "@app/ee/services/group/group-service";
import { pendingGroupAdditionDALFactory } from "@app/ee/services/group/pending-group-addition-dal";
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal"; import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal";
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
@@ -217,6 +218,7 @@ export const registerRoutes = async (
const groupProjectDAL = groupProjectDALFactory(db); const groupProjectDAL = groupProjectDALFactory(db);
const groupProjectMembershipRoleDAL = groupProjectMembershipRoleDALFactory(db); const groupProjectMembershipRoleDAL = groupProjectMembershipRoleDALFactory(db);
const userGroupMembershipDAL = userGroupMembershipDALFactory(db); const userGroupMembershipDAL = userGroupMembershipDALFactory(db);
const pendingGroupAdditionDAL = pendingGroupAdditionDALFactory(db);
const secretScanningDAL = secretScanningDALFactory(db); const secretScanningDAL = secretScanningDALFactory(db);
const licenseDAL = licenseDALFactory(db); const licenseDAL = licenseDALFactory(db);
const dynamicSecretDAL = dynamicSecretDALFactory(db); const dynamicSecretDAL = dynamicSecretDALFactory(db);
@@ -268,6 +270,7 @@ export const registerRoutes = async (
projectDAL, projectDAL,
projectBotDAL, projectBotDAL,
projectKeyDAL, projectKeyDAL,
pendingGroupAdditionDAL,
permissionService, permissionService,
licenseService licenseService
}); });
@@ -290,6 +293,11 @@ export const registerRoutes = async (
projectDAL, projectDAL,
projectMembershipDAL, projectMembershipDAL,
groupDAL, groupDAL,
groupProjectDAL,
userGroupMembershipDAL,
projectKeyDAL,
projectBotDAL,
pendingGroupAdditionDAL,
permissionService, permissionService,
smtpService smtpService
}); });
@@ -344,6 +352,12 @@ export const registerRoutes = async (
smtpService, smtpService,
authDAL, authDAL,
userDAL, userDAL,
pendingGroupAdditionDAL,
userGroupMembershipDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
groupProjectDAL,
orgDAL, orgDAL,
orgService, orgService,
licenseService licenseService
@@ -1,10 +1,17 @@
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { OrgMembershipStatus } from "@app/db/schemas"; import { OrgMembershipStatus } from "@app/db/schemas";
import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns";
import { TPendingGroupAdditionDALFactory } from "@app/ee/services/group/pending-group-addition-dal";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { isDisposableEmail } from "@app/lib/validator"; import { isDisposableEmail } from "@app/lib/validator";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types"; import { TokenType } from "../auth-token/auth-token-types";
@@ -20,6 +27,12 @@ import { AuthMethod, AuthTokenType } from "./auth-type";
type TAuthSignupDep = { type TAuthSignupDep = {
authDAL: TAuthDALFactory; authDAL: TAuthDALFactory;
userDAL: TUserDALFactory; userDAL: TUserDALFactory;
pendingGroupAdditionDAL: Pick<TPendingGroupAdditionDALFactory, "deletePendingGroupAdditionsByUserIds">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
orgService: Pick<TOrgServiceFactory, "createOrganization">; orgService: Pick<TOrgServiceFactory, "createOrganization">;
orgDAL: TOrgDALFactory; orgDAL: TOrgDALFactory;
tokenService: TAuthTokenServiceFactory; tokenService: TAuthTokenServiceFactory;
@@ -31,6 +44,12 @@ export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>;
export const authSignupServiceFactory = ({ export const authSignupServiceFactory = ({
authDAL, authDAL,
userDAL, userDAL,
pendingGroupAdditionDAL,
userGroupMembershipDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
groupProjectDAL,
tokenService, tokenService,
smtpService, smtpService,
orgService, orgService,
@@ -168,6 +187,20 @@ export const authSignupServiceFactory = ({
const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))]; const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))];
await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId))); await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId)));
console.log("conv A");
await convertPendingGroupAdditionsToGroupMemberships({
userIds: [user.id],
userDAL,
pendingGroupAdditionDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL
});
console.log("conv B");
const tokenSession = await tokenService.getUserTokenSession({ const tokenSession = await tokenService.getUserTokenSession({
userAgent, userAgent,
ip, ip,
@@ -225,13 +258,16 @@ export const authSignupServiceFactory = ({
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
authorization authorization
}: TCompleteAccountInviteDTO) => { }: TCompleteAccountInviteDTO) => {
console.log("conv 0");
const user = await userDAL.findUserByUsername(email); const user = await userDAL.findUserByUsername(email);
if (!user || (user && user.isAccepted)) { if (!user || (user && user.isAccepted)) {
throw new Error("Failed to complete account for complete user"); throw new Error("Failed to complete account for complete user");
} }
console.log("conv 1");
validateSignUpAuthorization(authorization, user.id); validateSignUpAuthorization(authorization, user.id);
console.log("conv 2");
const [orgMembership] = await orgDAL.findMembership({ const [orgMembership] = await orgDAL.findMembership({
inviteEmail: email, inviteEmail: email,
status: OrgMembershipStatus.Invited status: OrgMembershipStatus.Invited
@@ -242,9 +278,12 @@ export const authSignupServiceFactory = ({
name: "complete account invite" name: "complete account invite"
}); });
console.log("conv 3");
const updateduser = await authDAL.transaction(async (tx) => { const updateduser = await authDAL.transaction(async (tx) => {
console.log("conv 4");
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx); const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
if (!us) throw new Error("User not found"); if (!us) throw new Error("User not found");
console.log("conv 5");
const userEncKey = await userDAL.upsertUserEncryptionKey( const userEncKey = await userDAL.upsertUserEncryptionKey(
us.id, us.id,
{ {
@@ -261,15 +300,33 @@ export const authSignupServiceFactory = ({
}, },
tx tx
); );
console.log("conv 6");
const updatedMembersips = await orgDAL.updateMembership( const updatedMembersips = await orgDAL.updateMembership(
{ inviteEmail: email, status: OrgMembershipStatus.Invited }, { inviteEmail: email, status: OrgMembershipStatus.Invited },
{ userId: us.id, status: OrgMembershipStatus.Accepted }, { userId: us.id, status: OrgMembershipStatus.Accepted },
tx tx
); );
console.log("conv 7");
const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))]; const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))];
console.log("conv 8");
await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId))); await Promise.allSettled(uniqueOrgId.map((orgId) => licenseService.updateSubscriptionOrgMemberCount(orgId)));
console.log("conv AA");
await convertPendingGroupAdditionsToGroupMemberships({
userIds: [user.id],
userDAL,
pendingGroupAdditionDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx
});
console.log("conv BB");
return { info: us, key: userEncKey }; return { info: us, key: userEncKey };
}); });
+2 -2
View File
@@ -81,9 +81,9 @@ export const projectDALFactory = (db: TDbClient) => {
} }
}; };
const findProjectGhostUser = async (projectId: string) => { const findProjectGhostUser = async (projectId: string, tx?: Knex) => {
try { try {
const ghostUser = await db(TableName.ProjectMembership) const ghostUser = await (tx || db)(TableName.ProjectMembership)
.where({ projectId }) .where({ projectId })
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.select(selectAllTableCols(TableName.Users)) .select(selectAllTableCols(TableName.Users))
+14
View File
@@ -34,6 +34,19 @@ export const userDALFactory = (db: TDbClient) => {
} }
}; };
const findUserEncKeyByUsernameBatch = async ({ usernames }: { usernames: string[] }, tx?: Knex) => {
try {
return await (tx || db)(TableName.Users)
.where({
isGhost: false
})
.whereIn("username", usernames)
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`);
} catch (error) {
throw new DatabaseError({ error, name: "Find user enc by email batch" });
}
};
const findUserEncKeyByUserId = async (userId: string) => { const findUserEncKeyByUserId = async (userId: string) => {
try { try {
const user = await db(TableName.Users) const user = await db(TableName.Users)
@@ -123,6 +136,7 @@ export const userDALFactory = (db: TDbClient) => {
...userOrm, ...userOrm,
findUserByUsername, findUserByUsername,
findUserEncKeyByUsername, findUserEncKeyByUsername,
findUserEncKeyByUsernameBatch, // TODO: if successful, replace findUserEncKeyByUsername with this
findUserEncKeyByUserId, findUserEncKeyByUserId,
updateUserEncryptionByUserId, updateUserEncryptionByUserId,
findUserByProjectMembershipId, findUserByProjectMembershipId,
@@ -1,32 +1,27 @@
import { useState } from "react"; import { useState } from "react";
import { faMagnifyingGlass,faUsers } from "@fortawesome/free-solid-svg-icons"; import { faMagnifyingGlass, faUsers } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { import {
Button, Button,
EmptyState, EmptyState,
Input, Input,
Modal, Modal,
ModalContent, ModalContent,
Pagination, Pagination,
Table, Table,
TableContainer, TableContainer,
TableSkeleton, TableSkeleton,
TBody, TBody,
Td, Td,
Th, Th,
THead, THead,
Tr} from "@app/components/v2"; Tr
import { } from "@app/components/v2";
OrgPermissionActions, import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
OrgPermissionSubjects import { useAddUserToGroup, useListGroupUsers, useRemoveUserFromGroup } from "@app/hooks/api";
} from "@app/context";
import {
useAddUserToGroup,
useListGroupUsers,
useRemoveUserFromGroup} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = { type Props = {
@@ -34,136 +29,127 @@ type Props = {
handlePopUpToggle: (popUpName: keyof UsePopUpState<["groupMembers"]>, state?: boolean) => void; handlePopUpToggle: (popUpName: keyof UsePopUpState<["groupMembers"]>, state?: boolean) => void;
}; };
export const OrgGroupMembersModal = ({ export const OrgGroupMembersModal = ({ popUp, handlePopUpToggle }: Props) => {
popUp, const [page, setPage] = useState(1);
handlePopUpToggle const [perPage, setPerPage] = useState(10);
}: Props) => { const [searchMemberFilter, setSearchMemberFilter] = useState("");
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(10);
const [searchMemberFilter, setSearchMemberFilter] = useState("");
const popUpData = popUp?.groupMembers?.data as { const popUpData = popUp?.groupMembers?.data as {
slug: string; slug: string;
}; };
const { data, isLoading } = useListGroupUsers({ const { data, isLoading } = useListGroupUsers({
groupSlug: popUpData?.slug, groupSlug: popUpData?.slug,
offset: (page - 1) * perPage, offset: (page - 1) * perPage,
limit: perPage, limit: perPage,
username: searchMemberFilter username: searchMemberFilter
}); });
const { mutateAsync: assignMutateAsync } = useAddUserToGroup(); const { mutateAsync: assignMutateAsync } = useAddUserToGroup();
const { mutateAsync: unassignMutateAsync } = useRemoveUserFromGroup(); const { mutateAsync: unassignMutateAsync } = useRemoveUserFromGroup();
const handleAssignment = async (username: string, assign: boolean) => { const handleAssignment = async (username: string, assign: boolean) => {
try { try {
if (!popUpData?.slug) return; if (!popUpData?.slug) return;
if (assign) { if (assign) {
await assignMutateAsync({ await assignMutateAsync({
username, username,
slug: popUpData.slug slug: popUpData.slug
}); });
} else { } else {
await unassignMutateAsync({ await unassignMutateAsync({
username, username,
slug: popUpData.slug slug: popUpData.slug
}); });
} }
createNotification({ createNotification({
text: `Successfully ${assign ? "assigned" : "removed "} user ${assign ? "to" : "from"} group`, text: `Successfully ${assign ? "assigned" : "removed "} user ${
type: "success" assign ? "to" : "from"
}); } group`,
} catch (err) { type: "success"
createNotification({ });
text: `Failed to ${assign ? "assigned" : "remove"} user ${assign ? "to" : "from"} group`, } catch (err) {
type: "error" createNotification({
}); text: `Failed to ${assign ? "assigned" : "remove"} user ${assign ? "to" : "from"} group`,
} type: "error"
});
} }
};
return ( return (
<Modal <Modal
isOpen={popUp?.groupMembers?.isOpen} isOpen={popUp?.groupMembers?.isOpen}
onOpenChange={(isOpen) => { onOpenChange={(isOpen) => {
handlePopUpToggle("groupMembers", isOpen); handlePopUpToggle("groupMembers", isOpen);
}} }}
> >
<ModalContent title="Manage Group Members"> <ModalContent title="Manage Group Members">
<Input <Input
value={searchMemberFilter} value={searchMemberFilter}
onChange={(e) => setSearchMemberFilter(e.target.value)} onChange={(e) => setSearchMemberFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search members..." placeholder="Search members..."
/> />
<TableContainer className="mt-4"> <TableContainer className="mt-4">
<Table> <Table>
<THead> <THead>
<Tr> <Tr>
<Th>User</Th> <Th>User</Th>
<Th /> <Th />
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{isLoading && <TableSkeleton columns={2} innerKey="group-users" />} {isLoading && <TableSkeleton columns={2} innerKey="group-users" />}
{!isLoading && data?.users?.map(({ {!isLoading &&
id, data?.users?.map(({ id, firstName, lastName, username, isPartOfGroup }) => {
firstName, return (
lastName, <Tr className="items-center" key={`group-user-${id}`}>
username, <Td>
isPartOfGroup <p>{`${firstName ?? "-"} ${lastName ?? ""}`}</p>
}) => { <p>{username}</p>
return ( </Td>
<Tr className="items-center" key={`group-user-${id}`}> <Td className="flex justify-end">
<Td> <OrgPermissionCan
<p>{`${firstName} ${lastName}`}</p> I={OrgPermissionActions.Edit}
<p>{username}</p> a={OrgPermissionSubjects.Groups}
</Td> >
<Td className="flex justify-end"> {(isAllowed) => {
<OrgPermissionCan return (
I={OrgPermissionActions.Edit} <Button
a={OrgPermissionSubjects.Groups} isLoading={isLoading}
> isDisabled={!isAllowed}
{(isAllowed) => { colorSchema="primary"
return ( variant="outline_bg"
<Button type="submit"
isLoading={isLoading} onClick={() => handleAssignment(username, !isPartOfGroup)}
isDisabled={!isAllowed} >
colorSchema="primary" {isPartOfGroup ? "Unassign" : "Assign"}
variant="outline_bg" </Button>
type="submit" );
onClick={() => handleAssignment(username, !isPartOfGroup)} }}
> </OrgPermissionCan>
{isPartOfGroup ? "Unassign" : "Assign"} </Td>
</Button> </Tr>
); );
}} })}
</OrgPermissionCan> </TBody>
</Td> </Table>
</Tr> {!isLoading && data?.totalCount !== undefined && (
); <Pagination
})} count={data.totalCount}
</TBody> page={page}
</Table> perPage={perPage}
{!isLoading && data?.totalCount !== undefined && ( onChangePage={(newPage) => setPage(newPage)}
<Pagination onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
count={data.totalCount} />
page={page} )}
perPage={perPage} {!isLoading && !data?.users?.length && (
onChangePage={(newPage) => setPage(newPage)} <EmptyState title="No users found" icon={faUsers} />
onChangePerPage={(newPerPage) => setPerPage(newPerPage)} )}
/> </TableContainer>
)} </ModalContent>
{!isLoading && !data?.users?.length && ( </Modal>
<EmptyState );
title="No users found" };
icon={faUsers}
/>
)}
</TableContainer>
</ModalContent>
</Modal>
);
}