feat(rbac): implemented backend base apis for rbac permission system

This commit is contained in:
Akhil Mohan
2023-08-14 20:44:55 +05:30
parent 6bca7dcc58
commit 1bf9041ac9
19 changed files with 833 additions and 327 deletions

View File

@@ -10,6 +10,8 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@aws-sdk/client-secrets-manager": "^3.319.0", "@aws-sdk/client-secrets-manager": "^3.319.0",
"@casl/ability": "^6.5.0",
"@casl/mongoose": "^7.2.1",
"@godaddy/terminus": "^4.12.0", "@godaddy/terminus": "^4.12.0",
"@node-saml/passport-saml": "^4.0.4", "@node-saml/passport-saml": "^4.0.4",
"@octokit/rest": "^19.0.5", "@octokit/rest": "^19.0.5",
@@ -59,7 +61,8 @@
"typescript": "^4.9.3", "typescript": "^4.9.3",
"utility-types": "^3.10.0", "utility-types": "^3.10.0",
"winston": "^3.8.2", "winston": "^3.8.2",
"winston-loki": "^6.0.7" "winston-loki": "^6.0.6",
"zod": "^3.21.4"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "^29.3.1", "@jest/globals": "^29.3.1",
@@ -3339,6 +3342,26 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true "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": { "node_modules/@colors/colors": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@@ -6174,6 +6197,37 @@
"url": "https://opencollective.com/typescript-eslint" "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": { "node_modules/@xmldom/xmldom": {
"version": "0.8.10", "version": "0.8.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
@@ -16626,6 +16680,14 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "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": { "dependencies": {
@@ -19324,6 +19386,20 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true "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": { "@colors/colors": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@@ -21575,6 +21651,37 @@
"eslint-visitor-keys": "^3.3.0" "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": { "@xmldom/xmldom": {
"version": "0.8.10", "version": "0.8.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", "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", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true "dev": true
},
"zod": {
"version": "3.21.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
"integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw=="
} }
} }
} }

View File

@@ -1,6 +1,8 @@
{ {
"dependencies": { "dependencies": {
"@aws-sdk/client-secrets-manager": "^3.319.0", "@aws-sdk/client-secrets-manager": "^3.319.0",
"@casl/ability": "^6.5.0",
"@casl/mongoose": "^7.2.1",
"@godaddy/terminus": "^4.12.0", "@godaddy/terminus": "^4.12.0",
"@node-saml/passport-saml": "^4.0.4", "@node-saml/passport-saml": "^4.0.4",
"@octokit/rest": "^19.0.5", "@octokit/rest": "^19.0.5",
@@ -50,7 +52,8 @@
"typescript": "^4.9.3", "typescript": "^4.9.3",
"utility-types": "^3.10.0", "utility-types": "^3.10.0",
"winston": "^3.8.2", "winston": "^3.8.2",
"winston-loki": "^6.0.7" "winston-loki": "^6.0.6",
"zod": "^3.21.4"
}, },
"name": "infisical-api", "name": "infisical-api",
"version": "1.0.0", "version": "1.0.0",

View File

@@ -16,6 +16,7 @@ import * as workspaceController from "./workspaceController";
import * as secretScanningController from "./secretScanningController"; import * as secretScanningController from "./secretScanningController";
import * as webhookController from "./webhookController"; import * as webhookController from "./webhookController";
import * as secretImportController from "./secretImportController"; import * as secretImportController from "./secretImportController";
import * as roleController from "./roleController";
export { export {
authController, authController,
@@ -35,5 +36,6 @@ export {
workspaceController, workspaceController,
secretScanningController, secretScanningController,
webhookController, webhookController,
roleController,
secretImportController secretImportController
}; };

View 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
]
}
});
};

View File

