mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(rbac): implemented backend base apis for rbac permission system
This commit is contained in:
114
backend/package-lock.json
generated
114
backend/package-lock.json
generated
@@ -10,6 +10,8 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-secrets-manager": "^3.319.0",
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@casl/mongoose": "^7.2.1",
|
||||
"@godaddy/terminus": "^4.12.0",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
@@ -59,7 +61,8 @@
|
||||
"typescript": "^4.9.3",
|
||||
"utility-types": "^3.10.0",
|
||||
"winston": "^3.8.2",
|
||||
"winston-loki": "^6.0.7"
|
||||
"winston-loki": "^6.0.6",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.3.1",
|
||||
@@ -3339,6 +3342,26 @@
|
||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@casl/ability": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.5.0.tgz",
|
||||
"integrity": "sha512-3guc94ugr5ylZQIpJTLz0CDfwNi0mxKVECj1vJUPAvs+Lwunh/dcuUjwzc4MHM9D8JOYX0XUZMEPedpB3vIbOw==",
|
||||
"dependencies": {
|
||||
"@ucast/mongo2js": "^1.3.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/stalniy/casl/blob/master/BACKERS.md"
|
||||
}
|
||||
},
|
||||
"node_modules/@casl/mongoose": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@casl/mongoose/-/mongoose-7.2.1.tgz",
|
||||
"integrity": "sha512-pojgSWYKNIwFM6wWDNct1YD0+8nIxhe2jp5jBbK8JGU60dEs2o0Yw3mCo2y7nBwbvRC2oEots/BlLMVb1Wdo8A==",
|
||||
"peerDependencies": {
|
||||
"@casl/ability": "^6.3.2",
|
||||
"mongoose": "^6.0.13 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@colors/colors": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
|
||||
@@ -6174,6 +6197,37 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@ucast/core": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz",
|
||||
"integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g=="
|
||||
},
|
||||
"node_modules/@ucast/js": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.3.tgz",
|
||||
"integrity": "sha512-jBBqt57T5WagkAjqfCIIE5UYVdaXYgGkOFYv2+kjq2AVpZ2RIbwCo/TujJpDlwTVluUI+WpnRpoGU2tSGlEvFQ==",
|
||||
"dependencies": {
|
||||
"@ucast/core": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ucast/mongo": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz",
|
||||
"integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==",
|
||||
"dependencies": {
|
||||
"@ucast/core": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ucast/mongo2js": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.4.tgz",
|
||||
"integrity": "sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==",
|
||||
"dependencies": {
|
||||
"@ucast/core": "^1.6.1",
|
||||
"@ucast/js": "^3.0.0",
|
||||
"@ucast/mongo": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.8.10",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||
@@ -16626,6 +16680,14 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.21.4",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
|
||||
"integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -19324,6 +19386,20 @@
|
||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||
"dev": true
|
||||
},
|
||||
"@casl/ability": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.5.0.tgz",
|
||||
"integrity": "sha512-3guc94ugr5ylZQIpJTLz0CDfwNi0mxKVECj1vJUPAvs+Lwunh/dcuUjwzc4MHM9D8JOYX0XUZMEPedpB3vIbOw==",
|
||||
"requires": {
|
||||
"@ucast/mongo2js": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"@casl/mongoose": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@casl/mongoose/-/mongoose-7.2.1.tgz",
|
||||
"integrity": "sha512-pojgSWYKNIwFM6wWDNct1YD0+8nIxhe2jp5jBbK8JGU60dEs2o0Yw3mCo2y7nBwbvRC2oEots/BlLMVb1Wdo8A==",
|
||||
"requires": {}
|
||||
},
|
||||
"@colors/colors": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
|
||||
@@ -21575,6 +21651,37 @@
|
||||
"eslint-visitor-keys": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"@ucast/core": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz",
|
||||
"integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g=="
|
||||
},
|
||||
"@ucast/js": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.3.tgz",
|
||||
"integrity": "sha512-jBBqt57T5WagkAjqfCIIE5UYVdaXYgGkOFYv2+kjq2AVpZ2RIbwCo/TujJpDlwTVluUI+WpnRpoGU2tSGlEvFQ==",
|
||||
"requires": {
|
||||
"@ucast/core": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@ucast/mongo": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz",
|
||||
"integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==",
|
||||
"requires": {
|
||||
"@ucast/core": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"@ucast/mongo2js": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.4.tgz",
|
||||
"integrity": "sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==",
|
||||
"requires": {
|
||||
"@ucast/core": "^1.6.1",
|
||||
"@ucast/js": "^3.0.0",
|
||||
"@ucast/mongo": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"@xmldom/xmldom": {
|
||||
"version": "0.8.10",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||
@@ -29273,6 +29380,11 @@
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"zod": {
|
||||
"version": "3.21.4",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
|
||||
"integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-secrets-manager": "^3.319.0",
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@casl/mongoose": "^7.2.1",
|
||||
"@godaddy/terminus": "^4.12.0",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
@@ -50,7 +52,8 @@
|
||||
"typescript": "^4.9.3",
|
||||
"utility-types": "^3.10.0",
|
||||
"winston": "^3.8.2",
|
||||
"winston-loki": "^6.0.7"
|
||||
"winston-loki": "^6.0.6",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"name": "infisical-api",
|
||||
"version": "1.0.0",
|
||||
|
||||
@@ -16,6 +16,7 @@ import * as workspaceController from "./workspaceController";
|
||||
import * as secretScanningController from "./secretScanningController";
|
||||
import * as webhookController from "./webhookController";
|
||||
import * as secretImportController from "./secretImportController";
|
||||
import * as roleController from "./roleController";
|
||||
|
||||
export {
|
||||
authController,
|
||||
@@ -35,5 +36,6 @@ export {
|
||||
workspaceController,
|
||||
secretScanningController,
|
||||
webhookController,
|
||||
roleController,
|
||||
secretImportController
|
||||
};
|
||||
|
||||
159
backend/src/controllers/v1/roleController.ts
Normal file
159
backend/src/controllers/v1/roleController.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { Request, Response } from "express";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import Role from "../../models/role";
|
||||
import {
|
||||
GeneralPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
adminPermissions,
|
||||
getUserOrgPermissions,
|
||||
memberPermissions
|
||||
} from "../../services/RoleService";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import {
|
||||
CreateRoleSchema,
|
||||
DeleteRoleSchema,
|
||||
GetRoleSchema,
|
||||
UpdateRoleSchema
|
||||
} from "../../validation";
|
||||
|
||||
export const createRole = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { workspaceId, name, description, slug, permissions, orgId }
|
||||
} = await validateRequest(CreateRoleSchema, req);
|
||||
|
||||
const orgPermission = await getUserOrgPermissions(req.user.id, orgId);
|
||||
if (orgPermission.cannot(GeneralPermissionActions.Create, OrgPermissionSubjects.Role)) {
|
||||
throw BadRequestError({ message: "User doesn't have the permission." });
|
||||
}
|
||||
|
||||
const isOrgRole = !workspaceId; // if workspaceid is provided then its a workspace rule
|
||||
|
||||
const existingRole = await Role.findOne({ organization: orgId, workspace: workspaceId, slug });
|
||||
if (existingRole) {
|
||||
throw BadRequestError({ message: "Role already exist" });
|
||||
}
|
||||
|
||||
const role = new Role({
|
||||
organization: orgId,
|
||||
workspace: workspaceId,
|
||||
isOrgRole,
|
||||
name,
|
||||
slug,
|
||||
permissions,
|
||||
description
|
||||
});
|
||||
await role.save();
|
||||
|
||||
res.status(200).json({
|
||||
message: "Successfully created role",
|
||||
data: {
|
||||
role
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const updateRole = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { id },
|
||||
body: { name, description, slug, permissions, workspaceId, orgId }
|
||||
} = await validateRequest(UpdateRoleSchema, req);
|
||||
const isOrgRole = !workspaceId; // if workspaceid is provided then its a workspace rule
|
||||
|
||||
const orgPermission = await getUserOrgPermissions(req.user.id, orgId);
|
||||
if (orgPermission.cannot(GeneralPermissionActions.Edit, OrgPermissionSubjects.Role)) {
|
||||
throw BadRequestError({ message: "User doesn't have the permission." });
|
||||
}
|
||||
|
||||
if (slug) {
|
||||
const existingRole = await Role.findOne({
|
||||
organization: orgId,
|
||||
slug,
|
||||
isOrgRole,
|
||||
workspace: workspaceId
|
||||
});
|
||||
if (existingRole && existingRole.id !== id) {
|
||||
throw BadRequestError({ message: "Role already exist" });
|
||||
}
|
||||
}
|
||||
|
||||
const role = await Role.findByIdAndUpdate(
|
||||
id,
|
||||
{ name, description, slug, permissions },
|
||||
{ returnDocument: "after" }
|
||||
);
|
||||
|
||||
if (!role) {
|
||||
throw BadRequestError({ message: "Role not found" });
|
||||
}
|
||||
res.status(200).json({
|
||||
message: "Successfully updated role",
|
||||
data: {
|
||||
role
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteRole = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { id }
|
||||
} = await validateRequest(DeleteRoleSchema, req);
|
||||
|
||||
const role = await Role.findById(id);
|
||||
if (!role) {
|
||||
throw BadRequestError({ message: "Role not found" });
|
||||
}
|
||||
|
||||
const orgPermission = await getUserOrgPermissions(req.user.id, role.organization.toString());
|
||||
if (orgPermission.cannot(GeneralPermissionActions.Delete, OrgPermissionSubjects.Role)) {
|
||||
throw BadRequestError({ message: "User doesn't have the permission." });
|
||||
}
|
||||
await Role.findByIdAndDelete(role.id);
|
||||
|
||||
res.status(200).json({
|
||||
message: "Successfully deleted role",
|
||||
data: {
|
||||
role
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getRoles = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { workspaceId, orgId }
|
||||
} = await validateRequest(GetRoleSchema, req);
|
||||
const isOrgRole = !workspaceId;
|
||||
|
||||
const orgPermission = await getUserOrgPermissions(req.user.id, orgId);
|
||||
if (orgPermission.cannot(GeneralPermissionActions.Read, OrgPermissionSubjects.Role)) {
|
||||
throw BadRequestError({ message: "User doesn't have the permission." });
|
||||
}
|
||||
|
||||
const roles = await Role.find({ organization: orgId, isOrgRole, workspace: workspaceId });
|
||||
|
||||
res.status(200).json({
|
||||
message: "Successfully fetched role list",
|
||||
data: {
|
||||
roles: [
|
||||
{
|
||||
name: "Owner",
|
||||
slug: "owner",
|
||||
description: "Complete administration access over the organization.",
|
||||
permissions: adminPermissions.rules
|
||||
},
|
||||
{
|
||||
name: "Admin",
|
||||
slug: "admin",
|
||||
description: "Complete administration access over the organization",
|
||||
permissions: adminPermissions.rules
|
||||
},
|
||||
{
|
||||
name: "Member",
|
||||
slug: "member",
|
||||
description: "Non-administrative role in an organization",
|
||||
permissions: memberPermissions.rules
|
||||
},
|
||||
...roles
|
||||
]
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
Membership,
|
||||
MembershipOrg,
|
||||
ServiceAccount,
|
||||
Workspace,
|
||||
} from "../../models";
|
||||
import { Membership, MembershipOrg, ServiceAccount, Workspace } from "../../models";
|
||||
import { deleteMembershipOrg } from "../../helpers/membershipOrg";
|
||||
import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
|
||||
import Role from "../../models/role";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import { CUSTOM } from "../../variables";
|
||||
|
||||
/**
|
||||
* Return memberships for organization with id [organizationId]
|
||||
@@ -15,7 +13,7 @@ import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
|
||||
* @param res
|
||||
*/
|
||||
export const getOrganizationMemberships = async (req: Request, res: Response) => {
|
||||
/*
|
||||
/*
|
||||
#swagger.summary = 'Return organization memberships'
|
||||
#swagger.description = 'Return organization memberships'
|
||||
|
||||
@@ -48,16 +46,16 @@ export const getOrganizationMemberships = async (req: Request, res: Response) =>
|
||||
}
|
||||
}
|
||||
*/
|
||||
const { organizationId } = req.params;
|
||||
const { organizationId } = req.params;
|
||||
|
||||
const memberships = await MembershipOrg.find({
|
||||
organization: organizationId,
|
||||
}).populate("user", "+publicKey");
|
||||
const memberships = await MembershipOrg.find({
|
||||
organization: organizationId
|
||||
}).populate("user", "+publicKey");
|
||||
|
||||
return res.status(200).send({
|
||||
memberships,
|
||||
});
|
||||
}
|
||||
return res.status(200).send({
|
||||
memberships
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update role of membership with id [membershipId] to role [role]
|
||||
@@ -65,7 +63,7 @@ export const getOrganizationMemberships = async (req: Request, res: Response) =>
|
||||
* @param res
|
||||
*/
|
||||
export const updateOrganizationMembership = async (req: Request, res: Response) => {
|
||||
/*
|
||||
/*
|
||||
#swagger.summary = 'Update organization membership'
|
||||
#swagger.description = 'Update organization membership'
|
||||
|
||||
@@ -118,22 +116,37 @@ export const updateOrganizationMembership = async (req: Request, res: Response)
|
||||
}
|
||||
}
|
||||
*/
|
||||
const { membershipId } = req.params;
|
||||
const { role } = req.body;
|
||||
const { membershipId } = req.params;
|
||||
const { role } = req.body;
|
||||
|
||||
const membership = await MembershipOrg.findByIdAndUpdate(
|
||||
membershipId,
|
||||
{
|
||||
role,
|
||||
}, {
|
||||
new: true,
|
||||
}
|
||||
);
|
||||
const isCustomRole = !["admin", "member", "owner"].includes(role);
|
||||
if (isCustomRole) {
|
||||
const orgRole = await Role.findOne({ slug: role, isOrgRole: true });
|
||||
if (!orgRole) throw BadRequestError({ message: "Role not found" });
|
||||
|
||||
return res.status(200).send({
|
||||
membership,
|
||||
const membership = await MembershipOrg.findByIdAndUpdate(membershipId, {
|
||||
role: CUSTOM,
|
||||
customRole: orgRole
|
||||
});
|
||||
}
|
||||
return res.status(200).send({
|
||||
membership
|
||||
});
|
||||
}
|
||||
|
||||
const membership = await MembershipOrg.findByIdAndUpdate(
|
||||
membershipId,
|
||||
{
|
||||
role
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
membership
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete organization membership with id [membershipId]
|
||||
@@ -142,7 +155,7 @@ export const updateOrganizationMembership = async (req: Request, res: Response)
|
||||
* @returns
|
||||
*/
|
||||
export const deleteOrganizationMembership = async (req: Request, res: Response) => {
|
||||
/*
|
||||
/*
|
||||
#swagger.summary = 'Delete organization membership'
|
||||
#swagger.description = 'Delete organization membership'
|
||||
|
||||
@@ -178,21 +191,21 @@ export const deleteOrganizationMembership = async (req: Request, res: Response)
|
||||
}
|
||||
}
|
||||
*/
|
||||
const { membershipId } = req.params;
|
||||
const { membershipId } = req.params;
|
||||
|
||||
// delete organization membership
|
||||
const membership = await deleteMembershipOrg({
|
||||
membershipOrgId: membershipId,
|
||||
});
|
||||
// delete organization membership
|
||||
const membership = await deleteMembershipOrg({
|
||||
membershipOrgId: membershipId
|
||||
});
|
||||
|
||||
await updateSubscriptionOrgQuantity({
|
||||
organizationId: membership.organization.toString(),
|
||||
});
|
||||
await updateSubscriptionOrgQuantity({
|
||||
organizationId: membership.organization.toString()
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
membership,
|
||||
});
|
||||
}
|
||||
return res.status(200).send({
|
||||
membership
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return workspaces for organization with id [organizationId] that user has
|
||||
@@ -201,7 +214,7 @@ export const deleteOrganizationMembership = async (req: Request, res: Response)
|
||||
* @param res
|
||||
*/
|
||||
export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
|
||||
/*
|
||||
/*
|
||||
#swagger.summary = 'Return projects in organization that user is part of'
|
||||
#swagger.description = 'Return projects in organization that user is part of'
|
||||
|
||||
@@ -234,31 +247,31 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) =>
|
||||
}
|
||||
}
|
||||
*/
|
||||
const { organizationId } = req.params;
|
||||
const { organizationId } = req.params;
|
||||
|
||||
const workspacesSet = new Set(
|
||||
(
|
||||
await Workspace.find(
|
||||
{
|
||||
organization: organizationId,
|
||||
},
|
||||
"_id"
|
||||
)
|
||||
).map((w) => w._id.toString())
|
||||
);
|
||||
const workspacesSet = new Set(
|
||||
(
|
||||
await Workspace.find(
|
||||
{
|
||||
organization: organizationId
|
||||
},
|
||||
"_id"
|
||||
)
|
||||
).map((w) => w._id.toString())
|
||||
);
|
||||
|
||||
const workspaces = (
|
||||
await Membership.find({
|
||||
user: req.user._id,
|
||||
}).populate("workspace")
|
||||
)
|
||||
const workspaces = (
|
||||
await Membership.find({
|
||||
user: req.user._id
|
||||
}).populate("workspace")
|
||||
)
|
||||
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
||||
.map((m) => m.workspace);
|
||||
|
||||
return res.status(200).send({
|
||||
workspaces,
|
||||
});
|
||||
}
|
||||
return res.status(200).send({
|
||||
workspaces
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return service accounts for organization with id [organizationId]
|
||||
@@ -266,13 +279,13 @@ return res.status(200).send({
|
||||
* @param res
|
||||
*/
|
||||
export const getOrganizationServiceAccounts = async (req: Request, res: Response) => {
|
||||
const { organizationId } = req.params;
|
||||
const { organizationId } = req.params;
|
||||
|
||||
const serviceAccounts = await ServiceAccount.find({
|
||||
organization: new Types.ObjectId(organizationId),
|
||||
});
|
||||
const serviceAccounts = await ServiceAccount.find({
|
||||
organization: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
serviceAccounts,
|
||||
});
|
||||
}
|
||||
return res.status(200).send({
|
||||
serviceAccounts
|
||||
});
|
||||
};
|
||||
|
||||
@@ -11,29 +11,29 @@ import { BadRequestError, MembershipNotFoundError } from "../utils/errors";
|
||||
* @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId]
|
||||
*/
|
||||
export const validateMembership = async ({
|
||||
userId,
|
||||
workspaceId,
|
||||
acceptedRoles,
|
||||
userId,
|
||||
workspaceId,
|
||||
acceptedRoles
|
||||
}: {
|
||||
userId: Types.ObjectId | string;
|
||||
workspaceId: Types.ObjectId | string;
|
||||
acceptedRoles?: Array<"admin" | "member">;
|
||||
acceptedRoles?: Array<"admin" | "member" | "custom">;
|
||||
}) => {
|
||||
const membership = await Membership.findOne({
|
||||
user: userId,
|
||||
workspace: workspaceId,
|
||||
workspace: workspaceId
|
||||
}).populate("workspace");
|
||||
|
||||
if (!membership) {
|
||||
throw MembershipNotFoundError({
|
||||
message: "Failed to find workspace membership",
|
||||
message: "Failed to find workspace membership"
|
||||
});
|
||||
}
|
||||
|
||||
if (acceptedRoles) {
|
||||
if (!acceptedRoles.includes(membership.role)) {
|
||||
throw BadRequestError({
|
||||
message: "Failed authorization for membership role",
|
||||
message: "Failed authorization for membership role"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export const validateMembership = async ({
|
||||
* @return {Object} membership - membership
|
||||
*/
|
||||
export const findMembership = async (queryObj: any) => {
|
||||
const membership = await Membership.findOne(queryObj);
|
||||
const membership = await Membership.findOne(queryObj);
|
||||
return membership;
|
||||
};
|
||||
|
||||
@@ -60,9 +60,9 @@ export const findMembership = async (queryObj: any) => {
|
||||
* @param {String[]} obj.roles - roles of users.
|
||||
*/
|
||||
export const addMemberships = async ({
|
||||
userIds,
|
||||
workspaceId,
|
||||
roles,
|
||||
userIds,
|
||||
workspaceId,
|
||||
roles
|
||||
}: {
|
||||
userIds: string[];
|
||||
workspaceId: string;
|
||||
@@ -74,15 +74,15 @@ export const addMemberships = async ({
|
||||
filter: {
|
||||
user: userId,
|
||||
workspace: workspaceId,
|
||||
role: roles[idx],
|
||||
role: roles[idx]
|
||||
},
|
||||
update: {
|
||||
user: userId,
|
||||
workspace: workspaceId,
|
||||
role: roles[idx],
|
||||
role: roles[idx]
|
||||
},
|
||||
upsert: true,
|
||||
},
|
||||
upsert: true
|
||||
}
|
||||
};
|
||||
});
|
||||
await Membership.bulkWrite(operations as any);
|
||||
@@ -94,8 +94,8 @@ export const addMemberships = async ({
|
||||
* @param {String} obj.membershipId - id of membership to delete
|
||||
*/
|
||||
export const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
|
||||
const deletedMembership = await Membership.findOneAndDelete({
|
||||
_id: membershipId,
|
||||
const deletedMembership = await Membership.findOneAndDelete({
|
||||
_id: membershipId
|
||||
});
|
||||
|
||||
// delete keys associated with the membership
|
||||
@@ -103,9 +103,9 @@ export const deleteMembership = async ({ membershipId }: { membershipId: string
|
||||
// case: membership had a registered user
|
||||
await Key.deleteMany({
|
||||
receiver: deletedMembership.user,
|
||||
workspace: deletedMembership.workspace,
|
||||
workspace: deletedMembership.workspace
|
||||
});
|
||||
}
|
||||
|
||||
return deletedMembership;
|
||||
return deletedMembership;
|
||||
};
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
Key,
|
||||
Membership,
|
||||
MembershipOrg,
|
||||
Workspace,
|
||||
} from "../models";
|
||||
import {
|
||||
MembershipOrgNotFoundError,
|
||||
UnauthorizedRequestError,
|
||||
} from "../utils/errors";
|
||||
import { Key, Membership, MembershipOrg, Workspace } from "../models";
|
||||
import { MembershipOrgNotFoundError, UnauthorizedRequestError } from "../utils/errors";
|
||||
|
||||
/**
|
||||
* Validate that user with id [userId] is a member of organization with id [organizationId]
|
||||
@@ -19,39 +11,43 @@ import {
|
||||
* @param {String[]} obj.acceptedRoles
|
||||
*/
|
||||
export const validateMembershipOrg = async ({
|
||||
userId,
|
||||
organizationId,
|
||||
acceptedRoles,
|
||||
acceptedStatuses,
|
||||
userId,
|
||||
organizationId,
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
}: {
|
||||
userId: Types.ObjectId;
|
||||
organizationId: Types.ObjectId;
|
||||
acceptedRoles?: Array<"owner" | "admin" | "member">;
|
||||
acceptedStatuses?: Array<"invited" | "accepted">;
|
||||
userId: Types.ObjectId;
|
||||
organizationId: Types.ObjectId;
|
||||
acceptedRoles?: Array<"owner" | "admin" | "member" | "custom">;
|
||||
acceptedStatuses?: Array<"invited" | "accepted">;
|
||||
}) => {
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
user: userId,
|
||||
organization: organizationId,
|
||||
});
|
||||
const membershipOrg = await MembershipOrg.findOne({
|
||||
user: userId,
|
||||
organization: organizationId
|
||||
});
|
||||
|
||||
if (!membershipOrg) {
|
||||
throw MembershipOrgNotFoundError({ message: "Failed to find organization membership" });
|
||||
}
|
||||
if (!membershipOrg) {
|
||||
throw MembershipOrgNotFoundError({ message: "Failed to find organization membership" });
|
||||
}
|
||||
|
||||
if (acceptedRoles) {
|
||||
if (!acceptedRoles.includes(membershipOrg.role)) {
|
||||
throw UnauthorizedRequestError({ message: "Failed to validate organization membership role" });
|
||||
}
|
||||
}
|
||||
if (acceptedRoles) {
|
||||
if (!acceptedRoles.includes(membershipOrg.role)) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed to validate organization membership role"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (acceptedStatuses) {
|
||||
if (!acceptedStatuses.includes(membershipOrg.status)) {
|
||||
throw UnauthorizedRequestError({ message: "Failed to validate organization membership status" });
|
||||
}
|
||||
}
|
||||
if (acceptedStatuses) {
|
||||
if (!acceptedStatuses.includes(membershipOrg.status)) {
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed to validate organization membership status"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return membershipOrg;
|
||||
}
|
||||
return membershipOrg;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return organization membership matching criteria specified in
|
||||
@@ -60,8 +56,8 @@ export const validateMembershipOrg = async ({
|
||||
* @return {Object} membershipOrg - membership
|
||||
*/
|
||||
export const findMembershipOrg = (queryObj: any) => {
|
||||
const membershipOrg = MembershipOrg.findOne(queryObj);
|
||||
return membershipOrg;
|
||||
const membershipOrg = MembershipOrg.findOne(queryObj);
|
||||
return membershipOrg;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -73,15 +69,15 @@ export const findMembershipOrg = (queryObj: any) => {
|
||||
* @param {String[]} obj.roles - roles of users.
|
||||
*/
|
||||
export const addMembershipsOrg = async ({
|
||||
userIds,
|
||||
organizationId,
|
||||
roles,
|
||||
statuses,
|
||||
userIds,
|
||||
organizationId,
|
||||
roles,
|
||||
statuses
|
||||
}: {
|
||||
userIds: string[];
|
||||
organizationId: string;
|
||||
roles: string[];
|
||||
statuses: string[];
|
||||
userIds: string[];
|
||||
organizationId: string;
|
||||
roles: string[];
|
||||
statuses: string[];
|
||||
}) => {
|
||||
const operations = userIds.map((userId, idx) => {
|
||||
return {
|
||||
@@ -90,16 +86,16 @@ export const addMembershipsOrg = async ({
|
||||
user: userId,
|
||||
organization: organizationId,
|
||||
role: roles[idx],
|
||||
status: statuses[idx],
|
||||
status: statuses[idx]
|
||||
},
|
||||
update: {
|
||||
user: userId,
|
||||
organization: organizationId,
|
||||
role: roles[idx],
|
||||
status: statuses[idx],
|
||||
status: statuses[idx]
|
||||
},
|
||||
upsert: true,
|
||||
},
|
||||
upsert: true
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -111,13 +107,9 @@ export const addMembershipsOrg = async ({
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.membershipOrgId - id of organization membership to delete
|
||||
*/
|
||||
export const deleteMembershipOrg = async ({
|
||||
membershipOrgId,
|
||||
}: {
|
||||
membershipOrgId: string;
|
||||
}) => {
|
||||
export const deleteMembershipOrg = async ({ membershipOrgId }: { membershipOrgId: string }) => {
|
||||
const deletedMembershipOrg = await MembershipOrg.findOneAndDelete({
|
||||
_id: membershipOrgId,
|
||||
_id: membershipOrgId
|
||||
});
|
||||
|
||||
if (!deletedMembershipOrg) throw new Error("Failed to delete organization membership");
|
||||
@@ -128,24 +120,24 @@ export const deleteMembershipOrg = async ({
|
||||
|
||||
const workspaces = (
|
||||
await Workspace.find({
|
||||
organization: deletedMembershipOrg.organization,
|
||||
organization: deletedMembershipOrg.organization
|
||||
})
|
||||
).map((w) => w._id.toString());
|
||||
|
||||
await Membership.deleteMany({
|
||||
user: deletedMembershipOrg.user,
|
||||
workspace: {
|
||||
$in: workspaces,
|
||||
},
|
||||
$in: workspaces
|
||||
}
|
||||
});
|
||||
|
||||
await Key.deleteMany({
|
||||
receiver: deletedMembershipOrg.user,
|
||||
workspace: {
|
||||
$in: workspaces,
|
||||
},
|
||||
$in: workspaces
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return deletedMembershipOrg;
|
||||
return deletedMembershipOrg;
|
||||
};
|
||||
17
backend/src/helpers/validation.ts
Normal file
17
backend/src/helpers/validation.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Request } from "express";
|
||||
import { AnyZodObject, ZodError, z } from "zod";
|
||||
import { BadRequestError } from "../utils/errors";
|
||||
|
||||
export async function validateRequest<T extends AnyZodObject>(
|
||||
schema: T,
|
||||
req: Request
|
||||
): Promise<z.infer<T>> {
|
||||
try {
|
||||
return schema.parseAsync(req);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
throw BadRequestError({ message: error.message });
|
||||
}
|
||||
return BadRequestError({ message: JSON.stringify(error) });
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
membership as v1MembershipRouter,
|
||||
organization as v1OrganizationRouter,
|
||||
password as v1PasswordRouter,
|
||||
role as v1RoleRouter,
|
||||
secretImport as v1SecretImportRouter,
|
||||
secret as v1SecretRouter,
|
||||
secretsFolder as v1SecretsFolder,
|
||||
@@ -173,6 +174,7 @@ const main = async () => {
|
||||
app.use("/api/v1/secret-scanning", v1SecretScanningRouter);
|
||||
app.use("/api/v1/webhooks", v1WebhooksRouter);
|
||||
app.use("/api/v1/secret-imports", v1SecretImportRouter);
|
||||
app.use("/api/v1/roles", v1RoleRouter);
|
||||
|
||||
// v2 routes (improvements)
|
||||
app.use("/api/v2/signup", v2SignupRouter);
|
||||
|
||||
@@ -1,55 +1,60 @@
|
||||
import { Schema, Types, model } from "mongoose";
|
||||
import { ADMIN, MEMBER } from "../variables";
|
||||
import { ADMIN, CUSTOM, MEMBER } from "../variables";
|
||||
|
||||
export interface IMembershipPermission {
|
||||
environmentSlug: string,
|
||||
ability: string
|
||||
environmentSlug: string;
|
||||
ability: string;
|
||||
}
|
||||
|
||||
export interface IMembership {
|
||||
_id: Types.ObjectId;
|
||||
user: Types.ObjectId;
|
||||
inviteEmail?: string;
|
||||
workspace: Types.ObjectId;
|
||||
role: "admin" | "member";
|
||||
deniedPermissions: IMembershipPermission[]
|
||||
_id: Types.ObjectId;
|
||||
user: Types.ObjectId;
|
||||
inviteEmail?: string;
|
||||
workspace: Types.ObjectId;
|
||||
role: "admin" | "member" | "custom";
|
||||
customRole: Types.ObjectId;
|
||||
deniedPermissions: IMembershipPermission[];
|
||||
}
|
||||
|
||||
const membershipSchema = new Schema<IMembership>(
|
||||
{
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
},
|
||||
inviteEmail: {
|
||||
type: String,
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
required: true,
|
||||
},
|
||||
deniedPermissions: {
|
||||
type: [
|
||||
{
|
||||
environmentSlug: String,
|
||||
ability: {
|
||||
type: String,
|
||||
enum: ["read", "write"],
|
||||
},
|
||||
},
|
||||
],
|
||||
default: [],
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
enum: [ADMIN, MEMBER],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
{
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User"
|
||||
},
|
||||
inviteEmail: {
|
||||
type: String
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
required: true
|
||||
},
|
||||
deniedPermissions: {
|
||||
type: [
|
||||
{
|
||||
environmentSlug: String,
|
||||
ability: {
|
||||
type: String,
|
||||
enum: ["read", "write"]
|
||||
}
|
||||
}
|
||||
],
|
||||
default: []
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
enum: [ADMIN, MEMBER, CUSTOM],
|
||||
required: true
|
||||
},
|
||||
customRole: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Role"
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
export const Membership = model<IMembership>("Membership", membershipSchema);
|
||||
@@ -1,45 +1,49 @@
|
||||
import { Document, Schema, Types, model } from "mongoose";
|
||||
import { ACCEPTED, ADMIN, INVITED, MEMBER, OWNER } from "../variables";
|
||||
import { ACCEPTED, ADMIN, CUSTOM, INVITED, MEMBER, OWNER } from "../variables";
|
||||
|
||||
export interface IMembershipOrg extends Document {
|
||||
_id: Types.ObjectId;
|
||||
user: Types.ObjectId;
|
||||
inviteEmail: string;
|
||||
organization: Types.ObjectId;
|
||||
role: "owner" | "admin" | "member";
|
||||
status: "invited" | "accepted";
|
||||
_id: Types.ObjectId;
|
||||
user: Types.ObjectId;
|
||||
inviteEmail: string;
|
||||
organization: Types.ObjectId;
|
||||
role: "owner" | "admin" | "member" | "custom";
|
||||
customRole: Types.ObjectId;
|
||||
status: "invited" | "accepted";
|
||||
}
|
||||
|
||||
const membershipOrgSchema = new Schema(
|
||||
{
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
},
|
||||
inviteEmail: {
|
||||
type: String,
|
||||
},
|
||||
organization: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Organization",
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
enum: [OWNER, ADMIN, MEMBER],
|
||||
required: true,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: [INVITED, ACCEPTED],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
{
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User"
|
||||
},
|
||||
inviteEmail: {
|
||||
type: String
|
||||
},
|
||||
organization: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Organization"
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
enum: [OWNER, ADMIN, MEMBER, CUSTOM],
|
||||
required: true
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: [INVITED, ACCEPTED],
|
||||
required: true
|
||||
},
|
||||
customRole: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Role"
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
export const MembershipOrg = model<IMembershipOrg>(
|
||||
"MembershipOrg",
|
||||
membershipOrgSchema
|
||||
);
|
||||
const MembershipOrg = model<IMembershipOrg>("MembershipOrg", membershipOrgSchema);
|
||||
|
||||
export default MembershipOrg;
|
||||
|
||||
53
backend/src/models/role.ts
Normal file
53
backend/src/models/role.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Schema, Types, model } from "mongoose";
|
||||
|
||||
export interface IRole {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
permissions: Array<unknown>;
|
||||
workspace: Types.ObjectId;
|
||||
organization: Types.ObjectId;
|
||||
isOrgRole: boolean;
|
||||
}
|
||||
|
||||
const roleSchema = new Schema<IRole>(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
organization: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Organization",
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace"
|
||||
},
|
||||
isOrgRole: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
select: false
|
||||
},
|
||||
description: {
|
||||
type: String
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
permissions: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const Role = model<IRole>("Role", roleSchema);
|
||||
|
||||
export default Role;
|
||||
@@ -17,6 +17,7 @@ import integrationAuth from "./integrationAuth";
|
||||
import secretsFolder from "./secretsFolder";
|
||||
import webhooks from "./webhook";
|
||||
import secretImport from "./secretImport";
|
||||
import role from "./role";
|
||||
|
||||
export {
|
||||
signup,
|
||||
@@ -37,5 +38,6 @@ export {
|
||||
integrationAuth,
|
||||
secretsFolder,
|
||||
webhooks,
|
||||
secretImport
|
||||
secretImport,
|
||||
role
|
||||
};
|
||||
|
||||
20
backend/src/routes/v1/role.ts
Normal file
20
backend/src/routes/v1/role.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import express from "express";
|
||||
import { roleController } from "../../controllers/v1";
|
||||
import { requireAuth } from "../../middleware";
|
||||
import { AuthMode } from "../../variables";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post("/", requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), roleController.createRole);
|
||||
|
||||
router.patch("/:id", requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), roleController.updateRole);
|
||||
|
||||
router.delete(
|
||||
"/:id",
|
||||
requireAuth({ acceptedAuthModes: [AuthMode.JWT] }),
|
||||
roleController.deleteRole
|
||||
);
|
||||
|
||||
router.get("/", requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), roleController.getRoles);
|
||||
|
||||
export default router;
|
||||
@@ -1,88 +1,82 @@
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
requireMembershipOrgAuth,
|
||||
requireOrganizationAuth,
|
||||
validateRequest,
|
||||
requireAuth,
|
||||
requireMembershipOrgAuth,
|
||||
requireOrganizationAuth,
|
||||
validateRequest
|
||||
} from "../../middleware";
|
||||
import { body, param } from "express-validator";
|
||||
import {
|
||||
ACCEPTED,
|
||||
ADMIN,
|
||||
AuthMode,
|
||||
MEMBER,
|
||||
OWNER
|
||||
} from "../../variables";
|
||||
import { ACCEPTED, ADMIN, AuthMode, MEMBER, OWNER } from "../../variables";
|
||||
import { organizationsController } from "../../controllers/v2";
|
||||
|
||||
// TODO: /POST to create membership
|
||||
|
||||
router.get(
|
||||
"/:organizationId/memberships",
|
||||
param("organizationId").exists().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED],
|
||||
}),
|
||||
organizationsController.getOrganizationMemberships
|
||||
"/:organizationId/memberships",
|
||||
param("organizationId").exists().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
organizationsController.getOrganizationMemberships
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/:organizationId/memberships/:membershipId",
|
||||
param("organizationId").exists().trim(),
|
||||
param("membershipId").exists().trim(),
|
||||
body("role").exists().isString().trim().isIn([OWNER, ADMIN, MEMBER]),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED],
|
||||
}),
|
||||
requireMembershipOrgAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED],
|
||||
}),
|
||||
organizationsController.updateOrganizationMembership
|
||||
"/:organizationId/memberships/:membershipId",
|
||||
param("organizationId").exists().trim(),
|
||||
param("membershipId").exists().trim(),
|
||||
body("role").exists().isString().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
requireMembershipOrgAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
organizationsController.updateOrganizationMembership
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:organizationId/memberships/:membershipId",
|
||||
param("organizationId").exists().trim(),
|
||||
param("membershipId").exists().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED],
|
||||
}),
|
||||
requireMembershipOrgAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED],
|
||||
}),
|
||||
organizationsController.deleteOrganizationMembership
|
||||
"/:organizationId/memberships/:membershipId",
|
||||
param("organizationId").exists().trim(),
|
||||
param("membershipId").exists().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
requireMembershipOrgAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
organizationsController.deleteOrganizationMembership
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/:organizationId/workspaces",
|
||||
param("organizationId").exists().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY],
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED],
|
||||
}),
|
||||
organizationsController.getOrganizationWorkspaces
|
||||
"/:organizationId/workspaces",
|
||||
param("organizationId").exists().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED]
|
||||
}),
|
||||
organizationsController.getOrganizationWorkspaces
|
||||
);
|
||||
|
||||
router.get( // TODO endpoint: deprecate service accounts
|
||||
|
||||
74
backend/src/services/RoleService.ts
Normal file
74
backend/src/services/RoleService.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@casl/ability";
|
||||
import { MembershipOrg } from "../models";
|
||||
import { IRole } from "../models/role";
|
||||
import { BadRequestError, UnauthorizedRequestError } from "../utils/errors";
|
||||
|
||||
export enum GeneralPermissionActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum OrgPermissionSubjects {
|
||||
Workspace = "workspace",
|
||||
Role = "role"
|
||||
}
|
||||
|
||||
export type OrgPermissionSet =
|
||||
| [GeneralPermissionActions, OrgPermissionSubjects.Workspace]
|
||||
| [GeneralPermissionActions, OrgPermissionSubjects.Role];
|
||||
|
||||
const buildAdminPermission = () => {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||
// ws permissions
|
||||
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
||||
can(GeneralPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||
can(GeneralPermissionActions.Edit, OrgPermissionSubjects.Workspace);
|
||||
can(GeneralPermissionActions.Delete, OrgPermissionSubjects.Workspace);
|
||||
// role permission
|
||||
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Role);
|
||||
can(GeneralPermissionActions.Create, OrgPermissionSubjects.Role);
|
||||
can(GeneralPermissionActions.Edit, OrgPermissionSubjects.Role);
|
||||
can(GeneralPermissionActions.Delete, OrgPermissionSubjects.Role);
|
||||
|
||||
return build();
|
||||
};
|
||||
|
||||
export const adminPermissions = buildAdminPermission();
|
||||
|
||||
const buildMemberPermission = () => {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||
|
||||
// ws permissions
|
||||
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
||||
can(GeneralPermissionActions.Read, OrgPermissionSubjects.Role);
|
||||
|
||||
return build();
|
||||
};
|
||||
|
||||
export const memberPermissions = buildMemberPermission();
|
||||
|
||||
export const getUserOrgPermissions = async (userId: string, orgId: string) => {
|
||||
// TODO(akhilmhdh): speed this up by pulling from cache later
|
||||
const orgMembership = await MembershipOrg.findOne({ user: userId, organization: orgId })
|
||||
.populate<{ customRole: IRole & { permissions: RawRuleOf<MongoAbility<OrgPermissionSet>>[] } }>(
|
||||
"customRole"
|
||||
)
|
||||
.exec();
|
||||
|
||||
if (!orgMembership || (orgMembership.role === "custom" && !orgMembership.customRole)) {
|
||||
throw UnauthorizedRequestError({ message: "User doesn't belong to organization" });
|
||||
}
|
||||
|
||||
if (orgMembership.role === "admin" || orgMembership.role === "owner") return adminPermissions;
|
||||
|
||||
if (orgMembership.role === "member") return memberPermissions;
|
||||
|
||||
if (orgMembership.role === "custom") {
|
||||
const permission = createMongoAbility<OrgPermissionSet>(orgMembership.customRole.permissions);
|
||||
return permission;
|
||||
}
|
||||
|
||||
throw BadRequestError({ message: "User role not found" });
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./user";
|
||||
export * from "./role";
|
||||
export * from "./workspace";
|
||||
export * from "./bot";
|
||||
export * from "./integration";
|
||||
|
||||
52
backend/src/validation/role.ts
Normal file
52
backend/src/validation/role.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const CreateRoleSchema = z.object({
|
||||
body: z.object({
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
workspaceId: z.string().optional(),
|
||||
orgId: z.string(),
|
||||
permissions: z
|
||||
.object({
|
||||
subject: z.string(),
|
||||
action: z.string(),
|
||||
condition: z.record(z.union([z.string(), z.number()]))
|
||||
})
|
||||
.array()
|
||||
})
|
||||
});
|
||||
|
||||
export const UpdateRoleSchema = z.object({
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
slug: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
workspaceId: z.string().optional(),
|
||||
orgId: z.string(),
|
||||
permissions: z
|
||||
.object({
|
||||
subject: z.string(),
|
||||
action: z.string(),
|
||||
condition: z.record(z.union([z.string(), z.number()]))
|
||||
})
|
||||
.array()
|
||||
.optional()
|
||||
})
|
||||
});
|
||||
|
||||
export const DeleteRoleSchema = z.object({
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
})
|
||||
});
|
||||
|
||||
export const GetRoleSchema = z.object({
|
||||
query: z.object({
|
||||
workspaceId: z.string().optional(),
|
||||
orgId: z.string()
|
||||
})
|
||||
});
|
||||
@@ -2,6 +2,7 @@
|
||||
export const OWNER = "owner";
|
||||
export const ADMIN = "admin";
|
||||
export const MEMBER = "member";
|
||||
export const CUSTOM = "custom";
|
||||
|
||||
// membership statuses
|
||||
export const INVITED = "invited";
|
||||
|
||||
Reference in New Issue
Block a user