From 5b923c25b5df933927307153d7ea070fd6f5695e Mon Sep 17 00:00:00 2001
From: Tuan Dang
Date: Sat, 25 Nov 2023 18:37:20 +0700
Subject: [PATCH] Added authz logic to MI
---
.../controllers/v2/organizationsController.ts | 7 +
.../src/controllers/v2/workspaceController.ts | 36 +++--
.../v3/machineIdentityController.ts | 126 +++++++++---------
backend/src/ee/models/auditLog/types.ts | 1 -
backend/src/ee/services/ProjectRoleService.ts | 74 +++++++++-
backend/src/ee/services/RoleService.ts | 87 +++++++++---
backend/src/validation/machineIdentity.ts | 3 +-
.../src/context/OrgPermissionContext/types.ts | 4 +-
.../context/ProjectPermissionContext/types.ts | 4 +-
.../src/views/Org/MembersPage/MembersPage.tsx | 2 +-
.../AddMachineIdentityModal.tsx | 18 +--
.../MachineIdentitySection.tsx | 4 +-
.../MachineIdentityTable.tsx | 22 +--
.../OrgRoleModifySection.tsx | 10 +-
.../OrgRoleModifySection.utils.ts | 3 +-
.../views/Project/MembersPage/MembersPage.tsx | 2 +-
.../AddMachineIdentityModal.tsx | 6 +-
.../MachineIdentitySection.tsx | 4 +-
.../MachineIdentityTable.tsx | 6 +-
19 files changed, 286 insertions(+), 133 deletions(-)
diff --git a/backend/src/controllers/v2/organizationsController.ts b/backend/src/controllers/v2/organizationsController.ts
index 4c6a72823..0ff16d00e 100644
--- a/backend/src/controllers/v2/organizationsController.ts
+++ b/backend/src/controllers/v2/organizationsController.ts
@@ -393,6 +393,13 @@ export const getOrganizationMachineMemberships = async (req: Request, res: Respo
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgServiceMembersV2, req);
+
+ const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
+
+ ForbiddenError.from(permission).throwUnlessCan(
+ OrgPermissionActions.Read,
+ OrgPermissionSubjects.MachineIdentity
+ );
const machineMemberships = await MachineMembershipOrg.find({
organization: new Types.ObjectId(organizationId)
diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts
index 6099645ed..55f0b1617 100644
--- a/backend/src/controllers/v2/workspaceController.ts
+++ b/backend/src/controllers/v2/workspaceController.ts
@@ -24,10 +24,12 @@ import * as reqValidator from "../../validation";
import {
ProjectPermissionActions,
ProjectPermissionSub,
- getAuthDataProjectPermissions
+ getAuthDataProjectPermissions,
+ getRolePermissions,
+ isAtLeastAsPrivilegedWorkspace
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
-import { BadRequestError, ResourceNotFoundError } from "../../utils/errors";
+import { BadRequestError, ForbiddenRequestError, ResourceNotFoundError } from "../../utils/errors";
import { ADMIN, MEMBER, VIEWER } from "../../variables";
interface V2PushSecret {
@@ -523,7 +525,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
- ProjectPermissionSub.ServiceTokens
+ ProjectPermissionSub.MachineIdentity
);
let machineMembership = await MachineMembership.findOne({
@@ -532,7 +534,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
});
if (machineMembership) throw BadRequestError({
- message: "Machine identity already exists in workspace"
+ message: `Machine identity with id ${machineId} already exists in workspace with id ${workspaceId}`
});
const machineIdentity = await MachineIdentity.findById(machineId);
@@ -545,6 +547,13 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
message: "Failed to add machine identity to workspace in another organization"
});
+ const rolePermission = await getRolePermissions(role, workspaceId);
+ const hasRequiredPrivileges = isAtLeastAsPrivilegedWorkspace(permission, rolePermission);
+
+ if (!hasRequiredPrivileges) throw ForbiddenRequestError({
+ message: "Failed to add a more privileged MI to workspace"
+ });
+
let customRole;
if (role) {
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
@@ -591,8 +600,8 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
});
ForbiddenError.from(permission).throwUnlessCan(
- ProjectPermissionActions.Create,
- ProjectPermissionSub.ServiceTokens
+ ProjectPermissionActions.Edit,
+ ProjectPermissionSub.MachineIdentity
);
let machineMembership = await MachineMembership.findOne({
@@ -601,7 +610,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
});
if (!machineMembership) throw BadRequestError({
- message: "Machine identity does not exist in workspace"
+ message: `Machine identity with id ${machineId} does not exist in workspace with id ${workspaceId}`
});
const machineIdentity = await MachineIdentity.findById(machineId);
@@ -611,7 +620,14 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
if (!workspace) throw ResourceNotFoundError();
if (!machineIdentity.organization.equals(workspace.organization)) throw BadRequestError({
- message: "Failed to add machine identity to workspace in another organization"
+ message: "Failed to update machine identity in workspace in another organization"
+ });
+
+ const rolePermission = await getRolePermissions(role, workspaceId);
+ const hasRequiredPrivileges = isAtLeastAsPrivilegedWorkspace(permission, rolePermission);
+
+ if (!hasRequiredPrivileges) throw ForbiddenRequestError({
+ message: "Failed to update MI to a more privileged role"
});
let customRole;
@@ -665,7 +681,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
- ProjectPermissionSub.ServiceTokens
+ ProjectPermissionSub.MachineIdentity
);
const machineMembership = await MachineMembership.findOneAndDelete({
@@ -698,7 +714,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
- ProjectPermissionSub.ServiceTokens
+ ProjectPermissionSub.MachineIdentity
);
const machineMemberships = await MachineMembership.find({
diff --git a/backend/src/ee/controllers/v3/machineIdentityController.ts b/backend/src/ee/controllers/v3/machineIdentityController.ts
index 96292962c..be26ea7b0 100644
--- a/backend/src/ee/controllers/v3/machineIdentityController.ts
+++ b/backend/src/ee/controllers/v3/machineIdentityController.ts
@@ -16,13 +16,21 @@ import {
import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../../validation/machineIdentity";
import { createToken } from "../../../helpers/auth";
-
-
-import { BadRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../../utils/errors";
+import {
+ getOrgRolePermissions,
+ getUserOrgPermissions,
+ isAtLeastAsPrivilegedOrg
+} from "../../services/RoleService";
+import { BadRequestError, ForbiddenRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../../utils/errors";
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
import { EEAuditLogService, EELicenseService } from "../../services";
import { getAuthSecret } from "../../../config";
import { ADMIN, AuthTokenType, CUSTOM, MEMBER } from "../../../variables";
+import {
+ OrgPermissionActions,
+ OrgPermissionSubjects
+} from "../../services/RoleService";
+import { ForbiddenError } from "@casl/ability";
/**
* Return machine identity access and refresh token as per refresh operation
@@ -32,12 +40,12 @@ import { ADMIN, AuthTokenType, CUSTOM, MEMBER } from "../../../variables";
export const refreshToken = async (req: Request, res: Response) => {
const {
body: {
- refresh_token
+ refreshToken
}
} = await validateRequest(reqValidator.RefreshTokenV3, req);
const decodedToken = (
- jwt.verify(refresh_token, await getAuthSecret())
+ jwt.verify(refreshToken, await getAuthSecret())
);
if (decodedToken.authTokenType !== AuthTokenType.SERVICE_REFRESH_TOKEN) throw UnauthorizedRequestError();
@@ -55,15 +63,15 @@ import { ADMIN, AuthTokenType, CUSTOM, MEMBER } from "../../../variables";
}
const response: {
- refresh_token?: string;
- access_token: string;
- expires_in: number;
- token_type: string;
+ refreshToken?: string;
+ accessToken: string;
+ expiresIn: number;
+ tokenType: string;
} = {
- refresh_token,
- access_token: "",
- expires_in: 0,
- token_type: "Bearer"
+ refreshToken,
+ accessToken: "",
+ expiresIn: 0,
+ tokenType: "Bearer"
};
if (machineIdentity.isRefreshTokenRotationEnabled) {
@@ -81,7 +89,7 @@ import { ADMIN, AuthTokenType, CUSTOM, MEMBER } from "../../../variables";
if (!machineIdentity) throw BadRequestError();
- response.refresh_token = createToken({
+ response.refreshToken = createToken({
payload: {
serviceTokenDataId: machineIdentity._id.toString(),
authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN,
@@ -91,9 +99,9 @@ import { ADMIN, AuthTokenType, CUSTOM, MEMBER } from "../../../variables";
});
}
- response.access_token = createToken({
+ response.accessToken = createToken({
payload: {
- serviceTokenDataId: machineIdentity._id.toString(),
+ serviceTokenDataId: machineIdentity._id.toString(), // TODO: fix this
authTokenType: AuthTokenType.SERVICE_ACCESS_TOKEN,
tokenVersion: machineIdentity.tokenVersion
},
@@ -101,7 +109,7 @@ import { ADMIN, AuthTokenType, CUSTOM, MEMBER } from "../../../variables";
secret: await getAuthSecret()
});
- response.expires_in = machineIdentity.accessTokenTTL;
+ response.expiresIn = machineIdentity.accessTokenTTL;
await MachineIdentity.findByIdAndUpdate(
machineIdentity._id,
@@ -135,22 +143,23 @@ export const createMachineIdentity = async (req: Request, res: Response) => {
isRefreshTokenRotationEnabled
}
} = await validateRequest(reqValidator.CreateMachineIdentityV3, req);
-
- // const { permission } = await getAuthDataProjectPermissions({
- // authData: req.authData,
- // workspaceId: new Types.ObjectId(workspaceId)
- // });
-
- // ForbiddenError.from(permission).throwUnlessCan(
- // ProjectPermissionActions.Create,
- // ProjectPermissionSub.ServiceTokens
- // );
- // const workspace = await Workspace.findById(workspaceId);
- // if (!workspace) throw BadRequestError({ message: "Workspace not found" });
+ const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
+
+ ForbiddenError.from(permission).throwUnlessCan(
+ OrgPermissionActions.Create,
+ OrgPermissionSubjects.MachineIdentity
+ );
+
+ const rolePermission = await getOrgRolePermissions(role, organizationId);
+ const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission);
+
+ if (!hasRequiredPrivileges) throw ForbiddenRequestError({
+ message: "Failed to create a more privileged MI"
+ });
const organization = await Organization.findById(organizationId);
- if (!organization) throw BadRequestError({ message: "Organization not found" });
+ if (!organization) throw BadRequestError({ message: `Organization with id ${organizationId} not found` });
const isCustomRole = ![ADMIN, MEMBER].includes(role);
@@ -217,7 +226,7 @@ export const createMachineIdentity = async (req: Request, res: Response) => {
const refreshToken = createToken({
payload: {
- serviceTokenDataId: machineIdentity._id.toString(),
+ serviceTokenDataId: machineIdentity._id.toString(), // TODO: update
authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN,
tokenVersion: machineIdentity.tokenVersion
},
@@ -248,7 +257,7 @@ export const createMachineIdentity = async (req: Request, res: Response) => {
}
/**
- * Update service token V3 data with id [serviceTokenDataId]
+ * Update machine identity with id [machineId]
* @param req
* @param res
* @returns
@@ -258,7 +267,6 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
params: { machineId },
body: {
name,
- isActive,
role,
trustedIps,
expiresIn,
@@ -269,21 +277,24 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
let machineIdentity = await MachineIdentity.findById(machineId);
if (!machineIdentity) throw ResourceNotFoundError({
- message: "Service token not found"
+ message: `Machine identity with id ${machineId} not found`
});
-
- // const { permission } = await getAuthDataProjectPermissions({
- // authData: req.authData,
- // workspaceId: serviceTokenData.workspace
- // });
-
- // ForbiddenError.from(permission).throwUnlessCan(
- // ProjectPermissionActions.Edit,
- // ProjectPermissionSub.ServiceTokens
- // );
- // const workspace = await Workspace.findById(serviceTokenData.workspace);
- // if (!workspace) throw BadRequestError({ message: "Workspace not found" });
+ const { permission } = await getUserOrgPermissions(req.user._id, machineIdentity.organization.toString());
+
+ ForbiddenError.from(permission).throwUnlessCan(
+ OrgPermissionActions.Edit,
+ OrgPermissionSubjects.MachineIdentity
+ );
+
+ if (role) {
+ const rolePermission = await getOrgRolePermissions(role, machineIdentity.organization.toString());
+ const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission);
+
+ if (!hasRequiredPrivileges) throw ForbiddenRequestError({
+ message: "Failed to update MI to a more privileged role"
+ });
+ }
let customRole;
if (role) {
@@ -329,7 +340,6 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
machineId,
{
name,
- isActive,
trustedIps: reformattedTrustedIps,
expiresAt,
accessTokenTTL,
@@ -341,7 +351,7 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
);
if (!machineIdentity) throw BadRequestError({
- message: "Failed to update service token"
+ message: `Failed to update machine identity with id ${machineId}`
});
await MachineMembershipOrg.findOneAndUpdate(
@@ -370,7 +380,6 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
type: EventType.UPDATE_MACHINE_IDENTITY,
metadata: {
name: machineIdentity.name,
- isActive,
role,
trustedIps: reformattedTrustedIps as Array,
expiresAt
@@ -387,7 +396,7 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
}
/**
- * Delete service token data with id [serviceTokenDataId]
+ * Delete machine identity with id [machineId]
* @param req
* @param res
* @returns
@@ -399,18 +408,15 @@ export const deleteMachineIdentity = async (req: Request, res: Response) => {
let machineIdentity = await MachineIdentity.findById(machineId);
if (!machineIdentity) throw ResourceNotFoundError({
- message: "Service token not found"
+ message: `Machine identity with id ${machineId} not found`
});
+
+ const { permission } = await getUserOrgPermissions(req.user._id, machineIdentity.organization.toString());
- // const { permission } = await getAuthDataProjectPermissions({
- // authData: req.authData,
- // workspaceId: serviceTokenData.workspace
- // });
-
- // ForbiddenError.from(permission).throwUnlessCan(
- // ProjectPermissionActions.Delete,
- // ProjectPermissionSub.ServiceTokens
- // );
+ ForbiddenError.from(permission).throwUnlessCan(
+ OrgPermissionActions.Delete,
+ OrgPermissionSubjects.MachineIdentity
+ );
machineIdentity = await MachineIdentity.findByIdAndDelete(machineId);
diff --git a/backend/src/ee/models/auditLog/types.ts b/backend/src/ee/models/auditLog/types.ts
index 859606901..bd79b5bec 100644
--- a/backend/src/ee/models/auditLog/types.ts
+++ b/backend/src/ee/models/auditLog/types.ts
@@ -235,7 +235,6 @@ interface UpdateMachineIdentityEvent {
type: EventType.UPDATE_MACHINE_IDENTITY;
metadata: {
name?: string;
- isActive?: boolean;
role?: string;
trustedIps?: Array;
expiresAt?: Date;
diff --git a/backend/src/ee/services/ProjectRoleService.ts b/backend/src/ee/services/ProjectRoleService.ts
index d4f25394a..f20114b87 100644
--- a/backend/src/ee/services/ProjectRoleService.ts
+++ b/backend/src/ee/services/ProjectRoleService.ts
@@ -11,7 +11,7 @@ import { UnauthorizedRequestError } from "../../utils/errors";
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
import picomatch from "picomatch";
import { AuthData } from "../../interfaces/middleware";
-import { ActorType, IRole } from "../models";
+import { ActorType, IRole, Role } from "../models";
import {
IMachineIdentity,
MachineMembership,
@@ -20,6 +20,7 @@ import {
} from "../../models";
import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables";
import { checkIPAgainstBlocklist } from "../../utils/ip";
+import { BadRequestError } from "../../utils/errors";
const $glob: FieldInstruction = {
type: "field",
@@ -60,7 +61,8 @@ export enum ProjectPermissionSub {
Secrets = "secrets",
SecretRollback = "secret-rollback",
SecretApproval = "secret-approval",
- SecretRotation = "secret-rotation"
+ SecretRotation = "secret-rotation",
+ MachineIdentity = "machine-identity"
}
type SubjectFields = {
@@ -85,6 +87,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
+ | [ProjectPermissionActions, ProjectPermissionSub.MachineIdentity]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
@@ -131,6 +134,11 @@ const buildAdminPermission = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
+ can(ProjectPermissionActions.Read, ProjectPermissionSub.MachineIdentity);
+ can(ProjectPermissionActions.Create, ProjectPermissionSub.MachineIdentity);
+ can(ProjectPermissionActions.Edit, ProjectPermissionSub.MachineIdentity);
+ can(ProjectPermissionActions.Delete, ProjectPermissionSub.MachineIdentity);
+
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
@@ -196,6 +204,11 @@ const buildMemberPermission = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
+ can(ProjectPermissionActions.Read, ProjectPermissionSub.MachineIdentity);
+ can(ProjectPermissionActions.Create, ProjectPermissionSub.MachineIdentity);
+ can(ProjectPermissionActions.Edit, ProjectPermissionSub.MachineIdentity);
+ can(ProjectPermissionActions.Delete, ProjectPermissionSub.MachineIdentity);
+
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
@@ -236,6 +249,7 @@ const buildViewerPermission = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
+ can(ProjectPermissionActions.Read, ProjectPermissionSub.MachineIdentity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
@@ -337,3 +351,59 @@ export const getAuthDataProjectPermissions = async ({
throw UnauthorizedRequestError();
}
}
+
+export const getRolePermissions = async (role: string, workspaceId: string) => {
+ const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
+ if (isCustomRole) {
+ const workspaceRole = await Role.findOne({
+ slug: role,
+ isOrgRole: false,
+ workspace: new Types.ObjectId(workspaceId)
+ });
+
+ if (!workspaceRole) throw BadRequestError({ message: "Role not found" });
+
+ return createMongoAbility(workspaceRole.permissions as RawRuleOf>[], {
+ conditionsMatcher
+ });
+ }
+
+ switch (role) {
+ case ADMIN:
+ return adminProjectPermissions;
+ case MEMBER:
+ return memberProjectPermissions;
+ case VIEWER:
+ return viewerProjectPermission;
+ default:
+ throw BadRequestError({ message: "Role not found" });
+ }
+}
+
+/**
+ * Extracts and formats permissions from a CASL Ability object or a raw permission set.
+ * @param ability
+ * @returns
+ */
+ const extractPermissions = (ability: MongoAbility | ProjectPermissionSet) => {
+ return ability.A.map((permission: any) => `${permission.action}_${permission.subject}`);
+}
+
+/**
+ * Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
+ * The function checks if all permissions in the second set are contained within the first set and if the first set has equal or more permissions.
+ *
+*/
+export const isAtLeastAsPrivilegedWorkspace = (permissions1: MongoAbility | ProjectPermissionSet, permissions2: MongoAbility | ProjectPermissionSet) => {
+
+ const set1 = new Set(extractPermissions(permissions1));
+ const set2 = new Set(extractPermissions(permissions2));
+
+ for (const perm of set2) {
+ if (!set1.has(perm)) {
+ return false;
+ }
+ }
+
+ return set1.size >= set2.size;
+}
\ No newline at end of file
diff --git a/backend/src/ee/services/RoleService.ts b/backend/src/ee/services/RoleService.ts
index 8c14d9d57..f4d65cb75 100644
--- a/backend/src/ee/services/RoleService.ts
+++ b/backend/src/ee/services/RoleService.ts
@@ -1,8 +1,9 @@
+import { Types } from "mongoose";
import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@casl/ability";
import { MembershipOrg } from "../../models";
-import { IRole } from "../models";
+import { IRole, Role } from "../models";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
-import { ACCEPTED } from "../../variables";
+import { ACCEPTED, ADMIN, CUSTOM, MEMBER } from "../../variables";
import { conditionsMatcher } from "./ProjectRoleService";
export enum OrgPermissionActions {
@@ -21,7 +22,7 @@ export enum OrgPermissionSubjects {
Sso = "sso",
Billing = "billing",
SecretScanning = "secret-scanning",
- ServiceTokens = "service-tokens" // TODO: consider renaming
+ MachineIdentity = "machine-identity"
}
export type OrgPermissionSet =
@@ -34,7 +35,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
- | [OrgPermissionActions, OrgPermissionSubjects.ServiceTokens];
+ | [OrgPermissionActions, OrgPermissionSubjects.MachineIdentity];
const buildAdminPermission = () => {
const { can, build } = new AbilityBuilder>(createMongoAbility);
@@ -77,10 +78,10 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing);
- can(OrgPermissionActions.Read, OrgPermissionSubjects.ServiceTokens);
- can(OrgPermissionActions.Create, OrgPermissionSubjects.ServiceTokens);
- can(OrgPermissionActions.Edit, OrgPermissionSubjects.ServiceTokens);
- can(OrgPermissionActions.Delete, OrgPermissionSubjects.ServiceTokens);
+ can(OrgPermissionActions.Read, OrgPermissionSubjects.MachineIdentity);
+ can(OrgPermissionActions.Create, OrgPermissionSubjects.MachineIdentity);
+ can(OrgPermissionActions.Edit, OrgPermissionSubjects.MachineIdentity);
+ can(OrgPermissionActions.Delete, OrgPermissionSubjects.MachineIdentity);
return build({ conditionsMatcher });
};
@@ -105,10 +106,10 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
- can(OrgPermissionActions.Read, OrgPermissionSubjects.ServiceTokens);
- can(OrgPermissionActions.Create, OrgPermissionSubjects.ServiceTokens);
- can(OrgPermissionActions.Edit, OrgPermissionSubjects.ServiceTokens);
- can(OrgPermissionActions.Delete, OrgPermissionSubjects.ServiceTokens);
+ can(OrgPermissionActions.Read, OrgPermissionSubjects.MachineIdentity);
+ can(OrgPermissionActions.Create, OrgPermissionSubjects.MachineIdentity);
+ can(OrgPermissionActions.Edit, OrgPermissionSubjects.MachineIdentity);
+ can(OrgPermissionActions.Delete, OrgPermissionSubjects.MachineIdentity);
return build({ conditionsMatcher });
};
@@ -132,11 +133,11 @@ export const getUserOrgPermissions = async (userId: string, orgId: string) => {
throw UnauthorizedRequestError({ message: "User doesn't belong to organization" });
}
- if (membership.role === "admin") return { permission: adminPermissions, membership };
+ if (membership.role === ADMIN) return { permission: adminPermissions, membership };
- if (membership.role === "member") return { permission: memberPermissions, membership };
+ if (membership.role === MEMBER) return { permission: memberPermissions, membership };
- if (membership.role === "custom") {
+ if (membership.role === CUSTOM) {
const permission = createMongoAbility(membership.customRole.permissions, {
conditionsMatcher
});
@@ -144,4 +145,58 @@ export const getUserOrgPermissions = async (userId: string, orgId: string) => {
}
throw BadRequestError({ message: "User role not found" });
-};
\ No newline at end of file
+};
+
+export const getOrgRolePermissions = async (role: string, orgId: string) => {
+ const isCustomRole = ![ADMIN, MEMBER].includes(role);
+ if (isCustomRole) {
+ const orgRole = await Role.findOne({
+ slug: role,
+ isOrgRole: true,
+ organization: new Types.ObjectId(orgId)
+ });
+
+ if (!orgRole) throw BadRequestError({ message: "Org Role not found" });
+
+ return createMongoAbility(orgRole.permissions as RawRuleOf>[], {
+ conditionsMatcher
+ });
+ }
+
+ switch (role) {
+ case ADMIN:
+ return adminPermissions;
+ case MEMBER:
+ return memberPermissions;
+ default:
+ throw BadRequestError({ message: "User org role not found" });
+ }
+}
+
+/**
+ * Extracts and formats permissions from a CASL Ability object or a raw permission set.
+ * @param ability
+ * @returns
+ */
+const extractPermissions = (ability: MongoAbility | OrgPermissionSet) => {
+ return ability.A.map((permission: any) => `${permission.action}_${permission.subject}`);
+}
+
+/**
+ * Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
+ * The function checks if all permissions in the second set are contained within the first set and if the first set has equal or more permissions.
+ *
+*/
+export const isAtLeastAsPrivilegedOrg = (permissions1: MongoAbility | OrgPermissionSet, permissions2: MongoAbility | OrgPermissionSet) => {
+
+ const set1 = new Set(extractPermissions(permissions1));
+ const set2 = new Set(extractPermissions(permissions2));
+
+ for (const perm of set2) {
+ if (!set1.has(perm)) {
+ return false;
+ }
+ }
+
+ return set1.size >= set2.size;
+}
\ No newline at end of file
diff --git a/backend/src/validation/machineIdentity.ts b/backend/src/validation/machineIdentity.ts
index 6536f874e..32be61c99 100644
--- a/backend/src/validation/machineIdentity.ts
+++ b/backend/src/validation/machineIdentity.ts
@@ -3,7 +3,7 @@ import { MEMBER } from "../variables";
export const RefreshTokenV3 = z.object({
body: z.object({
- refresh_token: z.string().trim()
+ refreshToken: z.string().trim()
})
});
@@ -31,7 +31,6 @@ export const UpdateMachineIdentityV3 = z.object({
}),
body: z.object({
name: z.string().trim().optional(),
- isActive: z.boolean().optional(),
role: z.string().trim().min(1).optional(),
trustedIps: z
.object({
diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts
index 8f2c95fbf..65315cb16 100644
--- a/frontend/src/context/OrgPermissionContext/types.ts
+++ b/frontend/src/context/OrgPermissionContext/types.ts
@@ -16,7 +16,7 @@ export enum OrgPermissionSubjects {
Sso = "sso",
Billing = "billing",
SecretScanning = "secret-scanning",
- ServiceTokens = "service-tokens"
+ MachineIdentity = "machine-identity"
}
export type OrgPermissionSet =
@@ -29,6 +29,6 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
- | [OrgPermissionActions, OrgPermissionSubjects.ServiceTokens];
+ | [OrgPermissionActions, OrgPermissionSubjects.MachineIdentity];
export type TOrgPermission = MongoAbility;
diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts
index 5ae91e756..60d33afbd 100644
--- a/frontend/src/context/ProjectPermissionContext/types.ts
+++ b/frontend/src/context/ProjectPermissionContext/types.ts
@@ -22,7 +22,8 @@ export enum ProjectPermissionSub {
Secrets = "secrets",
SecretRollback = "secret-rollback",
SecretApproval = "secret-approval",
- SecretRotation = "secret-rotation"
+ SecretRotation = "secret-rotation",
+ MachineIdentity = "machine-identity"
}
type SubjectFields = {
@@ -44,6 +45,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.Environments]
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
+ | [ProjectPermissionActions, ProjectPermissionSub.MachineIdentity]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
diff --git a/frontend/src/views/Org/MembersPage/MembersPage.tsx b/frontend/src/views/Org/MembersPage/MembersPage.tsx
index b6338a96d..3b85e2cd4 100644
--- a/frontend/src/views/Org/MembersPage/MembersPage.tsx
+++ b/frontend/src/views/Org/MembersPage/MembersPage.tsx
@@ -26,7 +26,7 @@ export const MembersPage = withPermission(
People
Machine Identities
- Roles
+ Organization Roles
diff --git a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/AddMachineIdentityModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/AddMachineIdentityModal.tsx
index b76205f08..9439c6daf 100644
--- a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/AddMachineIdentityModal.tsx
+++ b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/AddMachineIdentityModal.tsx
@@ -32,7 +32,7 @@ import {
useGetRoles,
useUpdateMachineIdentity
} from "@app/hooks/api";
-import { ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types";
+import { MachineTrustedIp } from "@app/hooks/api/machineIdentities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
enum TabSections {
@@ -50,7 +50,7 @@ const expirations = [
];
const schema = yup.object({
- name: yup.string().required("ST V3 name is required"),
+ name: yup.string().required("MI name is required"),
expiresIn: yup.string(),
accessTokenTTL: yup
.string()
@@ -63,7 +63,7 @@ const schema = yup.object({
return !Number.isNaN(num) && num > 0 && String(num) === value;
})
.required("Access Token TTL is required"),
- role: yup.string().required("ST V3 role is required"),
+ role: yup.string(),
trustedIps: yup
.array(
yup.object({
@@ -146,7 +146,7 @@ export const AddMachineIdentityModal = ({
name: string;
slug: string;
};
- trustedIps: ServiceTokenV3TrustedIp[];
+ trustedIps: MachineTrustedIp[];
accessTokenTTL: number;
isRefreshTokenRotationEnabled: boolean;
};
@@ -161,7 +161,7 @@ export const AddMachineIdentityModal = ({
trustedIps: machineIdentity.trustedIps.map(({
ipAddress,
prefix
- }: ServiceTokenV3TrustedIp) => {
+ }: MachineTrustedIp) => {
return ({
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
});
@@ -206,9 +206,9 @@ export const AddMachineIdentityModal = ({
await updateMutateAsync({
machineId: machineIdentity.machineId,
name,
- role,
+ role: role || undefined,
trustedIps,
- expiresIn: expiresIn === "" ? undefined : Number(expiresIn),
+ expiresIn: (!expiresIn) ? undefined : Number(expiresIn),
accessTokenTTL: Number(accessTokenTTL),
isRefreshTokenRotationEnabled
});
@@ -218,10 +218,10 @@ export const AddMachineIdentityModal = ({
const { refreshToken } = await createMutateAsync({
name,
- role,
+ role: role || undefined,
organizationId: orgId,
trustedIps,
- expiresIn: expiresIn === "" ? undefined : Number(expiresIn),
+ expiresIn: (!expiresIn) ? undefined : Number(expiresIn),
accessTokenTTL: Number(accessTokenTTL),
isRefreshTokenRotationEnabled
});
diff --git a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentitySection.tsx b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentitySection.tsx
index 852e8d96a..7237d7746 100644
--- a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentitySection.tsx
+++ b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentitySection.tsx
@@ -53,7 +53,7 @@ export const MachineIdentitySection = withPermission(
{(isAllowed) => (