@@ -1,21 +1,19 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import { Membership, MembershipOrg, ServiceAccount, Workspace } from "../../models";
Membership,
MembershipOrg,
ServiceAccount,
Workspace,
} from "../../models";
import { deleteMembershipOrg } from "../../helpers/membershipOrg"; import { deleteMembershipOrg } from "../../helpers/membershipOrg";
import { updateSubscriptionOrgQuantity } from "../../helpers/organization"; 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] * Return memberships for organization with id [organizationId]
* @param req * @param req
* @param res * @param res
*/ */
export const getOrganizationMemberships = async (req: Request, res: Response) => { export const getOrganizationMemberships = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return organization memberships' #swagger.summary = 'Return organization memberships'
#swagger.description = 'Return organization memberships' #swagger.description = 'Return organization memberships'
@@ -48,24 +46,24 @@ export const getOrganizationMemberships = async (req: Request, res: Response) =>
} }
} }
*/ */
const { organizationId } = req.params; const { organizationId } = req.params;
const memberships = await MembershipOrg.find({ const memberships = await MembershipOrg.find({
organization: organizationId, organization: organizationId
}).populate("user", "+publicKey"); }).populate("user", "+publicKey");
return res.status(200).send({ return res.status(200).send({
memberships, memberships
}); });
} };
/** /**
* Update role of membership with id [membershipId] to role [role] * Update role of membership with id [membershipId] to role [role]
* @param req * @param req
* @param res * @param res
*/ */
export const updateOrganizationMembership = async (req: Request, res: Response) => { export const updateOrganizationMembership = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Update organization membership' #swagger.summary = 'Update organization membership'
#swagger.description = 'Update organization membership' #swagger.description = 'Update organization membership'
@@ -118,31 +116,46 @@ export const updateOrganizationMembership = async (req: Request, res: Response)
} }
} }
*/ */
const { membershipId } = req.params; const { membershipId } = req.params;
const { role } = req.body; const { role } = req.body;
const membership = await MembershipOrg.findByIdAndUpdate( const isCustomRole = !["admin", "member", "owner"].includes(role);
membershipId, if (isCustomRole) {
{ const orgRole = await Role.findOne({ slug: role, isOrgRole: true });
role, if (!orgRole) throw BadRequestError({ message: "Role not found" });
}, {
new: true, const membership = await MembershipOrg.findByIdAndUpdate(membershipId, {
} role: CUSTOM,
); customRole: orgRole
return res.status(200).send({
membership,
}); });
} 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] * Delete organization membership with id [membershipId]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const deleteOrganizationMembership = async (req: Request, res: Response) => { export const deleteOrganizationMembership = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Delete organization membership' #swagger.summary = 'Delete organization membership'
#swagger.description = 'Delete organization membership' #swagger.description = 'Delete organization membership'
@@ -178,30 +191,30 @@ 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,
});
await updateSubscriptionOrgQuantity({ // delete organization membership
organizationId: membership.organization.toString(), const membership = await deleteMembershipOrg({
}); membershipOrgId: membershipId
});
return res.status(200).send({ await updateSubscriptionOrgQuantity({
membership, organizationId: membership.organization.toString()
}); });
}
return res.status(200).send({
membership
});
};
/** /**
* Return workspaces for organization with id [organizationId] that user has * Return workspaces for organization with id [organizationId] that user has
* access to * access to
* @param req * @param req
* @param res * @param res
*/ */
export const getOrganizationWorkspaces = async (req: Request, res: Response) => { export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return projects in organization that user is part of' #swagger.summary = 'Return projects in organization that user is part of'
#swagger.description = 'Return projects in organization that user is part of' #swagger.description = 'Return projects in organization that user is part of'
@@ -234,45 +247,45 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) =>
} }
} }
*/ */
const { organizationId } = req.params; const { organizationId } = req.params;
const workspacesSet = new Set( const workspacesSet = new Set(
( (
await Workspace.find( await Workspace.find(
{ {
organization: organizationId, organization: organizationId
}, },
"_id" "_id"
) )
).map((w) => w._id.toString()) ).map((w) => w._id.toString())
); );
const workspaces = ( const workspaces = (
await Membership.find({ await Membership.find({
user: req.user._id, user: req.user._id
}).populate("workspace") }).populate("workspace")
) )
.filter((m) => workspacesSet.has(m.workspace._id.toString())) .filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace); .map((m) => m.workspace);
return res.status(200).send({ return res.status(200).send({
workspaces, workspaces
}); });
} };
/** /**
* Return service accounts for organization with id [organizationId] * Return service accounts for organization with id [organizationId]
* @param req * @param req
* @param res * @param res
*/ */
export const getOrganizationServiceAccounts = async (req: Request, res: Response) => { export const getOrganizationServiceAccounts = async (req: Request, res: Response) => {
const { organizationId } = req.params; const { organizationId } = req.params;
const serviceAccounts = await ServiceAccount.find({ const serviceAccounts = await ServiceAccount.find({
organization: new Types.ObjectId(organizationId), organization: new Types.ObjectId(organizationId)
}); });
return res.status(200).send({ return res.status(200).send({
serviceAccounts, serviceAccounts
}); });
} };

View File

