feat(rbac): implemented project based permission loading and role management

This commit is contained in:
Akhil Mohan
2023-09-08 21:22:36 +05:30
parent aac3168c80
commit 520a553ea1
63 changed files with 2237 additions and 488 deletions
+54 -25
View File
@@ -14,9 +14,15 @@ import {
DeleteRoleSchema,
GetRoleSchema,
GetUserPermission,
GetUserProjectPermission,
UpdateRoleSchema
} from "../../validation";
import { packRules } from "@casl/ability/extra";
import {
adminProjectPermissions,
getUserProjectPermissions,
viewerProjectPermission
} from "../../services/ProjectRoleService";
export const createRole = async (req: Request, res: Response) => {
const {
@@ -130,35 +136,45 @@ export const getRoles = async (req: Request, res: Response) => {
throw BadRequestError({ message: "User doesn't have the permission." });
}
const roles = await Role.find({ organization: orgId, isOrgRole, workspace: workspaceId });
const customRoles = await Role.find({ organization: orgId, isOrgRole, workspace: workspaceId });
const roles = [
{
_id: "admin",
name: "Admin",
slug: "admin",
description: "Complete administration access over the organization",
permissions: isOrgRole ? adminPermissions.rules : adminProjectPermissions.rules
},
{
_id: "member",
name: "Member",
slug: "member",
description: "Non-administrative role in an organization",
permissions: isOrgRole ? memberPermissions.rules : adminProjectPermissions.rules
},
{
_id: "viewer",
name: "Viewer",
slug: "viewer",
description: "Non-administrative role in an organization",
permissions: isOrgRole ? viewerProjectPermission.rules : viewerProjectPermission.rules
},
...customRoles
];
if (isOrgRole) {
roles.unshift({
_id: "owner",
name: "Owner",
slug: "owner",
description: "Complete administration access over the organization.",
permissions: adminPermissions.rules
});
}
res.status(200).json({
message: "Successfully fetched role list",
data: {
roles: [
{
_id: "owner",
name: "Owner",
slug: "owner",
description: "Complete administration access over the organization.",
permissions: adminPermissions.rules
},
{
_id: "admin",
name: "Admin",
slug: "admin",
description: "Complete administration access over the organization",
permissions: adminPermissions.rules
},
{
_id: "member",
name: "Member",
slug: "member",
description: "Non-administrative role in an organization",
permissions: memberPermissions.rules
},
...roles
]
roles
}
});
};
@@ -175,3 +191,16 @@ export const getUserPermissions = async (req: Request, res: Response) => {
}
});
};
export const getUserWorkspacePermissions = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(GetUserProjectPermission, req);
const { permission } = await getUserProjectPermissions(req.user.id, workspaceId);
res.status(200).json({
data: {
permissions: packRules(permission.rules)
}
});
};
+1 -1
View File
@@ -17,7 +17,7 @@ export const validateMembership = async ({
}: {
userId: Types.ObjectId | string;
workspaceId: Types.ObjectId | string;
acceptedRoles?: Array<"admin" | "member" | "custom">;
acceptedRoles?: Array<"admin" | "member" | "custom" | "viewer">;
}) => {
const membership = await Membership.findOne({
user: userId,
+3 -3
View File
@@ -1,5 +1,5 @@
import { Schema, Types, model } from "mongoose";
import { ADMIN, CUSTOM, MEMBER } from "../variables";
import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../variables";
export interface IMembershipPermission {
environmentSlug: string;
@@ -11,7 +11,7 @@ export interface IMembership {
user: Types.ObjectId;
inviteEmail?: string;
workspace: Types.ObjectId;
role: "admin" | "member" | "custom";
role: "admin" | "member" | "viewer" | "custom";
customRole: Types.ObjectId;
deniedPermissions: IMembershipPermission[];
}
@@ -44,7 +44,7 @@ const membershipSchema = new Schema<IMembership>(
},
role: {
type: String,
enum: [ADMIN, MEMBER, CUSTOM],
enum: [ADMIN, MEMBER, VIEWER, CUSTOM],
required: true
},
customRole: {
+7 -1
View File
@@ -19,9 +19,15 @@ router.get("/", requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), roleControll
// get a user permissions in an org
router.get(
"/:orgId/permissions",
"/organization/:orgId/permissions",
requireAuth({ acceptedAuthModes: [AuthMode.JWT] }),
roleController.getUserPermissions
);
router.get(
"/workspace/:workspaceId/permissions",
requireAuth({ acceptedAuthModes: [AuthMode.JWT] }),
roleController.getUserWorkspacePermissions
);
export default router;
+205
View File
@@ -0,0 +1,205 @@
import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@casl/ability";
import { Membership } 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 ProjectPermission {
Role = "role",
Member = "member",
Settings = "settings",
Integrations = "integrations",
Webhooks = "webhooks",
ServiceTokens = "service-tokens",
Environments = "environments",
Tags = "tags",
AuditLogs = "audit-logs",
IpAllowList = "ip-allowlist",
Workspace = "workspace",
Secrets = "secrets",
SecretImports = "secret-imports",
Folders = "folders"
}
export type ProjectPermissionSet =
| [GeneralPermissionActions, ProjectPermission.Secrets]
| [GeneralPermissionActions, ProjectPermission.Folders]
| [GeneralPermissionActions, ProjectPermission.SecretImports]
| [GeneralPermissionActions, ProjectPermission.Role]
| [GeneralPermissionActions, ProjectPermission.Tags]
| [GeneralPermissionActions, ProjectPermission.Member]
| [GeneralPermissionActions, ProjectPermission.Integrations]
| [GeneralPermissionActions, ProjectPermission.Webhooks]
| [GeneralPermissionActions, ProjectPermission.AuditLogs]
| [GeneralPermissionActions, ProjectPermission.Environments]
| [GeneralPermissionActions, ProjectPermission.IpAllowList]
| [GeneralPermissionActions, ProjectPermission.Settings]
| [GeneralPermissionActions, ProjectPermission.ServiceTokens]
| [GeneralPermissionActions.Delete, ProjectPermission.Workspace]
| [GeneralPermissionActions.Edit, ProjectPermission.Workspace];
const buildAdminPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(GeneralPermissionActions.Read, ProjectPermission.Secrets);
can(GeneralPermissionActions.Create, ProjectPermission.Secrets);
can(GeneralPermissionActions.Edit, ProjectPermission.Secrets);
can(GeneralPermissionActions.Delete, ProjectPermission.Secrets);
can(GeneralPermissionActions.Read, ProjectPermission.Folders);
can(GeneralPermissionActions.Create, ProjectPermission.Folders);
can(GeneralPermissionActions.Edit, ProjectPermission.Folders);
can(GeneralPermissionActions.Delete, ProjectPermission.Folders);
can(GeneralPermissionActions.Read, ProjectPermission.SecretImports);
can(GeneralPermissionActions.Create, ProjectPermission.SecretImports);
can(GeneralPermissionActions.Edit, ProjectPermission.SecretImports);
can(GeneralPermissionActions.Delete, ProjectPermission.SecretImports);
can(GeneralPermissionActions.Read, ProjectPermission.Member);
can(GeneralPermissionActions.Create, ProjectPermission.Member);
can(GeneralPermissionActions.Edit, ProjectPermission.Member);
can(GeneralPermissionActions.Delete, ProjectPermission.Member);
can(GeneralPermissionActions.Read, ProjectPermission.Role);
can(GeneralPermissionActions.Create, ProjectPermission.Role);
can(GeneralPermissionActions.Edit, ProjectPermission.Role);
can(GeneralPermissionActions.Delete, ProjectPermission.Role);
can(GeneralPermissionActions.Read, ProjectPermission.Integrations);
can(GeneralPermissionActions.Create, ProjectPermission.Integrations);
can(GeneralPermissionActions.Edit, ProjectPermission.Integrations);
can(GeneralPermissionActions.Delete, ProjectPermission.Integrations);
can(GeneralPermissionActions.Read, ProjectPermission.Webhooks);
can(GeneralPermissionActions.Create, ProjectPermission.Webhooks);
can(GeneralPermissionActions.Edit, ProjectPermission.Webhooks);
can(GeneralPermissionActions.Delete, ProjectPermission.Webhooks);
can(GeneralPermissionActions.Read, ProjectPermission.ServiceTokens);
can(GeneralPermissionActions.Create, ProjectPermission.ServiceTokens);
can(GeneralPermissionActions.Edit, ProjectPermission.ServiceTokens);
can(GeneralPermissionActions.Delete, ProjectPermission.ServiceTokens);
can(GeneralPermissionActions.Read, ProjectPermission.Settings);
can(GeneralPermissionActions.Create, ProjectPermission.Settings);
can(GeneralPermissionActions.Edit, ProjectPermission.Settings);
can(GeneralPermissionActions.Delete, ProjectPermission.Settings);
can(GeneralPermissionActions.Read, ProjectPermission.Environments);
can(GeneralPermissionActions.Create, ProjectPermission.Environments);
can(GeneralPermissionActions.Edit, ProjectPermission.Environments);
can(GeneralPermissionActions.Delete, ProjectPermission.Environments);
can(GeneralPermissionActions.Read, ProjectPermission.Tags);
can(GeneralPermissionActions.Create, ProjectPermission.Tags);
can(GeneralPermissionActions.Edit, ProjectPermission.Tags);
can(GeneralPermissionActions.Delete, ProjectPermission.Tags);
can(GeneralPermissionActions.Read, ProjectPermission.AuditLogs);
can(GeneralPermissionActions.Create, ProjectPermission.AuditLogs);
can(GeneralPermissionActions.Edit, ProjectPermission.AuditLogs);
can(GeneralPermissionActions.Delete, ProjectPermission.AuditLogs);
can(GeneralPermissionActions.Read, ProjectPermission.IpAllowList);
can(GeneralPermissionActions.Create, ProjectPermission.IpAllowList);
can(GeneralPermissionActions.Edit, ProjectPermission.IpAllowList);
can(GeneralPermissionActions.Delete, ProjectPermission.IpAllowList);
can(GeneralPermissionActions.Edit, ProjectPermission.Workspace);
can(GeneralPermissionActions.Delete, ProjectPermission.IpAllowList);
return build();
};
export const adminProjectPermissions = buildAdminPermission();
const buildMemberPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(GeneralPermissionActions.Read, ProjectPermission.Secrets);
can(GeneralPermissionActions.Create, ProjectPermission.Secrets);
can(GeneralPermissionActions.Edit, ProjectPermission.Secrets);
can(GeneralPermissionActions.Delete, ProjectPermission.Secrets);
can(GeneralPermissionActions.Read, ProjectPermission.Folders);
can(GeneralPermissionActions.Create, ProjectPermission.Folders);
can(GeneralPermissionActions.Edit, ProjectPermission.Folders);
can(GeneralPermissionActions.Delete, ProjectPermission.Folders);
can(GeneralPermissionActions.Read, ProjectPermission.SecretImports);
can(GeneralPermissionActions.Create, ProjectPermission.SecretImports);
can(GeneralPermissionActions.Edit, ProjectPermission.SecretImports);
can(GeneralPermissionActions.Delete, ProjectPermission.SecretImports);
can(GeneralPermissionActions.Read, ProjectPermission.Member);
can(GeneralPermissionActions.Read, ProjectPermission.Role);
can(GeneralPermissionActions.Read, ProjectPermission.Integrations);
can(GeneralPermissionActions.Read, ProjectPermission.Webhooks);
can(GeneralPermissionActions.Read, ProjectPermission.ServiceTokens);
can(GeneralPermissionActions.Read, ProjectPermission.Settings);
can(GeneralPermissionActions.Read, ProjectPermission.Environments);
can(GeneralPermissionActions.Read, ProjectPermission.Tags);
can(GeneralPermissionActions.Read, ProjectPermission.AuditLogs);
can(GeneralPermissionActions.Read, ProjectPermission.IpAllowList);
return build();
};
export const memberProjectPermissions = buildMemberPermission();
const buildViewerPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(GeneralPermissionActions.Read, ProjectPermission.Secrets);
can(GeneralPermissionActions.Read, ProjectPermission.Folders);
can(GeneralPermissionActions.Read, ProjectPermission.SecretImports);
can(GeneralPermissionActions.Read, ProjectPermission.Member);
can(GeneralPermissionActions.Read, ProjectPermission.Role);
can(GeneralPermissionActions.Read, ProjectPermission.Integrations);
can(GeneralPermissionActions.Read, ProjectPermission.Webhooks);
can(GeneralPermissionActions.Read, ProjectPermission.ServiceTokens);
can(GeneralPermissionActions.Read, ProjectPermission.Settings);
can(GeneralPermissionActions.Read, ProjectPermission.Environments);
can(GeneralPermissionActions.Read, ProjectPermission.Tags);
can(GeneralPermissionActions.Read, ProjectPermission.AuditLogs);
can(GeneralPermissionActions.Read, ProjectPermission.IpAllowList);
return build();
};
export const viewerProjectPermission = buildViewerPermission();
export const getUserProjectPermissions = async (userId: string, workspaceId: string) => {
// TODO(akhilmhdh): speed this up by pulling from cache later
const membership = await Membership.findOne({
user: userId,
workspace: workspaceId
})
.populate<{
customRole: IRole & { permissions: RawRuleOf<MongoAbility<ProjectPermissionSet>>[] };
}>("customRole")
.exec();
console.log(membership, userId, workspaceId);
if (!membership || (membership.role === "custom" && !membership.customRole)) {
throw UnauthorizedRequestError({ message: "User doesn't belong to organization" });
}
if (membership.role === "admin") return { permission: adminProjectPermissions, membership };
if (membership.role === "member") return { permission: memberProjectPermissions, membership };
if (membership.role === "viewer") return { permission: memberProjectPermissions, membership };
if (membership.role === "custom") {
const permission = createMongoAbility<ProjectPermissionSet>(membership.customRole.permissions);
return { permission, membership };
}
throw BadRequestError({ message: "User role not found" });
};
+6
View File
@@ -56,3 +56,9 @@ export const GetUserPermission = z.object({
orgId: z.string().trim()
})
});
export const GetUserProjectPermission = z.object({
params: z.object({
workspaceId: z.string().trim()
})
});
+1
View File
@@ -2,6 +2,7 @@
export const OWNER = "owner";
export const ADMIN = "admin";
export const MEMBER = "member";
export const VIEWER = "viewer";
export const CUSTOM = "custom";
// membership statuses