Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add missing audit logger for pipeline stage deploy #3239

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions forge/auditLog/formatters.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ const isObject = (obj) => {
/**
* Generate a standard format body for the audit log display and database.
* Any items null or missing must not generate a property in the body
* @param {{ error?, team?, project?, sourceProject?, targetProject?, device?, sourceDevice?, targetDevice?, user?, stack?, billingSession?, subscription?, license?, updates?, snapshot?, role?, projectType?, info? } == {}} objects objects to include in body
* @returns {{ error?, team?, project?, sourceProject?, targetProject?, device?, user?, stack?, billingSession?, subscription?, license?, updates?, snapshot?, role?, projectType? info? }
* @param {{ error?, team?, project?, sourceProject?, targetProject?, device?, sourceDevice?, targetDevice?, user?, stack?, billingSession?, subscription?, license?, updates?, snapshot?, pipeline?, pipelineStage?, pipelineStageTarget?, role?, projectType?, info?, interval?, threshold? } == {}} objects objects to include in body
* @returns {{ error?, team?, project?, sourceProject?, targetProject?, device?, user?, stack?, billingSession?, subscription?, license?, updates?, snapshot?, pipeline?, pipelineStage?, pipelineStageTarget?, role?, projectType? info?, interval?, threshold? }
*/
const generateBody = ({ error, team, application, project, sourceProject, targetProject, device, sourceDevice, targetDevice, user, stack, billingSession, subscription, license, updates, snapshot, pipeline, pipelineStage, role, projectType, info, interval, threshold } = {}) => {
const generateBody = ({ error, team, application, project, sourceProject, targetProject, device, sourceDevice, targetDevice, user, stack, billingSession, subscription, license, updates, snapshot, pipeline, pipelineStage, pipelineStageTarget, role, projectType, info, interval, threshold } = {}) => {
const body = {}

if (isObject(error) || typeof error === 'string') {
Expand Down Expand Up @@ -69,6 +69,9 @@ const generateBody = ({ error, team, application, project, sourceProject, target
if (isObject(pipelineStage)) {
body.pipelineStage = pipelineStageObject(pipelineStage)
}
if (isObject(pipelineStageTarget)) {
body.pipelineStageTarget = pipelineStageObject(pipelineStageTarget)
}
if (isObject(role) || typeof role === 'number') {
body.role = roleObject(role)
}
Expand Down Expand Up @@ -152,6 +155,7 @@ const formatLogEntry = (auditLogDbRow) => {
info: body?.info,
pipeline: body?.pipeline,
pipelineStage: body?.pipelineStage,
pipelineStageTarget: body?.pipelineStageTarget,
interval: body?.interval,
threshold: body?.threshold
})
Expand Down
3 changes: 3 additions & 0 deletions forge/auditLog/team.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ module.exports = {
},
async stageAdded (actionedBy, error, team, application, pipeline, pipelineStage) {
await log('application.pipeline.stage-added', actionedBy, team?.id, generateBody({ error, team, application, pipeline, pipelineStage }))
},
async stageDeployed (actionedBy, error, team, application, pipeline, sourcePipelineStage, targetPipelineStage) {
await log('application.pipeline.stage-deployed', actionedBy, team?.id, generateBody({ error, team, application, pipeline, pipelineStage: sourcePipelineStage, pipelineStageTarget: targetPipelineStage }))
}
}
}
Expand Down
17 changes: 14 additions & 3 deletions forge/ee/routes/pipeline/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ const { ControllerError } = require('../../../lib/errors.js')
const { registerPermissions } = require('../../../lib/permissions')
const { Roles } = require('../../../lib/roles.js')

// Declare getLogger functions to provide type hints / quick code nav / code completion
/** @type {import('../../../../forge/auditLog/team').getLoggers} */
const getTeamLogger = (app) => { return app.auditLog.Team }

module.exports = async function (app) {
const teamLogger = getTeamLogger(app)

registerPermissions({
'pipeline:read': { description: 'View a pipeline', role: Roles.Member },
'pipeline:create': { description: 'Create a pipeline', role: Roles.Owner },
Expand Down Expand Up @@ -490,6 +496,7 @@ module.exports = async function (app) {
const user = request.session.User

let repliedEarly = false
let sourceDeployed, deployTarget
try {
const sourceStage = await app.db.models.PipelineStage.byId(
request.params.stageId
Expand Down Expand Up @@ -519,12 +526,14 @@ module.exports = async function (app) {
targetStage
}
)
sourceDeployed = sourceInstance
} else if (sourceDevice) {
sourceSnapshot = await app.db.controllers.Pipeline.getOrCreateSnapshotForSourceDevice(
sourceStage,
sourceDevice,
request.body?.sourceSnapshotId
)
sourceDeployed = sourceDevice
} else {
throw new Error('No source device or instance found.')
}
Expand All @@ -546,7 +555,7 @@ module.exports = async function (app) {

reply.code(200).send({ status: 'importing' })
repliedEarly = true

deployTarget = targetInstance
await deployPromise
} else if (targetDevice) {
const deployPromise = app.db.controllers.Pipeline.deploySnapshotToDevice(
Expand All @@ -559,7 +568,7 @@ module.exports = async function (app) {

reply.code(200).send({ status: 'importing' })
repliedEarly = true

deployTarget = targetDevice
await deployPromise
} else if (targetDeviceGroup) {
const deployPromise = app.db.controllers.Pipeline.deploySnapshotToDeviceGroup(
Expand All @@ -571,11 +580,13 @@ module.exports = async function (app) {

reply.code(200).send({ status: 'importing' })
repliedEarly = true

deployTarget = targetDeviceGroup
await deployPromise
} else {
throw new Error('No target device or instance found.')
}

await teamLogger.application.pipeline.stageDeployed(request.session.User, null, request.application.Team, request.application, request.pipeline, sourceDeployed, deployTarget)
} catch (err) {
if (repliedEarly) {
console.warn('Deploy failed, but response already sent', err)
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/audit-log/AuditEntryIcon.vue
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ const iconMap = {
'application.pipeline.updated',
'application.pipeline.deleted',
'application.pipeline.stage-added',
'application.pipeline.stage-deployed',
'project.assigned-to-pipeline-stage'
],
resource: [
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/components/audit-log/AuditEntryVerbose.vue
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,12 @@
<span v-if="!error && entry.body?.pipeline && entry.body?.pipelineStage">Pipeline Stage '{{ entry.body.pipelineStage?.name }}' was added to the DevOps Pipeline '{{ entry.body.pipeline?.name }}' {{ entry.body.application ? `in Application '${entry.body.application.name}'` : '' }}</span>
<span v-else-if="!error">Pipeline data not found in audit entry.</span>
</template>
<template v-else-if="entry.event === 'application.pipeline.stage-deployed'">
<label>{{ AuditEvents[entry.event] }}</label>
<span v-if="!error && entry.body?.pipeline && entry.body?.pipelineStage && !entry.body?.pipelineStageTarget">Pipeline Stage '{{ entry.body.pipelineStage.name }}' in DevOps Pipeline '{{ entry.body.pipeline.name }}' {{ entry.body.application ? `in Application '${entry.body.application.name}'` : '' }} was deployed</span>
<span v-if="!error && entry.body?.pipeline && entry.body?.pipelineStage && entry.body?.pipelineStageTarget">Pipeline Stage '{{ entry.body.pipelineStage.name }}' in DevOps Pipeline '{{ entry.body.pipeline.name }}' {{ entry.body.application ? `in Application '${entry.body.application.name}'` : '' }} was deployed to '{{ entry.body.pipelineStageTarget.name }}'</span>
<span v-else-if="!error">Pipeline data not found in audit entry.</span>
</template>

<!-- Application Device Events -->
<template v-else-if="entry.event === 'application.device.assigned'">
Expand Down
1 change: 1 addition & 0 deletions frontend/src/data/audit-events.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"application.pipeline.updated": "DevOps Pipeline Updated",
"application.pipeline.deleted": "DevOps Pipeline Deleted",
"application.pipeline.stage-added": "Pipeline Stage Added",
"application.pipeline.stage-deployed": "Pipeline Stage Deployed",
"project.created": "Instance Created",
"project.deleted": "Instance Deleted",
"project.duplicated": "Instance Duplicated",
Expand Down
94 changes: 94 additions & 0 deletions test/unit/forge/auditLog/team_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ describe('Audit Log > Team', async function () {
let ROLE
let BILLING_SESSION
let SUBSCRIPTION
let APPLICATION
let PIPELINE
let PIPELINE_STAGE_1
let PIPELINE_STAGE_2

// temporarily assign the logger purely for type info & intellisense
// so that xxxxxLogger.yyy.zzz function parameters are offered
Expand All @@ -37,6 +41,10 @@ describe('Audit Log > Team', async function () {
ROLE = Roles.Member
BILLING_SESSION = { id: 'billing_session' }
SUBSCRIPTION = { subscription: 'subscription' }
APPLICATION = await app.db.models.Application.create({ name: 'application1', TeamId: TEAM.id })
PIPELINE = { id: 'pipeline', name: 'pipeline', ApplicationId: APPLICATION.id }
PIPELINE_STAGE_1 = { id: 'pipeline_stage_1', name: 'pipeline stage 1', NextStageId: 'pipeline_stage_2', PipelineId: PIPELINE.id }
PIPELINE_STAGE_2 = { id: 'pipeline_stage_2', name: 'pipeline stage 2', NextStageId: null, PipelineId: PIPELINE.id }
})
after(async () => {
await app.close()
Expand Down Expand Up @@ -549,4 +557,90 @@ describe('Audit Log > Team', async function () {
logEntry.body.info.should.have.property('tokenName', 'Token Name 4')
})
// #endregion

// #region Team - application
describe('Application events', async function () {
it('Provides a logger for creating an application in a team', async function () {
await teamLogger.application.created(ACTIONED_BY, null, TEAM, APPLICATION)
// check log stored
const logEntry = await getLog()
logEntry.should.have.property('event', 'application.created')
logEntry.should.have.property('scope', { id: TEAM.hashid, type: 'team' })
logEntry.should.have.property('body').and.be.an.Object()
logEntry.body.should.only.have.keys('application', 'team')
})

it('Provides a logger for updating an application in a team', async function () {
await teamLogger.application.updated(ACTIONED_BY, null, TEAM, APPLICATION, [{ key: 'name', old: 'old', new: 'new' }])
// check log stored
const logEntry = await getLog()
logEntry.should.have.property('event', 'application.updated')
logEntry.should.have.property('scope', { id: TEAM.hashid, type: 'team' })
logEntry.should.have.property('body').and.be.an.Object()
logEntry.body.should.only.have.keys('application', 'team', 'updates')
})

it('Provides a logger for deleting an application in a team', async function () {
await teamLogger.application.deleted(ACTIONED_BY, null, TEAM, APPLICATION)
// check log stored
const logEntry = await getLog()
logEntry.should.have.property('event', 'application.deleted')
logEntry.should.have.property('scope', { id: TEAM.hashid, type: 'team' })
logEntry.should.have.property('body').and.be.an.Object()
logEntry.body.should.only.have.keys('application', 'team')
})

describe('Pipeline events', async function () {
it('Provides a logger for creating a pipeline in an application', async function () {
await teamLogger.application.pipeline.created(ACTIONED_BY, null, TEAM, APPLICATION, PIPELINE)
// check log stored
const logEntry = await getLog()
logEntry.should.have.property('event', 'application.pipeline.created')
logEntry.should.have.property('scope', { id: TEAM.hashid, type: 'team' })
logEntry.should.have.property('body').and.be.an.Object()
logEntry.body.should.only.have.keys('application', 'team', 'pipeline')
})

it('Provides a logger for updating a pipeline in an application', async function () {
await teamLogger.application.pipeline.updated(ACTIONED_BY, null, TEAM, APPLICATION, PIPELINE, [{ key: 'name', old: 'old', new: 'new' }])
// check log stored
const logEntry = await getLog()
logEntry.should.have.property('event', 'application.pipeline.updated')
logEntry.should.have.property('scope', { id: TEAM.hashid, type: 'team' })
logEntry.should.have.property('body').and.be.an.Object()
logEntry.body.should.only.have.keys('application', 'team', 'pipeline', 'updates')
})

it('Provides a logger for deleting a pipeline in an application', async function () {
await teamLogger.application.pipeline.deleted(ACTIONED_BY, null, TEAM, APPLICATION, PIPELINE)
// check log stored
const logEntry = await getLog()
logEntry.should.have.property('event', 'application.pipeline.deleted')
logEntry.should.have.property('scope', { id: TEAM.hashid, type: 'team' })
logEntry.should.have.property('body').and.be.an.Object()
logEntry.body.should.only.have.keys('application', 'team', 'pipeline')
})

it('Provides a logger for adding a stage to a pipeline in an application', async function () {
await teamLogger.application.pipeline.stageAdded(ACTIONED_BY, null, TEAM, APPLICATION, PIPELINE, PIPELINE_STAGE_1)
// check log stored
const logEntry = await getLog()
logEntry.should.have.property('event', 'application.pipeline.stage-added')
logEntry.should.have.property('scope', { id: TEAM.hashid, type: 'team' })
logEntry.should.have.property('body').and.be.an.Object()
logEntry.body.should.only.have.keys('application', 'team', 'pipeline', 'pipelineStage')
})

it('Provides a logger for deploying a stage in a pipeline in an application', async function () {
await teamLogger.application.pipeline.stageDeployed(ACTIONED_BY, null, TEAM, APPLICATION, PIPELINE, PIPELINE_STAGE_1, PIPELINE_STAGE_2)
// check log stored
const logEntry = await getLog()
logEntry.should.have.property('event', 'application.pipeline.stage-deployed')
logEntry.should.have.property('scope', { id: TEAM.hashid, type: 'team' })
logEntry.should.have.property('body').and.be.an.Object()
logEntry.body.should.only.have.keys('application', 'team', 'pipeline', 'pipelineStage', 'pipelineStageTarget')
})
})
})
// #endregion
})