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:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/integration': minor
|
||||
'@backstage/plugin-scaffolder-backend': minor
|
||||
---
|
||||
|
||||
Add support for Basic Auth for Bitbucket Server.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': minor
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Add support for Bearer Authorization header / token-based auth at Git commands.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': minor
|
||||
---
|
||||
|
||||
User Bearer Authorization header at Git commands with token-based auth at Bitbucket Server.
|
||||
@@ -26,6 +26,16 @@ integrations:
|
||||
token: ${BITBUCKET_SERVER_TOKEN}
|
||||
```
|
||||
|
||||
or with Basic Auth
|
||||
|
||||
```yaml
|
||||
integrations:
|
||||
bitbucketServer:
|
||||
- host: bitbucket.company.com
|
||||
username: ${BITBUCKET_SERVER_USERNAME}
|
||||
password: ${BITBUCKET_SERVER_PASSWORD}
|
||||
```
|
||||
|
||||
Directly under the `bitbucketServer` key is a list of provider configurations, where
|
||||
you can list the Bitbucket Server providers you want to fetch data from. Each entry is
|
||||
a structure with the following elements:
|
||||
@@ -34,5 +44,9 @@ a structure with the following elements:
|
||||
- `token` (optional):
|
||||
An [personal access token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html)
|
||||
as expected by Bitbucket Server.
|
||||
- `username` (optional):
|
||||
use for [Basic Auth](https://developer.atlassian.com/server/bitbucket/how-tos/command-line-rest/#authentication) for Bitbucket Server.
|
||||
- `password` (optional):
|
||||
use for [Basic Auth](https://developer.atlassian.com/server/bitbucket/how-tos/command-line-rest/#authentication) for Bitbucket Server.
|
||||
- `apiBaseUrl` (optional): The URL of the Bitbucket Server API. For self-hosted
|
||||
installations, it is commonly at `https://<host>/rest/api/1.0`.
|
||||
|
||||
@@ -373,6 +373,7 @@ export class Git {
|
||||
static fromAuth: (options: {
|
||||
username?: string;
|
||||
password?: string;
|
||||
token?: string;
|
||||
logger?: Logger;
|
||||
}) => Git;
|
||||
// (undocumented)
|
||||
|
||||
@@ -26,6 +26,7 @@ describe('Git', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('add', () => {
|
||||
it('should call isomorphic-git add with the correct arguments', async () => {
|
||||
const git = Git.fromAuth({});
|
||||
@@ -146,6 +147,33 @@ describe('Git', () => {
|
||||
onAuth: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('should call isomorphic-git with the correct arguments (Bearer)', async () => {
|
||||
const url = 'http://github.com/some/repo';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
token: 'test',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
|
||||
await git.clone({ url, dir });
|
||||
|
||||
expect(isomorphic.clone).toHaveBeenCalledWith({
|
||||
fs,
|
||||
http,
|
||||
url,
|
||||
dir,
|
||||
singleBranch: true,
|
||||
depth: 1,
|
||||
onProgress: expect.any(Function),
|
||||
headers: {
|
||||
Authorization: 'Bearer test',
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
},
|
||||
onAuth: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass a function that returns the authorization as the onAuth handler', async () => {
|
||||
const url = 'http://github.com/some/repo';
|
||||
const dir = '/some/mock/dir';
|
||||
@@ -164,7 +192,7 @@ describe('Git', () => {
|
||||
expect(onAuth()).toEqual(auth);
|
||||
});
|
||||
|
||||
it('should propogate the data from the error handler', async () => {
|
||||
it('should propagate the data from the error handler', async () => {
|
||||
const url = 'http://github.com/some/repo';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
@@ -234,6 +262,31 @@ describe('Git', () => {
|
||||
onAuth: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('should call isomorphic-git with the correct arguments (Bearer)', async () => {
|
||||
const remote = 'http://github.com/some/repo';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
token: 'test',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
|
||||
await git.fetch({ remote, dir });
|
||||
|
||||
expect(isomorphic.fetch).toHaveBeenCalledWith({
|
||||
fs,
|
||||
http,
|
||||
remote,
|
||||
dir,
|
||||
onProgress: expect.any(Function),
|
||||
headers: {
|
||||
Authorization: 'Bearer test',
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
},
|
||||
onAuth: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass a function that returns the authorization as the onAuth handler', async () => {
|
||||
const remote = 'http://github.com/some/repo';
|
||||
const dir = '/some/mock/dir';
|
||||
@@ -252,7 +305,7 @@ describe('Git', () => {
|
||||
expect(onAuth()).toEqual(auth);
|
||||
});
|
||||
|
||||
it('should propogate the data from the error handler', async () => {
|
||||
it('should propagate the data from the error handler', async () => {
|
||||
const remote = 'http://github.com/some/repo';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
@@ -348,6 +401,35 @@ describe('Git', () => {
|
||||
onAuth: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('should call isomorphic-git with the correct arguments (Bearer)', async () => {
|
||||
const remote = 'origin';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
token: 'test',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
const remoteRef = 'master';
|
||||
const force = true;
|
||||
|
||||
await git.push({ dir, remote, remoteRef, force });
|
||||
|
||||
expect(isomorphic.push).toHaveBeenCalledWith({
|
||||
fs,
|
||||
http,
|
||||
remote,
|
||||
dir,
|
||||
remoteRef,
|
||||
force,
|
||||
onProgress: expect.any(Function),
|
||||
headers: {
|
||||
Authorization: 'Bearer test',
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
},
|
||||
onAuth: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('should call isomorphic-git with remoteRef parameter', async () => {
|
||||
const remote = 'origin';
|
||||
const remoteRef = 'refs/for/master';
|
||||
@@ -373,6 +455,7 @@ describe('Git', () => {
|
||||
onAuth: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass a function that returns the authorization as the onAuth handler', async () => {
|
||||
const remote = 'origin';
|
||||
const dir = '/some/mock/dir';
|
||||
@@ -393,7 +476,7 @@ describe('Git', () => {
|
||||
expect(onAuth()).toEqual(auth);
|
||||
});
|
||||
|
||||
it('should propogate the data from the error handler', async () => {
|
||||
it('should propagate the data from the error handler', async () => {
|
||||
const remote = 'origin';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
|
||||
@@ -24,13 +24,17 @@ import fs from 'fs-extra';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
/*
|
||||
provider username password
|
||||
GitHub 'x-access-token' token
|
||||
BitBucket 'x-token-auth' token
|
||||
GitLab 'oauth2' token
|
||||
provider username password
|
||||
Azure 'notempty' token
|
||||
Bitbucket Cloud 'x-token-auth' token
|
||||
Bitbucket Server username password or token
|
||||
GitHub 'x-access-token' token
|
||||
GitLab 'oauth2' token
|
||||
|
||||
From : https://isomorphic-git.org/docs/en/onAuth with fix for GitHub
|
||||
|
||||
Azure 'notempty' token
|
||||
Or token provided as `token` for Bearer auth header
|
||||
instead of Basic Auth (e.g., Bitbucket Server).
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -39,13 +43,23 @@ Azure 'notempty' token
|
||||
* @public
|
||||
*/
|
||||
export class Git {
|
||||
private readonly headers: {
|
||||
[x: string]: string;
|
||||
};
|
||||
|
||||
private constructor(
|
||||
private readonly config: {
|
||||
username?: string;
|
||||
password?: string;
|
||||
token?: string;
|
||||
logger?: Logger;
|
||||
},
|
||||
) {}
|
||||
) {
|
||||
this.headers = {
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
...(config.token ? { Authorization: `Bearer ${config.token}` } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
async add(options: { dir: string; filepath: string }): Promise<void> {
|
||||
const { dir, filepath } = options;
|
||||
@@ -116,9 +130,7 @@ export class Git {
|
||||
depth: depth ?? 1,
|
||||
noCheckout,
|
||||
onProgress: this.onProgressHandler(),
|
||||
headers: {
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
},
|
||||
headers: this.headers,
|
||||
onAuth: this.onAuth,
|
||||
});
|
||||
} catch (ex) {
|
||||
@@ -155,7 +167,7 @@ export class Git {
|
||||
dir,
|
||||
remote,
|
||||
onProgress: this.onProgressHandler(),
|
||||
headers: { 'user-agent': 'git/@isomorphic-git' },
|
||||
headers: this.headers,
|
||||
onAuth: this.onAuth,
|
||||
});
|
||||
} catch (ex) {
|
||||
@@ -222,9 +234,7 @@ export class Git {
|
||||
onProgress: this.onProgressHandler(),
|
||||
remoteRef,
|
||||
force,
|
||||
headers: {
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
},
|
||||
headers: this.headers,
|
||||
remote,
|
||||
onAuth: this.onAuth,
|
||||
});
|
||||
@@ -290,9 +300,10 @@ export class Git {
|
||||
static fromAuth = (options: {
|
||||
username?: string;
|
||||
password?: string;
|
||||
token?: string;
|
||||
logger?: Logger;
|
||||
}) => {
|
||||
const { username, password, logger } = options;
|
||||
return new Git({ username, password, logger });
|
||||
const { username, password, token, logger } = options;
|
||||
return new Git({ username, password, token, logger });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -150,6 +150,8 @@ export type BitbucketServerIntegrationConfig = {
|
||||
host: string;
|
||||
apiBaseUrl: string;
|
||||
token?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
|
||||
Vendored
+10
@@ -92,6 +92,16 @@ export interface Config {
|
||||
* @visibility secret
|
||||
*/
|
||||
token?: string;
|
||||
/**
|
||||
* Username used to authenticate requests with Basic Auth.
|
||||
* @visibility secret
|
||||
*/
|
||||
username?: string;
|
||||
/**
|
||||
* Password (or token as password) used to authenticate requests with Basic Auth.
|
||||
* @visibility secret
|
||||
*/
|
||||
password?: string;
|
||||
/**
|
||||
* The base url for the Bitbucket Server API, for example https://<host>/rest/api/1.0
|
||||
* @visibility frontend
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('readBitbucketServerIntegrationConfig', () => {
|
||||
);
|
||||
}
|
||||
|
||||
it('reads all values', () => {
|
||||
it('reads all values, token', () => {
|
||||
const output = readBitbucketServerIntegrationConfig(
|
||||
buildConfig({
|
||||
host: 'a.com',
|
||||
@@ -70,6 +70,23 @@ describe('readBitbucketServerIntegrationConfig', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('reads all values, basic auth', () => {
|
||||
const output = readBitbucketServerIntegrationConfig(
|
||||
buildConfig({
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
username: 'u',
|
||||
password: 'p',
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
host: 'a.com',
|
||||
apiBaseUrl: 'https://a.com/api',
|
||||
username: 'u',
|
||||
password: 'p',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects funky configs', () => {
|
||||
const valid: any = {
|
||||
host: 'a.com',
|
||||
|
||||
@@ -46,6 +46,24 @@ export type BitbucketServerIntegrationConfig = {
|
||||
* If no token is specified, anonymous access is used.
|
||||
*/
|
||||
token?: string;
|
||||
|
||||
/**
|
||||
* The credentials for Basic Authentication for requests to a Bitbucket Server provider.
|
||||
*
|
||||
* If `token` was provided, it will be preferred.
|
||||
*
|
||||
* See https://developer.atlassian.com/server/bitbucket/how-tos/command-line-rest/#authentication
|
||||
*/
|
||||
username?: string;
|
||||
|
||||
/**
|
||||
* The credentials for Basic Authentication for requests to a Bitbucket Server provider.
|
||||
*
|
||||
* If `token` was provided, it will be preferred.
|
||||
*
|
||||
* See https://developer.atlassian.com/server/bitbucket/how-tos/command-line-rest/#authentication
|
||||
*/
|
||||
password?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -60,6 +78,8 @@ export function readBitbucketServerIntegrationConfig(
|
||||
const host = config.getString('host');
|
||||
let apiBaseUrl = config.getOptionalString('apiBaseUrl');
|
||||
const token = config.getOptionalString('token');
|
||||
const username = config.getOptionalString('username');
|
||||
const password = config.getOptionalString('password');
|
||||
|
||||
if (!isValidHost(host)) {
|
||||
throw new Error(
|
||||
@@ -77,6 +97,8 @@ export function readBitbucketServerIntegrationConfig(
|
||||
host,
|
||||
apiBaseUrl,
|
||||
token,
|
||||
username,
|
||||
password,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,13 @@ describe('bitbucketServer core', () => {
|
||||
apiBaseUrl: '',
|
||||
token: 'A',
|
||||
};
|
||||
const withoutToken: BitbucketServerIntegrationConfig = {
|
||||
const withBasicAuth: BitbucketServerIntegrationConfig = {
|
||||
host: '',
|
||||
apiBaseUrl: '',
|
||||
username: 'u',
|
||||
password: 'p',
|
||||
};
|
||||
const withoutCredentials: BitbucketServerIntegrationConfig = {
|
||||
host: '',
|
||||
apiBaseUrl: '',
|
||||
};
|
||||
@@ -45,7 +51,11 @@ describe('bitbucketServer core', () => {
|
||||
.Authorization,
|
||||
).toEqual('Bearer A');
|
||||
expect(
|
||||
(getBitbucketServerRequestOptions(withoutToken).headers as any)
|
||||
(getBitbucketServerRequestOptions(withBasicAuth).headers as any)
|
||||
.Authorization,
|
||||
).toEqual('Basic dTpw');
|
||||
expect(
|
||||
(getBitbucketServerRequestOptions(withoutCredentials).headers as any)
|
||||
.Authorization,
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -140,6 +140,10 @@ export function getBitbucketServerRequestOptions(
|
||||
if (config.token) {
|
||||
headers.Authorization = `Bearer ${config.token}`;
|
||||
}
|
||||
if (config.username && config.password) {
|
||||
const buffer = Buffer.from(`${config.username}:${config.password}`, 'utf8');
|
||||
headers.Authorization = `Basic ${buffer.toString('base64')}`;
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
+112
-14
@@ -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',
|
||||
|
||||
+21
-13
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user