Skip to content
Open
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
6 changes: 5 additions & 1 deletion forge/comms/aclManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,8 @@ module.exports = function (app) {
{ topic: /^ff\/v1\/[^/]+\/d\/[^/]+\/state$/ },
// - ff/v1/<team>/a/<application>/created|updated|deleted
{ topic: /^ff\/v1\/[^/]+\/a\/[^/]+\/(created|updated|deleted)$/ },
// - ff/v1/<team>/p/<instance>/created|updated|deleted
{ topic: /^ff\/v1\/[^/]+\/p\/[^/]+\/(created|updated|deleted)$/ },
// ff/v1/platform/sync
{ topic: /^ff\/v1\/platform\/sync$/ },
// ff/v1/platform/leader
Expand Down Expand Up @@ -564,7 +566,9 @@ module.exports = function (app) {
// - ff/v1/<team>/d/+/state
{ topic: /^ff\/v1\/([^/]+)\/d\/([^/]+)\/state$/, verify: 'checkTeamStateSub' },
// - ff/v1/<team>/a/+/created|updated|deleted
{ topic: /^ff\/v1\/([^/]+)\/a\/([^/]+)\/(created|updated|deleted)$/, verify: 'checkTeamStateSub' }
{ topic: /^ff\/v1\/([^/]+)\/a\/([^/]+)\/(created|updated|deleted)$/, verify: 'checkTeamStateSub' },
// - ff/v1/<team>/p/+/created|updated|deleted
{ topic: /^ff\/v1\/([^/]+)\/p\/([^/]+)\/(created|updated|deleted)$/, verify: 'checkTeamStateSub' }
],
pub: []
},
Expand Down
11 changes: 11 additions & 0 deletions forge/routes/api/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ module.exports = async function (app) {
const projectViewPromise = app.db.views.Project.project(project)
const projectStatePromise = project.liveState()

app.comms?.team?.notifyEntityLifecycle(team.hashid, 'p', project.id, 'created', await app.db.views.Project.project(project, { includeSettings: false }))

reply.send({ ...await projectViewPromise, ...await projectStatePromise })
})
/**
Expand Down Expand Up @@ -281,9 +283,16 @@ module.exports = async function (app) {
})
}

const teamHash = request.project.Team?.hashid
const instanceId = request.project.id

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

if (teamHash) {
app.comms?.team?.notifyEntityLifecycle(teamHash, 'p', instanceId, 'deleted')
}
reply.send({ status: 'okay' })
} catch (err) {
reply.code(500).send({ code: 'unexpected_error', error: err.toString() })
Expand Down Expand Up @@ -604,6 +613,8 @@ module.exports = async function (app) {
if (changesToProjectDefinition) {
await unSuspendProject(resumeProject, targetState)
}

app.comms?.team?.notifyEntityLifecycle(request.project.Team.hashid, 'p', request.project.id, 'updated', await app.db.views.Project.project(request.project, { includeSettings: false }))
} catch (error) {
app.log.error('Error while updating project:')
app.log.error(error)
Expand Down
40 changes: 40 additions & 0 deletions test/unit/forge/comms/authRoutesV2_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1510,6 +1510,46 @@ describe('Broker Auth v2 API', async function () {
topic: `ff/v1/${TestObjects.ATeam.hashid}/a/an-application/deleted`
})
})
it('allows a team member to subscribe to the instance lifecycle wildcards', async function () {
await allowRead({
username: teamFrontendUsername,
topic: `ff/v1/${TestObjects.ATeam.hashid}/p/+/created`
})
await allowRead({
username: teamFrontendUsername,
topic: `ff/v1/${TestObjects.ATeam.hashid}/p/+/updated`
})
await allowRead({
username: teamFrontendUsername,
topic: `ff/v1/${TestObjects.ATeam.hashid}/p/+/deleted`
})
})
it('denies subscribe to another team\'s instance lifecycle wildcard', async function () {
await denyRead({
username: teamFrontendUsername,
topic: `ff/v1/${otherTeam.hashid}/p/+/created`
})
})
it('denies fe-team from publishing instance lifecycle topics (read-only client)', async function () {
await denyWrite({
username: teamFrontendUsername,
topic: `ff/v1/${TestObjects.ATeam.hashid}/p/+/created`
})
})
it('allows forge_platform to publish instance lifecycle topics', async function () {
await allowWrite({
username: 'forge_platform',
topic: `ff/v1/${TestObjects.ATeam.hashid}/p/an-instance/created`
})
await allowWrite({
username: 'forge_platform',
topic: `ff/v1/${TestObjects.ATeam.hashid}/p/an-instance/updated`
})
await allowWrite({
username: 'forge_platform',
topic: `ff/v1/${TestObjects.ATeam.hashid}/p/an-instance/deleted`
})
})
it('denies fe-team from publishing to state (read-only client)', async function () {
await denyWrite({
username: teamFrontendUsername,
Expand Down
61 changes: 61 additions & 0 deletions test/unit/forge/routes/api/project_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,67 @@ describe('Project API', function () {
})
})

describe('Lifecycle publishing', function () {
let notifySpy

beforeEach(function () {
notifySpy = sinon.spy(app.comms.team, 'notifyEntityLifecycle')
})

afterEach(function () {
notifySpy.restore()
})

it('publishes created on create', async function () {
const response = await app.inject({
method: 'POST',
url: '/api/v1/projects',
payload: {
name: generateProjectName(),
applicationId: TestObjects.ApplicationA.hashid,
projectType: TestObjects.projectType1.hashid,
template: TestObjects.template1.hashid,
stack: TestObjects.stack1.hashid
},
cookies: { sid: TestObjects.tokens.alice }
})
response.statusCode.should.equal(200)
const result = response.json()

notifySpy.calledWith(TestObjects.ATeam.hashid, 'p', result.id, 'created').should.be.true()
const data = notifySpy.getCall(0).args[4]
data.should.have.property('id', result.id)
data.should.not.have.property('settings')
})

it('publishes updated on a synchronous update without leaking settings', async function () {
const project = await createInstance()
const response = await app.inject({
method: 'PUT',
url: `/api/v1/projects/${project.id}`,
payload: { launcherSettings: { disableAutoSafeMode: true } },
cookies: { sid: TestObjects.tokens.alice }
})
response.statusCode.should.equal(200)

notifySpy.calledWith(TestObjects.ATeam.hashid, 'p', project.id, 'updated').should.be.true()
notifySpy.getCall(0).args[4].should.not.have.property('settings')
})

it('publishes deleted on delete without a data payload', async function () {
const project = await createInstance({ start: true })
const response = await app.inject({
method: 'DELETE',
url: `/api/v1/projects/${project.id}`,
cookies: { sid: TestObjects.tokens.alice }
})
response.statusCode.should.equal(200)

notifySpy.calledWith(TestObjects.ATeam.hashid, 'p', project.id, 'deleted').should.be.true()
should(notifySpy.getCall(0).args[4]).be.undefined()
})
})

describe('Update Project', function () {
describe('Change project type', function () {
it('Changes the type, stack, and restores the project to original state', async function () {
Expand Down
Loading