@@ -11,29 +11,29 @@ import { BadRequestError, MembershipNotFoundError } from "../utils/errors";
* @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId] * @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId]
*/ */
export const validateMembership = async ({ export const validateMembership = async ({
userId, userId,
workspaceId, workspaceId,
acceptedRoles, acceptedRoles
}: { }: {
userId: Types.ObjectId | string; userId: Types.ObjectId | string;
workspaceId: Types.ObjectId | string; workspaceId: Types.ObjectId | string;
acceptedRoles?: Array<"admin" | "member">; acceptedRoles?: Array<"admin" | "member" | "custom">;
}) => { }) => {
const membership = await Membership.findOne({ const membership = await Membership.findOne({
user: userId, user: userId,
workspace: workspaceId, workspace: workspaceId
}).populate("workspace"); }).populate("workspace");
if (!membership) { if (!membership) {
throw MembershipNotFoundError({ throw MembershipNotFoundError({
message: "Failed to find workspace membership", message: "Failed to find workspace membership"
}); });
} }
if (acceptedRoles) { if (acceptedRoles) {
if (!acceptedRoles.includes(membership.role)) { if (!acceptedRoles.includes(membership.role)) {
throw BadRequestError({ 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 * @return {Object} membership - membership
*/ */
export const findMembership = async (queryObj: any) => { export const findMembership = async (queryObj: any) => {
const membership = await Membership.findOne(queryObj); const membership = await Membership.findOne(queryObj);
return membership; return membership;
}; };
@@ -60,9 +60,9 @@ export const findMembership = async (queryObj: any) => {
* @param {String[]} obj.roles - roles of users. * @param {String[]} obj.roles - roles of users.
*/ */
export const addMemberships = async ({ export const addMemberships = async ({
userIds, userIds,
workspaceId, workspaceId,
roles, roles
}: { }: {
userIds: string[]; userIds: string[];
workspaceId: string; workspaceId: string;
@@ -74,15 +74,15 @@ export const addMemberships = async ({
filter: { filter: {
user: userId, user: userId,
workspace: workspaceId, workspace: workspaceId,
role: roles[idx], role: roles[idx]
}, },
update: { update: {
user: userId, user: userId,
workspace: workspaceId, workspace: workspaceId,
role: roles[idx], role: roles[idx]
}, },
upsert: true, upsert: true
}, }
}; };
}); });
await Membership.bulkWrite(operations as any); await Membership.bulkWrite(operations as any);
@@ -94,8 +94,8 @@ export const addMemberships = async ({
* @param {String} obj.membershipId - id of membership to delete * @param {String} obj.membershipId - id of membership to delete
*/ */
export const deleteMembership = async ({ membershipId }: { membershipId: string }) => { export const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
const deletedMembership = await Membership.findOneAndDelete({ const deletedMembership = await Membership.findOneAndDelete({
_id: membershipId, _id: membershipId
}); });
// delete keys associated with the membership // delete keys associated with the membership
@@ -103,9 +103,9 @@ export const deleteMembership = async ({ membershipId }: { membershipId: string
// case: membership had a registered user // case: membership had a registered user
await Key.deleteMany({ await Key.deleteMany({
receiver: deletedMembership.user, receiver: deletedMembership.user,
workspace: deletedMembership.workspace, workspace: deletedMembership.workspace
}); });
} }
return deletedMembership; return deletedMembership;
}; };

View File

@@ -1,14 +1,6 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import { Key, Membership, MembershipOrg, Workspace } from "../models";
Key, import { MembershipOrgNotFoundError, UnauthorizedRequestError } from "../utils/errors";
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] * Validate that user with id [userId] is a member of organization with id [organizationId]
@@ -19,39 +11,43 @@ import {
* @param {String[]} obj.acceptedRoles * @param {String[]} obj.acceptedRoles
*/ */
export const validateMembershipOrg = async ({ export const validateMembershipOrg = async ({
userId, userId,
organizationId, organizationId,
acceptedRoles, acceptedRoles,
acceptedStatuses, acceptedStatuses
}: { }: {
userId: Types.ObjectId; userId: Types.ObjectId;
organizationId: Types.ObjectId; organizationId: Types.ObjectId;
acceptedRoles?: Array<"owner" | "admin" | "member">; acceptedRoles?: Array<"owner" | "admin" | "member" | "custom">;
acceptedStatuses?: Array<"invited" | "accepted">; acceptedStatuses?: Array<"invited" | "accepted">;
}) => { }) => {
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({
user: userId, user: userId,
organization: organizationId, organization: organizationId
}); });
if (!membershipOrg) { if (!membershipOrg) {
throw MembershipOrgNotFoundError({ message: "Failed to find organization membership" }); throw MembershipOrgNotFoundError({ message: "Failed to find organization membership" });
} }
if (acceptedRoles) { if (acceptedRoles) {
if (!acceptedRoles.includes(membershipOrg.role)) { if (!acceptedRoles.includes(membershipOrg.role)) {
throw UnauthorizedRequestError({ message: "Failed to validate organization membership 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 * Return organization membership matching criteria specified in
@@ -60,8 +56,8 @@ export const validateMembershipOrg = async ({
* @return {Object} membershipOrg - membership * @return {Object} membershipOrg - membership
*/ */
export const findMembershipOrg = (queryObj: any) => { export const findMembershipOrg = (queryObj: any) => {
const membershipOrg = MembershipOrg.findOne(queryObj); const membershipOrg = MembershipOrg.findOne(queryObj);
return membershipOrg; return membershipOrg;
}; };
/** /**
@@ -73,15 +69,15 @@ export const findMembershipOrg = (queryObj: any) => {
* @param {String[]} obj.roles - roles of users. * @param {String[]} obj.roles - roles of users.
*/ */
export const addMembershipsOrg = async ({ export const addMembershipsOrg = async ({
userIds, userIds,
organizationId, organizationId,
roles, roles,
statuses, statuses
}: { }: {
userIds: string[]; userIds: string[];
organizationId: string; organizationId: string;
roles: string[]; roles: string[];
statuses: string[]; statuses: string[];
}) => { }) => {
const operations = userIds.map((userId, idx) => { const operations = userIds.map((userId, idx) => {
return { return {
@@ -90,16 +86,16 @@ export const addMembershipsOrg = async ({
user: userId, user: userId,
organization: organizationId, organization: organizationId,
role: roles[idx], role: roles[idx],
status: statuses[idx], status: statuses[idx]
}, },
update: { update: {
user: userId, user: userId,
organization: organizationId, organization: organizationId,
role: roles[idx], 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 {Object} obj
* @param {String} obj.membershipOrgId - id of organization membership to delete * @param {String} obj.membershipOrgId - id of organization membership to delete
*/ */
export const deleteMembershipOrg = async ({ export const deleteMembershipOrg = async ({ membershipOrgId }: { membershipOrgId: string }) => {
membershipOrgId,
}: {
membershipOrgId: string;
}) => {
const deletedMembershipOrg = await MembershipOrg.findOneAndDelete({ const deletedMembershipOrg = await MembershipOrg.findOneAndDelete({
_id: membershipOrgId, _id: membershipOrgId
}); });
if (!deletedMembershipOrg) throw new Error("Failed to delete organization membership"); if (!deletedMembershipOrg) throw new Error("Failed to delete organization membership");
@@ -128,24 +120,24 @@ export const deleteMembershipOrg = async ({
const workspaces = ( const workspaces = (
await Workspace.find({ await Workspace.find({
organization: deletedMembershipOrg.organization, organization: deletedMembershipOrg.organization
}) })
).map((w) => w._id.toString()); ).map((w) => w._id.toString());
await Membership.deleteMany({ await Membership.deleteMany({
user: deletedMembershipOrg.user, user: deletedMembershipOrg.user,
workspace: { workspace: {
$in: workspaces, $in: workspaces
}, }
}); });
await Key.deleteMany({ await Key.deleteMany({
receiver: deletedMembershipOrg.user, receiver: deletedMembershipOrg.user,
workspace: { workspace: {
$in: workspaces, $in: workspaces
}, }
}); });
} }
return deletedMembershipOrg; return deletedMembershipOrg;
}; };

View 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) });
}
}

View File

@@ -37,6 +37,7 @@ import {
membership as v1MembershipRouter, membership as v1MembershipRouter,
organization as v1OrganizationRouter, organization as v1OrganizationRouter,
password as v1PasswordRouter, password as v1PasswordRouter,
role as v1RoleRouter,
secretImport as v1SecretImportRouter, secretImport as v1SecretImportRouter,
secret as v1SecretRouter, secret as v1SecretRouter,
secretsFolder as v1SecretsFolder, secretsFolder as v1SecretsFolder,
@@ -173,6 +174,7 @@ const main = async () => {
app.use("/api/v1/secret-scanning", v1SecretScanningRouter); app.use("/api/v1/secret-scanning", v1SecretScanningRouter);
app.use("/api/v1/webhooks", v1WebhooksRouter); app.use("/api/v1/webhooks", v1WebhooksRouter);
app.use("/api/v1/secret-imports", v1SecretImportRouter); app.use("/api/v1/secret-imports", v1SecretImportRouter);
app.use("/api/v1/roles", v1RoleRouter);
// v2 routes (improvements) // v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter); app.use("/api/v2/signup", v2SignupRouter);

View File

@@ -1,55 +1,60 @@
import { Schema, Types, model } from "mongoose"; import { Schema, Types, model } from "mongoose";
import { ADMIN, MEMBER } from "../variables"; import { ADMIN, CUSTOM, MEMBER } from "../variables";
export interface IMembershipPermission { export interface IMembershipPermission {
environmentSlug: string, environmentSlug: string;
ability: string ability: string;
} }
export interface IMembership { export interface IMembership {
_id: Types.ObjectId; _id: Types.ObjectId;
user: Types.ObjectId; user: Types.ObjectId;
inviteEmail?: string; inviteEmail?: string;
workspace: Types.ObjectId; workspace: Types.ObjectId;
role: "admin" | "member"; role: "admin" | "member" | "custom";
deniedPermissions: IMembershipPermission[] customRole: Types.ObjectId;
deniedPermissions: IMembershipPermission[];
} }
const membershipSchema = new Schema<IMembership>( const membershipSchema = new Schema<IMembership>(
{ {
user: { user: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "User", ref: "User"
}, },
inviteEmail: { inviteEmail: {
type: String, type: String
}, },
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "Workspace", ref: "Workspace",
required: true, required: true
}, },
deniedPermissions: { deniedPermissions: {
type: [ type: [
{ {
environmentSlug: String, environmentSlug: String,
ability: { ability: {
type: String, type: String,
enum: ["read", "write"], enum: ["read", "write"]
}, }
}, }
], ],
default: [], default: []
}, },
role: { role: {
type: String, type: String,
enum: [ADMIN, MEMBER], enum: [ADMIN, MEMBER, CUSTOM],
required: true, required: true
}, },
}, customRole: {
{ type: Schema.Types.ObjectId,
timestamps: true, ref: "Role"
} }
},
{
timestamps: true
}
); );
export const Membership = model<IMembership>("Membership", membershipSchema); export const Membership = model<IMembership>("Membership", membershipSchema);

View File

@@ -1,45 +1,49 @@
import { Document, Schema, Types, model } from "mongoose"; 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 { export interface IMembershipOrg extends Document {
_id: Types.ObjectId; _id: Types.ObjectId;
user: Types.ObjectId; user: Types.ObjectId;
inviteEmail: string; inviteEmail: string;
organization: Types.ObjectId; organization: Types.ObjectId;
role: "owner" | "admin" | "member"; role: "owner" | "admin" | "member" | "custom";
status: "invited" | "accepted"; customRole: Types.ObjectId;
status: "invited" | "accepted";
} }
const membershipOrgSchema = new Schema( const membershipOrgSchema = new Schema(
{ {
user: { user: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "User", ref: "User"
}, },
inviteEmail: { inviteEmail: {
type: String, type: String
}, },
organization: { organization: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "Organization", ref: "Organization"
}, },
role: { role: {
type: String, type: String,
enum: [OWNER, ADMIN, MEMBER], enum: [OWNER, ADMIN, MEMBER, CUSTOM],
required: true, required: true
}, },
status: { status: {
type: String, type: String,
enum: [INVITED, ACCEPTED], enum: [INVITED, ACCEPTED],
required: true, required: true
}, },
}, customRole: {
{ type: Schema.Types.ObjectId,
timestamps: true, ref: "Role"
} }
},
{
timestamps: true
}
); );
export const MembershipOrg = model<IMembershipOrg>( const MembershipOrg = model<IMembershipOrg>("MembershipOrg", membershipOrgSchema);
"MembershipOrg",
membershipOrgSchema export default MembershipOrg;
);

View 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;

View File

@@ -17,6 +17,7 @@ import integrationAuth from "./integrationAuth";
import secretsFolder from "./secretsFolder"; import secretsFolder from "./secretsFolder";
import webhooks from "./webhook"; import webhooks from "./webhook";
import secretImport from "./secretImport"; import secretImport from "./secretImport";
import role from "./role";
export { export {
signup, signup,
@@ -37,5 +38,6 @@ export {
integrationAuth, integrationAuth,
secretsFolder, secretsFolder,
webhooks, webhooks,
secretImport secretImport,
role
}; };

View 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;

View File

@@ -1,88 +1,82 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { import {
requireAuth, requireAuth,
requireMembershipOrgAuth, requireMembershipOrgAuth,
requireOrganizationAuth, requireOrganizationAuth,
validateRequest, validateRequest
} from "../../middleware"; } from "../../middleware";
import { body, param } from "express-validator"; import { body, param } from "express-validator";
import { import { ACCEPTED, ADMIN, AuthMode, MEMBER, OWNER } from "../../variables";
ACCEPTED,
ADMIN,
AuthMode,
MEMBER,
OWNER
} from "../../variables";
import { organizationsController } from "../../controllers/v2"; import { organizationsController } from "../../controllers/v2";
// TODO: /POST to create membership // TODO: /POST to create membership
router.get( router.get(
"/:organizationId/memberships", "/:organizationId/memberships",
param("organizationId").exists().trim(), param("organizationId").exists().trim(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED], acceptedStatuses: [ACCEPTED]
}), }),
organizationsController.getOrganizationMemberships organizationsController.getOrganizationMemberships
); );
router.patch( router.patch(
"/:organizationId/memberships/:membershipId", "/:organizationId/memberships/:membershipId",
param("organizationId").exists().trim(), param("organizationId").exists().trim(),
param("membershipId").exists().trim(), param("membershipId").exists().trim(),
body("role").exists().isString().trim().isIn([OWNER, ADMIN, MEMBER]), body("role").exists().isString().trim(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN], acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED], acceptedStatuses: [ACCEPTED]
}), }),
requireMembershipOrgAuth({ requireMembershipOrgAuth({
acceptedRoles: [OWNER, ADMIN], acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED], acceptedStatuses: [ACCEPTED]
}), }),
organizationsController.updateOrganizationMembership organizationsController.updateOrganizationMembership
); );
router.delete( router.delete(
"/:organizationId/memberships/:membershipId", "/:organizationId/memberships/:membershipId",
param("organizationId").exists().trim(), param("organizationId").exists().trim(),
param("membershipId").exists().trim(), param("membershipId").exists().trim(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN], acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED], acceptedStatuses: [ACCEPTED]
}), }),
requireMembershipOrgAuth({ requireMembershipOrgAuth({
acceptedRoles: [OWNER, ADMIN], acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED], acceptedStatuses: [ACCEPTED]
}), }),
organizationsController.deleteOrganizationMembership organizationsController.deleteOrganizationMembership
); );
router.get( router.get(
"/:organizationId/workspaces", "/:organizationId/workspaces",
param("organizationId").exists().trim(), param("organizationId").exists().trim(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY], acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}), }),
requireOrganizationAuth({ requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN], acceptedRoles: [OWNER, ADMIN],
acceptedStatuses: [ACCEPTED], acceptedStatuses: [ACCEPTED]
}), }),
organizationsController.getOrganizationWorkspaces organizationsController.getOrganizationWorkspaces
); );
router.get( // TODO endpoint: deprecate service accounts router.get( // TODO endpoint: deprecate service accounts
@@ -99,4 +93,4 @@ router.get( // TODO endpoint: deprecate service accounts
organizationsController.getOrganizationServiceAccounts organizationsController.getOrganizationServiceAccounts
); );
export default router; export default router;

View 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" });
};

View File

@@ -1,4 +1,5 @@
export * from "./user"; export * from "./user";
export * from "./role";
export * from "./workspace"; export * from "./workspace";
export * from "./bot"; export * from "./bot";
export * from "./integration"; export * from "./integration";
@@ -8,4 +9,4 @@ export * from "./membershipOrg";
export * from "./organization"; export * from "./organization";
export * from "./secrets"; export * from "./secrets";
export * from "./serviceAccount"; export * from "./serviceAccount";
export * from "./serviceTokenData"; export * from "./serviceTokenData";

View 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()
})
});

View File

@@ -2,9 +2,10 @@
export const OWNER = "owner"; export const OWNER = "owner";
export const ADMIN = "admin"; export const ADMIN = "admin";
export const MEMBER = "member"; export const MEMBER = "member";
export const CUSTOM = "custom";
// membership statuses // membership statuses
export const INVITED = "invited"; export const INVITED = "invited";
// -- organization // -- organization
export const ACCEPTED = "accepted"; export const ACCEPTED = "accepted";