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

Multiple Instances: Application Audit Log Events #1979

Merged
merged 3 commits into from
Apr 13, 2023
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
12 changes: 11 additions & 1 deletion forge/auditLog/formatters.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const isObject = (obj) => {
* @param {{ error?, team?, project?, sourceProject?, targetProject?, device?, 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? }
*/
const generateBody = ({ error, team, project, sourceProject, targetProject, device, user, stack, billingSession, subscription, license, updates, snapshot, role, projectType, info } = {}) => {
const generateBody = ({ error, team, application, project, sourceProject, targetProject, device, user, stack, billingSession, subscription, license, updates, snapshot, role, projectType, info } = {}) => {
const body = {}

if (isObject(error) || typeof error === 'string') {
Expand All @@ -19,6 +19,9 @@ const generateBody = ({ error, team, project, sourceProject, targetProject, devi
if (isObject(team)) {
body.team = teamObject(team)
}
if (isObject(application)) {
body.application = applicationObject(application)
}
if (isObject(project)) {
body.project = projectObject(project)
}
Expand Down Expand Up @@ -111,6 +114,7 @@ const formatLogEntry = (auditLogDbRow) => {
formatted.body = generateBody({
error: body?.error,
team: body?.team,
application: body?.application,
project: body?.project,
sourceProject: body?.sourceProject,
targetProject: body?.targetProject,
Expand Down Expand Up @@ -182,6 +186,12 @@ const userObject = (user, unknownValue = null) => {
email: user?.email || unknownValue
}
}
const applicationObject = (application, unknownValue = null) => {
return {
id: application?.id || null,
name: application?.name || unknownValue
}
}
const projectObject = (project, unknownValue = null) => {
return {
id: project?.id || null,
Expand Down
13 changes: 13 additions & 0 deletions forge/auditLog/team.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,18 @@ module.exports = {
}
}

const application = {
async created (actionedBy, error, team, application) {
await log('application.created', actionedBy, team?.id, generateBody({ error, team, application }))
},
async updated (actionedBy, error, team, application, updates) {
await log('application.updated', actionedBy, team?.id, generateBody({ error, team, application, updates }))
},
async deleted (actionedBy, error, team, application) {
await log('application.deleted', actionedBy, team?.id, generateBody({ error, team, application }))
}
}

const billing = {
session: {
async created (actionedBy, error, team, billingSession) {
Expand Down Expand Up @@ -137,6 +149,7 @@ module.exports = {
}
return {
team,
application,
project,
billing
}
Expand Down
12 changes: 11 additions & 1 deletion forge/routes/api/application.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

module.exports = async function (app) {
app.addHook('preHandler', async (request, reply) => {
const applicationId = request.params.applicationId
Expand Down Expand Up @@ -72,6 +71,8 @@ module.exports = async function (app) {
return reply.status(500).send({ code: 'unexpected_error', error: err.toString() })
}

await app.auditLog.Team.application.created(request.session.User, null, team, application)

reply.send(app.db.views.Application.application(application))
})

Expand All @@ -95,8 +96,11 @@ module.exports = async function (app) {
app.put('/:applicationId', {
preHandler: app.needsPermission('project:edit') // TODO For now sharing project permissions
}, async (request, reply) => {
const updates = new app.auditLog.formatters.UpdatesCollection()

try {
const reqName = request.body.name?.trim()
updates.push('name', request.application.name, reqName)
request.application.name = reqName

await request.application.save()
Expand All @@ -107,6 +111,11 @@ module.exports = async function (app) {
return reply.code(500).send({ code: 'unexpected_error', error: error.toString() })
}

const team = request.application.Team
if (team) {
await app.auditLog.Team.application.updated(request.session.User, null, team, request.application, updates)
}

reply.send(app.db.views.Application.application(request.application))
})

Expand All @@ -126,6 +135,7 @@ module.exports = async function (app) {
}

await request.application.destroy()
await app.auditLog.Team.application.deleted(request.session.User, null, request.application.Team, request.application)

reply.send({ status: 'okay' })
} catch (err) {
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/components/audit-log/AuditEntryIcon.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<LogoutIcon v-if="icon === 'logout'" class="ff-icon text-gray-600" />
<ExclamationCircleIcon v-if="icon === 'error'" class="ff-icon text-red-500" />
<TicketIcon v-if="icon === 'token'" class="ff-icon text-blue-500" />
<TemplateIcon v-if="icon === 'template'" class="ff-icon text-red-700" />
</template>

<script>
Expand All @@ -29,7 +30,7 @@ import {
UserIcon, UserGroupIcon, LockClosedIcon,
MailIcon, LoginIcon, LogoutIcon, KeyIcon,
CurrencyDollarIcon, CogIcon, ExclamationCircleIcon,
ChipIcon, IdentificationIcon, TicketIcon
ChipIcon, IdentificationIcon, TicketIcon, TemplateIcon
} from '@heroicons/vue/outline'

const iconMap = {
Expand All @@ -43,6 +44,11 @@ const iconMap = {
'flows.set',
'library.set'
],
template: [
'application.created',
'application.updated',
'application.deleted'
],
project: [
'project.created',
'project.deleted',
Expand Down Expand Up @@ -191,6 +197,7 @@ export default {
LockClosedIcon,
ProjectIcon,
NodeRedIcon,
TemplateIcon,
ChipIcon,
ColorSwatchIcon,
MailIcon,
Expand Down
18 changes: 17 additions & 1 deletion frontend/src/components/audit-log/AuditEntryVerbose.vue
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,22 @@
<span v-else-if="!error">Update data not found in audit entry.</span>
</template>

<template v-else-if="entry.event === 'application.created'">
<label>{{ AuditEvents[entry.event] }}</label>
<span v-if="!error && entry.body?.application">Application {{ entry.body.application?.name }} was created {{ entry.body.team ? `in Team '${entry.body.team.name}'` : '' }}</span>
<span v-else-if="!error">Instance data not found in audit entry.</span>
</template>
<template v-else-if="entry.event === 'application.updated'">
<label>{{ AuditEvents[entry.event] }}</label>
<span v-if="!error && entry.body?.updates">The following updates have been made to the Application: <AuditEntryUpdates :updates="entry.body.updates" />.</span>
<span v-else-if="!error">Updates not found in audit entry.</span>
</template>
<template v-else-if="entry.event === 'application.deleted'">
<label>{{ AuditEvents[entry.event] }}</label>
<span v-if="!error && entry.body?.application">Application {{ entry.body.application?.name }} was deleted {{ entry.body.team ? `in Team '${entry.body.team.name}'` : '' }}</span>
<span v-else-if="!error">Application data not found in audit entry.</span>
</template>

<!-- Instance Events -->
<template v-else-if="entry.event === 'project.created'">
<label>{{ AuditEvents[entry.event] }}</label>
Expand Down Expand Up @@ -391,7 +407,7 @@

<!-- Catch All -->
<template v-else>
<label>{{ AuditEvents[entry.event] }}{{ entry.event }}</label>
<label>{{ AuditEvents[entry.event] }}: {{ entry.event }}</label>
<span>We have no details available for this event type</span>
</template>

Expand Down
3 changes: 3 additions & 0 deletions frontend/src/data/audit-events.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
"team.device.provisioning.created": "Device Provisioning Token Generated",
"team.device.provisioning.updated": "Device Provisioning Token Updated",
"team.device.provisioning.deleted": "Device Provisioning Token Deleted",
"application.created": "Application Created",
"application.updated": "Application Modified",
"application.deleted": "Application Deleted",
"project.created": "Instance Created",
"project.deleted": "Instance Deleted",
"project.duplicated": "Instance Duplicated",
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/pages/application/Settings.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<SectionTopMenu hero="Application Settings" />
<div class="flex flex-col sm:flex-row mt-9 ml-6">
<div class="flex flex-col sm:flex-row mt-9 ml-6" data-el="application-settings">
<SectionSideMenu :options="sideNavigation" />
<div class="space-y-6">
<FormHeading class="mb-6">Application Details</FormHeading>
Expand All @@ -9,18 +9,18 @@
Application ID
</FormRow>

<FormRow id="projectName" ref="appName" v-model="input.projectName" :type="editing ? 'text' : 'uneditable'">
<FormRow id="projectName" ref="appName" v-model="input.projectName" data-form="application-name" :type="editing ? 'text' : 'uneditable'">
Name
</FormRow>
</div>
<div class="space-x-4 whitespace-nowrap">
<template v-if="!editing">
<ff-button kind="primary" @click="editName">Edit Application Name</ff-button>
<ff-button kind="primary" data-action="application-edit" @click="editName">Edit Application Name</ff-button>
</template>
<template v-else>
<div class="flex gap-x-3">
<ff-button kind="secondary" @click="cancelEditName">Cancel</ff-button>
<ff-button kind="primary" :disabled="!formValid" @click="saveApplication">Save</ff-button>
<ff-button kind="primary" :disabled="!formValid" data-form="submit" @click="saveApplication">Save</ff-button>
</div>
</template>
</div>
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/pages/application/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,10 @@ export default {
},
navigation () {
return [
{ label: 'Node-RED Instances', path: `/application/${this.application.id}/instances`, tag: 'project-overview', icon: ProjectsIcon },
{ label: 'Node-RED Logs', path: `/application/${this.application.id}/logs`, tag: 'project-logs', icon: TerminalIcon },
{ label: 'Audit Log', path: `/application/${this.application.id}/activity`, tag: 'project-activity', icon: ViewListIcon },
{ label: 'Settings', path: `/application/${this.application.id}/settings`, tag: 'project-settings', icon: CogIcon }
{ label: 'Node-RED Instances', path: `/application/${this.application.id}/instances`, tag: 'application-overview', icon: ProjectsIcon },
{ label: 'Node-RED Logs', path: `/application/${this.application.id}/logs`, tag: 'application-logs', icon: TerminalIcon },
{ label: 'Audit Log', path: `/application/${this.application.id}/activity`, tag: 'application-activity', icon: ViewListIcon },
{ label: 'Settings', path: `/application/${this.application.id}/settings`, tag: 'application-settings', icon: CogIcon }
]
},
instancesArray () {
Expand Down
60 changes: 57 additions & 3 deletions test/e2e/frontend/cypress/tests/applications.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe('FlowForge - Applications', () => {
cy.get('[data-action="open-editor"]').should('exist')
})

it('are not permitted to have a duplicate name during creation', () => {
it('are not permitted to have a duplicate project name during creation', () => {
cy.request('GET', 'api/v1/teams', { failOnStatusCode: false }).then((response) => {
const team = response.body.teams[0]

Expand All @@ -92,6 +92,56 @@ describe('FlowForge - Applications', () => {
})
})

it('can be updated', () => {
const START_APPLICATION_NAME = `new-application-${Math.random().toString(36).substring(2, 7)}`
const UPDATED_APPLICATION_NAME = `updated-application-${Math.random().toString(36).substring(2, 7)}`

let team
cy.request('GET', 'api/v1/teams')
.then((response) => {
team = response.body.teams[0]
return cy.request('POST', '/api/v1/applications', {
name: START_APPLICATION_NAME,
teamId: team.id
})
})
.then((response) => {
cy.intercept('GET', '/api/*/applications/*').as('getApplication')

cy.visit('/')

cy.get('[data-nav="team-applications"]')

cy.wait('@getTeamApplications')

cy.contains(START_APPLICATION_NAME).click()

cy.wait('@getApplication')

cy.get('[data-nav="application-settings"]').click()

cy.get('[data-el="application-settings"]').within(() => {
cy.get('[data-action="application-edit"]').click()

cy.get('[data-form="application-name"] input[type="text"]').clear()
cy.get('[data-form="application-name"] input[type="text"]').type(UPDATED_APPLICATION_NAME)

cy.get('[data-form="submit"]').click()
})

// Name updated on application page
cy.get('[data-el="application-name"]').contains(UPDATED_APPLICATION_NAME)

cy.get('[data-nav="team-applications"]').click()

// Name updated on team page
cy.wait('@getTeamApplications')

cy.contains(UPDATED_APPLICATION_NAME).should('exist')
cy.contains(START_APPLICATION_NAME).should('not.exist')
})
})

it('can be deleted', () => {
const APPLICATION_NAME = `new-application-${Math.random().toString(36).substring(2, 7)}`

Expand Down Expand Up @@ -225,11 +275,15 @@ describe('FlowForge stores audit logs for an application', () => {
cy.visit('/team/ateam/audit-log')
})

it.skip('when it is created', () => {
it('when it is created', () => {
cy.get('.ff-audit-entry').contains('Application Created')
})

it.skip('when it is deleted', () => {
it('when it is updated', () => {
cy.get('.ff-audit-entry').contains('Application Modified')
})

it('when it is deleted', () => {
cy.get('.ff-audit-entry').contains('Application Deleted')
})
})