mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 18:28:27 +00:00
Update authz logic for MI
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import {
|
import {
|
||||||
|
IMachineIdentity,
|
||||||
Key,
|
Key,
|
||||||
MachineIdentity,
|
MachineIdentity,
|
||||||
MachineMembership,
|
MachineMembership,
|
||||||
@@ -8,7 +9,7 @@ import {
|
|||||||
ServiceTokenData,
|
ServiceTokenData,
|
||||||
Workspace
|
Workspace
|
||||||
} from "../../models";
|
} from "../../models";
|
||||||
import { Role } from "../../ee/models";
|
import { IRole, Role } from "../../ee/models";
|
||||||
import {
|
import {
|
||||||
pullSecrets as pull,
|
pullSecrets as pull,
|
||||||
v2PushSecrets as push,
|
v2PushSecrets as push,
|
||||||
@@ -534,24 +535,26 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (machineMembership) throw BadRequestError({
|
if (machineMembership) throw BadRequestError({
|
||||||
message: `Machine identity with id ${machineId} already exists in workspace with id ${workspaceId}`
|
message: `Machine identity with id ${machineId} already exists in project with id ${workspaceId}`
|
||||||
});
|
});
|
||||||
|
|
||||||
const machineIdentity = await MachineIdentity.findById(machineId);
|
const machineIdentity = await MachineIdentity.findById(machineId);
|
||||||
if (!machineIdentity) throw ResourceNotFoundError();
|
if (!machineIdentity) throw ResourceNotFoundError({
|
||||||
|
message: `Failed to find machine identity with id ${machineId}`
|
||||||
|
});
|
||||||
|
|
||||||
const workspace = await Workspace.findById(workspaceId);
|
const workspace = await Workspace.findById(workspaceId);
|
||||||
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 add machine identity to project in another organization"
|
||||||
});
|
});
|
||||||
|
|
||||||
const rolePermission = await getRolePermissions(role, workspaceId);
|
const rolePermission = await getRolePermissions(role, workspaceId);
|
||||||
const hasRequiredPrivileges = isAtLeastAsPrivilegedWorkspace(permission, rolePermission);
|
const isAsPrivilegedAsIntendedRole = isAtLeastAsPrivilegedWorkspace(permission, rolePermission);
|
||||||
|
|
||||||
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
if (!isAsPrivilegedAsIntendedRole) throw ForbiddenRequestError({
|
||||||
message: "Failed to add a more privileged MI to project"
|
message: "Failed to add MI to project with more privileged role"
|
||||||
});
|
});
|
||||||
|
|
||||||
let customRole;
|
let customRole;
|
||||||
@@ -604,29 +607,33 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
|
|||||||
ProjectPermissionSub.MachineIdentity
|
ProjectPermissionSub.MachineIdentity
|
||||||
);
|
);
|
||||||
|
|
||||||
let machineMembership = await MachineMembership.findOne({
|
let machineMembership = await MachineMembership
|
||||||
machineIdentity: new Types.ObjectId(machineId),
|
.findOne({
|
||||||
workspace: new Types.ObjectId(workspaceId)
|
machineIdentity: new Types.ObjectId(machineId),
|
||||||
});
|
workspace: new Types.ObjectId(workspaceId)
|
||||||
|
})
|
||||||
|
.populate<{
|
||||||
|
machineIdentity: IMachineIdentity,
|
||||||
|
customRole: IRole
|
||||||
|
}>("machineIdentity customRole");
|
||||||
|
|
||||||
if (!machineMembership) throw BadRequestError({
|
if (!machineMembership) throw BadRequestError({
|
||||||
message: `Machine identity with id ${machineId} does not exist in workspace with id ${workspaceId}`
|
message: `Machine identity with id ${machineId} does not exist in project with id ${workspaceId}`
|
||||||
});
|
});
|
||||||
|
|
||||||
const machineIdentity = await MachineIdentity.findById(machineId);
|
const machineIdentityRolePermission = await getRolePermissions(
|
||||||
if (!machineIdentity) throw ResourceNotFoundError();
|
machineMembership?.customRole?.slug ?? machineMembership.role,
|
||||||
|
machineMembership.workspace.toString()
|
||||||
const workspace = await Workspace.findById(workspaceId);
|
);
|
||||||
if (!workspace) throw ResourceNotFoundError();
|
const isAsPrivilegedAsMachine = isAtLeastAsPrivilegedWorkspace(permission, machineIdentityRolePermission);
|
||||||
|
if (!isAsPrivilegedAsMachine) throw ForbiddenRequestError({
|
||||||
if (!machineIdentity.organization.equals(workspace.organization)) throw BadRequestError({
|
message: "Failed to update role of more privileged MI"
|
||||||
message: "Failed to update machine identity in workspace in another organization"
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const rolePermission = await getRolePermissions(role, workspaceId);
|
const rolePermission = await getRolePermissions(role, workspaceId);
|
||||||
const hasRequiredPrivileges = isAtLeastAsPrivilegedWorkspace(permission, rolePermission);
|
const isAsPrivilegedAsIntendedRole = isAtLeastAsPrivilegedWorkspace(permission, rolePermission);
|
||||||
|
|
||||||
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
if (!isAsPrivilegedAsIntendedRole) throw ForbiddenRequestError({
|
||||||
message: "Failed to update MI to a more privileged role"
|
message: "Failed to update MI to a more privileged role"
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -646,7 +653,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
machineMembership = await MachineMembership.findOneAndUpdate(
|
machineMembership = await MachineMembership.findOneAndUpdate(
|
||||||
{
|
{
|
||||||
machineIdentity: machineIdentity._id,
|
machineIdentity: machineMembership.machineIdentity,
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -684,12 +691,30 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
|
|||||||
ProjectPermissionSub.MachineIdentity
|
ProjectPermissionSub.MachineIdentity
|
||||||
);
|
);
|
||||||
|
|
||||||
const machineMembership = await MachineMembership.findOneAndDelete({
|
const machineMembership = await MachineMembership
|
||||||
machineIdentity: new Types.ObjectId(machineId),
|
.findOne({
|
||||||
workspace: new Types.ObjectId(workspaceId)
|
machineIdentity: new Types.ObjectId(machineId),
|
||||||
|
workspace: new Types.ObjectId(workspaceId)
|
||||||
|
})
|
||||||
|
.populate<{
|
||||||
|
machineIdentity: IMachineIdentity,
|
||||||
|
customRole: IRole
|
||||||
|
}>("machineIdentity customRole");
|
||||||
|
|
||||||
|
if (!machineMembership) throw ResourceNotFoundError({
|
||||||
|
message: `Machine with id ${machineId} does not exist in project with id ${workspaceId}`
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!machineMembership) throw ResourceNotFoundError();
|
const machineIdentityRolePermission = await getRolePermissions(
|
||||||
|
machineMembership?.customRole?.slug ?? machineMembership.role,
|
||||||
|
machineMembership.workspace.toString()
|
||||||
|
);
|
||||||
|
const isAsPrivilegedAsMachine = isAtLeastAsPrivilegedWorkspace(permission, machineIdentityRolePermission);
|
||||||
|
if (!isAsPrivilegedAsMachine) throw ForbiddenRequestError({
|
||||||
|
message: "Failed to remove more privileged MI from project"
|
||||||
|
});
|
||||||
|
|
||||||
|
await MachineMembership.findByIdAndDelete(machineMembership._id);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
machineMembership
|
machineMembership
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { Request, Response } from "express";
|
|||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import {
|
import {
|
||||||
IMachineIdentity,
|
IMachineIdentity,
|
||||||
IMachineIdentityClientSecretData,
|
IMachineIdentityClientSecret,
|
||||||
IMachineIdentityTrustedIp,
|
IMachineIdentityTrustedIp,
|
||||||
MachineIdentity,
|
MachineIdentity,
|
||||||
MachineIdentityClientSecretData,
|
MachineIdentityClientSecret,
|
||||||
MachineMembership,
|
MachineMembership,
|
||||||
MachineMembershipOrg,
|
MachineMembershipOrg,
|
||||||
Organization,
|
Organization,
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
ActorType,
|
ActorType,
|
||||||
EventType,
|
EventType,
|
||||||
|
IRole,
|
||||||
Role
|
Role
|
||||||
} from "../../models";
|
} from "../../models";
|
||||||
import { validateRequest } from "../../../helpers/validation";
|
import { validateRequest } from "../../../helpers/validation";
|
||||||
@@ -25,7 +26,12 @@ import {
|
|||||||
getUserOrgPermissions,
|
getUserOrgPermissions,
|
||||||
isAtLeastAsPrivilegedOrg
|
isAtLeastAsPrivilegedOrg
|
||||||
} from "../../services/RoleService";
|
} from "../../services/RoleService";
|
||||||
import { BadRequestError, ForbiddenRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../../utils/errors";
|
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, getSaltRounds } from "../../../config";
|
import { getAuthSecret, getSaltRounds } from "../../../config";
|
||||||
@@ -38,15 +44,15 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import { checkIPAgainstBlocklist } from "../../../utils/ip";
|
import { checkIPAgainstBlocklist } from "../../../utils/ip";
|
||||||
import { getUserAgentType } from "../../../utils/posthog";
|
import { getUserAgentType } from "../../../utils/posthog";
|
||||||
|
|
||||||
const packageClientSecretData = (clientSecretData: IMachineIdentityClientSecretData) => ({
|
const packageClientSecretData = (machineIdentityClientSecret: IMachineIdentityClientSecret) => ({
|
||||||
_id: clientSecretData._id,
|
_id: machineIdentityClientSecret._id,
|
||||||
machineIdentity: clientSecretData.machineIdentity,
|
machineIdentity: machineIdentityClientSecret.machineIdentity,
|
||||||
isActive: clientSecretData.isActive,
|
isActive: machineIdentityClientSecret.isActive,
|
||||||
description: clientSecretData.description,
|
description: machineIdentityClientSecret.description,
|
||||||
clientSecretPrefix: clientSecretData.clientSecretPrefix,
|
clientSecretPrefix: machineIdentityClientSecret.clientSecretPrefix,
|
||||||
clientSecretNumUses: clientSecretData.clientSecretNumUses,
|
clientSecretNumUses: machineIdentityClientSecret.clientSecretNumUses,
|
||||||
clientSecretNumUsesLimit: clientSecretData.clientSecretNumUsesLimit,
|
clientSecretNumUsesLimit: machineIdentityClientSecret.clientSecretNumUsesLimit,
|
||||||
clientSecretTTL: clientSecretData.clientSecretTTL
|
clientSecretTTL: machineIdentityClientSecret.clientSecretTTL
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,7 +69,10 @@ export const getMIClientSecrets = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
const machineMembershipOrg = await MachineMembershipOrg.findOne({
|
const machineMembershipOrg = await MachineMembershipOrg.findOne({
|
||||||
machineIdentity: new Types.ObjectId(machineId)
|
machineIdentity: new Types.ObjectId(machineId)
|
||||||
}).populate<{ machineIdentity: IMachineIdentity }>("machineIdentity");
|
}).populate<{
|
||||||
|
machineIdentity: IMachineIdentity,
|
||||||
|
customRole: IRole
|
||||||
|
}>("machineIdentity customRole");
|
||||||
|
|
||||||
if (!machineMembershipOrg) throw ResourceNotFoundError();
|
if (!machineMembershipOrg) throw ResourceNotFoundError();
|
||||||
|
|
||||||
@@ -74,14 +83,17 @@ export const getMIClientSecrets = async (req: Request, res: Response) => {
|
|||||||
OrgPermissionSubjects.MachineIdentity
|
OrgPermissionSubjects.MachineIdentity
|
||||||
);
|
);
|
||||||
|
|
||||||
const rolePermission = await getOrgRolePermissions(machineMembershipOrg.role, machineMembershipOrg.organization.toString());
|
const rolePermission = await getOrgRolePermissions(
|
||||||
|
machineMembershipOrg?.customRole?.slug ?? machineMembershipOrg.role,
|
||||||
|
machineMembershipOrg.organization.toString()
|
||||||
|
);
|
||||||
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission);
|
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission);
|
||||||
|
|
||||||
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
||||||
message: "Failed to get client secrets for more privileged MI"
|
message: "Failed to get client secrets for more privileged MI"
|
||||||
});
|
});
|
||||||
|
|
||||||
const clientSecretData = await MachineIdentityClientSecretData
|
const clientSecretData = await MachineIdentityClientSecret
|
||||||
.find({
|
.find({
|
||||||
machineIdentity: machineMembershipOrg.machineIdentity,
|
machineIdentity: machineMembershipOrg.machineIdentity,
|
||||||
isActive: true
|
isActive: true
|
||||||
@@ -127,7 +139,10 @@ export const createMIClientSecret = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
const machineMembershipOrg = await MachineMembershipOrg.findOne({
|
const machineMembershipOrg = await MachineMembershipOrg.findOne({
|
||||||
machineIdentity: new Types.ObjectId(machineId)
|
machineIdentity: new Types.ObjectId(machineId)
|
||||||
}).populate<{ machineIdentity: IMachineIdentity }>("machineIdentity");
|
}).populate<{
|
||||||
|
machineIdentity: IMachineIdentity,
|
||||||
|
customRole: IRole
|
||||||
|
}>("machineIdentity customRole");
|
||||||
|
|
||||||
if (!machineMembershipOrg) throw ResourceNotFoundError();
|
if (!machineMembershipOrg) throw ResourceNotFoundError();
|
||||||
|
|
||||||
@@ -138,7 +153,10 @@ export const createMIClientSecret = async (req: Request, res: Response) => {
|
|||||||
OrgPermissionSubjects.MachineIdentity
|
OrgPermissionSubjects.MachineIdentity
|
||||||
);
|
);
|
||||||
|
|
||||||
const rolePermission = await getOrgRolePermissions(machineMembershipOrg.role, machineMembershipOrg.organization.toString());
|
const rolePermission = await getOrgRolePermissions(
|
||||||
|
machineMembershipOrg?.customRole?.slug ?? machineMembershipOrg.role,
|
||||||
|
machineMembershipOrg.organization.toString()
|
||||||
|
);
|
||||||
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission);
|
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission);
|
||||||
|
|
||||||
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
||||||
@@ -148,7 +166,7 @@ export const createMIClientSecret = async (req: Request, res: Response) => {
|
|||||||
const clientSecret = crypto.randomBytes(32).toString("hex");
|
const clientSecret = crypto.randomBytes(32).toString("hex");
|
||||||
const clientSecretHash = await bcrypt.hash(clientSecret, await getSaltRounds());
|
const clientSecretHash = await bcrypt.hash(clientSecret, await getSaltRounds());
|
||||||
|
|
||||||
const machineIdentityClientSecretData = await new MachineIdentityClientSecretData({
|
const machineIdentityClientSecret = await new MachineIdentityClientSecret({
|
||||||
machineIdentity: machineMembershipOrg.machineIdentity,
|
machineIdentity: machineMembershipOrg.machineIdentity,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
description,
|
description,
|
||||||
@@ -167,7 +185,7 @@ export const createMIClientSecret = async (req: Request, res: Response) => {
|
|||||||
metadata: {
|
metadata: {
|
||||||
machineId: machineMembershipOrg.machineIdentity._id.toString(),
|
machineId: machineMembershipOrg.machineIdentity._id.toString(),
|
||||||
clientId: machineMembershipOrg.machineIdentity.clientId,
|
clientId: machineMembershipOrg.machineIdentity.clientId,
|
||||||
clientSecretId: machineIdentityClientSecretData._id.toString()
|
clientSecretId: machineIdentityClientSecret._id.toString()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -177,7 +195,7 @@ export const createMIClientSecret = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
clientSecret,
|
clientSecret,
|
||||||
clientSecretData: packageClientSecretData(machineIdentityClientSecretData)
|
clientSecretData: packageClientSecretData(machineIdentityClientSecret)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,11 +212,18 @@ export const deleteMIClientSecret = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
} = await validateRequest(reqValidator.DeleteClientSecretV3, req);
|
} = await validateRequest(reqValidator.DeleteClientSecretV3, req);
|
||||||
|
|
||||||
const machineMembershipOrg = await MachineMembershipOrg.findOne({
|
const machineMembershipOrg = await MachineMembershipOrg
|
||||||
machineIdentity: new Types.ObjectId(machineId)
|
.findOne({
|
||||||
}).populate<{ machineIdentity: IMachineIdentity }>("machineIdentity");
|
machineIdentity: new Types.ObjectId(machineId)
|
||||||
|
})
|
||||||
|
.populate<{
|
||||||
|
machineIdentity: IMachineIdentity,
|
||||||
|
customRole: IRole
|
||||||
|
}>("machineIdentity customRole");
|
||||||
|
|
||||||
if (!machineMembershipOrg) throw ResourceNotFoundError();
|
if (!machineMembershipOrg) throw ResourceNotFoundError({
|
||||||
|
message: `Failed to find machine identity with id ${machineId}`
|
||||||
|
});
|
||||||
|
|
||||||
const { permission } = await getUserOrgPermissions(req.user._id, machineMembershipOrg.organization.toString());
|
const { permission } = await getUserOrgPermissions(req.user._id, machineMembershipOrg.organization.toString());
|
||||||
|
|
||||||
@@ -207,19 +232,22 @@ export const deleteMIClientSecret = async (req: Request, res: Response) => {
|
|||||||
OrgPermissionSubjects.MachineIdentity
|
OrgPermissionSubjects.MachineIdentity
|
||||||
);
|
);
|
||||||
|
|
||||||
const rolePermission = await getOrgRolePermissions(machineMembershipOrg.role, machineMembershipOrg.organization.toString());
|
const rolePermission = await getOrgRolePermissions(
|
||||||
|
machineMembershipOrg?.customRole?.slug ?? machineMembershipOrg.role,
|
||||||
|
machineMembershipOrg.organization.toString()
|
||||||
|
);
|
||||||
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission);
|
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission);
|
||||||
|
|
||||||
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
||||||
message: "Failed to delete client secrets for more privileged MI"
|
message: "Failed to delete client secrets for more privileged MI"
|
||||||
});
|
});
|
||||||
|
|
||||||
const clientSecretData = await MachineIdentityClientSecretData.findOneAndDelete({
|
const machineIdentityClientSecret = await MachineIdentityClientSecret.findOneAndDelete({
|
||||||
_id: clientSecretId,
|
_id: clientSecretId,
|
||||||
machineIdentity: machineId
|
machineIdentity: machineId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!clientSecretData) throw ResourceNotFoundError();
|
if (!machineIdentityClientSecret) throw ResourceNotFoundError();
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
await EEAuditLogService.createAuditLog(
|
||||||
req.authData,
|
req.authData,
|
||||||
@@ -237,7 +265,7 @@ export const deleteMIClientSecret = async (req: Request, res: Response) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
clientSecretData: packageClientSecretData(clientSecretData)
|
clientSecretData: packageClientSecretData(machineIdentityClientSecret)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,12 +295,12 @@ export const loginMI = async (req: Request, res: Response) => {
|
|||||||
trustedIps: machineIdentity.clientSecretTrustedIps
|
trustedIps: machineIdentity.clientSecretTrustedIps
|
||||||
});
|
});
|
||||||
|
|
||||||
const clientSecretData = await MachineIdentityClientSecretData.find({
|
const clientSecretData = await MachineIdentityClientSecret.find({
|
||||||
machineIdentity: machineIdentity._id,
|
machineIdentity: machineIdentity._id,
|
||||||
isActive: true
|
isActive: true
|
||||||
});
|
});
|
||||||
|
|
||||||
let validatedClientSecretDatum: IMachineIdentityClientSecretData | undefined;
|
let validatedClientSecretDatum: IMachineIdentityClientSecret | undefined;
|
||||||
|
|
||||||
for (const clientSecretDatum of clientSecretData) {
|
for (const clientSecretDatum of clientSecretData) {
|
||||||
const isSecretValid = await bcrypt.compare(
|
const isSecretValid = await bcrypt.compare(
|
||||||
@@ -298,13 +326,10 @@ export const loginMI = async (req: Request, res: Response) => {
|
|||||||
const expiresAt = new Date(new Date().getTime() + clientSecretTTL * 1000);
|
const expiresAt = new Date(new Date().getTime() + clientSecretTTL * 1000);
|
||||||
|
|
||||||
if (expiresAt < new Date()) {
|
if (expiresAt < new Date()) {
|
||||||
await MachineIdentityClientSecretData.findByIdAndUpdate(
|
await MachineIdentityClientSecret.findByIdAndUpdate(
|
||||||
validatedClientSecretDatum._id,
|
validatedClientSecretDatum._id,
|
||||||
{
|
{
|
||||||
isActive: false
|
isActive: false
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -315,7 +340,7 @@ export const loginMI = async (req: Request, res: Response) => {
|
|||||||
if (clientSecretNumUses > 0 && clientSecretNumUses === clientSecretNumUsesLimit) {
|
if (clientSecretNumUses > 0 && clientSecretNumUses === clientSecretNumUsesLimit) {
|
||||||
// number of times client secret can be used for
|
// number of times client secret can be used for
|
||||||
// a login operation reached
|
// a login operation reached
|
||||||
await MachineIdentityClientSecretData.findByIdAndUpdate(
|
await MachineIdentityClientSecret.findByIdAndUpdate(
|
||||||
validatedClientSecretDatum._id,
|
validatedClientSecretDatum._id,
|
||||||
{
|
{
|
||||||
isActive: false
|
isActive: false
|
||||||
@@ -329,7 +354,7 @@ export const loginMI = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// increment usage count by 1
|
// increment usage count by 1
|
||||||
await MachineIdentityClientSecretData.findByIdAndUpdate(
|
await MachineIdentityClientSecret.findByIdAndUpdate(
|
||||||
validatedClientSecretDatum._id,
|
validatedClientSecretDatum._id,
|
||||||
{
|
{
|
||||||
$inc: { clientSecretNumUses: 1 }
|
$inc: { clientSecretNumUses: 1 }
|
||||||
@@ -342,7 +367,7 @@ export const loginMI = async (req: Request, res: Response) => {
|
|||||||
// token version
|
// token version
|
||||||
const accessToken = createToken({
|
const accessToken = createToken({
|
||||||
payload: {
|
payload: {
|
||||||
machineId: machineIdentity._id.toString(), // consider changing to clientId and making it more extensible
|
machineId: machineIdentity._id.toString(),
|
||||||
clientSecretDataId: validatedClientSecretDatum._id.toString(),
|
clientSecretDataId: validatedClientSecretDatum._id.toString(),
|
||||||
authTokenType: AuthTokenType.MACHINE_ACCESS_TOKEN,
|
authTokenType: AuthTokenType.MACHINE_ACCESS_TOKEN,
|
||||||
tokenVersion: validatedClientSecretDatum.accessTokenVersion
|
tokenVersion: validatedClientSecretDatum.accessTokenVersion
|
||||||
@@ -525,22 +550,36 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
} = await validateRequest(reqValidator.UpdateMachineIdentityV3, req);
|
} = await validateRequest(reqValidator.UpdateMachineIdentityV3, req);
|
||||||
|
|
||||||
let machineIdentity = await MachineIdentity.findById(machineId);
|
const machineMembershipOrg = await MachineMembershipOrg
|
||||||
if (!machineIdentity) throw ResourceNotFoundError({
|
.findOne({
|
||||||
message: `Machine identity with id ${machineId} not found`
|
machineIdentity: new Types.ObjectId(machineId)
|
||||||
|
})
|
||||||
|
.populate<{
|
||||||
|
machineIdentity: IMachineIdentity,
|
||||||
|
customRole: IRole
|
||||||
|
}>("machineIdentity customRole");
|
||||||
|
|
||||||
|
if (!machineMembershipOrg) throw ResourceNotFoundError({
|
||||||
|
message: `Failed to find machine identity with id ${machineId}`
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: validate existing role (if it is currently admin then cant demote it)
|
const { permission } = await getUserOrgPermissions(req.user._id, machineMembershipOrg.organization.toString());
|
||||||
|
|
||||||
const { permission } = await getUserOrgPermissions(req.user._id, machineIdentity.organization.toString());
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
OrgPermissionActions.Edit,
|
OrgPermissionActions.Edit,
|
||||||
OrgPermissionSubjects.MachineIdentity
|
OrgPermissionSubjects.MachineIdentity
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const machineIdentityRolePermission = await getOrgRolePermissions(
|
||||||
|
machineMembershipOrg?.customRole?.slug ?? machineMembershipOrg.role,
|
||||||
|
machineMembershipOrg.organization.toString()
|
||||||
|
);
|
||||||
|
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, machineIdentityRolePermission);
|
||||||
|
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
||||||
|
message: "Failed to update more privileged MI"
|
||||||
|
});
|
||||||
|
|
||||||
if (role) {
|
if (role) {
|
||||||
const rolePermission = await getOrgRolePermissions(role, machineIdentity.organization.toString());
|
const rolePermission = await getOrgRolePermissions(role, machineMembershipOrg.organization.toString());
|
||||||
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission);
|
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission);
|
||||||
|
|
||||||
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
||||||
@@ -555,14 +594,14 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
customRole = await Role.findOne({
|
customRole = await Role.findOne({
|
||||||
slug: role,
|
slug: role,
|
||||||
isOrgRole: true,
|
isOrgRole: true,
|
||||||
organization: machineIdentity.organization
|
organization: machineMembershipOrg.organization
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!customRole) throw BadRequestError({ message: "Role not found" });
|
if (!customRole) throw BadRequestError({ message: "Role not found" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const plan = await EELicenseService.getPlan(machineIdentity.organization);
|
const plan = await EELicenseService.getPlan(machineMembershipOrg.organization);
|
||||||
|
|
||||||
// validate client secret trusted ips
|
// validate client secret trusted ips
|
||||||
let reformattedClientSecretTrustedIps;
|
let reformattedClientSecretTrustedIps;
|
||||||
@@ -600,7 +639,7 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
machineIdentity = await MachineIdentity.findByIdAndUpdate(
|
const machineIdentity = await MachineIdentity.findByIdAndUpdate(
|
||||||
machineId,
|
machineId,
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
@@ -669,38 +708,51 @@ export const deleteMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
params: { machineId }
|
params: { machineId }
|
||||||
} = await validateRequest(reqValidator.DeleteMachineIdentityV3, req);
|
} = await validateRequest(reqValidator.DeleteMachineIdentityV3, req);
|
||||||
|
|
||||||
let machineIdentity = await MachineIdentity.findById(machineId);
|
const machineMembershipOrg = await MachineMembershipOrg
|
||||||
if (!machineIdentity) throw ResourceNotFoundError({
|
.findOne({
|
||||||
message: `Machine identity with id ${machineId} not found`
|
machineIdentity: new Types.ObjectId(machineId)
|
||||||
|
})
|
||||||
|
.populate<{
|
||||||
|
machineIdentity: IMachineIdentity,
|
||||||
|
customRole: IRole
|
||||||
|
}>("machineIdentity customRole");
|
||||||
|
|
||||||
|
if (!machineMembershipOrg) throw ResourceNotFoundError({
|
||||||
|
message: `Failed to find machine identity with id ${machineId}`
|
||||||
});
|
});
|
||||||
|
|
||||||
const { permission } = await getUserOrgPermissions(req.user._id, machineIdentity.organization.toString());
|
const { permission } = await getUserOrgPermissions(req.user._id, machineMembershipOrg.organization.toString());
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
OrgPermissionActions.Delete,
|
OrgPermissionActions.Delete,
|
||||||
OrgPermissionSubjects.MachineIdentity
|
OrgPermissionSubjects.MachineIdentity
|
||||||
);
|
);
|
||||||
|
|
||||||
machineIdentity = await MachineIdentity.findByIdAndDelete(machineId);
|
const machineIdentityRolePermission = await getOrgRolePermissions(
|
||||||
|
machineMembershipOrg?.customRole?.slug ?? machineMembershipOrg.role,
|
||||||
if (!machineIdentity) throw BadRequestError({
|
machineMembershipOrg.organization.toString()
|
||||||
message: "Failed to delete service token"
|
);
|
||||||
|
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, machineIdentityRolePermission);
|
||||||
|
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
||||||
|
message: "Failed to delete more privileged MI"
|
||||||
});
|
});
|
||||||
|
|
||||||
const machineMembershipOrg = await MachineMembershipOrg.findOneAndDelete({
|
const machineIdentity = await MachineIdentity.findByIdAndDelete(machineMembershipOrg.machineIdentity);
|
||||||
machineIdentity: machineIdentity._id,
|
if (!machineIdentity) throw ResourceNotFoundError({
|
||||||
|
message: `Machine identity with id ${machineId} not found`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await MachineMembershipOrg.findByIdAndDelete(machineMembershipOrg._id);
|
||||||
|
|
||||||
if (!machineMembershipOrg) throw BadRequestError({
|
if (!machineMembershipOrg) throw BadRequestError({
|
||||||
message: "Failed to delete service token"
|
message: `Failed to delete machine identity with id ${machineId}`
|
||||||
});
|
});
|
||||||
|
|
||||||
await MachineMembership.deleteMany({
|
await MachineMembership.deleteMany({
|
||||||
machineIdentity: machineIdentity._id,
|
machineIdentity: machineMembershipOrg.machineIdentity
|
||||||
});
|
});
|
||||||
|
|
||||||
await MachineIdentityClientSecretData.deleteMany({
|
await MachineIdentityClientSecret.deleteMany({
|
||||||
machineIdentity: machineIdentity._id
|
machineIdentity: machineMembershipOrg.machineIdentity
|
||||||
});
|
});
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
await EEAuditLogService.createAuditLog(
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ router.delete(
|
|||||||
machineIdentityController.deleteMIClientSecret
|
machineIdentityController.deleteMIClientSecret
|
||||||
);
|
);
|
||||||
|
|
||||||
// consider moving to /auth/app/login
|
// consider moving to /auth/machine/login
|
||||||
router.post(
|
router.post(
|
||||||
"/login",
|
"/login",
|
||||||
machineIdentityController.loginMI
|
machineIdentityController.loginMI
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class EELicenseService {
|
|||||||
secretVersioning: true,
|
secretVersioning: true,
|
||||||
pitRecovery: false,
|
pitRecovery: false,
|
||||||
ipAllowlisting: false,
|
ipAllowlisting: false,
|
||||||
rbac: false,
|
rbac: true,
|
||||||
customRateLimits: false,
|
customRateLimits: false,
|
||||||
customAlerts: false,
|
customAlerts: false,
|
||||||
auditLogs: false,
|
auditLogs: false,
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ const main = async () => {
|
|||||||
app.use("/api/v1/sso", eeSSORouter);
|
app.use("/api/v1/sso", eeSSORouter);
|
||||||
app.use("/api/v1/cloud-products", eeCloudProductsRouter);
|
app.use("/api/v1/cloud-products", eeCloudProductsRouter);
|
||||||
app.use("/api/v3/api-key", v3apiKeyDataRouter);
|
app.use("/api/v3/api-key", v3apiKeyDataRouter);
|
||||||
app.use("/api/v3/machines", v3MachineIdentityRouter);
|
app.use("/api/v3/machines", v3MachineIdentityRouter); // TODO: consider moving to v1
|
||||||
app.use("/api/v1/secret-rotation-providers", v1SecretRotationProviderRouter);
|
app.use("/api/v1/secret-rotation-providers", v1SecretRotationProviderRouter);
|
||||||
app.use("/api/v1/secret-rotations", v1SecretRotation);
|
app.use("/api/v1/secret-rotations", v1SecretRotation);
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export * from "./userAction";
|
|||||||
export * from "./workspace";
|
export * from "./workspace";
|
||||||
export * from "./serviceTokenData"; // TODO: deprecate
|
export * from "./serviceTokenData"; // TODO: deprecate
|
||||||
export * from "./machineIdentity";
|
export * from "./machineIdentity";
|
||||||
export * from "./machineIdentityClientSecretData";
|
export * from "./machineIdentityClientSecret";
|
||||||
export * from "./machineMembershipOrg";
|
export * from "./machineMembershipOrg";
|
||||||
export * from "./machineMembership";
|
export * from "./machineMembership";
|
||||||
export * from "./apiKeyData"; // TODO: deprecate
|
export * from "./apiKeyData"; // TODO: deprecate
|
||||||
|
|||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
import { Document, Schema, Types, model } from "mongoose";
|
import { Document, Schema, Types, model } from "mongoose";
|
||||||
|
|
||||||
export interface IMachineIdentityClientSecretData extends Document {
|
export interface IMachineIdentityClientSecret extends Document {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
machineIdentity: Types.ObjectId;
|
machineIdentity: Types.ObjectId;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@@ -16,7 +16,7 @@ export interface IMachineIdentityClientSecretData extends Document {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
const machineIdentityClientSecretDataSchema = new Schema(
|
const machineIdentityClientSecretSchema = new Schema(
|
||||||
{
|
{
|
||||||
machineIdentity: {
|
machineIdentity: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
@@ -74,8 +74,8 @@ const machineIdentityClientSecretDataSchema = new Schema(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
machineIdentityClientSecretDataSchema.index(
|
machineIdentityClientSecretSchema.index(
|
||||||
{ machineIdentity: 1, isActive: 1 }
|
{ machineIdentity: 1, isActive: 1 }
|
||||||
)
|
)
|
||||||
|
|
||||||
export const MachineIdentityClientSecretData = model<IMachineIdentityClientSecretData>("MachineIdentityClientSecretData", machineIdentityClientSecretDataSchema);
|
export const MachineIdentityClientSecret = model<IMachineIdentityClientSecret>("MachineIdentityClientSecret", machineIdentityClientSecretSchema);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import { MachineIdentity, MachineIdentityClientSecretData } from "../../../models";
|
import { MachineIdentity, MachineIdentityClientSecret } from "../../../models";
|
||||||
import { getAuthSecret } from "../../../config";
|
import { getAuthSecret } from "../../../config";
|
||||||
import { AuthTokenType } from "../../../variables";
|
import { AuthTokenType } from "../../../variables";
|
||||||
import { UnauthorizedRequestError } from "../../errors";
|
import { UnauthorizedRequestError } from "../../errors";
|
||||||
@@ -18,14 +18,14 @@ export const validateMachineIdentity = async ({
|
|||||||
|
|
||||||
if (decodedToken.authTokenType !== AuthTokenType.MACHINE_ACCESS_TOKEN) throw UnauthorizedRequestError();
|
if (decodedToken.authTokenType !== AuthTokenType.MACHINE_ACCESS_TOKEN) throw UnauthorizedRequestError();
|
||||||
|
|
||||||
const machineIdentityClientSecretData = await MachineIdentityClientSecretData.findOne({
|
const machineIdentityClientSecret = await MachineIdentityClientSecret.findOne({
|
||||||
_id: new Types.ObjectId(decodedToken.clientSecretDataId),
|
_id: new Types.ObjectId(decodedToken.clientSecretDataId),
|
||||||
isActive: true
|
isActive: true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!machineIdentityClientSecretData) throw UnauthorizedRequestError();
|
if (!machineIdentityClientSecret) throw UnauthorizedRequestError();
|
||||||
|
|
||||||
if (decodedToken.tokenVersion !== machineIdentityClientSecretData.accessTokenVersion) {
|
if (decodedToken.tokenVersion !== machineIdentityClientSecret.accessTokenVersion) {
|
||||||
// TODO: raise alarm
|
// TODO: raise alarm
|
||||||
throw UnauthorizedRequestError({
|
throw UnauthorizedRequestError({
|
||||||
message: "Failed to authenticate",
|
message: "Failed to authenticate",
|
||||||
@@ -33,7 +33,7 @@ export const validateMachineIdentity = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const machineIdentity = await MachineIdentity.findByIdAndUpdate(
|
const machineIdentity = await MachineIdentity.findByIdAndUpdate(
|
||||||
machineIdentityClientSecretData.machineIdentity,
|
machineIdentityClientSecret.machineIdentity,
|
||||||
{
|
{
|
||||||
accessTokenLastUsed: new Date(),
|
accessTokenLastUsed: new Date(),
|
||||||
$inc: { accessTokenUsageCount: 1 }
|
$inc: { accessTokenUsageCount: 1 }
|
||||||
|
|||||||
@@ -132,6 +132,13 @@ In the following steps, we explore how to create and use MIs for your applicatio
|
|||||||
- You are attempting to access a `/raw` secrets endpoint that requires your project to disable E2EE.
|
- You are attempting to access a `/raw` secrets endpoint that requires your project to disable E2EE.
|
||||||
- The client secret/access token is being used from an untrusted IP.
|
- The client secret/access token is being used from an untrusted IP.
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
<Accordion title="Why can I not create, read, update, or delete a machine identity?">
|
||||||
|
There are a few reasons for why this might happen:
|
||||||
|
|
||||||
|
- You have insufficient organization permissions to create, read, update, delete machine identities.
|
||||||
|
- The MI you are trying to read, update, or delete is more privileged than yourself.
|
||||||
|
- The role you are trying to create a MI for or update a MI to is more privileged than yours.
|
||||||
|
</Accordion>
|
||||||
<Accordion title="Can you provide examples for using glob patterns?">
|
<Accordion title="Can you provide examples for using glob patterns?">
|
||||||
1. `/**`: This pattern matches all folders at any depth in the directory structure. For example, it would match folders like `/folder1/`, `/folder1/subfolder/`, and so on.
|
1. `/**`: This pattern matches all folders at any depth in the directory structure. For example, it would match folders like `/folder1/`, `/folder1/subfolder/`, and so on.
|
||||||
|
|
||||||
|
|||||||
+1
@@ -244,6 +244,7 @@ export const AddMachineIdentityModal = ({
|
|||||||
|
|
||||||
reset();
|
reset();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
const error = err as any;
|
const error = err as any;
|
||||||
const text = error?.response?.data?.message
|
const text = error?.response?.data?.message
|
||||||
?? `Failed to ${popUp?.machineIdentity?.data ? "updated" : "created"} machine identity`;
|
?? `Failed to ${popUp?.machineIdentity?.data ? "updated" : "created"} machine identity`;
|
||||||
|
|||||||
+4
-1
@@ -81,8 +81,11 @@ export const MachineIdentityTable = ({
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
const error = err as any;
|
||||||
|
const text = error?.response?.data?.message ?? "Failed to update machine identity role"
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Failed to update machine identity role",
|
text,
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-8
@@ -35,7 +35,7 @@ export const MachineIdentitySection = withProjectPermission(
|
|||||||
"upgradePlan"
|
"upgradePlan"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const onRemoveServiceTokenDataSubmit = async (machineId: string) => {
|
const onRemoveMachineIdentitySubmit = async (machineId: string) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
await deleteMutateAsync({
|
await deleteMutateAsync({
|
||||||
@@ -44,17 +44,20 @@ export const MachineIdentitySection = withProjectPermission(
|
|||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully removed service account from project",
|
text: "Successfully removed machine identity from project",
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
|
|
||||||
handlePopUpClose("deleteMachineIdentity");
|
handlePopUpClose("deleteMachineIdentity");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({
|
const error = err as any;
|
||||||
text: "Failed to delete service account from project",
|
const text = error?.response?.data?.message ?? "Failed to remove machine identity from project"
|
||||||
type: "error"
|
|
||||||
});
|
createNotification({
|
||||||
|
text,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +99,7 @@ export const MachineIdentitySection = withProjectPermission(
|
|||||||
onChange={(isOpen) => handlePopUpToggle("deleteMachineIdentity", isOpen)}
|
onChange={(isOpen) => handlePopUpToggle("deleteMachineIdentity", isOpen)}
|
||||||
deleteKey="confirm"
|
deleteKey="confirm"
|
||||||
onDeleteApproved={() =>
|
onDeleteApproved={() =>
|
||||||
onRemoveServiceTokenDataSubmit(
|
onRemoveMachineIdentitySubmit(
|
||||||
(popUp?.deleteMachineIdentity?.data as { machineId: string })?.machineId
|
(popUp?.deleteMachineIdentity?.data as { machineId: string })?.machineId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -89,8 +89,11 @@ export const MachineIdentityTable = ({
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
const error = err as any;
|
||||||
|
const text = error?.response?.data?.message ?? "Failed to update machine identity role"
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Failed to update machine identity role",
|
text,
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user