Merge pull request #12711 from Bonial-International-GmbH/pjungermann/bitbucketServer/auth

feat: support Basic Auth and token-only at Git commands for Bitbucket Server
This commit is contained in:
Ben Lambert
2022-07-25 17:06:08 +02:00
committed by GitHub
17 changed files with 371 additions and 56 deletions
@@ -33,8 +33,6 @@ jest.mock('@backstage/backend-common', () => ({
}));
const mockedGit = Git.fromAuth({
username: 'test-user',
password: 'test-password',
logger: getVoidLogger(),
});
@@ -101,6 +99,22 @@ describe('initRepoAndPush', () => {
});
});
it('with token', async () => {
await initRepoAndPush({
dir: '/test/repo/dir/',
remoteUrl: 'git@github.com:test/repo.git',
auth: {
token: 'test-token',
},
logger: getVoidLogger(),
});
expect(mockedGit.init).toHaveBeenCalledWith({
dir: '/test/repo/dir/',
defaultBranch: 'master',
});
});
it('allows overriding the default branch', async () => {
await initRepoAndPush({
dir: '/test/repo/dir/',
@@ -83,15 +83,17 @@ export async function initRepoAndPush({
}: {
dir: string;
remoteUrl: string;
auth: { username: string; password: string };
// For use cases where token has to be used with Basic Auth
// it has to be provided as password together with a username
// which may be a fixed value defined by the provider.
auth: { username: string; password: string } | { token: string };
logger: Logger;
defaultBranch?: string;
commitMessage?: string;
gitAuthorInfo?: { name?: string; email?: string };
}): Promise<void> {
const git = Git.fromAuth({
username: auth.username,
password: auth.password,
...auth,
logger,
});
@@ -137,7 +139,10 @@ export async function commitAndPushRepo({
remoteRef,
}: {
dir: string;
auth: { username: string; password: string };
// For use cases where token has to be used with Basic Auth
// it has to be provided as password together with a username
// which may be a fixed value defined by the provider.
auth: { username: string; password: string } | { token: string };
logger: Logger;
commitMessage: string;
gitAuthorInfo?: { name?: string; email?: string };
@@ -145,8 +150,7 @@ export async function commitAndPushRepo({
remoteRef?: string;
}): Promise<void> {
const git = Git.fromAuth({
username: auth.username,
password: auth.password,
...auth,
logger,
});
@@ -36,7 +36,13 @@ describe('publish:bitbucketServer', () => {
apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0',
},
{
host: 'notoken.bitbucket.com',
host: 'basic-auth.bitbucket.com',
username: 'test-user',
password: 'test-password',
apiBaseUrl: 'https://basic-auth.bitbucket.com/rest/api/1.0',
},
{
host: 'no-credentials.bitbucket.com',
},
],
},
@@ -96,21 +102,21 @@ describe('publish:bitbucketServer', () => {
).rejects.toThrow(/No matching integration configuration/);
});
it('should throw if there is no token in the integration config that is returned', async () => {
it('should throw if there no credentials in the integration config that is returned', async () => {
await expect(
action.handler({
...mockContext,
input: {
...mockContext.input,
repoUrl: 'notoken.bitbucket.com?project=project&repo=repo',
repoUrl: 'no-credentials.bitbucket.com?project=project&repo=repo',
},
}),
).rejects.toThrow(
/Authorization has not been provided for notoken.bitbucket.com/,
/Authorization has not been provided for no-credentials.bitbucket.com/,
);
});
it('should call the correct APIs', async () => {
it('should call the correct APIs with token', async () => {
expect.assertions(2);
server.use(
rest.post(
@@ -150,12 +156,54 @@ describe('publish:bitbucketServer', () => {
});
});
it('should call the correct APIs with basic auth', async () => {
expect.assertions(2);
server.use(
rest.post(
'https://basic-auth.bitbucket.com/rest/api/1.0/projects/project/repos',
(req, res, ctx) => {
expect(req.headers.get('Authorization')).toBe(
'Basic dGVzdC11c2VyOnRlc3QtcGFzc3dvcmQ=',
);
expect(req.body).toEqual({ public: false, name: 'repo' });
return res(
ctx.status(201),
ctx.set('Content-Type', 'application/json'),
ctx.json({
links: {
self: [
{
href: 'https://bitbucket.mycompany.com/projects/project/repos/repo',
},
],
clone: [
{
name: 'http',
href: 'https://bitbucket.mycompany.com/scm/project/repo',
},
],
},
}),
);
},
),
);
await action.handler({
...mockContext,
input: {
...mockContext.input,
repoUrl: 'basic-auth.bitbucket.com?project=project&repo=repo',
},
});
});
it('should work if the token is provided through ctx.input', async () => {
expect.assertions(2);
const token = 'user-token';
server.use(
rest.post(
'https://notoken.bitbucket.com/rest/api/1.0/projects/project/repos',
'https://no-credentials.bitbucket.com/rest/api/1.0/projects/project/repos',
(req, res, ctx) => {
expect(req.headers.get('Authorization')).toBe(`Bearer ${token}`);
expect(req.body).toEqual({ public: false, name: 'repo' });
@@ -185,7 +233,7 @@ describe('publish:bitbucketServer', () => {
...mockContext,
input: {
...mockContext.input,
repoUrl: 'notoken.bitbucket.com?project=project&repo=repo',
repoUrl: 'no-credentials.bitbucket.com?project=project&repo=repo',
token: token,
},
});
@@ -273,7 +321,7 @@ describe('publish:bitbucketServer', () => {
});
});
it('should call initAndPush with the correct values', async () => {
it('should call initAndPush with the correct values with token', async () => {
server.use(
rest.post(
'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos',
@@ -309,7 +357,57 @@ describe('publish:bitbucketServer', () => {
dir: mockContext.workspacePath,
remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo',
defaultBranch: 'master',
auth: { username: 'x-token-auth', password: 'thing' },
auth: { token: 'thing' },
logger: mockContext.logger,
gitAuthorInfo: {},
});
});
it('should call initAndPush with the correct values with basic auth', async () => {
server.use(
rest.post(
'https://basic-auth.bitbucket.com/rest/api/1.0/projects/project/repos',
(req, res, ctx) => {
expect(req.headers.get('Authorization')).toBe(
'Basic dGVzdC11c2VyOnRlc3QtcGFzc3dvcmQ=',
);
expect(req.body).toEqual({ public: false, name: 'repo' });
return res(
ctx.status(201),
ctx.set('Content-Type', 'application/json'),
ctx.json({
links: {
self: [
{
href: 'https://bitbucket.mycompany.com/projects/project/repos/repo',
},
],
clone: [
{
name: 'http',
href: 'https://bitbucket.mycompany.com/scm/project/repo',
},
],
},
}),
);
},
),
);
await action.handler({
...mockContext,
input: {
...mockContext.input,
repoUrl: 'basic-auth.bitbucket.com?project=project&repo=repo',
},
});
expect(initRepoAndPush).toHaveBeenCalledWith({
dir: mockContext.workspacePath,
remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo',
defaultBranch: 'master',
auth: { username: 'test-user', password: 'test-password' },
logger: mockContext.logger,
gitAuthorInfo: {},
});
@@ -357,7 +455,7 @@ describe('publish:bitbucketServer', () => {
dir: mockContext.workspacePath,
remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo',
defaultBranch: 'main',
auth: { username: 'x-token-auth', password: 'thing' },
auth: { token: 'thing' },
logger: mockContext.logger,
gitAuthorInfo: {},
});
@@ -373,7 +471,7 @@ describe('publish:bitbucketServer', () => {
apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0',
},
{
host: 'notoken.bitbucket.com',
host: 'no-credentials.bitbucket.com',
},
],
},
@@ -426,7 +524,7 @@ describe('publish:bitbucketServer', () => {
expect(initRepoAndPush).toHaveBeenCalledWith({
dir: mockContext.workspacePath,
remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo',
auth: { username: 'x-token-auth', password: 'thing' },
auth: { token: 'thing' },
logger: mockContext.logger,
defaultBranch: 'master',
gitAuthorInfo: { name: 'Test', email: 'example@example.com' },
@@ -443,7 +541,7 @@ describe('publish:bitbucketServer', () => {
apiBaseUrl: 'https://hosted.bitbucket.com/rest/api/1.0',
},
{
host: 'notoken.bitbucket.com',
host: 'no-credentials.bitbucket.com',
},
],
},
@@ -493,7 +591,7 @@ describe('publish:bitbucketServer', () => {
expect(initRepoAndPush).toHaveBeenCalledWith({
dir: mockContext.workspacePath,
remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo',
auth: { username: 'x-token-auth', password: 'thing' },
auth: { token: 'thing' },
logger: mockContext.logger,
defaultBranch: 'master',
commitMessage: 'Test commit message',
@@ -15,7 +15,10 @@
*/
import { InputError } from '@backstage/errors';
import { ScmIntegrationRegistry } from '@backstage/integration';
import {
getBitbucketServerRequestOptions,
ScmIntegrationRegistry,
} from '@backstage/integration';
import fetch, { Response, RequestInit } from 'node-fetch';
import { initRepoAndPush } from '../helpers';
import { createTemplateAction } from '../../createTemplateAction';
@@ -79,10 +82,6 @@ const createRepository = async (opts: {
return { remoteUrl, repoContentsUrl };
};
const getAuthorizationHeader = (config: { token: string }) => {
return `Bearer ${config.token}`;
};
const performEnableLFS = async (opts: {
authorization: string;
host: string;
@@ -213,14 +212,19 @@ export function createPublishBitbucketServerAction(options: {
}
const token = ctx.input.token ?? integrationConfig.config.token;
if (!token) {
const authConfig = {
...integrationConfig.config,
...{ token },
};
const reqOpts = getBitbucketServerRequestOptions(authConfig);
const authorization = reqOpts.headers.Authorization;
if (!authorization) {
throw new Error(
`Authorization has not been provided for ${integrationConfig.config.host}. Please add either token to the Integrations config or a user login auth token`,
`Authorization has not been provided for ${integrationConfig.config.host}. Please add either (a) a user login auth token, or (b) a token or (c) username + password to the integration config.`,
);
}
const authorization = getAuthorizationHeader({ token });
const apiBaseUrl = integrationConfig.config.apiBaseUrl;
const { remoteUrl, repoContentsUrl } = await createRepository({
@@ -237,10 +241,14 @@ export function createPublishBitbucketServerAction(options: {
email: config.getOptionalString('scaffolder.defaultAuthor.email'),
};
const auth = {
username: 'x-token-auth',
password: token,
};
const auth = authConfig.token
? {
token: token!,
}
: {
username: authConfig.username!,
password: authConfig.password!,
};
await initRepoAndPush({
dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath),