diff --git a/server/resources/link.js b/server/resources/link.js index c1df1606..c58b628d 100644 --- a/server/resources/link.js +++ b/server/resources/link.js @@ -12,6 +12,7 @@ server.method('link.get', get, {}) server.method('link.list', list, {}) server.method('link.remove', remove, {}) server.method('link.checkCompany', checkCompany, {}) +server.method('link.checkAttendeeAccess', checkAttendeeAccess, {}) async function create(authorId, link, author, editionId) { let _link = { @@ -180,3 +181,27 @@ async function checkCompany(userId, companyId, editionId) { throw Boom.notFound('company not found') } } + +// Restrict user-scoped calls to the authenticated attendee. +// Team/admin scopes keep their existing elevated access. +// this should not be even necessary if the call didnt need the attendeeId +// to be passed in the first place... but i won't refactor the frontend lol +async function checkAttendeeAccess(credentials, attendeeId) { + if (!credentials || !credentials.user || !credentials.user.id) { + log.error({ + requester: credentials && credentials.user ? credentials.user.id : undefined, + scope: credentials ? credentials.scope : undefined, + attendeeId: attendeeId + }, 'missing credentials for attendee access check') + throw Boom.unauthorized('missing credentials') + } + + if (credentials.scope !== 'user') { + return + } + + if (credentials.user.id !== attendeeId) { + log.error({ requester: credentials.user.id, attendeeId: attendeeId }, 'forbidden attendee link access') + throw Boom.forbidden('forbidden attendee link access') + } +} diff --git a/server/routes/link/handlers.js b/server/routes/link/handlers.js index 2fe003b0..e1260f6e 100644 --- a/server/routes/link/handlers.js +++ b/server/routes/link/handlers.js @@ -74,6 +74,7 @@ exports.createAttendeeLink = { handler: async function (request, h) { try { const edition = await request.server.methods.deck.getLatestEdition() + await request.server.methods.link.checkAttendeeAccess(request.auth.credentials, request.params.attendeeId) await request.server.methods.link.checkCompany(request.payload.userId, request.payload.companyId, edition.id) let link = await request.server.methods.link.create(request.params.attendeeId, request.payload, "attendee", edition.id) return h.response(render(link)) @@ -154,6 +155,7 @@ exports.updateAttendeeLink = { handler: async function (request, h) { try { const edition = await request.server.methods.deck.getLatestEdition() + await request.server.methods.link.checkAttendeeAccess(request.auth.credentials, request.params.attendeeId) let link = await request.server.methods.link.update(request.params, edition.id, request.payload, "attendee") return h.response(render(link)) } catch (err) { @@ -205,6 +207,7 @@ exports.getAttendeeLink = { handler: async function (request, h) { try { const edition = await request.server.methods.deck.getLatestEdition() + await request.server.methods.link.checkAttendeeAccess(request.auth.credentials, request.params.attendeeId) let link = await request.server.methods.link.get(request.params, edition.id, 'attendee') return h.response(render(link)) } catch (err) { @@ -264,6 +267,7 @@ exports.listAttendeeLinks = { }, handler: async function (request, h) { try { + await request.server.methods.link.checkAttendeeAccess(request.auth.credentials, request.params.attendeeId) let user = await request.server.methods.user.get(request.auth.credentials.user.id) if (!user) { log.error('user not found') @@ -384,6 +388,7 @@ exports.removeAttendeeLink = { handler: async function (request, h) { try { const edition = await request.server.methods.deck.getLatestEdition() + await request.server.methods.link.checkAttendeeAccess(request.auth.credentials, request.params.attendeeId) let link = await request.server.methods.link.remove(request.params, edition.id, "attendee") if (!link) { log.error({ err: 'not found', link: edition.id }, 'error deleting attendee link')