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
@@ -393,6 +393,13 @@ export const getOrganizationMachineMemberships = async (req: Request, res: Respo
const { const {
params: { organizationId } params: { organizationId }
} = await validateRequest(reqValidator.GetOrgServiceMembersV2, req); } = 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({ const machineMemberships = await MachineMembershipOrg.find({
organization: new Types.ObjectId(organizationId) organization: new Types.ObjectId(organizationId)
@@ -24,10 +24,12 @@ import * as reqValidator from "../../validation";
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSub, ProjectPermissionSub,
getAuthDataProjectPermissions getAuthDataProjectPermissions,
getRolePermissions,
isAtLeastAsPrivilegedWorkspace
} from "../../ee/services/ProjectRoleService"; } from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { BadRequestError, ResourceNotFoundError } from "../../utils/errors"; import { BadRequestError, ForbiddenRequestError, ResourceNotFoundError } from "../../utils/errors";
import { ADMIN, MEMBER, VIEWER } from "../../variables"; import { ADMIN, MEMBER, VIEWER } from "../../variables";
interface V2PushSecret { interface V2PushSecret {
@@ -523,7 +525,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
ProjectPermissionSub.ServiceTokens ProjectPermissionSub.MachineIdentity
); );
let machineMembership = await MachineMembership.findOne({ let machineMembership = await MachineMembership.findOne({
@@ -532,7 +534,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
}); });
if (machineMembership) throw BadRequestError({ 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); 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" 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; let customRole;
if (role) { if (role) {
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role); const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
@@ -591,8 +600,8 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Edit,
ProjectPermissionSub.ServiceTokens ProjectPermissionSub.MachineIdentity
); );
let machineMembership = await MachineMembership.findOne({ let machineMembership = await MachineMembership.findOne({
@@ -601,7 +610,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
}); });
if (!machineMembership) throw BadRequestError({ 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); const machineIdentity = await MachineIdentity.findById(machineId);
@@ -611,7 +620,14 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
if (!workspace) throw ResourceNotFoundError(); if (!workspace) throw ResourceNotFoundError();
if (!machineIdentity.organization.equals(workspace.organization)) throw BadRequestError({ 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; let customRole;
@@ -665,7 +681,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionActions.Delete,
ProjectPermissionSub.ServiceTokens ProjectPermissionSub.MachineIdentity
); );
const machineMembership = await MachineMembership.findOneAndDelete({ const machineMembership = await MachineMembership.findOneAndDelete({
@@ -698,7 +714,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
ProjectPermissionSub.ServiceTokens ProjectPermissionSub.MachineIdentity
); );
const machineMemberships = await MachineMembership.find({ const machineMemberships = await MachineMembership.find({
@@ -16,13 +16,21 @@ import {
import { validateRequest } from "../../../helpers/validation"; import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../../validation/machineIdentity"; import * as reqValidator from "../../../validation/machineIdentity";
import { createToken } from "../../../helpers/auth"; import { createToken } from "../../../helpers/auth";
import {
getOrgRolePermissions,
import { BadRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../../utils/errors"; getUserOrgPermissions,
isAtLeastAsPrivilegedOrg
} from "../../services/RoleService";
import { BadRequestError, ForbiddenRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../../utils/errors";
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip"; import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
import { EEAuditLogService, EELicenseService } from "../../services"; import { EEAuditLogService, EELicenseService } from "../../services";
import { getAuthSecret } from "../../../config"; import { getAuthSecret } from "../../../config";
import { ADMIN, AuthTokenType, CUSTOM, MEMBER } from "../../../variables"; 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 * 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) => { export const refreshToken = async (req: Request, res: Response) => {
const { const {
body: { body: {
refresh_token refreshToken
} }
} = await validateRequest(reqValidator.RefreshTokenV3, req); } = await validateRequest(reqValidator.RefreshTokenV3, req);
const decodedToken = <jwt.ServiceRefreshTokenJwtPayload>( const decodedToken = <jwt.ServiceRefreshTokenJwtPayload>(
jwt.verify(refresh_token, await getAuthSecret()) jwt.verify(refreshToken, await getAuthSecret())
); );
if (decodedToken.authTokenType !== AuthTokenType.SERVICE_REFRESH_TOKEN) throw UnauthorizedRequestError(); if (decodedToken.authTokenType !== AuthTokenType.SERVICE_REFRESH_TOKEN) throw UnauthorizedRequestError();
@@ -55,15 +63,15 @@ import { ADMIN, AuthTokenType, CUSTOM, MEMBER } from "../../../variables";
} }
const response: { const response: {
refresh_token?: string; refreshToken?: string;
access_token: string; accessToken: string;
expires_in: number; expiresIn: number;
token_type: string; tokenType: string;
} = { } = {
refresh_token, refreshToken,
access_token: "", accessToken: "",
expires_in: 0, expiresIn: 0,
token_type: "Bearer" tokenType: "Bearer"
}; };
if (machineIdentity.isRefreshTokenRotationEnabled) { if (machineIdentity.isRefreshTokenRotationEnabled) {
@@ -81,7 +89,7 @@ import { ADMIN, AuthTokenType, CUSTOM, MEMBER } from "../../../variables";
if (!machineIdentity) throw BadRequestError(); if (!machineIdentity) throw BadRequestError();
response.refresh_token = createToken({ response.refreshToken = createToken({
payload: { payload: {
serviceTokenDataId: machineIdentity._id.toString(), serviceTokenDataId: machineIdentity._id.toString(),
authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN, authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN,
@@ -91,9 +99,9 @@ import { ADMIN, AuthTokenType, CUSTOM, MEMBER } from "../../../variables";
}); });
} }
response.access_token = createToken({ response.accessToken = createToken({
payload: { payload: {
serviceTokenDataId: machineIdentity._id.toString(), serviceTokenDataId: machineIdentity._id.toString(), // TODO: fix this
authTokenType: AuthTokenType.SERVICE_ACCESS_TOKEN, authTokenType: AuthTokenType.SERVICE_ACCESS_TOKEN,
tokenVersion: machineIdentity.tokenVersion tokenVersion: machineIdentity.tokenVersion
}, },
@@ -101,7 +109,7 @@ import { ADMIN, AuthTokenType, CUSTOM, MEMBER } from "../../../variables";
secret: await getAuthSecret() secret: await getAuthSecret()
}); });
response.expires_in = machineIdentity.accessTokenTTL; response.expiresIn = machineIdentity.accessTokenTTL;
await MachineIdentity.findByIdAndUpdate( await MachineIdentity.findByIdAndUpdate(
machineIdentity._id, machineIdentity._id,
@@ -135,22 +143,23 @@ export const createMachineIdentity = async (req: Request, res: Response) => {
isRefreshTokenRotationEnabled isRefreshTokenRotationEnabled
} }
} = await validateRequest(reqValidator.CreateMachineIdentityV3, req); } = 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); const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
// if (!workspace) throw BadRequestError({ message: "Workspace not found" });
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); 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); const isCustomRole = ![ADMIN, MEMBER].includes(role);
@@ -217,7 +226,7 @@ export const createMachineIdentity = async (req: Request, res: Response) => {
const refreshToken = createToken({ const refreshToken = createToken({
payload: { payload: {
serviceTokenDataId: machineIdentity._id.toString(), serviceTokenDataId: machineIdentity._id.toString(), // TODO: update
authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN, authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN,
tokenVersion: machineIdentity.tokenVersion 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 req
* @param res * @param res
* @returns * @returns
@@ -258,7 +267,6 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
params: { machineId }, params: { machineId },
body: { body: {
name, name,
isActive,
role, role,
trustedIps, trustedIps,
expiresIn, expiresIn,
@@ -269,21 +277,24 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
let machineIdentity = await MachineIdentity.findById(machineId); let machineIdentity = await MachineIdentity.findById(machineId);
if (!machineIdentity) throw ResourceNotFoundError({ 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); const { permission } = await getUserOrgPermissions(req.user._id, machineIdentity.organization.toString());
// if (!workspace) throw BadRequestError({ message: "Workspace not found" });
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; let customRole;
if (role) { if (role) {
@@ -329,7 +340,6 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
machineId, machineId,
{ {
name, name,
isActive,
trustedIps: reformattedTrustedIps, trustedIps: reformattedTrustedIps,
expiresAt, expiresAt,
accessTokenTTL, accessTokenTTL,
@@ -341,7 +351,7 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
); );
if (!machineIdentity) throw BadRequestError({ if (!machineIdentity) throw BadRequestError({
message: "Failed to update service token" message: `Failed to update machine identity with id ${machineId}`
}); });
await MachineMembershipOrg.findOneAndUpdate( await MachineMembershipOrg.findOneAndUpdate(
@@ -370,7 +380,6 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
type: EventType.UPDATE_MACHINE_IDENTITY, type: EventType.UPDATE_MACHINE_IDENTITY,
metadata: { metadata: {
name: machineIdentity.name, name: machineIdentity.name,
isActive,
role, role,
trustedIps: reformattedTrustedIps as Array<IMachineIdentityTrustedIp>, trustedIps: reformattedTrustedIps as Array<IMachineIdentityTrustedIp>,
expiresAt 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 req
* @param res * @param res
* @returns * @returns
@@ -399,18 +408,15 @@ export const deleteMachineIdentity = async (req: Request, res: Response) => {
let machineIdentity = await MachineIdentity.findById(machineId); let machineIdentity = await MachineIdentity.findById(machineId);
if (!machineIdentity) throw ResourceNotFoundError({ 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({ ForbiddenError.from(permission).throwUnlessCan(
// authData: req.authData, OrgPermissionActions.Delete,
// workspaceId: serviceTokenData.workspace OrgPermissionSubjects.MachineIdentity
// }); );
// ForbiddenError.from(permission).throwUnlessCan(
// ProjectPermissionActions.Delete,
// ProjectPermissionSub.ServiceTokens
// );
machineIdentity = await MachineIdentity.findByIdAndDelete(machineId); machineIdentity = await MachineIdentity.findByIdAndDelete(machineId);
-1
View File
@@ -235,7 +235,6 @@ interface UpdateMachineIdentityEvent {
type: EventType.UPDATE_MACHINE_IDENTITY; type: EventType.UPDATE_MACHINE_IDENTITY;
metadata: { metadata: {
name?: string; name?: string;
isActive?: boolean;
role?: string; role?: string;
trustedIps?: Array<IMachineIdentityTrustedIp>; trustedIps?: Array<IMachineIdentityTrustedIp>;
expiresAt?: Date; expiresAt?: Date;
+72 -2
View File
@@ -11,7 +11,7 @@ import { UnauthorizedRequestError } from "../../utils/errors";
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js"; import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
import picomatch from "picomatch"; import picomatch from "picomatch";
import { AuthData } from "../../interfaces/middleware"; import { AuthData } from "../../interfaces/middleware";
import { ActorType, IRole } from "../models"; import { ActorType, IRole, Role } from "../models";
import { import {
IMachineIdentity, IMachineIdentity,
MachineMembership, MachineMembership,
@@ -20,6 +20,7 @@ import {
} from "../../models"; } from "../../models";
import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables"; import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables";
import { checkIPAgainstBlocklist } from "../../utils/ip"; import { checkIPAgainstBlocklist } from "../../utils/ip";
import { BadRequestError } from "../../utils/errors";
const $glob: FieldInstruction<string> = { const $glob: FieldInstruction<string> = {
type: "field", type: "field",
@@ -60,7 +61,8 @@ export enum ProjectPermissionSub {
Secrets = "secrets", Secrets = "secrets",
SecretRollback = "secret-rollback", SecretRollback = "secret-rollback",
SecretApproval = "secret-approval", SecretApproval = "secret-approval",
SecretRotation = "secret-rotation" SecretRotation = "secret-rotation",
MachineIdentity = "machine-identity"
} }
type SubjectFields = { type SubjectFields = {
@@ -85,6 +87,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
| [ProjectPermissionActions, ProjectPermissionSub.MachineIdentity]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
@@ -131,6 +134,11 @@ const buildAdminPermission = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Delete, 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.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
@@ -196,6 +204,11 @@ const buildMemberPermission = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Delete, 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.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
@@ -236,6 +249,7 @@ const buildViewerPermission = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Read, ProjectPermissionSub.MachineIdentity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
@@ -337,3 +351,59 @@ export const getAuthDataProjectPermissions = async ({
throw UnauthorizedRequestError(); 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;
}
+71 -16
View File
@@ -1,8 +1,9 @@
import { Types } from "mongoose";
import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@casl/ability"; import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@casl/ability";
import { MembershipOrg } from "../../models"; import { MembershipOrg } from "../../models";
import { IRole } from "../models"; import { IRole, Role } from "../models";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { ACCEPTED } from "../../variables"; import { ACCEPTED, ADMIN, CUSTOM, MEMBER } from "../../variables";
import { conditionsMatcher } from "./ProjectRoleService"; import { conditionsMatcher } from "./ProjectRoleService";
export enum OrgPermissionActions { export enum OrgPermissionActions {
@@ -21,7 +22,7 @@ export enum OrgPermissionSubjects {
Sso = "sso", Sso = "sso",
Billing = "billing", Billing = "billing",
SecretScanning = "secret-scanning", SecretScanning = "secret-scanning",
ServiceTokens = "service-tokens" // TODO: consider renaming MachineIdentity = "machine-identity"
} }
export type OrgPermissionSet = export type OrgPermissionSet =
@@ -34,7 +35,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Sso] | [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.ServiceTokens]; | [OrgPermissionActions, OrgPermissionSubjects.MachineIdentity];
const buildAdminPermission = () => { const buildAdminPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility); const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
@@ -77,10 +78,10 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Read, OrgPermissionSubjects.ServiceTokens); can(OrgPermissionActions.Read, OrgPermissionSubjects.MachineIdentity);
can(OrgPermissionActions.Create, OrgPermissionSubjects.ServiceTokens); can(OrgPermissionActions.Create, OrgPermissionSubjects.MachineIdentity);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ServiceTokens); can(OrgPermissionActions.Edit, OrgPermissionSubjects.MachineIdentity);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ServiceTokens); can(OrgPermissionActions.Delete, OrgPermissionSubjects.MachineIdentity);
return build({ conditionsMatcher }); return build({ conditionsMatcher });
}; };
@@ -105,10 +106,10 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning); can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning); can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Read, OrgPermissionSubjects.ServiceTokens); can(OrgPermissionActions.Read, OrgPermissionSubjects.MachineIdentity);
can(OrgPermissionActions.Create, OrgPermissionSubjects.ServiceTokens); can(OrgPermissionActions.Create, OrgPermissionSubjects.MachineIdentity);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ServiceTokens); can(OrgPermissionActions.Edit, OrgPermissionSubjects.MachineIdentity);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ServiceTokens); can(OrgPermissionActions.Delete, OrgPermissionSubjects.MachineIdentity);
return build({ conditionsMatcher }); return build({ conditionsMatcher });
}; };
@@ -132,11 +133,11 @@ export const getUserOrgPermissions = async (userId: string, orgId: string) => {
throw UnauthorizedRequestError({ message: "User doesn't belong to organization" }); 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, { const permission = createMongoAbility<OrgPermissionSet>(membership.customRole.permissions, {
conditionsMatcher conditionsMatcher
}); });
@@ -144,4 +145,58 @@ export const getUserOrgPermissions = async (userId: string, orgId: string) => {
} }
throw BadRequestError({ message: "User role not found" }); 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;
}
+1 -2
View File
@@ -3,7 +3,7 @@ import { MEMBER } from "../variables";
export const RefreshTokenV3 = z.object({ export const RefreshTokenV3 = z.object({
body: 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({ body: z.object({
name: z.string().trim().optional(), name: z.string().trim().optional(),
isActive: z.boolean().optional(),
role: z.string().trim().min(1).optional(), role: z.string().trim().min(1).optional(),
trustedIps: z trustedIps: z
.object({ .object({
@@ -16,7 +16,7 @@ export enum OrgPermissionSubjects {
Sso = "sso", Sso = "sso",
Billing = "billing", Billing = "billing",
SecretScanning = "secret-scanning", SecretScanning = "secret-scanning",
ServiceTokens = "service-tokens" MachineIdentity = "machine-identity"
} }
export type OrgPermissionSet = export type OrgPermissionSet =
@@ -29,6 +29,6 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Sso] | [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.ServiceTokens]; | [OrgPermissionActions, OrgPermissionSubjects.MachineIdentity];
export type TOrgPermission = MongoAbility<OrgPermissionSet>; export type TOrgPermission = MongoAbility<OrgPermissionSet>;
@@ -22,7 +22,8 @@ export enum ProjectPermissionSub {
Secrets = "secrets", Secrets = "secrets",
SecretRollback = "secret-rollback", SecretRollback = "secret-rollback",
SecretApproval = "secret-approval", SecretApproval = "secret-approval",
SecretRotation = "secret-rotation" SecretRotation = "secret-rotation",
MachineIdentity = "machine-identity"
} }
type SubjectFields = { type SubjectFields = {
@@ -44,6 +45,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.Environments] | [ProjectPermissionActions, ProjectPermissionSub.Environments]
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList] | [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
| [ProjectPermissionActions, ProjectPermissionSub.Settings] | [ProjectPermissionActions, ProjectPermissionSub.Settings]
| [ProjectPermissionActions, ProjectPermissionSub.MachineIdentity]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
@@ -26,7 +26,7 @@ export const MembersPage = withPermission(
<TabList> <TabList>
<Tab value={TabSections.Member}>People</Tab> <Tab value={TabSections.Member}>People</Tab>
<Tab value={TabSections.MachineIdentities}>Machine Identities</Tab> <Tab value={TabSections.MachineIdentities}>Machine Identities</Tab>
<Tab value={TabSections.Roles}>Roles</Tab> <Tab value={TabSections.Roles}>Organization Roles</Tab>
</TabList> </TabList>
<TabPanel value={TabSections.Member}> <TabPanel value={TabSections.Member}>
<OrgMembersTab /> <OrgMembersTab />
@@ -32,7 +32,7 @@ import {
useGetRoles, useGetRoles,
useUpdateMachineIdentity useUpdateMachineIdentity
} from "@app/hooks/api"; } 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"; import { UsePopUpState } from "@app/hooks/usePopUp";
enum TabSections { enum TabSections {
@@ -50,7 +50,7 @@ const expirations = [
]; ];
const schema = yup.object({ const schema = yup.object({
name: yup.string().required("ST V3 name is required"), name: yup.string().required("MI name is required"),
expiresIn: yup.string(), expiresIn: yup.string(),
accessTokenTTL: yup accessTokenTTL: yup
.string() .string()
@@ -63,7 +63,7 @@ const schema = yup.object({
return !Number.isNaN(num) && num > 0 && String(num) === value; return !Number.isNaN(num) && num > 0 && String(num) === value;
}) })
.required("Access Token TTL is required"), .required("Access Token TTL is required"),
role: yup.string().required("ST V3 role is required"), role: yup.string(),
trustedIps: yup trustedIps: yup
.array( .array(
yup.object({ yup.object({
@@ -146,7 +146,7 @@ export const AddMachineIdentityModal = ({
name: string; name: string;
slug: string; slug: string;
}; };
trustedIps: ServiceTokenV3TrustedIp[]; trustedIps: MachineTrustedIp[];
accessTokenTTL: number; accessTokenTTL: number;
isRefreshTokenRotationEnabled: boolean; isRefreshTokenRotationEnabled: boolean;
}; };
@@ -161,7 +161,7 @@ export const AddMachineIdentityModal = ({
trustedIps: machineIdentity.trustedIps.map(({ trustedIps: machineIdentity.trustedIps.map(({
ipAddress, ipAddress,
prefix prefix
}: ServiceTokenV3TrustedIp) => { }: MachineTrustedIp) => {
return ({ return ({
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}` ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
}); });
@@ -206,9 +206,9 @@ export const AddMachineIdentityModal = ({
await updateMutateAsync({ await updateMutateAsync({
machineId: machineIdentity.machineId, machineId: machineIdentity.machineId,
name, name,
role, role: role || undefined,
trustedIps, trustedIps,
expiresIn: expiresIn === "" ? undefined : Number(expiresIn), expiresIn: (!expiresIn) ? undefined : Number(expiresIn),
accessTokenTTL: Number(accessTokenTTL), accessTokenTTL: Number(accessTokenTTL),
isRefreshTokenRotationEnabled isRefreshTokenRotationEnabled
}); });
@@ -218,10 +218,10 @@ export const AddMachineIdentityModal = ({
const { refreshToken } = await createMutateAsync({ const { refreshToken } = await createMutateAsync({
name, name,
role, role: role || undefined,
organizationId: orgId, organizationId: orgId,
trustedIps, trustedIps,
expiresIn: expiresIn === "" ? undefined : Number(expiresIn), expiresIn: (!expiresIn) ? undefined : Number(expiresIn),
accessTokenTTL: Number(accessTokenTTL), accessTokenTTL: Number(accessTokenTTL),
isRefreshTokenRotationEnabled isRefreshTokenRotationEnabled
}); });
@@ -53,7 +53,7 @@ export const MachineIdentitySection = withPermission(
</p> </p>
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Create} I={OrgPermissionActions.Create}
a={OrgPermissionSubjects.ServiceTokens} a={OrgPermissionSubjects.MachineIdentity}
> >
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
@@ -92,5 +92,5 @@ export const MachineIdentitySection = withPermission(
</div> </div>
); );
}, },
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.ServiceTokens } { action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.MachineIdentity }
); );
@@ -1,4 +1,3 @@
import { useCallback } from "react";
import { faPencil,faServer, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faPencil,faServer, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns"; import { format } from "date-fns";
@@ -28,7 +27,7 @@ import {
useGetRoles, useGetRoles,
useUpdateMachineIdentity useUpdateMachineIdentity
} from "@app/hooks/api"; } 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"; import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = { type Props = {
@@ -42,7 +41,7 @@ type Props = {
name: string; name: string;
slug: string; slug: string;
}; };
trustedIps?: ServiceTokenV3TrustedIp[]; trustedIps?: MachineTrustedIp[];
accessTokenTTL?: number; accessTokenTTL?: number;
isRefreshTokenRotationEnabled?: boolean; isRefreshTokenRotationEnabled?: boolean;
} }
@@ -115,13 +114,6 @@ export const MachineIdentityTable = ({
// }); // });
// } // }
// } // }
const findRoleFromId = useCallback(
(roleId: string) => {
return (roles || []).find(({ _id: id }) => id === roleId);
},
[roles]
);
return ( return (
<TableContainer> <TableContainer>
@@ -163,7 +155,7 @@ export const MachineIdentityTable = ({
{/* <Td> {/* <Td>
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Edit} I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ServiceTokens} a={OrgPermissionSubjects.MachineIdentity}
> >
{(isAllowed) => ( {(isAllowed) => (
<Switch <Switch
@@ -183,13 +175,13 @@ export const MachineIdentityTable = ({
<Td> <Td>
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Edit} I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ServiceTokens} a={OrgPermissionSubjects.MachineIdentity}
> >
{(isAllowed) => { {(isAllowed) => {
return ( return (
<Select <Select
value={ value={
role === "custom" ? findRoleFromId(customRole)?.slug : role role === "custom" ? customRole.slug : role
} }
isDisabled={!isAllowed} isDisabled={!isAllowed}
className="w-40 bg-mineshaft-600" className="w-40 bg-mineshaft-600"
@@ -231,7 +223,7 @@ export const MachineIdentityTable = ({
<Td className="flex justify-end"> <Td className="flex justify-end">
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Edit} I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ServiceTokens} a={OrgPermissionSubjects.MachineIdentity}
> >
{(isAllowed) => ( {(isAllowed) => (
<IconButton <IconButton
@@ -258,7 +250,7 @@ export const MachineIdentityTable = ({
</OrgPermissionCan> </OrgPermissionCan>
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionActions.Delete} I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.ServiceTokens} a={OrgPermissionSubjects.MachineIdentity}
> >
{(isAllowed) => ( {(isAllowed) => (
<IconButton <IconButton
@@ -5,10 +5,10 @@ import {
faContactCard, faContactCard,
faMagnifyingGlass, faMagnifyingGlass,
faMoneyBill, faMoneyBill,
faServer,
faSignIn, faSignIn,
faUserCog, faUserCog,
faUsers faUsers} from "@fortawesome/free-solid-svg-icons";
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@@ -40,6 +40,12 @@ const SIMPLE_PERMISSION_OPTIONS = [
icon: faUsers, icon: faUsers,
formName: "member" 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", title: "Billing & usage",
subtitle: "Modify organization subscription plan", subtitle: "Modify organization subscription plan",
@@ -31,7 +31,8 @@ export const formSchema = z.object({
"incident-contact": generalPermissionSchema, "incident-contact": generalPermissionSchema,
"secret-scanning": generalPermissionSchema, "secret-scanning": generalPermissionSchema,
sso: generalPermissionSchema, sso: generalPermissionSchema,
billing: generalPermissionSchema billing: generalPermissionSchema,
"machine-identity": generalPermissionSchema
}) })
.optional() .optional()
}); });
@@ -32,7 +32,7 @@ export const MembersPage = withProjectPermission(
<Tab value={TabSections.Member}>People</Tab> <Tab value={TabSections.Member}>People</Tab>
<Tab value={TabSections.MachineIdentities}>Machine Identities</Tab> <Tab value={TabSections.MachineIdentities}>Machine Identities</Tab>
<Tab value={TabSections.ServiceTokens}>Service Tokens</Tab> <Tab value={TabSections.ServiceTokens}>Service Tokens</Tab>
<Tab value={TabSections.Roles}>Roles</Tab> <Tab value={TabSections.Roles}>Project Roles</Tab>
</TabList> </TabList>
<TabPanel value={TabSections.Member}> <TabPanel value={TabSections.Member}>
<motion.div <motion.div
@@ -26,8 +26,8 @@ import {
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = yup.object({ const schema = yup.object({
machineId: yup.string().required("ST V3 id is required"), machineId: yup.string().required("Machine identity id is required"),
role: yup.string().required("ST V3 role is required") role: yup.string().required("Machine identity role is required")
}).required(); }).required();
export type FormData = yup.InferType<typeof schema>; export type FormData = yup.InferType<typeof schema>;
@@ -124,7 +124,7 @@ export const AddMachineIdentityModal = ({
defaultValue={filteredMachineMembershipOrgs?.[0]?._id} defaultValue={filteredMachineMembershipOrgs?.[0]?._id}
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl <FormControl
label="Service Account" label="Machine Identity"
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
> >
@@ -66,7 +66,7 @@ export const MachineIdentitySection = withProjectPermission(
</p> </p>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Create} I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.ServiceTokens} a={ProjectPermissionSub.MachineIdentity}
> >
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
@@ -104,5 +104,5 @@ export const MachineIdentitySection = withProjectPermission(
</div> </div>
); );
}, },
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.ServiceTokens } { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.MachineIdentity }
); );
@@ -135,7 +135,7 @@ export const MachineIdentityTable = ({
<Td> <Td>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.ServiceTokens} a={ProjectPermissionSub.MachineIdentity}
> >
{(isAllowed) => { {(isAllowed) => {
return ( return (
@@ -156,7 +156,7 @@ export const MachineIdentityTable = ({
{(roles || []) {(roles || [])
.map(({ slug, name: roleName }) => ( .map(({ slug, name: roleName }) => (
<SelectItem value={slug} key={`owner-option-${slug}`}> <SelectItem value={slug} key={`owner-option-${slug}`}>
{roleName} {roleName}
</SelectItem> </SelectItem>
))} ))}
</Select> </Select>
@@ -168,7 +168,7 @@ export const MachineIdentityTable = ({
<Td className="flex justify-end"> <Td className="flex justify-end">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.ServiceTokens} a={ProjectPermissionSub.MachineIdentity}
> >
{(isAllowed) => ( {(isAllowed) => (
<IconButton <IconButton