diff --git a/backend/src/controllers/v2/organizationsController.ts b/backend/src/controllers/v2/organizationsController.ts index 684273e24..5db8b6eb1 100644 --- a/backend/src/controllers/v2/organizationsController.ts +++ b/backend/src/controllers/v2/organizationsController.ts @@ -15,7 +15,7 @@ import { } from "../../helpers/organization"; import { addMembershipsOrg } from "../../helpers/membershipOrg"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; -import { ACCEPTED, ADMIN, CUSTOM, MEMBER } from "../../variables"; +import { ACCEPTED, ADMIN, CUSTOM, MEMBER, NO_ACCESS } from "../../variables"; import * as reqValidator from "../../validation/organization"; import { validateRequest } from "../../helpers/validation"; import { @@ -153,7 +153,7 @@ export const updateOrganizationMembership = async (req: Request, res: Response) OrgPermissionSubjects.Member ); - const isCustomRole = ![ADMIN, MEMBER].includes(role); + const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role); if (isCustomRole) { const orgRole = await Role.findOne({ slug: role, diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index 55e5ea7d1..1e27ec356 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -30,7 +30,7 @@ import { } from "../../ee/services/ProjectRoleService"; import { ForbiddenError } from "@casl/ability"; import { BadRequestError, ForbiddenRequestError, ResourceNotFoundError } from "../../utils/errors"; -import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables"; +import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../../variables"; interface V2PushSecret { type: string; // personal or shared @@ -556,7 +556,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => { let customRole; if (role) { - const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role); + const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role); if (isCustomRole) { customRole = await Role.findOne({ slug: role, @@ -632,7 +632,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => { let customRole; if (role) { - const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role); + const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role); if (isCustomRole) { customRole = await Role.findOne({ slug: role, diff --git a/backend/src/ee/controllers/v1/roleController.ts b/backend/src/ee/controllers/v1/roleController.ts index cef14e79f..47707ae60 100644 --- a/backend/src/ee/controllers/v1/roleController.ts +++ b/backend/src/ee/controllers/v1/roleController.ts @@ -15,6 +15,7 @@ import { adminProjectPermissions, getAuthDataProjectPermissions, memberProjectPermissions, + noAccessProjectPermissions, viewerProjectPermission } from "../../services/ProjectRoleService"; import { @@ -22,7 +23,8 @@ import { OrgPermissionSubjects, adminPermissions, getUserOrgPermissions, - memberPermissions + memberPermissions, + noAccessPermissions } from "../../services/RoleService"; import { BadRequestError } from "../../../utils/errors"; import { Role } from "../../models"; @@ -195,6 +197,13 @@ export const getRoles = async (req: Request, res: Response) => { description: "Complete administration access over the organization", permissions: isOrgRole ? adminPermissions.rules : adminProjectPermissions.rules }, + { + _id: "no-access", + name: "No Access", + slug: "no-access", + description: "No access to any resources in the organization", + permissions: isOrgRole ? noAccessPermissions.rules : noAccessProjectPermissions.rules + }, { _id: "member", name: isOrgRole ? "Member" : "Developer", diff --git a/backend/src/ee/controllers/v3/machineIdentityController.ts b/backend/src/ee/controllers/v3/machineIdentityController.ts index be26ea7b0..07b093fe2 100644 --- a/backend/src/ee/controllers/v3/machineIdentityController.ts +++ b/backend/src/ee/controllers/v3/machineIdentityController.ts @@ -25,7 +25,7 @@ import { BadRequestError, ForbiddenRequestError, ResourceNotFoundError, Unauthor import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip"; import { EEAuditLogService, EELicenseService } from "../../services"; import { getAuthSecret } from "../../../config"; -import { ADMIN, AuthTokenType, CUSTOM, MEMBER } from "../../../variables"; +import { ADMIN, AuthTokenType, CUSTOM, MEMBER, NO_ACCESS } from "../../../variables"; import { OrgPermissionActions, OrgPermissionSubjects @@ -143,7 +143,7 @@ export const createMachineIdentity = async (req: Request, res: Response) => { isRefreshTokenRotationEnabled } } = await validateRequest(reqValidator.CreateMachineIdentityV3, req); - + const { permission } = await getUserOrgPermissions(req.user._id, organizationId); ForbiddenError.from(permission).throwUnlessCan( @@ -161,7 +161,7 @@ export const createMachineIdentity = async (req: Request, res: Response) => { const organization = await Organization.findById(organizationId); if (!organization) throw BadRequestError({ message: `Organization with id ${organizationId} not found` }); - const isCustomRole = ![ADMIN, MEMBER].includes(role); + const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role); let customRole; if (isCustomRole) { @@ -298,7 +298,7 @@ export const updateMachineIdentity = async (req: Request, res: Response) => { let customRole; if (role) { - const isCustomRole = ![ADMIN, MEMBER].includes(role); + const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role); if (isCustomRole) { customRole = await Role.findOne({ slug: role, diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index 6baea04dc..7052263c5 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -66,7 +66,7 @@ class EELicenseService { secretVersioning: true, pitRecovery: false, ipAllowlisting: false, - rbac: false, + rbac: true, customRateLimits: false, customAlerts: false, auditLogs: false, diff --git a/backend/src/ee/services/ProjectRoleService.ts b/backend/src/ee/services/ProjectRoleService.ts index bd9f68253..225f72534 100644 --- a/backend/src/ee/services/ProjectRoleService.ts +++ b/backend/src/ee/services/ProjectRoleService.ts @@ -18,7 +18,7 @@ import { Membership, ServiceTokenData } from "../../models"; -import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables"; +import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../../variables"; import { checkIPAgainstBlocklist } from "../../utils/ip"; import { BadRequestError } from "../../utils/errors"; @@ -262,6 +262,13 @@ const buildViewerPermission = () => { export const viewerProjectPermission = buildViewerPermission(); +const buildNoAccessProjectPermission = () => { + const { build } = new AbilityBuilder>(createMongoAbility); + return build({ conditionsMatcher }); +} + +export const noAccessProjectPermissions = buildNoAccessProjectPermission(); + /** * Return permissions for user/service pertaining to workspace with id [workspaceId] * @@ -275,7 +282,7 @@ export const getAuthDataProjectPermissions = async ({ authData: AuthData; workspaceId: Types.ObjectId; }) => { - let role: "admin" | "member" | "viewer" | "custom"; + let role: "admin" | "member" | "viewer" | "no-access" | "custom"; let customRole; switch (authData.actor.type) { @@ -338,6 +345,8 @@ export const getAuthDataProjectPermissions = async ({ return { permission: memberProjectPermissions }; case VIEWER: return { permission: viewerProjectPermission }; + case NO_ACCESS: + return { permission: noAccessProjectPermissions }; case CUSTOM: { if (!customRole) throw UnauthorizedRequestError(); return { @@ -353,7 +362,7 @@ export const getAuthDataProjectPermissions = async ({ } export const getRolePermissions = async (role: string, workspaceId: string) => { - const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role); + const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role); if (isCustomRole) { const workspaceRole = await Role.findOne({ slug: role, @@ -375,6 +384,8 @@ export const getRolePermissions = async (role: string, workspaceId: string) => { return memberProjectPermissions; case VIEWER: return viewerProjectPermission; + case NO_ACCESS: + return noAccessProjectPermissions; default: throw BadRequestError({ message: "Role not found" }); } diff --git a/backend/src/ee/services/RoleService.ts b/backend/src/ee/services/RoleService.ts index a1e191330..018520915 100644 --- a/backend/src/ee/services/RoleService.ts +++ b/backend/src/ee/services/RoleService.ts @@ -3,7 +3,7 @@ import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@ca import { MembershipOrg } from "../../models"; import { IRole, Role } from "../models"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; -import { ACCEPTED, ADMIN, CUSTOM, MEMBER } from "../../variables"; +import { ACCEPTED, ADMIN, CUSTOM, MEMBER, NO_ACCESS} from "../../variables"; import { conditionsMatcher } from "./ProjectRoleService"; export enum OrgPermissionActions { @@ -116,6 +116,13 @@ const buildMemberPermission = () => { export const memberPermissions = buildMemberPermission(); +const buildNoAccessPermission = () => { + const { build } = new AbilityBuilder>(createMongoAbility); + return build({ conditionsMatcher }); +} + +export const noAccessPermissions = buildNoAccessPermission(); + export const getUserOrgPermissions = async (userId: string, orgId: string) => { // TODO(akhilmhdh): speed this up by pulling from cache later @@ -136,6 +143,8 @@ export const getUserOrgPermissions = async (userId: string, orgId: string) => { if (membership.role === ADMIN) return { permission: adminPermissions, membership }; if (membership.role === MEMBER) return { permission: memberPermissions, membership }; + + if (membership.role === NO_ACCESS) return { permission: noAccessPermissions, membership } if (membership.role === CUSTOM) { const permission = createMongoAbility(membership.customRole.permissions, { @@ -148,7 +157,7 @@ export const getUserOrgPermissions = async (userId: string, orgId: string) => { }; export const getOrgRolePermissions = async (role: string, orgId: string) => { - const isCustomRole = ![ADMIN, MEMBER].includes(role); + const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role); if (isCustomRole) { const orgRole = await Role.findOne({ slug: role, @@ -168,6 +177,8 @@ export const getOrgRolePermissions = async (role: string, orgId: string) => { return adminPermissions; case MEMBER: return memberPermissions; + case NO_ACCESS: + return noAccessPermissions; default: throw BadRequestError({ message: "User org role not found" }); } diff --git a/backend/src/helpers/membership.ts b/backend/src/helpers/membership.ts index 7700c21ee..d08d07a45 100644 --- a/backend/src/helpers/membership.ts +++ b/backend/src/helpers/membership.ts @@ -17,7 +17,7 @@ export const validateMembership = async ({ }: { userId: Types.ObjectId | string; workspaceId: Types.ObjectId | string; - acceptedRoles?: Array<"admin" | "member" | "custom" | "viewer">; + acceptedRoles?: Array<"admin" | "member" | "custom" | "viewer" | "no-access">; }) => { const membership = await Membership.findOne({ user: userId, diff --git a/backend/src/helpers/membershipOrg.ts b/backend/src/helpers/membershipOrg.ts index 46f5fbf56..9f2e8d93c 100644 --- a/backend/src/helpers/membershipOrg.ts +++ b/backend/src/helpers/membershipOrg.ts @@ -18,7 +18,7 @@ export const validateMembershipOrg = async ({ }: { userId: Types.ObjectId; organizationId: Types.ObjectId; - acceptedRoles?: Array<"owner" | "admin" | "member" | "custom">; + acceptedRoles?: Array<"owner" | "admin" | "member" | "custom" | "no-access">; acceptedStatuses?: Array<"invited" | "accepted">; }) => { const membershipOrg = await MembershipOrg.findOne({ diff --git a/backend/src/models/machineMembership.ts b/backend/src/models/machineMembership.ts index 95dcb749d..ab0859e8d 100644 --- a/backend/src/models/machineMembership.ts +++ b/backend/src/models/machineMembership.ts @@ -1,11 +1,11 @@ import { Schema, Types, model } from "mongoose"; -import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../variables"; +import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../variables"; export interface IMachineMembership { _id: Types.ObjectId; machineIdentity: Types.ObjectId; workspace: Types.ObjectId; - role: "admin" | "member" | "viewer" | "custom"; + role: "admin" | "member" | "viewer" | "no-access" | "custom"; customRole: Types.ObjectId; } @@ -22,7 +22,7 @@ const machineMembershipSchema = new Schema( }, role: { type: String, - enum: [ADMIN, MEMBER, VIEWER, CUSTOM], + enum: [ADMIN, MEMBER, VIEWER, CUSTOM, NO_ACCESS], required: true }, customRole: { diff --git a/backend/src/models/machineMembershipOrg.ts b/backend/src/models/machineMembershipOrg.ts index ae455e6c8..5be4a3e53 100644 --- a/backend/src/models/machineMembershipOrg.ts +++ b/backend/src/models/machineMembershipOrg.ts @@ -1,11 +1,11 @@ import { Schema, Types, model } from "mongoose"; -import { ADMIN, CUSTOM, MEMBER } from "../variables"; +import { ADMIN, CUSTOM, MEMBER, NO_ACCESS} from "../variables"; export interface IMachineMembershipOrg { _id: Types.ObjectId; machineIdentity: Types.ObjectId; organization: Types.ObjectId; - role: "admin" | "member" | "viewer" | "custom"; + role: "admin" | "member" | "viewer" | "no-access" | "custom"; customRole: Types.ObjectId; } @@ -21,7 +21,7 @@ const machineMembershipOrgSchema = new Schema( }, role: { type: String, - enum: [ADMIN, MEMBER, CUSTOM], + enum: [ADMIN, MEMBER, NO_ACCESS, CUSTOM], required: true }, customRole: { diff --git a/backend/src/models/membership.ts b/backend/src/models/membership.ts index 22a3819e2..c09fa2779 100644 --- a/backend/src/models/membership.ts +++ b/backend/src/models/membership.ts @@ -1,5 +1,5 @@ import { Schema, Types, model } from "mongoose"; -import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../variables"; +import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, 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" | "viewer" | "custom"; + role: "admin" | "member" | "viewer" | "no-access" | "custom"; customRole: Types.ObjectId; deniedPermissions: IMembershipPermission[]; } @@ -44,7 +44,7 @@ const membershipSchema = new Schema( }, role: { type: String, - enum: [ADMIN, MEMBER, VIEWER, CUSTOM], + enum: [ADMIN, MEMBER, VIEWER, NO_ACCESS, CUSTOM], required: true }, customRole: { diff --git a/backend/src/models/membershipOrg.ts b/backend/src/models/membershipOrg.ts index 09b16be84..d83b58e02 100644 --- a/backend/src/models/membershipOrg.ts +++ b/backend/src/models/membershipOrg.ts @@ -1,12 +1,12 @@ import { Document, Schema, Types, model } from "mongoose"; -import { ACCEPTED, ADMIN, CUSTOM, INVITED, MEMBER } from "../variables"; +import { ACCEPTED, ADMIN, CUSTOM, INVITED, MEMBER, NO_ACCESS } from "../variables"; export interface IMembershipOrg extends Document { _id: Types.ObjectId; user: Types.ObjectId; inviteEmail: string; organization: Types.ObjectId; - role: "owner" | "admin" | "member" | "custom"; + role: "owner" | "admin" | "member" | "no-access" | "custom"; customRole: Types.ObjectId; status: "invited" | "accepted"; } @@ -26,7 +26,7 @@ const membershipOrgSchema = new Schema( }, role: { type: String, - enum: [ADMIN, MEMBER, CUSTOM], + enum: [ADMIN, MEMBER, NO_ACCESS, CUSTOM], required: true }, status: { diff --git a/backend/src/variables/organization.ts b/backend/src/variables/organization.ts index 5e796e357..c8eb52863 100644 --- a/backend/src/variables/organization.ts +++ b/backend/src/variables/organization.ts @@ -3,6 +3,7 @@ export const OWNER = "owner"; // depreciated export const ADMIN = "admin"; export const MEMBER = "member"; export const VIEWER = "viewer"; +export const NO_ACCESS = "no-access"; export const CUSTOM = "custom"; // membership statuses diff --git a/frontend/src/hooks/api/machineIdentities/types.ts b/frontend/src/hooks/api/machineIdentities/types.ts index 64a610380..829c3745e 100644 --- a/frontend/src/hooks/api/machineIdentities/types.ts +++ b/frontend/src/hooks/api/machineIdentities/types.ts @@ -28,7 +28,7 @@ export type MachineMembershipOrg = { _id: string; machineIdentity: MachineIdentity; organization: string; - role: "admin" | "member" | "viewer" | "custom"; + role: "admin" | "member" | "viewer" | "no-access" | "custom"; customRole?: TRole; createdAt: string; updatedAt: string; @@ -38,7 +38,7 @@ export type MachineMembership = { _id: string; machineIdentity: MachineIdentity; organization: string; - role: "admin" | "member" | "viewer" | "custom"; + role: "admin" | "member" | "viewer" | "no-access" | "custom"; customRole?: TRole; createdAt: string; updatedAt: string; diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index 3b57e8953..31169a3fc 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -44,7 +44,7 @@ export type OrgUser = { }; inviteEmail: string; organization: string; - role: "owner" | "admin" | "member" | "custom"; + role: "owner" | "admin" | "member" | "no-access" | "custom"; status: "invited" | "accepted" | "verified" | "completed"; deniedPermissions: any[]; customRole: string; diff --git a/frontend/src/views/Org/MembersPage/MembersPage.tsx b/frontend/src/views/Org/MembersPage/MembersPage.tsx index 3b85e2cd4..a1b1ed1a5 100644 --- a/frontend/src/views/Org/MembersPage/MembersPage.tsx +++ b/frontend/src/views/Org/MembersPage/MembersPage.tsx @@ -25,7 +25,14 @@ export const MembersPage = withPermission( People - Machine Identities + +
+

Machine Identities

+
+ Beta +
+
+
Organization Roles
diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx index 6122309b4..3a4bc3dfd 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable.tsx @@ -94,7 +94,7 @@ export const OrgRoleTable = ({ {isRolesLoading && } {(roles as TRole[])?.map((role) => { const { _id: id, name, slug } = role; - const isNonMutatable = ["owner", "admin", "member"].includes(slug); + const isNonMutatable = ["owner", "admin", "member", "no-access"].includes(slug); return ( diff --git a/frontend/src/views/Project/MembersPage/MembersPage.tsx b/frontend/src/views/Project/MembersPage/MembersPage.tsx index 521b7a07e..f8769626c 100644 --- a/frontend/src/views/Project/MembersPage/MembersPage.tsx +++ b/frontend/src/views/Project/MembersPage/MembersPage.tsx @@ -30,7 +30,14 @@ export const MembersPage = withProjectPermission( People - Machine Identities + +
+

Machine Identities

+
+ Beta +
+
+
Service Tokens Project Roles
diff --git a/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx b/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx index 0bddc7f21..62523e56b 100644 --- a/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx +++ b/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx @@ -188,7 +188,7 @@ export const MachineIdentityTable = ({ {!isLoading && data && data?.length === 0 && ( - + )} diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx index 402262f7a..7f432c192 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleList/ProjectRoleList.tsx @@ -101,7 +101,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => { {isRolesLoading && } {(roles as TRole[])?.map((role) => { const { _id: id, name, slug } = role; - const isNonMutatable = ["admin", "member", "viewer"].includes(slug); + const isNonMutatable = ["admin", "member", "viewer", "no-access"].includes(slug); return (