diff --git a/backend/src/ee/controllers/v3/machineIdentityController.ts b/backend/src/ee/controllers/v3/machineIdentityController.ts index 183c123ef..66e9c12d2 100644 --- a/backend/src/ee/controllers/v3/machineIdentityController.ts +++ b/backend/src/ee/controllers/v3/machineIdentityController.ts @@ -3,6 +3,7 @@ import crypto from "crypto"; import { Request, Response } from "express"; import { Types } from "mongoose"; import { + IMachineIdentity, IMachineIdentityClientSecretData, IMachineIdentityTrustedIp, MachineIdentity, @@ -12,6 +13,7 @@ import { Organization, } from "../../../models"; import { + ActorType, EventType, Role } from "../../models"; @@ -34,6 +36,7 @@ import { } from "../../services/RoleService"; import { ForbiddenError } from "@casl/ability"; import { checkIPAgainstBlocklist } from "../../../utils/ip"; +import { getUserAgentType } from "../../../utils/posthog"; const packageClientSecretData = (clientSecretData: IMachineIdentityClientSecretData) => ({ _id: clientSecretData._id, @@ -60,7 +63,7 @@ export const getMIClientSecrets = async (req: Request, res: Response) => { const machineMembershipOrg = await MachineMembershipOrg.findOne({ machineIdentity: new Types.ObjectId(machineId) - }); + }).populate<{ machineIdentity: IMachineIdentity }>("machineIdentity"); if (!machineMembershipOrg) throw ResourceNotFoundError(); @@ -85,6 +88,20 @@ export const getMIClientSecrets = async (req: Request, res: Response) => { }) .sort({ createdAt: -1 }) .limit(5); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.GET_MACHINE_IDENTITY_CLIENT_SECRETS, + metadata: { + machineId: machineMembershipOrg.machineIdentity._id.toString(), + clientId: machineMembershipOrg.machineIdentity.clientId, + } + }, + { + organizationId: machineMembershipOrg.organization + } + ); return res.status(200).send({ clientSecretData: clientSecretData.map((clientSecretDatum) => packageClientSecretData(clientSecretDatum)) @@ -110,7 +127,7 @@ export const createMIClientSecret = async (req: Request, res: Response) => { const machineMembershipOrg = await MachineMembershipOrg.findOne({ machineIdentity: new Types.ObjectId(machineId) - }); + }).populate<{ machineIdentity: IMachineIdentity }>("machineIdentity"); if (!machineMembershipOrg) throw ResourceNotFoundError(); @@ -147,6 +164,21 @@ export const createMIClientSecret = async (req: Request, res: Response) => { accessTokenVersion: 1, expiresAt }).save(); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.CREATE_MACHINE_IDENTITY_CLIENT_SECRET, + metadata: { + machineId: machineMembershipOrg.machineIdentity._id.toString(), + clientId: machineMembershipOrg.machineIdentity.clientId, + clientSecretId: machineIdentityClientSecretData._id.toString() + } + }, + { + organizationId: machineMembershipOrg.organization + } + ); return res.status(200).send({ clientSecret, @@ -169,7 +201,7 @@ export const deleteMIClientSecret = async (req: Request, res: Response) => { const machineMembershipOrg = await MachineMembershipOrg.findOne({ machineIdentity: new Types.ObjectId(machineId) - }); + }).populate<{ machineIdentity: IMachineIdentity }>("machineIdentity"); if (!machineMembershipOrg) throw ResourceNotFoundError(); @@ -194,6 +226,21 @@ export const deleteMIClientSecret = async (req: Request, res: Response) => { if (!clientSecretData) throw ResourceNotFoundError(); + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.DELETE_MACHINE_IDENTITY_CLIENT_SECRET, + metadata: { + machineId: machineMembershipOrg.machineIdentity._id.toString(), + clientId: machineMembershipOrg.machineIdentity.clientId, + clientSecretId: clientSecretId + } + }, + { + organizationId: machineMembershipOrg.organization + } + ); + return res.status(200).send({ clientSecretData: packageClientSecretData(clientSecretData) }) @@ -306,6 +353,35 @@ export const loginMI = async (req: Request, res: Response) => { secret: await getAuthSecret() }); + const userAgent = req.headers["user-agent"] ?? ""; + + await EEAuditLogService.createAuditLog( + { + actor: { + type: ActorType.MACHINE, + metadata: { + machineId: machineIdentity._id.toString(), + name: machineIdentity.name + } + }, + authPayload: machineIdentity, + ipAddress: req.realIP, + userAgent, + userAgentType: getUserAgentType(userAgent) + }, + { + type: EventType.LOGIN_MACHINE_IDENTITY, + metadata: { + machineId: machineIdentity._id.toString(), + clientId, + clientSecretId: validatedClientSecretDatum._id.toString() + } + }, + { + organizationId: machineIdentity.organization + } + ); + return res.status(200).send({ accessToken, expiresIn: machineIdentity.accessTokenTTL, diff --git a/backend/src/ee/models/auditLog/enums.ts b/backend/src/ee/models/auditLog/enums.ts index 629c13dc2..f3ffb8e51 100644 --- a/backend/src/ee/models/auditLog/enums.ts +++ b/backend/src/ee/models/auditLog/enums.ts @@ -34,6 +34,10 @@ export enum EventType { CREATE_MACHINE_IDENTITY = "create-machine-identity", UPDATE_MACHINE_IDENTITY = "update-machine-identity", DELETE_MACHINE_IDENTITY = "delete-machine-identity", + LOGIN_MACHINE_IDENTITY = "login-machine-identity", + CREATE_MACHINE_IDENTITY_CLIENT_SECRET = "create-machine-identity-secret", + DELETE_MACHINE_IDENTITY_CLIENT_SECRET = "delete-machine-identity-secret", + GET_MACHINE_IDENTITY_CLIENT_SECRETS = "get-machine-identity-secrets", CREATE_ENVIRONMENT = "create-environment", UPDATE_ENVIRONMENT = "update-environment", DELETE_ENVIRONMENT = "delete-environment", diff --git a/backend/src/ee/models/auditLog/types.ts b/backend/src/ee/models/auditLog/types.ts index a7b802113..e03bc1f7a 100644 --- a/backend/src/ee/models/auditLog/types.ts +++ b/backend/src/ee/models/auditLog/types.ts @@ -225,6 +225,8 @@ interface DeleteServiceTokenEvent { }; } +// TODO: review all logging for MIs including params etc. + interface CreateMachineIdentityEvent { type: EventType.CREATE_MACHINE_IDENTITY; metadata: { @@ -257,6 +259,41 @@ interface DeleteMachineIdentityEvent { }; } +interface LoginMachineIdentityEvent { + type: EventType.LOGIN_MACHINE_IDENTITY ; + metadata: { + machineId: string; + clientId: string; + clientSecretId: string; + }; +} + +interface CreateMachineIdentitySecretEvent { + type: EventType.CREATE_MACHINE_IDENTITY_CLIENT_SECRET ; + metadata: { + machineId: string; + clientId: string; + clientSecretId: string; + }; +} + +interface DeleteMachineIdentitySecretEvent { + type: EventType.DELETE_MACHINE_IDENTITY_CLIENT_SECRET ; + metadata: { + machineId: string; + clientId: string; + clientSecretId: string; + }; +} + +interface GetMachineIdentitySecretsEvent { + type: EventType.GET_MACHINE_IDENTITY_CLIENT_SECRETS ; + metadata: { + machineId: string; + clientId: string; + }; +} + interface CreateEnvironmentEvent { type: EventType.CREATE_ENVIRONMENT; metadata: { @@ -502,6 +539,10 @@ export type Event = | CreateMachineIdentityEvent | UpdateMachineIdentityEvent | DeleteMachineIdentityEvent + | CreateMachineIdentitySecretEvent + | DeleteMachineIdentitySecretEvent + | LoginMachineIdentityEvent + | GetMachineIdentitySecretsEvent | CreateEnvironmentEvent | UpdateEnvironmentEvent | DeleteEnvironmentEvent diff --git a/frontend/src/views/Org/MembersPage/MembersPage.tsx b/frontend/src/views/Org/MembersPage/MembersPage.tsx index 09641cfcf..a2d7e5011 100644 --- a/frontend/src/views/Org/MembersPage/MembersPage.tsx +++ b/frontend/src/views/Org/MembersPage/MembersPage.tsx @@ -29,7 +29,7 @@ export const MembersPage = withPermission(

App Clients

- Beta + New
diff --git a/frontend/src/views/Project/MembersPage/MembersPage.tsx b/frontend/src/views/Project/MembersPage/MembersPage.tsx index 1fa9c7cd0..551b088e2 100644 --- a/frontend/src/views/Project/MembersPage/MembersPage.tsx +++ b/frontend/src/views/Project/MembersPage/MembersPage.tsx @@ -34,7 +34,7 @@ export const MembersPage = withProjectPermission(

App Clients

- Beta + New