Skip to content

Commit

Permalink
feat(rbac): implemented project based permission loading and role man…
Browse files Browse the repository at this point in the history
…agement
  • Loading branch information
akhilmhdh committed Sep 8, 2023
1 parent aac3168 commit 520a553
Show file tree
Hide file tree
Showing 63 changed files with 2,237 additions and 488 deletions.
79 changes: 54 additions & 25 deletions backend/src/controllers/v1/roleController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
});
};
Expand All @@ -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)
}
});
};
2 changes: 1 addition & 1 deletion backend/src/helpers/membership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions backend/src/models/membership.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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[];
}
Expand Down Expand Up @@ -44,7 +44,7 @@ const membershipSchema = new Schema<IMembership>(
},
role: {
type: String,
enum: [ADMIN, MEMBER, CUSTOM],
enum: [ADMIN, MEMBER, VIEWER, CUSTOM],
required: true
},
customRole: {
Expand Down
8 changes: 7 additions & 1 deletion backend/src/routes/v1/role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 205 additions & 0 deletions backend/src/services/ProjectRoleService.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 6 additions & 0 deletions backend/src/validation/role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 change: 1 addition & 0 deletions backend/src/variables/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 520a553

Please sign in to comment.