Added authz logic to MI

This commit is contained in:
Tuan Dang
2023-11-25 18:37:20 +07:00
parent 29016fbb23
commit 5b923c25b5
19 changed files with 286 additions and 133 deletions

View File

@@ -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)

View File

@@ -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({

View File

@@ -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.ServiceRefreshTokenJwtPayload>(
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<IMachineIdentityTrustedIp>,
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);

View File

@@ -235,7 +235,6 @@ interface UpdateMachineIdentityEvent {
type: EventType.UPDATE_MACHINE_IDENTITY;
metadata: {
name?: string;
isActive?: boolean;
role?: string;
trustedIps?: Array<IMachineIdentityTrustedIp>;
expiresAt?: Date;

View File

@@ -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<string> = {
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<ProjectPermissionSet>(workspaceRole.permissions as RawRuleOf<MongoAbility<ProjectPermissionSet>>[], {
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> | 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> | ProjectPermissionSet, permissions2: MongoAbility<ProjectPermissionSet> | 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;
}

View File

@@ -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<MongoAbility<OrgPermissionSet>>(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<OrgPermissionSet>(membership.customRole.permissions, {
conditionsMatcher
});
@@ -144,4 +145,58 @@ export const getUserOrgPermissions = async (userId: string, orgId: string) => {
}
throw BadRequestError({ message: "User role not found" });
};
};
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<OrgPermissionSet>(orgRole.permissions as RawRuleOf<MongoAbility<OrgPermissionSet>>[], {
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> | 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> | OrgPermissionSet, permissions2: MongoAbility<OrgPermissionSet> | 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;
}

View File

@@ -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({

View File

@@ -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<OrgPermissionSet>;

View File

@@ -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]

View File

@@ -26,7 +26,7 @@ export const MembersPage = withPermission(
<TabList>
<Tab value={TabSections.Member}>People</Tab>
<Tab value={TabSections.MachineIdentities}>Machine Identities</Tab>
<Tab value={TabSections.Roles}>Roles</Tab>
<Tab value={TabSections.Roles}>Organization Roles</Tab>
</TabList>
<TabPanel value={TabSections.Member}>
<OrgMembersTab />

View File

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

View File

@@ -53,7 +53,7 @@ export const MachineIdentitySection = withPermission(
</p>
<OrgPermissionCan
I={OrgPermissionActions.Create}
a={OrgPermissionSubjects.ServiceTokens}
a={OrgPermissionSubjects.MachineIdentity}
>
{(isAllowed) => (
<Button
@@ -92,5 +92,5 @@ export const MachineIdentitySection = withPermission(
</div>
);
},
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.ServiceTokens }
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.MachineIdentity }
);

View File

@@ -1,4 +1,3 @@
import { useCallback } from "react";
import { faPencil,faServer, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
@@ -28,7 +27,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";
type Props = {
@@ -42,7 +41,7 @@ type Props = {
name: string;
slug: string;
};
trustedIps?: ServiceTokenV3TrustedIp[];
trustedIps?: MachineTrustedIp[];
accessTokenTTL?: number;
isRefreshTokenRotationEnabled?: boolean;
}
@@ -115,13 +114,6 @@ export const MachineIdentityTable = ({
// });
// }
// }
const findRoleFromId = useCallback(
(roleId: string) => {
return (roles || []).find(({ _id: id }) => id === roleId);
},
[roles]
);
return (
<TableContainer>
@@ -163,7 +155,7 @@ export const MachineIdentityTable = ({
{/* <Td>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ServiceTokens}
a={OrgPermissionSubjects.MachineIdentity}
>
{(isAllowed) => (
<Switch
@@ -183,13 +175,13 @@ export const MachineIdentityTable = ({
<Td>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ServiceTokens}
a={OrgPermissionSubjects.MachineIdentity}
>
{(isAllowed) => {
return (
<Select
value={
role === "custom" ? findRoleFromId(customRole)?.slug : role
role === "custom" ? customRole.slug : role
}
isDisabled={!isAllowed}
className="w-40 bg-mineshaft-600"
@@ -231,7 +223,7 @@ export const MachineIdentityTable = ({
<Td className="flex justify-end">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ServiceTokens}
a={OrgPermissionSubjects.MachineIdentity}
>
{(isAllowed) => (
<IconButton
@@ -258,7 +250,7 @@ export const MachineIdentityTable = ({
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.ServiceTokens}
a={OrgPermissionSubjects.MachineIdentity}
>
{(isAllowed) => (
<IconButton

View File

@@ -5,10 +5,10 @@ import {
faContactCard,
faMagnifyingGlass,
faMoneyBill,
faServer,
faSignIn,
faUserCog,
faUsers
} from "@fortawesome/free-solid-svg-icons";
faUsers} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -40,6 +40,12 @@ const SIMPLE_PERMISSION_OPTIONS = [
icon: faUsers,
formName: "member"
},
{
title: "Machine identity management",
subtitle: "Create, view, update and remove machine identities from the organization",
icon: faServer,
formName: "machine-identity"
},
{
title: "Billing & usage",
subtitle: "Modify organization subscription plan",

View File

@@ -31,7 +31,8 @@ export const formSchema = z.object({
"incident-contact": generalPermissionSchema,
"secret-scanning": generalPermissionSchema,
sso: generalPermissionSchema,
billing: generalPermissionSchema
billing: generalPermissionSchema,
"machine-identity": generalPermissionSchema
})
.optional()
});

View File

@@ -32,7 +32,7 @@ export const MembersPage = withProjectPermission(
<Tab value={TabSections.Member}>People</Tab>
<Tab value={TabSections.MachineIdentities}>Machine Identities</Tab>
<Tab value={TabSections.ServiceTokens}>Service Tokens</Tab>
<Tab value={TabSections.Roles}>Roles</Tab>
<Tab value={TabSections.Roles}>Project Roles</Tab>
</TabList>
<TabPanel value={TabSections.Member}>
<motion.div

View File

@@ -26,8 +26,8 @@ import {
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = yup.object({
machineId: yup.string().required("ST V3 id is required"),
role: yup.string().required("ST V3 role is required")
machineId: yup.string().required("Machine identity id is required"),
role: yup.string().required("Machine identity role is required")
}).required();
export type FormData = yup.InferType<typeof schema>;
@@ -124,7 +124,7 @@ export const AddMachineIdentityModal = ({
defaultValue={filteredMachineMembershipOrgs?.[0]?._id}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Service Account"
label="Machine Identity"
errorText={error?.message}
isError={Boolean(error)}
>

View File

@@ -66,7 +66,7 @@ export const MachineIdentitySection = withProjectPermission(
</p>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.ServiceTokens}
a={ProjectPermissionSub.MachineIdentity}
>
{(isAllowed) => (
<Button
@@ -104,5 +104,5 @@ export const MachineIdentitySection = withProjectPermission(
</div>
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.ServiceTokens }
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.MachineIdentity }
);

View File

@@ -135,7 +135,7 @@ export const MachineIdentityTable = ({
<Td>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.ServiceTokens}
a={ProjectPermissionSub.MachineIdentity}
>
{(isAllowed) => {
return (
@@ -156,7 +156,7 @@ export const MachineIdentityTable = ({
{(roles || [])
.map(({ slug, name: roleName }) => (
<SelectItem value={slug} key={`owner-option-${slug}`}>
{roleName}
{roleName}
</SelectItem>
))}
</Select>
@@ -168,7 +168,7 @@ export const MachineIdentityTable = ({
<Td className="flex justify-end">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.ServiceTokens}
a={ProjectPermissionSub.MachineIdentity}
>
{(isAllowed) => (
<IconButton