Skip to content

Commit 69a780f

Browse files
committed
[server] add basic support for BitBucket Server
1 parent a33a4a0 commit 69a780f

12 files changed

+602
-0
lines changed

components/server/ee/src/auth/host-container-mapping.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ export class HostContainerMappingEE extends HostContainerMapping {
2121
return (modules || []).concat([gitlabContainerModuleEE]);
2222
case "Bitbucket":
2323
return (modules || []).concat([bitbucketContainerModuleEE]);
24+
// case "BitbucketServer":
25+
// FIXME
26+
// return (modules || []).concat([bitbucketContainerModuleEE]);
2427
case "GitHub":
2528
return (modules || []).concat([gitHubContainerModuleEE]);
2629
default:

components/server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"/dist"
2828
],
2929
"dependencies": {
30+
"@atlassian/bitbucket-server": "^0.0.6",
3031
"@gitbeaker/node": "^25.6.0",
3132
"@gitpod/content-service": "0.1.5",
3233
"@gitpod/gitpod-db": "0.1.5",

components/server/src/auth/host-container-mapping.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { githubContainerModule } from "../github/github-container-module";
99
import { gitlabContainerModule } from "../gitlab/gitlab-container-module";
1010
import { genericAuthContainerModule } from "./oauth-container-module";
1111
import { bitbucketContainerModule } from "../bitbucket/bitbucket-container-module";
12+
import { bitbucketServerContainerModule } from "../bitbucket-server/bitbucket-server-container-module";
1213

1314
@injectable()
1415
export class HostContainerMapping {
@@ -23,6 +24,8 @@ export class HostContainerMapping {
2324
return [genericAuthContainerModule];
2425
case "Bitbucket":
2526
return [bitbucketContainerModule];
27+
case "BitbucketServer":
28+
return [bitbucketServerContainerModule];
2629
default:
2730
return undefined;
2831
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Copyright (c) 2020 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License-AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { User, Token } from "@gitpod/gitpod-protocol";
8+
import { APIClient, Bitbucket } from "bitbucket";
9+
import { inject, injectable } from "inversify";
10+
import { AuthProviderParams } from "../auth/auth-provider";
11+
import { BitbucketTokenHelper } from "../bitbucket/bitbucket-token-handler";
12+
13+
@injectable()
14+
export class BitbucketServerApiFactory {
15+
16+
@inject(AuthProviderParams) protected readonly config: AuthProviderParams;
17+
@inject(BitbucketTokenHelper) protected readonly tokenHelper: BitbucketTokenHelper;
18+
19+
/**
20+
* Returns a Bitbucket API client for the given user.
21+
* @param user The user the API client should be created for.
22+
*/
23+
public async create(user: User): Promise<APIClient> {
24+
const token = await this.tokenHelper.getTokenWithScopes(user, []);
25+
return this.createBitbucket(this.baseUrl, token);
26+
}
27+
28+
protected createBitbucket(baseUrl: string, token: Token): APIClient {
29+
return new Bitbucket({
30+
baseUrl,
31+
auth: {
32+
token: token.value
33+
}
34+
});
35+
}
36+
37+
protected get baseUrl(): string {
38+
return `https://api.${this.config.host}/2.0`;
39+
}
40+
}
41+
42+
@injectable()
43+
export class BasicAuthBitbucketServerApiFactory extends BitbucketServerApiFactory {
44+
protected createBitbucket(baseUrl: string, token: Token): APIClient {
45+
46+
return new Bitbucket({
47+
baseUrl,
48+
auth: {
49+
username: token.username!,
50+
password: token.value
51+
}
52+
});
53+
}
54+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* Copyright (c) 2021 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License-AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { AuthProviderInfo } from "@gitpod/gitpod-protocol";
8+
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
9+
import * as express from "express";
10+
import { injectable } from "inversify";
11+
import fetch from "node-fetch";
12+
import { AuthUserSetup } from "../auth/auth-provider";
13+
import { GenericAuthProvider } from "../auth/generic-auth-provider";
14+
import { BitbucketServerOAuthScopes } from "./bitbucket-server-oauth-scopes";
15+
import BitbucketServer = require("@atlassian/bitbucket-server")
16+
17+
@injectable()
18+
export class BitbucketServerAuthProvider extends GenericAuthProvider {
19+
20+
get info(): AuthProviderInfo {
21+
return {
22+
...this.defaultInfo(),
23+
scopes: BitbucketServerOAuthScopes.ALL,
24+
requirements: {
25+
default: BitbucketServerOAuthScopes.Requirements.DEFAULT,
26+
publicRepo: BitbucketServerOAuthScopes.Requirements.DEFAULT,
27+
privateRepo: BitbucketServerOAuthScopes.Requirements.DEFAULT,
28+
},
29+
}
30+
}
31+
32+
/**
33+
* Augmented OAuthConfig for Bitbucket
34+
*/
35+
protected get oauthConfig() {
36+
const oauth = this.params.oauth!;
37+
const scopeSeparator = " ";
38+
return <typeof oauth>{
39+
...oauth,
40+
authorizationUrl: oauth.authorizationUrl || `https://${this.params.host}/rest/oauth2/latest/authorize`,
41+
tokenUrl: oauth.tokenUrl || `https://${this.params.host}/rest/oauth2/latest/token`,
42+
settingsUrl: oauth.settingsUrl || `https://${this.params.host}/plugins/servlet/oauth/users/access-tokens/`,
43+
scope: BitbucketServerOAuthScopes.ALL.join(scopeSeparator),
44+
scopeSeparator
45+
};
46+
}
47+
48+
protected get tokenUsername(): string {
49+
return "x-token-auth";
50+
}
51+
52+
authorize(req: express.Request, res: express.Response, next: express.NextFunction, scope?: string[]): void {
53+
super.authorize(req, res, next, scope ? scope : BitbucketServerOAuthScopes.Requirements.DEFAULT);
54+
}
55+
56+
protected readAuthUserSetup = async (accessToken: string, _tokenResponse: object) => {
57+
try {
58+
59+
log.warn(`(${this.strategyName}) accessToken ${accessToken}`);
60+
61+
const fetchResult = await fetch(`https://${this.params.host}/plugins/servlet/applinks/whoami`, {
62+
headers: {
63+
"Authorization": `Bearer ${accessToken}`,
64+
}
65+
});
66+
if (!fetchResult.ok) {
67+
throw new Error(fetchResult.statusText);
68+
}
69+
const username = await fetchResult.text();
70+
if (!username) {
71+
throw new Error("username missing");
72+
}
73+
74+
log.warn(`(${this.strategyName}) username ${username}`);
75+
76+
const options = {
77+
baseUrl: `https://${this.params.host}`,
78+
};
79+
const client = new BitbucketServer(options);
80+
81+
client.authenticate({ type: "token", token: accessToken });
82+
const result = await client.api.getUser({ userSlug: username });
83+
84+
const user = result.data;
85+
// const headers = result.headers;
86+
87+
// const currentScopes = this.normalizeScopes((headers as any)["x-oauth-scopes"]
88+
// .split(",")
89+
// .map((s: string) => s.trim())
90+
// );
91+
92+
// TODO: check if user.active === true?
93+
94+
return <AuthUserSetup>{
95+
authUser: {
96+
authId: `${user.id!}`,
97+
authName: user.name!,
98+
primaryEmail: user.emailAddress!,
99+
name: user.displayName!,
100+
// avatarUrl: user.links!.avatar!.href // TODO
101+
},
102+
currentScopes: BitbucketServerOAuthScopes.ALL, // TODO
103+
}
104+
105+
} catch (error) {
106+
log.error(`(${this.strategyName}) Reading current user info failed`, error, { accessToken, error });
107+
throw error;
108+
}
109+
}
110+
111+
protected normalizeScopes(scopes: string[]) {
112+
const set = new Set(scopes);
113+
for (const item of set.values()) {
114+
if (!(BitbucketServerOAuthScopes.Requirements.DEFAULT.includes(item))) {
115+
set.delete(item);
116+
}
117+
}
118+
return Array.from(set).sort();
119+
}
120+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Copyright (c) 2020 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License-AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { ContainerModule } from "inversify";
8+
import { AuthProvider } from "../auth/auth-provider";
9+
import { BitbucketApiFactory } from '../bitbucket/bitbucket-api-factory';
10+
import { BitbucketFileProvider } from "../bitbucket/bitbucket-file-provider";
11+
import { BitbucketLanguagesProvider } from "../bitbucket/bitbucket-language-provider";
12+
import { BitbucketRepositoryProvider } from "../bitbucket/bitbucket-repository-provider";
13+
import { BitbucketTokenHelper } from "../bitbucket/bitbucket-token-handler";
14+
import { FileProvider, LanguagesProvider, RepositoryHost, RepositoryProvider } from "../repohost";
15+
import { IContextParser } from "../workspace/context-parser";
16+
import { BitbucketServerApiFactory } from "./bitbucket-api-factory";
17+
import { BitbucketServerAuthProvider } from "./bitbucket-server-auth-provider";
18+
import { BitbucketServerContextParser } from "./bitbucket-server-context-parser";
19+
20+
export const bitbucketServerContainerModule = new ContainerModule((bind, _unbind, _isBound, _rebind) => {
21+
bind(RepositoryHost).toSelf().inSingletonScope();
22+
bind(BitbucketServerApiFactory).toSelf().inSingletonScope();
23+
bind(BitbucketFileProvider).toSelf().inSingletonScope();
24+
bind(FileProvider).toService(BitbucketFileProvider);
25+
bind(BitbucketServerContextParser).toSelf().inSingletonScope();
26+
bind(BitbucketLanguagesProvider).toSelf().inSingletonScope();
27+
bind(LanguagesProvider).toService(BitbucketLanguagesProvider);
28+
bind(IContextParser).toService(BitbucketServerContextParser);
29+
bind(BitbucketRepositoryProvider).toSelf().inSingletonScope();
30+
bind(RepositoryProvider).toService(BitbucketRepositoryProvider);
31+
bind(BitbucketServerAuthProvider).toSelf().inSingletonScope();
32+
bind(AuthProvider).to(BitbucketServerAuthProvider).inSingletonScope();
33+
bind(BitbucketTokenHelper).toSelf().inSingletonScope();
34+
// bind(BitbucketTokenValidator).toSelf().inSingletonScope(); // TODO
35+
// bind(IGitTokenValidator).toService(BitbucketTokenValidator);
36+
});

0 commit comments

Comments
 (0)