diff --git a/.changeset/six-dots-agree.md b/.changeset/six-dots-agree.md new file mode 100644 index 0000000000..97f9b3017f --- /dev/null +++ b/.changeset/six-dots-agree.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-defaults': patch +'@backstage/integration': patch +--- + +UrlReader: Fix handling of access tokens for GitLab readURL requests diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts index b7ccf165ad..337325f6f8 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts @@ -108,9 +108,6 @@ describe('GitlabUrlReader', () => { config: createConfig(), response: expect.objectContaining({ url: 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', - headers: expect.objectContaining({ - 'private-token': '', - }), }), }, { @@ -119,7 +116,7 @@ describe('GitlabUrlReader', () => { response: expect.objectContaining({ url: 'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', headers: expect.objectContaining({ - 'private-token': '0123456789', + authorization: 'Bearer 0123456789', }), }), }, @@ -252,10 +249,10 @@ describe('GitlabUrlReader', () => { it('should return the file when using a user token', async () => { worker.use( rest.get('*/api/v4/projects/user%2Fproject', (req, res, ctx) => { - if (req.headers.get('private-token') !== 'gl-user-token') { + if (req.headers.get('authorization') !== 'Bearer gl-user-token') { return res( - ctx.status(403), - ctx.json({ message: 'Not Authorized' }), + ctx.status(401), + ctx.json({ message: '401 Unauthorized' }), ); } return res(ctx.status(200), ctx.json({ id: 12345 })); @@ -571,10 +568,10 @@ describe('GitlabUrlReader', () => { it('should return the file when using a user token', async () => { worker.use( rest.get('*/api/v4/projects/user%2Fproject', (req, res, ctx) => { - if (req.headers.get('private-token') !== 'gl-user-token') { + if (req.headers.get('authorization') !== 'Bearer gl-user-token') { return res( - ctx.status(403), - ctx.json({ message: 'Not Authorized' }), + ctx.status(401), + ctx.json({ message: '401 Unauthorized' }), ); } return res(ctx.status(200), ctx.json({ id: 12345 })); @@ -737,10 +734,10 @@ describe('GitlabUrlReader', () => { (_, res, ctx) => res(ctx.status(200), ctx.json({ id: 12345 })), ), rest.get('*/api/v4/projects/user%2Fproject', (req, res, ctx) => { - if (req.headers.get('private-token') !== 'gl-user-token') { + if (req.headers.get('authorization') !== 'Bearer gl-user-token') { return res( - ctx.status(403), - ctx.json({ message: 'Not Authorized' }), + ctx.status(401), + ctx.json({ message: '401 Unauthorized' }), ); } return res(ctx.status(200), ctx.json({ id: 12345 })); @@ -798,10 +795,10 @@ describe('GitlabUrlReader', () => { (_, res, ctx) => res(ctx.status(404)), ), rest.get('*/api/v4/projects/user%2Fproject', (req, res, ctx) => { - if (req.headers.get('private-token') !== 'gl-user-token') { + if (req.headers.get('authorization') !== 'Bearer gl-user-token') { return res( - ctx.status(403), - ctx.json({ message: 'Not Authorized' }), + ctx.status(401), + ctx.json({ message: '401 Unauthorized' }), ); } return res(ctx.status(200), ctx.json({ id: 12345 })); @@ -857,21 +854,19 @@ describe('GitlabUrlReader', () => { beforeEach(() => { worker.use( rest.get('*/api/v4/projects/group%2Fproject', (req, res, ctx) => { - // the private-token header must be included on API calls - if (req.headers.get('private-token') !== 'gl-dummy-token') { + if (req.headers.get('authorization') !== 'Bearer gl-dummy-token') { return res( - ctx.status(403), - ctx.json({ message: 'Not Authorized' }), + ctx.status(401), + ctx.json({ message: '401 Unauthorized' }), ); } return res(ctx.status(200), ctx.json({ id: 12345 })); }), rest.get('*/api/v4/projects/user%2Fproject', (req, res, ctx) => { - // the private-token header must be included on API calls - if (req.headers.get('private-token') !== 'gl-user-token') { + if (req.headers.get('authorization') !== 'Bearer gl-user-token') { return res( - ctx.status(403), - ctx.json({ message: 'Not Authorized' }), + ctx.status(401), + ctx.json({ message: '401 Unauthorized' }), ); } return res(ctx.status(200), ctx.json({ id: 12345 })); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts index 3e5497a307..6985631c3f 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts @@ -398,6 +398,12 @@ export class GitlabUrlReader implements UrlReaderService { ); const data = await result.json(); if (!result.ok) { + if (result.status === 401) { + throw new Error( + 'GitLab Error: 401 - Unauthorized. The access token used is either expired, or does not have permission to read the project', + ); + } + throw new Error(`Gitlab error: ${data.error}, ${data.error_description}`); } return Number(data.id); diff --git a/packages/integration/src/gitlab/core.test.ts b/packages/integration/src/gitlab/core.test.ts index 81adc29701..d514dba755 100644 --- a/packages/integration/src/gitlab/core.test.ts +++ b/packages/integration/src/gitlab/core.test.ts @@ -188,7 +188,7 @@ describe('gitlab core', () => { }); describe('getGitLabRequestOptions', () => { - it('should return Authorization header when oauthToken is provided', () => { + it('should return Authorization bearer header when a token is provided', () => { const token = '1234567890'; const result = getGitLabRequestOptions( configSelfHosteWithRelativePath, @@ -202,29 +202,12 @@ describe('gitlab core', () => { }); }); - it('should return private-token header when gl-token is provided', () => { - const token = 'glpat-1234566'; - const result = getGitLabRequestOptions( - configSelfHosteWithRelativePath, - token, - ); + it('should return Authorization bearer header using the config token when no token is provided', () => { + const result = getGitLabRequestOptions(configSelfHosteWithRelativePath); expect(result).toEqual({ headers: { - 'PRIVATE-TOKEN': token, - }, - }); - }); - - it('should return private-token header when oauthToken is undefined', () => { - const oauthToken = undefined; - const result = getGitLabRequestOptions( - configSelfHosteWithRelativePath, - oauthToken, - ); - expect(result).toEqual({ - headers: { - 'PRIVATE-TOKEN': configSelfHosteWithRelativePath.token, + Authorization: `Bearer ${configSelfHosteWithRelativePath.token}`, }, }); }); diff --git a/packages/integration/src/gitlab/core.ts b/packages/integration/src/gitlab/core.ts index 4a17b2a15e..dbe463604c 100644 --- a/packages/integration/src/gitlab/core.ts +++ b/packages/integration/src/gitlab/core.ts @@ -56,20 +56,17 @@ export function getGitLabRequestOptions( config: GitLabIntegrationConfig, token?: string, ): { headers: Record } { - if (token) { - // If token comes from the user and starts with "gl", it's a private token (see https://docs.gitlab.com/ee/security/token_overview.html#token-prefixes) - return { - headers: token.startsWith('gl') - ? { 'PRIVATE-TOKEN': token } - : { Authorization: `Bearer ${token}` }, // Otherwise, it's a bearer token - }; + const headers: Record = {}; + + const accessToken = token || config.token; + if (accessToken) { + // OAuth, Personal, Project, and Group access tokens can all be passed via + // a bearer authorization header + // https://docs.gitlab.com/api/rest/authentication/#personalprojectgroup-access-tokens + headers.Authorization = `Bearer ${accessToken}`; } - // If token not provided, fetch the integration token - const { token: configToken = '' } = config; - return { - headers: { 'PRIVATE-TOKEN': configToken }, - }; + return { headers }; } // Converts @@ -149,6 +146,12 @@ export async function getProjectId( const data = await response.json(); if (!response.ok) { + if (response.status === 401) { + throw new Error( + 'GitLab Error: 401 - Unauthorized. The access token used is either expired, or does not have permission to read the project', + ); + } + throw new Error( `GitLab Error '${data.error}', ${data.error_description}`, );