mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 07:26:45 +00:00
Finish preliminary capability for adding incomplete users to groups
This commit is contained in:
Vendored
+8
@@ -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);
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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>>;
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
+140
-154
@@ -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 {
|
|
||||||
slug: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data, isLoading } = useListGroupUsers({
|
|
||||||
groupSlug: popUpData?.slug,
|
|
||||||
offset: (page - 1) * perPage,
|
|
||||||
limit: perPage,
|
|
||||||
username: searchMemberFilter
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutateAsync: assignMutateAsync } = useAddUserToGroup();
|
|
||||||
const { mutateAsync: unassignMutateAsync } = useRemoveUserFromGroup();
|
|
||||||
|
|
||||||
const handleAssignment = async (username: string, assign: boolean) => {
|
|
||||||
try {
|
|
||||||
if (!popUpData?.slug) return;
|
|
||||||
|
|
||||||
if (assign) {
|
|
||||||
await assignMutateAsync({
|
|
||||||
username,
|
|
||||||
slug: popUpData.slug
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await unassignMutateAsync({
|
|
||||||
username,
|
|
||||||
slug: popUpData.slug
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
createNotification({
|
const popUpData = popUp?.groupMembers?.data as {
|
||||||
text: `Successfully ${assign ? "assigned" : "removed "} user ${assign ? "to" : "from"} group`,
|
slug: string;
|
||||||
type: "success"
|
};
|
||||||
});
|
|
||||||
} catch (err) {
|
const { data, isLoading } = useListGroupUsers({
|
||||||
createNotification({
|
groupSlug: popUpData?.slug,
|
||||||
text: `Failed to ${assign ? "assigned" : "remove"} user ${assign ? "to" : "from"} group`,
|
offset: (page - 1) * perPage,
|
||||||
type: "error"
|
limit: perPage,
|
||||||
});
|
username: searchMemberFilter
|
||||||
}
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: assignMutateAsync } = useAddUserToGroup();
|
||||||
|
const { mutateAsync: unassignMutateAsync } = useRemoveUserFromGroup();
|
||||||
|
|
||||||
|
const handleAssignment = async (username: string, assign: boolean) => {
|
||||||
|
try {
|
||||||
|
if (!popUpData?.slug) return;
|
||||||
|
|
||||||
|
if (assign) {
|
||||||
|
await assignMutateAsync({
|
||||||
|
username,
|
||||||
|
slug: popUpData.slug
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await unassignMutateAsync({
|
||||||
|
username,
|
||||||
|
slug: popUpData.slug
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: `Successfully ${assign ? "assigned" : "removed "} user ${
|
||||||
|
assign ? "to" : "from"
|
||||||
|
} group`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
createNotification({
|
||||||
|
text: `Failed to ${assign ? "assigned" : "remove"} user ${assign ? "to" : "from"} group`,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
};
|
||||||
return (
|
|
||||||
<Modal
|
return (
|
||||||
isOpen={popUp?.groupMembers?.isOpen}
|
<Modal
|
||||||
onOpenChange={(isOpen) => {
|
isOpen={popUp?.groupMembers?.isOpen}
|
||||||
handlePopUpToggle("groupMembers", isOpen);
|
onOpenChange={(isOpen) => {
|
||||||
}}
|
handlePopUpToggle("groupMembers", isOpen);
|
||||||
>
|
}}
|
||||||
<ModalContent title="Manage Group Members">
|
>
|
||||||
<Input
|
<ModalContent title="Manage Group Members">
|
||||||
value={searchMemberFilter}
|
<Input
|
||||||
onChange={(e) => setSearchMemberFilter(e.target.value)}
|
value={searchMemberFilter}
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
onChange={(e) => setSearchMemberFilter(e.target.value)}
|
||||||
placeholder="Search members..."
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
/>
|
placeholder="Search members..."
|
||||||
<TableContainer className="mt-4">
|
/>
|
||||||
<Table>
|
<TableContainer className="mt-4">
|
||||||
<THead>
|
<Table>
|
||||||
<Tr>
|
<THead>
|
||||||
<Th>User</Th>
|
<Tr>
|
||||||
<Th />
|
<Th>User</Th>
|
||||||
</Tr>
|
<Th />
|
||||||
</THead>
|
</Tr>
|
||||||
<TBody>
|
</THead>
|
||||||
{isLoading && <TableSkeleton columns={2} innerKey="group-users" />}
|
<TBody>
|
||||||
{!isLoading && data?.users?.map(({
|
{isLoading && <TableSkeleton columns={2} innerKey="group-users" />}
|
||||||
id,
|
{!isLoading &&
|
||||||
firstName,
|
data?.users?.map(({ id, firstName, lastName, username, isPartOfGroup }) => {
|
||||||
lastName,
|
return (
|
||||||
username,
|
<Tr className="items-center" key={`group-user-${id}`}>
|
||||||
isPartOfGroup
|
<Td>
|
||||||
}) => {
|
<p>{`${firstName ?? "-"} ${lastName ?? ""}`}</p>
|
||||||
return (
|
<p>{username}</p>
|
||||||
<Tr className="items-center" key={`group-user-${id}`}>
|
</Td>
|
||||||
<Td>
|
<Td className="flex justify-end">
|
||||||
<p>{`${firstName} ${lastName}`}</p>
|
<OrgPermissionCan
|
||||||
<p>{username}</p>
|
I={OrgPermissionActions.Edit}
|
||||||
</Td>
|
a={OrgPermissionSubjects.Groups}
|
||||||
<Td className="flex justify-end">
|
>
|
||||||
<OrgPermissionCan
|
{(isAllowed) => {
|
||||||
I={OrgPermissionActions.Edit}
|
return (
|
||||||
a={OrgPermissionSubjects.Groups}
|
<Button
|
||||||
>
|
isLoading={isLoading}
|
||||||
{(isAllowed) => {
|
isDisabled={!isAllowed}
|
||||||
return (
|
colorSchema="primary"
|
||||||
<Button
|
variant="outline_bg"
|
||||||
isLoading={isLoading}
|
type="submit"
|
||||||
isDisabled={!isAllowed}
|
onClick={() => handleAssignment(username, !isPartOfGroup)}
|
||||||
colorSchema="primary"
|
>
|
||||||
variant="outline_bg"
|
{isPartOfGroup ? "Unassign" : "Assign"}
|
||||||
type="submit"
|
</Button>
|
||||||
onClick={() => handleAssignment(username, !isPartOfGroup)}
|
);
|
||||||
>
|
}}
|
||||||
{isPartOfGroup ? "Unassign" : "Assign"}
|
</OrgPermissionCan>
|
||||||
</Button>
|
</Td>
|
||||||
);
|
</Tr>
|
||||||
}}
|
);
|
||||||
</OrgPermissionCan>
|
})}
|
||||||
</Td>
|
</TBody>
|
||||||
</Tr>
|
</Table>
|
||||||
);
|
{!isLoading && data?.totalCount !== undefined && (
|
||||||
})}
|
<Pagination
|
||||||
</TBody>
|
count={data.totalCount}
|
||||||
</Table>
|
page={page}
|
||||||
{!isLoading && data?.totalCount !== undefined && (
|
perPage={perPage}
|
||||||
<Pagination
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
count={data.totalCount}
|
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||||
page={page}
|
/>
|
||||||
perPage={perPage}
|
)}
|
||||||
onChangePage={(newPage) => setPage(newPage)}
|
{!isLoading && !data?.users?.length && (
|
||||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
<EmptyState title="No users found" icon={faUsers} />
|
||||||
/>
|
)}
|
||||||
)}
|
</TableContainer>
|
||||||
{!isLoading && !data?.users?.length && (
|
</ModalContent>
|
||||||
<EmptyState
|
</Modal>
|
||||||
title="No users found"
|
);
|
||||||
icon={faUsers}
|
};
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</TableContainer>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user