Add Bitbucket Cloud OAuth support
Signed-off-by: Jake Smith <jakemgsmith@gmail.com>
This commit is contained in:
committed by
Fredrik Adelöw
parent
e404b12604
commit
959e6ecc5a
@@ -14,7 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BitbucketCloudIntegrationConfig } from '@backstage/integration';
|
||||
import {
|
||||
BitbucketCloudIntegrationConfig,
|
||||
getBitbucketCloudOAuthToken,
|
||||
} from '@backstage/integration';
|
||||
import fetch, { Request } from 'cross-fetch';
|
||||
import { Models } from './models';
|
||||
import { WithPagination } from './pagination';
|
||||
@@ -139,22 +142,32 @@ export class BitbucketCloudClient {
|
||||
}
|
||||
|
||||
private async request(req: Request): Promise<Response> {
|
||||
return fetch(req, { headers: this.getAuthHeaders() }).then(
|
||||
(response: Response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Unexpected response for ${req.method} ${req.url}. Expected 200 but got ${response.status} - ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
const headers = await this.getAuthHeaders();
|
||||
return fetch(req, { headers }).then((response: Response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Unexpected response for ${req.method} ${req.url}. Expected 200 but got ${response.status} - ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
);
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
private getAuthHeaders(): Record<string, string> {
|
||||
private async getAuthHeaders(): Promise<Record<string, string>> {
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
// OAuth authentication (clientId/clientSecret)
|
||||
if (this.config.clientId && this.config.clientSecret) {
|
||||
const token = await getBitbucketCloudOAuthToken(
|
||||
this.config.clientId,
|
||||
this.config.clientSecret,
|
||||
);
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
return headers;
|
||||
}
|
||||
|
||||
// Basic authentication (username/token or username/appPassword)
|
||||
if (
|
||||
this.config.username &&
|
||||
(this.config.token ?? this.config.appPassword)
|
||||
|
||||
@@ -216,7 +216,7 @@ export function createPublishBitbucketCloudAction(options: {
|
||||
);
|
||||
}
|
||||
|
||||
const authorization = getAuthorizationHeader(
|
||||
const authorization = await getAuthorizationHeader(
|
||||
ctx.input.token ? { token: ctx.input.token } : integrationConfig.config,
|
||||
);
|
||||
|
||||
|
||||
+7
-5
@@ -22,11 +22,11 @@ jest.mock('bitbucket', () => ({
|
||||
}));
|
||||
|
||||
describe('bitbucketCloud:branchRestriction:create', () => {
|
||||
it('getBitbucketClient should return the correct headers with username and password', () => {
|
||||
it('getBitbucketClient should return the correct headers with username and password', async () => {
|
||||
expect.assertions(1);
|
||||
const username = 'username';
|
||||
const password = 'password';
|
||||
getBitbucketClient({ username: username, appPassword: password });
|
||||
await getBitbucketClient({ username: username, appPassword: password });
|
||||
expect(Bitbucket).toHaveBeenCalledWith({
|
||||
auth: {
|
||||
username: username,
|
||||
@@ -35,12 +35,14 @@ describe('bitbucketCloud:branchRestriction:create', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('getBitbucketClient should throw if only one of username or password is provided', () => {
|
||||
it('getBitbucketClient should throw if only one of username or password is provided', async () => {
|
||||
expect.assertions(2);
|
||||
const username = 'username';
|
||||
const password = 'password';
|
||||
|
||||
expect(() => getBitbucketClient({ username })).toThrow(Error);
|
||||
expect(() => getBitbucketClient({ appPassword: password })).toThrow(Error);
|
||||
await expect(getBitbucketClient({ username })).rejects.toThrow(Error);
|
||||
await expect(getBitbucketClient({ appPassword: password })).rejects.toThrow(
|
||||
Error,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+3
-1
@@ -37,6 +37,8 @@ const createBitbucketCloudBranchRestriction = async (opts: {
|
||||
token?: string;
|
||||
username?: string;
|
||||
appPassword?: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
};
|
||||
}) => {
|
||||
const {
|
||||
@@ -52,7 +54,7 @@ const createBitbucketCloudBranchRestriction = async (opts: {
|
||||
authorization,
|
||||
} = opts;
|
||||
|
||||
const bitbucket = getBitbucketClient(authorization);
|
||||
const bitbucket = await getBitbucketClient(authorization);
|
||||
return await bitbucket.branchrestrictions.create({
|
||||
_body: {
|
||||
groups: groups,
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ export const createBitbucketPipelinesRunAction = (options: {
|
||||
const host = 'bitbucket.org';
|
||||
const integrationConfig = integrations.bitbucketCloud.byHost(host);
|
||||
|
||||
const authorization = getAuthorizationHeader(
|
||||
const authorization = await getAuthorizationHeader(
|
||||
token ? { token } : integrationConfig!.config,
|
||||
);
|
||||
let response: Response;
|
||||
|
||||
+1
-1
@@ -317,7 +317,7 @@ export function createPublishBitbucketCloudPullRequestAction(options: {
|
||||
);
|
||||
}
|
||||
|
||||
const authorization = getAuthorizationHeader(
|
||||
const authorization = await getAuthorizationHeader(
|
||||
ctx.input.token ? { token: ctx.input.token } : integrationConfig.config,
|
||||
);
|
||||
|
||||
|
||||
@@ -15,12 +15,28 @@
|
||||
*/
|
||||
|
||||
import { Bitbucket } from 'bitbucket';
|
||||
import { getBitbucketCloudOAuthToken } from '@backstage/integration';
|
||||
|
||||
export const getBitbucketClient = (config: {
|
||||
export const getBitbucketClient = async (config: {
|
||||
token?: string;
|
||||
username?: string;
|
||||
appPassword?: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
}) => {
|
||||
// If OAuth credentials provided, fetch token
|
||||
if (config.clientId && config.clientSecret) {
|
||||
const token = await getBitbucketCloudOAuthToken(
|
||||
config.clientId,
|
||||
config.clientSecret,
|
||||
);
|
||||
return new Bitbucket({
|
||||
auth: {
|
||||
token,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (config.token) {
|
||||
return new Bitbucket({
|
||||
auth: {
|
||||
@@ -38,15 +54,26 @@ export const getBitbucketClient = (config: {
|
||||
});
|
||||
}
|
||||
throw new Error(
|
||||
`Authorization has not been provided for Bitbucket Cloud. Please add either provide a username and token or username and appPassword to the Integrations config`,
|
||||
`Authorization has not been provided for Bitbucket Cloud. Please provide either OAuth credentials (clientId/clientSecret), username and token, or username and appPassword in the Integrations config`,
|
||||
);
|
||||
};
|
||||
|
||||
export const getAuthorizationHeader = (config: {
|
||||
export const getAuthorizationHeader = async (config: {
|
||||
username?: string;
|
||||
appPassword?: string;
|
||||
token?: string;
|
||||
}) => {
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
}): Promise<string> => {
|
||||
// OAuth authentication
|
||||
if (config.clientId && config.clientSecret) {
|
||||
const token = await getBitbucketCloudOAuthToken(
|
||||
config.clientId,
|
||||
config.clientSecret,
|
||||
);
|
||||
return `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// TODO: appPassword can be removed once fully
|
||||
// deprecated by BitBucket on 9th June 2026.
|
||||
if (config.username && (config.token ?? config.appPassword)) {
|
||||
@@ -62,6 +89,6 @@ export const getAuthorizationHeader = (config: {
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Authorization has not been provided for Bitbucket Cloud. Please add either provide a username and token or username and appPassword to the Integrations config`,
|
||||
`Authorization has not been provided for Bitbucket Cloud. Please provide either OAuth credentials (clientId/clientSecret), username and token, or username and appPassword in the Integrations config`,
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user