Skip to content

Commit 7c2fd47

Browse files
committed
[server] add basic support for BitBucket Server
1 parent 30da763 commit 7c2fd47

12 files changed

+624
-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: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* Copyright (c) 2022 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 fetch from 'node-fetch';
8+
import { User } from "@gitpod/gitpod-protocol";
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 BitbucketServerApi {
15+
16+
@inject(AuthProviderParams) protected readonly config: AuthProviderParams;
17+
@inject(BitbucketTokenHelper) protected readonly tokenHelper: BitbucketTokenHelper;
18+
19+
public async runQuery<T>(user: User, urlPath: string): Promise<T> {
20+
const token = (await this.tokenHelper.getTokenWithScopes(user, [])).value;
21+
const fullUrl = `${this.baseUrl}${urlPath}`;
22+
const response = await fetch(fullUrl, {
23+
method: 'POST',
24+
headers: {
25+
'Content-Type': 'application/json',
26+
'Authorization': `Bearer ${token}`
27+
}
28+
});
29+
if (!response.ok) {
30+
throw Error(response.statusText);
31+
}
32+
const result = await response.json();
33+
return result as T;
34+
}
35+
36+
protected get baseUrl(): string {
37+
return `https://${this.config.host}/rest/api/1.0`;
38+
}
39+
40+
getRepository(user: User, params: { kind: "projects" | "users", userOrProject: string; repositorySlug: string; }): Promise<BitbucketServer.Repository> {
41+
return this.runQuery<BitbucketServer.Repository>(user, `/${params.kind}/${params.userOrProject}/repos/${params.repositorySlug}`);
42+
}
43+
44+
getCommits(user: User, params: { kind: "projects" | "users", userOrProject: string, repositorySlug: string, q?: { limit: number } }): Promise<BitbucketServer.Paginated<BitbucketServer.Commit>> {
45+
return this.runQuery<BitbucketServer.Paginated<BitbucketServer.Commit>>(user, `/${params.kind}/${params.userOrProject}/repos/${params.repositorySlug}/commits`);
46+
}
47+
}
48+
49+
50+
export namespace BitbucketServer {
51+
export interface Repository {
52+
id: number;
53+
slug: string;
54+
name: string;
55+
public: boolean;
56+
links: {
57+
clone: {
58+
href: string;
59+
name: string;
60+
}[]
61+
}
62+
project: Project;
63+
}
64+
65+
export interface Project {
66+
key: string;
67+
id: number;
68+
name: string;
69+
public: boolean;
70+
}
71+
72+
export interface User {
73+
"name": string,
74+
"emailAddress": string,
75+
"id": number,
76+
"displayName": string,
77+
"active": boolean,
78+
"slug": string,
79+
"type": string,
80+
"links": {
81+
"self": [
82+
{
83+
"href": string
84+
}
85+
]
86+
}
87+
}
88+
89+
export interface Commit {
90+
"id": string,
91+
"displayId": string,
92+
"author": BitbucketServer.User
93+
}
94+
95+
export interface Paginated<T> {
96+
isLastPage?: boolean;
97+
limit?: number;
98+
size?: number;
99+
start?: number;
100+
values?: T[];
101+
[k: string]: any;
102+
}
103+
104+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/**
2+
* Copyright (c) 2022 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 * as BitbucketServer from "@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+
const fetchResult = await fetch(`https://${this.params.host}/plugins/servlet/applinks/whoami`, {
59+
headers: {
60+
"Authorization": `Bearer ${accessToken}`,
61+
}
62+
});
63+
if (!fetchResult.ok) {
64+
throw new Error(fetchResult.statusText);
65+
}
66+
const username = await fetchResult.text();
67+
if (!username) {
68+
throw new Error("username missing");
69+
}
70+
71+
log.warn(`(${this.strategyName}) username ${username}`);
72+
73+
const options = {
74+
baseUrl: `https://${this.params.host}`,
75+
};
76+
const client = new BitbucketServer(options);
77+
78+
client.authenticate({ type: "token", token: accessToken });
79+
const result = await client.api.getUser({ userSlug: username });
80+
81+
const user = result.data;
82+
83+
// TODO: check if user.active === true?
84+
85+
return <AuthUserSetup>{
86+
authUser: {
87+
authId: `${user.id!}`,
88+
authName: user.slug!,
89+
primaryEmail: user.emailAddress!,
90+
name: user.displayName!,
91+
// avatarUrl: user.links!.avatar!.href // TODO
92+
},
93+
currentScopes: BitbucketServerOAuthScopes.ALL,
94+
}
95+
96+
} catch (error) {
97+
log.error(`(${this.strategyName}) Reading current user info failed`, error, { accessToken, error });
98+
throw error;
99+
}
100+
}
101+
102+
protected normalizeScopes(scopes: string[]) {
103+
const set = new Set(scopes);
104+
for (const item of set.values()) {
105+
if (!(BitbucketServerOAuthScopes.Requirements.DEFAULT.includes(item))) {
106+
set.delete(item);
107+
}
108+
}
109+
return Array.from(set).sort();
110+
}
111+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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 { BitbucketServerApi } from "./bitbucket-server-api";
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(BitbucketServerApi).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(BitbucketApiFactory).toSelf().inSingletonScope();
35+
// bind(BitbucketTokenValidator).toSelf().inSingletonScope(); // TODO
36+
// bind(IGitTokenValidator).toService(BitbucketTokenValidator);
37+
});

0 commit comments

Comments
 (0)