Skip to content
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
7 changes: 5 additions & 2 deletions api/src/artefacts/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,11 @@ router.post('/npm/:id', async (req, res, next) => {
router.get('/:id/download', async (req, res, next) => {
try {
const caller = await resolveCaller(req)
const filter = artefactAccessFilter(caller)
const artefact = await getArtefact(req.params.id, filter)
// Download is "I already know the id" — unlike listing, hiding the
// artefact behind 404 is unhelpful: callers can't distinguish "doesn't
// exist" from "you can't have it". Look up unfiltered and let
// assertDownloadAccess decide between 200 and 403.
const artefact = await getArtefactById(req.params.id)
if (!artefact) throw httpError(404, 'artefact not found')
await assertDownloadAccess(caller, artefact)
if (!artefact.path) throw httpError(404, 'no content uploaded for this artefact')
Expand Down
28 changes: 28 additions & 0 deletions tests/artefacts-file.api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,34 @@ test.describe('File artefacts', () => {
expect(err.status).toBe(403)
}
})

// The download endpoint reports a permission error (403) rather than 404
// for callers who have a grant but are not in the artefact's
// privateAccess. Listing still hides such artefacts (404 on GET /:id),
// but download is "I already know the id" — see router note.
test('download of private artefact when caller is not in privateAccess returns 403', async () => {
const admin = await superAdmin
await admin.patch('/api/v1/artefacts/terrain', { public: false, privateAccess: [] })
await admin.post('/api/v1/access-grants', { account: { type: 'organization', id: 'test1' } })

const ax = await axiosAuth('test1-admin1', { org: 'test1' })
try {
await ax.get('/api/v1/artefacts/terrain/download')
expect(true).toBe(false)
} catch (err: any) {
expect(err.status).toBe(403)
}
})

test('download of a non-existent artefact returns 404', async () => {
const ax = await superAdmin
try {
await ax.get('/api/v1/artefacts/does-not-exist/download')
expect(true).toBe(false)
} catch (err: any) {
expect(err.status).toBe(404)
}
})
})

test.describe('PATCH & DELETE', () => {
Expand Down
4 changes: 3 additions & 1 deletion tests/read-keys.api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ test.describe('Read API key access', () => {
await ax.get('/api/v1/artefacts/' + encodeURIComponent('@test/other-pkg@3') + '/download')
expect(true).toBe(false)
} catch (err: any) {
expect(err.status).toBe(404)
// Download endpoint surfaces 403 for permission errors so callers
// can distinguish "missing" from "not allowed" — see router note.
expect(err.status).toBe(403)
}
})
})
Expand Down
Loading