diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index e8d68d06a..7f2cc3441 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -1,6 +1,7 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import { + IMachineIdentity, Key, MachineIdentity, MachineMembership, @@ -8,7 +9,7 @@ import { ServiceTokenData, Workspace } from "../../models"; -import { Role } from "../../ee/models"; +import { IRole, Role } from "../../ee/models"; import { pullSecrets as pull, v2PushSecrets as push, @@ -534,24 +535,26 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => { }); 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); - if (!machineIdentity) throw ResourceNotFoundError(); + if (!machineIdentity) throw ResourceNotFoundError({ + message: `Failed to find machine identity with id ${machineId}` + }); const workspace = await Workspace.findById(workspaceId); 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 add machine identity to project in another organization" }); const rolePermission = await getRolePermissions(role, workspaceId); - const hasRequiredPrivileges = isAtLeastAsPrivilegedWorkspace(permission, rolePermission); + const isAsPrivilegedAsIntendedRole = isAtLeastAsPrivilegedWorkspace(permission, rolePermission); - if (!hasRequiredPrivileges) throw ForbiddenRequestError({ - message: "Failed to add a more privileged MI to project" + if (!isAsPrivilegedAsIntendedRole) throw ForbiddenRequestError({ + message: "Failed to add MI to project with more privileged role" }); let customRole; @@ -604,29 +607,33 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => { ProjectPermissionSub.MachineIdentity ); - let machineMembership = await MachineMembership.findOne({ - machineIdentity: new Types.ObjectId(machineId), - workspace: new Types.ObjectId(workspaceId) - }); + let machineMembership = await MachineMembership + .findOne({ + machineIdentity: new Types.ObjectId(machineId), + workspace: new Types.ObjectId(workspaceId) + }) + .populate<{ + machineIdentity: IMachineIdentity, + customRole: IRole + }>("machineIdentity customRole"); 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); - if (!workspace) throw ResourceNotFoundError(); - - if (!machineIdentity.organization.equals(workspace.organization)) throw BadRequestError({ - message: "Failed to update machine identity in workspace in another organization" + const machineIdentityRolePermission = await getRolePermissions( + machineMembership?.customRole?.slug ?? machineMembership.role, + machineMembership.workspace.toString() + ); + 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 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" }); @@ -646,7 +653,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => { machineMembership = await MachineMembership.findOneAndUpdate( { - machineIdentity: machineIdentity._id, + machineIdentity: machineMembership.machineIdentity, workspace: new Types.ObjectId(workspaceId), }, { @@ -684,12 +691,30 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => { ProjectPermissionSub.MachineIdentity ); - const machineMembership = await MachineMembership.findOneAndDelete({ - machineIdentity: new Types.ObjectId(machineId), - workspace: new Types.ObjectId(workspaceId) + const machineMembership = await MachineMembership + .findOne({ + 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({ machineMembership diff --git a/backend/src/ee/controllers/v3/machineIdentityController.ts b/backend/src/ee/controllers/v3/machineIdentityController.ts index 0a77c1550..489338362 100644 --- a/backend/src/ee/controllers/v3/machineIdentityController.ts +++ b/backend/src/ee/controllers/v3/machineIdentityController.ts @@ -4,10 +4,10 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import { IMachineIdentity, - IMachineIdentityClientSecretData, + IMachineIdentityClientSecret, IMachineIdentityTrustedIp, MachineIdentity, - MachineIdentityClientSecretData, + MachineIdentityClientSecret, MachineMembership, MachineMembershipOrg, Organization, @@ -15,6 +15,7 @@ import { import { ActorType, EventType, + IRole, Role } from "../../models"; import { validateRequest } from "../../../helpers/validation"; @@ -25,7 +26,12 @@ import { getUserOrgPermissions, isAtLeastAsPrivilegedOrg } 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 { EEAuditLogService, EELicenseService } from "../../services"; import { getAuthSecret, getSaltRounds } from "../../../config"; @@ -38,15 +44,15 @@ import { ForbiddenError } from "@casl/ability"; import { checkIPAgainstBlocklist } from "../../../utils/ip"; import { getUserAgentType } from "../../../utils/posthog"; -const packageClientSecretData = (clientSecretData: IMachineIdentityClientSecretData) => ({ - _id: clientSecretData._id, - machineIdentity: clientSecretData.machineIdentity, - isActive: clientSecretData.isActive, - description: clientSecretData.description, - clientSecretPrefix: clientSecretData.clientSecretPrefix, - clientSecretNumUses: clientSecretData.clientSecretNumUses, - clientSecretNumUsesLimit: clientSecretData.clientSecretNumUsesLimit, - clientSecretTTL: clientSecretData.clientSecretTTL +const packageClientSecretData = (machineIdentityClientSecret: IMachineIdentityClientSecret) => ({ + _id: machineIdentityClientSecret._id, + machineIdentity: machineIdentityClientSecret.machineIdentity, + isActive: machineIdentityClientSecret.isActive, + description: machineIdentityClientSecret.description, + clientSecretPrefix: machineIdentityClientSecret.clientSecretPrefix, + clientSecretNumUses: machineIdentityClientSecret.clientSecretNumUses, + clientSecretNumUsesLimit: machineIdentityClientSecret.clientSecretNumUsesLimit, + clientSecretTTL: machineIdentityClientSecret.clientSecretTTL }); /** @@ -63,7 +69,10 @@ export const getMIClientSecrets = async (req: Request, res: Response) => { const machineMembershipOrg = await MachineMembershipOrg.findOne({ machineIdentity: new Types.ObjectId(machineId) - }).populate<{ machineIdentity: IMachineIdentity }>("machineIdentity"); + }).populate<{ + machineIdentity: IMachineIdentity, + customRole: IRole + }>("machineIdentity customRole"); if (!machineMembershipOrg) throw ResourceNotFoundError(); @@ -74,14 +83,17 @@ export const getMIClientSecrets = async (req: Request, res: Response) => { 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); if (!hasRequiredPrivileges) throw ForbiddenRequestError({ message: "Failed to get client secrets for more privileged MI" }); - const clientSecretData = await MachineIdentityClientSecretData + const clientSecretData = await MachineIdentityClientSecret .find({ machineIdentity: machineMembershipOrg.machineIdentity, isActive: true @@ -127,7 +139,10 @@ export const createMIClientSecret = async (req: Request, res: Response) => { const machineMembershipOrg = await MachineMembershipOrg.findOne({ machineIdentity: new Types.ObjectId(machineId) - }).populate<{ machineIdentity: IMachineIdentity }>("machineIdentity"); + }).populate<{ + machineIdentity: IMachineIdentity, + customRole: IRole + }>("machineIdentity customRole"); if (!machineMembershipOrg) throw ResourceNotFoundError(); @@ -138,7 +153,10 @@ export const createMIClientSecret = async (req: Request, res: Response) => { 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); if (!hasRequiredPrivileges) throw ForbiddenRequestError({ @@ -148,7 +166,7 @@ export const createMIClientSecret = async (req: Request, res: Response) => { const clientSecret = crypto.randomBytes(32).toString("hex"); const clientSecretHash = await bcrypt.hash(clientSecret, await getSaltRounds()); - const machineIdentityClientSecretData = await new MachineIdentityClientSecretData({ + const machineIdentityClientSecret = await new MachineIdentityClientSecret({ machineIdentity: machineMembershipOrg.machineIdentity, isActive: true, description, @@ -167,7 +185,7 @@ export const createMIClientSecret = async (req: Request, res: Response) => { metadata: { machineId: machineMembershipOrg.machineIdentity._id.toString(), 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({ clientSecret, - clientSecretData: packageClientSecretData(machineIdentityClientSecretData) + clientSecretData: packageClientSecretData(machineIdentityClientSecret) }); } @@ -194,11 +212,18 @@ export const deleteMIClientSecret = async (req: Request, res: Response) => { } } = await validateRequest(reqValidator.DeleteClientSecretV3, req); - const machineMembershipOrg = await MachineMembershipOrg.findOne({ - machineIdentity: new Types.ObjectId(machineId) - }).populate<{ machineIdentity: IMachineIdentity }>("machineIdentity"); + const machineMembershipOrg = await MachineMembershipOrg + .findOne({ + 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()); @@ -207,19 +232,22 @@ export const deleteMIClientSecret = async (req: Request, res: Response) => { 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); if (!hasRequiredPrivileges) throw ForbiddenRequestError({ message: "Failed to delete client secrets for more privileged MI" }); - const clientSecretData = await MachineIdentityClientSecretData.findOneAndDelete({ + const machineIdentityClientSecret = await MachineIdentityClientSecret.findOneAndDelete({ _id: clientSecretId, machineIdentity: machineId }); - if (!clientSecretData) throw ResourceNotFoundError(); + if (!machineIdentityClientSecret) throw ResourceNotFoundError(); await EEAuditLogService.createAuditLog( req.authData, @@ -237,7 +265,7 @@ export const deleteMIClientSecret = async (req: Request, res: Response) => { ); 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 }); - const clientSecretData = await MachineIdentityClientSecretData.find({ + const clientSecretData = await MachineIdentityClientSecret.find({ machineIdentity: machineIdentity._id, isActive: true }); - let validatedClientSecretDatum: IMachineIdentityClientSecretData | undefined; + let validatedClientSecretDatum: IMachineIdentityClientSecret | undefined; for (const clientSecretDatum of clientSecretData) { 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); if (expiresAt < new Date()) { - await MachineIdentityClientSecretData.findByIdAndUpdate( + await MachineIdentityClientSecret.findByIdAndUpdate( validatedClientSecretDatum._id, { isActive: false - }, - { - new: true } ); @@ -315,7 +340,7 @@ export const loginMI = async (req: Request, res: Response) => { if (clientSecretNumUses > 0 && clientSecretNumUses === clientSecretNumUsesLimit) { // number of times client secret can be used for // a login operation reached - await MachineIdentityClientSecretData.findByIdAndUpdate( + await MachineIdentityClientSecret.findByIdAndUpdate( validatedClientSecretDatum._id, { isActive: false @@ -329,7 +354,7 @@ export const loginMI = async (req: Request, res: Response) => { } // increment usage count by 1 - await MachineIdentityClientSecretData.findByIdAndUpdate( + await MachineIdentityClientSecret.findByIdAndUpdate( validatedClientSecretDatum._id, { $inc: { clientSecretNumUses: 1 } @@ -342,7 +367,7 @@ export const loginMI = async (req: Request, res: Response) => { // token version const accessToken = createToken({ payload: { - machineId: machineIdentity._id.toString(), // consider changing to clientId and making it more extensible + machineId: machineIdentity._id.toString(), clientSecretDataId: validatedClientSecretDatum._id.toString(), authTokenType: AuthTokenType.MACHINE_ACCESS_TOKEN, tokenVersion: validatedClientSecretDatum.accessTokenVersion @@ -525,22 +550,36 @@ export const updateMachineIdentity = async (req: Request, res: Response) => { } } = await validateRequest(reqValidator.UpdateMachineIdentityV3, req); - let machineIdentity = await MachineIdentity.findById(machineId); - if (!machineIdentity) throw ResourceNotFoundError({ - message: `Machine identity with id ${machineId} not found` + const machineMembershipOrg = await MachineMembershipOrg + .findOne({ + 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( OrgPermissionActions.Edit, 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) { - const rolePermission = await getOrgRolePermissions(role, machineIdentity.organization.toString()); + const rolePermission = await getOrgRolePermissions(role, machineMembershipOrg.organization.toString()); const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission); if (!hasRequiredPrivileges) throw ForbiddenRequestError({ @@ -555,14 +594,14 @@ export const updateMachineIdentity = async (req: Request, res: Response) => { customRole = await Role.findOne({ slug: role, isOrgRole: true, - organization: machineIdentity.organization + organization: machineMembershipOrg.organization }); 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 let reformattedClientSecretTrustedIps; @@ -600,7 +639,7 @@ export const updateMachineIdentity = async (req: Request, res: Response) => { }); } - machineIdentity = await MachineIdentity.findByIdAndUpdate( + const machineIdentity = await MachineIdentity.findByIdAndUpdate( machineId, { name, @@ -669,38 +708,51 @@ export const deleteMachineIdentity = async (req: Request, res: Response) => { params: { machineId } } = await validateRequest(reqValidator.DeleteMachineIdentityV3, req); - let machineIdentity = await MachineIdentity.findById(machineId); - if (!machineIdentity) throw ResourceNotFoundError({ - message: `Machine identity with id ${machineId} not found` - }); - - const { permission } = await getUserOrgPermissions(req.user._id, machineIdentity.organization.toString()); + const machineMembershipOrg = await MachineMembershipOrg + .findOne({ + 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, machineMembershipOrg.organization.toString()); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionActions.Delete, 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); - - if (!machineIdentity) throw BadRequestError({ - message: "Failed to delete service token" + const machineIdentity = await MachineIdentity.findByIdAndDelete(machineMembershipOrg.machineIdentity); + if (!machineIdentity) throw ResourceNotFoundError({ + message: `Machine identity with id ${machineId} not found` }); - const machineMembershipOrg = await MachineMembershipOrg.findOneAndDelete({ - machineIdentity: machineIdentity._id, - }); + await MachineMembershipOrg.findByIdAndDelete(machineMembershipOrg._id); if (!machineMembershipOrg) throw BadRequestError({ - message: "Failed to delete service token" + message: `Failed to delete machine identity with id ${machineId}` }); await MachineMembership.deleteMany({ - machineIdentity: machineIdentity._id, + machineIdentity: machineMembershipOrg.machineIdentity }); - await MachineIdentityClientSecretData.deleteMany({ - machineIdentity: machineIdentity._id + await MachineIdentityClientSecret.deleteMany({ + machineIdentity: machineMembershipOrg.machineIdentity }); await EEAuditLogService.createAuditLog( diff --git a/backend/src/ee/routes/v3/machineIdentity.ts b/backend/src/ee/routes/v3/machineIdentity.ts index 5b9649566..66d32b13c 100644 --- a/backend/src/ee/routes/v3/machineIdentity.ts +++ b/backend/src/ee/routes/v3/machineIdentity.ts @@ -28,7 +28,7 @@ router.delete( machineIdentityController.deleteMIClientSecret ); -// consider moving to /auth/app/login +// consider moving to /auth/machine/login router.post( "/login", machineIdentityController.loginMI diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index 6baea04dc..7052263c5 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -66,7 +66,7 @@ class EELicenseService { secretVersioning: true, pitRecovery: false, ipAllowlisting: false, - rbac: false, + rbac: true, customRateLimits: false, customAlerts: false, auditLogs: false, diff --git a/backend/src/index.ts b/backend/src/index.ts index 9d50af3ab..8c608b3c6 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -206,7 +206,7 @@ const main = async () => { app.use("/api/v1/sso", eeSSORouter); app.use("/api/v1/cloud-products", eeCloudProductsRouter); 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-rotations", v1SecretRotation); diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index ef6d2db80..b0f2b7c94 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -21,7 +21,7 @@ export * from "./userAction"; export * from "./workspace"; export * from "./serviceTokenData"; // TODO: deprecate export * from "./machineIdentity"; -export * from "./machineIdentityClientSecretData"; +export * from "./machineIdentityClientSecret"; export * from "./machineMembershipOrg"; export * from "./machineMembership"; export * from "./apiKeyData"; // TODO: deprecate diff --git a/backend/src/models/machineIdentityClientSecretData.ts b/backend/src/models/machineIdentityClientSecret.ts similarity index 85% rename from backend/src/models/machineIdentityClientSecretData.ts rename to backend/src/models/machineIdentityClientSecret.ts index 42a6c0f44..93b1851ef 100644 --- a/backend/src/models/machineIdentityClientSecretData.ts +++ b/backend/src/models/machineIdentityClientSecret.ts @@ -1,6 +1,6 @@ import { Document, Schema, Types, model } from "mongoose"; -export interface IMachineIdentityClientSecretData extends Document { +export interface IMachineIdentityClientSecret extends Document { _id: Types.ObjectId; machineIdentity: Types.ObjectId; isActive: boolean; @@ -16,7 +16,7 @@ export interface IMachineIdentityClientSecretData extends Document { createdAt: Date; } -const machineIdentityClientSecretDataSchema = new Schema( +const machineIdentityClientSecretSchema = new Schema( { machineIdentity: { type: Schema.Types.ObjectId, @@ -74,8 +74,8 @@ const machineIdentityClientSecretDataSchema = new Schema( } ); -machineIdentityClientSecretDataSchema.index( +machineIdentityClientSecretSchema.index( { machineIdentity: 1, isActive: 1 } ) -export const MachineIdentityClientSecretData = model("MachineIdentityClientSecretData", machineIdentityClientSecretDataSchema); \ No newline at end of file +export const MachineIdentityClientSecret = model("MachineIdentityClientSecret", machineIdentityClientSecretSchema); \ No newline at end of file diff --git a/backend/src/utils/authn/authModeValidators/machineIdentity.ts b/backend/src/utils/authn/authModeValidators/machineIdentity.ts index f8c9a7b27..31e317d20 100644 --- a/backend/src/utils/authn/authModeValidators/machineIdentity.ts +++ b/backend/src/utils/authn/authModeValidators/machineIdentity.ts @@ -1,6 +1,6 @@ import jwt from "jsonwebtoken"; import { Types } from "mongoose"; -import { MachineIdentity, MachineIdentityClientSecretData } from "../../../models"; +import { MachineIdentity, MachineIdentityClientSecret } from "../../../models"; import { getAuthSecret } from "../../../config"; import { AuthTokenType } from "../../../variables"; import { UnauthorizedRequestError } from "../../errors"; @@ -18,14 +18,14 @@ export const validateMachineIdentity = async ({ 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), isActive: true }); - if (!machineIdentityClientSecretData) throw UnauthorizedRequestError(); + if (!machineIdentityClientSecret) throw UnauthorizedRequestError(); - if (decodedToken.tokenVersion !== machineIdentityClientSecretData.accessTokenVersion) { + if (decodedToken.tokenVersion !== machineIdentityClientSecret.accessTokenVersion) { // TODO: raise alarm throw UnauthorizedRequestError({ message: "Failed to authenticate", @@ -33,7 +33,7 @@ export const validateMachineIdentity = async ({ } const machineIdentity = await MachineIdentity.findByIdAndUpdate( - machineIdentityClientSecretData.machineIdentity, + machineIdentityClientSecret.machineIdentity, { accessTokenLastUsed: new Date(), $inc: { accessTokenUsageCount: 1 } diff --git a/docs/documentation/platform/machine-identity.mdx b/docs/documentation/platform/machine-identity.mdx index 7865cd5d0..dc957cd19 100644 --- a/docs/documentation/platform/machine-identity.mdx +++ b/docs/documentation/platform/machine-identity.mdx @@ -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. - The client secret/access token is being used from an untrusted IP. + + 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. + 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. diff --git a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/AddMachineIdentityModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/AddMachineIdentityModal.tsx index 1c556c010..a6c026d3b 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/AddMachineIdentityModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/AddMachineIdentityModal.tsx @@ -244,6 +244,7 @@ export const AddMachineIdentityModal = ({ reset(); } catch (err) { + console.error(err); const error = err as any; const text = error?.response?.data?.message ?? `Failed to ${popUp?.machineIdentity?.data ? "updated" : "created"} machine identity`; diff --git a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx index 7fd70da34..2e53c30c3 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx @@ -81,8 +81,11 @@ export const MachineIdentityTable = ({ }); } catch (err) { console.error(err); + const error = err as any; + const text = error?.response?.data?.message ?? "Failed to update machine identity role" + createNotification({ - text: "Failed to update machine identity role", + text, type: "error" }); } diff --git a/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/MachineIdentitySection.tsx b/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/MachineIdentitySection.tsx index e02b03cdb..5083fbb94 100644 --- a/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/MachineIdentitySection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/MachineIdentitySection.tsx @@ -35,7 +35,7 @@ export const MachineIdentitySection = withProjectPermission( "upgradePlan" ] as const); - const onRemoveServiceTokenDataSubmit = async (machineId: string) => { + const onRemoveMachineIdentitySubmit = async (machineId: string) => { try { await deleteMutateAsync({ @@ -44,17 +44,20 @@ export const MachineIdentitySection = withProjectPermission( }); createNotification({ - text: "Successfully removed service account from project", + text: "Successfully removed machine identity from project", type: "success" }); handlePopUpClose("deleteMachineIdentity"); } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete service account from project", - type: "error" - }); + console.error(err); + const error = err as any; + const text = error?.response?.data?.message ?? "Failed to remove machine identity from project" + + createNotification({ + text, + type: "error" + }); } } @@ -96,7 +99,7 @@ export const MachineIdentitySection = withProjectPermission( onChange={(isOpen) => handlePopUpToggle("deleteMachineIdentity", isOpen)} deleteKey="confirm" onDeleteApproved={() => - onRemoveServiceTokenDataSubmit( + onRemoveMachineIdentitySubmit( (popUp?.deleteMachineIdentity?.data as { machineId: string })?.machineId ) } diff --git a/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx b/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx index 0bddc7f21..fa1d76b24 100644 --- a/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx +++ b/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx @@ -89,8 +89,11 @@ export const MachineIdentityTable = ({ }); } catch (err) { console.error(err); + const error = err as any; + const text = error?.response?.data?.message ?? "Failed to update machine identity role" + createNotification({ - text: "Failed to update machine identity role", + text, type: "error" }); }