mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Added authz logic to MI
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -235,7 +235,6 @@ interface UpdateMachineIdentityEvent {
|
||||
type: EventType.UPDATE_MACHINE_IDENTITY;
|
||||
metadata: {
|
||||
name?: string;
|
||||
isActive?: boolean;
|
||||
role?: string;
|
||||
trustedIps?: Array<IMachineIdentityTrustedIp>;
|
||||
expiresAt?: Date;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user