Update authz logic for MI

This commit is contained in:
Tuan Dang
2023-12-05 17:46:50 +07:00
parent c91f6521c1
commit 6787c0eaaa
13 changed files with 209 additions and 115 deletions
@@ -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);
if (!machineIdentity) throw ResourceNotFoundError();
const workspace = await Workspace.findById(workspaceId); const machineIdentityRolePermission = await getRolePermissions(
if (!workspace) throw ResourceNotFoundError(); machineMembership?.customRole?.slug ?? machineMembership.role,
machineMembership.workspace.toString()
if (!machineIdentity.organization.equals(workspace.organization)) throw BadRequestError({ );
message: "Failed to update machine identity in workspace in another organization" const isAsPrivilegedAsMachine = isAtLeastAsPrivilegedWorkspace(permission, machineIdentityRolePermission);
if (!isAsPrivilegedAsMachine) throw ForbiddenRequestError({
message: "Failed to update role of more privileged MI"
}); });
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, machineIdentity.organization.toString()); const { permission } = await getUserOrgPermissions(req.user._id, machineMembershipOrg.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<{
const { permission } = await getUserOrgPermissions(req.user._id, machineIdentity.organization.toString()); 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, machineMembershipOrg.organization.toString());
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete, OrgPermissionActions.Delete,
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 delete more privileged MI"
});
machineIdentity = await MachineIdentity.findByIdAndDelete(machineId); const machineIdentity = await MachineIdentity.findByIdAndDelete(machineMembershipOrg.machineIdentity);
if (!machineIdentity) throw ResourceNotFoundError({
if (!machineIdentity) throw BadRequestError({ message: `Machine identity with id ${machineId} not found`
message: "Failed to delete service token"
}); });
const machineMembershipOrg = await MachineMembershipOrg.findOneAndDelete({ await MachineMembershipOrg.findByIdAndDelete(machineMembershipOrg._id);
machineIdentity: machineIdentity._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(
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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
@@ -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.
@@ -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`;
@@ -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"
}); });
} }
@@ -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
) )
} }
@@ -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"
}); });
} }