Skip to content

Commit

Permalink
[server] add basic support for BitBucket Server
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexTugarev committed Feb 14, 2022
1 parent a48e177 commit df307b3
Show file tree
Hide file tree
Showing 12 changed files with 601 additions and 0 deletions.
3 changes: 3 additions & 0 deletions components/server/ee/src/auth/host-container-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export class HostContainerMappingEE extends HostContainerMapping {
return (modules || []).concat([gitlabContainerModuleEE]);
case "Bitbucket":
return (modules || []).concat([bitbucketContainerModuleEE]);
// case "BitbucketServer":
// FIXME
// return (modules || []).concat([bitbucketContainerModuleEE]);
case "GitHub":
return (modules || []).concat([gitHubContainerModuleEE]);
default:
Expand Down
1 change: 1 addition & 0 deletions components/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"/dist"
],
"dependencies": {
"@atlassian/bitbucket-server": "^0.0.6",
"@gitbeaker/node": "^25.6.0",
"@gitpod/content-service": "0.1.5",
"@gitpod/gitpod-db": "0.1.5",
Expand Down
3 changes: 3 additions & 0 deletions components/server/src/auth/host-container-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { githubContainerModule } from "../github/github-container-module";
import { gitlabContainerModule } from "../gitlab/gitlab-container-module";
import { genericAuthContainerModule } from "./oauth-container-module";
import { bitbucketContainerModule } from "../bitbucket/bitbucket-container-module";
import { bitbucketServerContainerModule } from "../bitbucket-server/bitbucket-server-container-module";

@injectable()
export class HostContainerMapping {
Expand All @@ -23,6 +24,8 @@ export class HostContainerMapping {
return [genericAuthContainerModule];
case "Bitbucket":
return [bitbucketContainerModule];
case "BitbucketServer":
return [bitbucketServerContainerModule];
default:
return undefined;
}
Expand Down
54 changes: 54 additions & 0 deletions components/server/src/bitbucket-server/bitbucket-api-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Copyright (c) 2020 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License-AGPL.txt in the project root for license information.
*/

import { User, Token } from "@gitpod/gitpod-protocol";
import { APIClient, Bitbucket } from "bitbucket";
import { inject, injectable } from "inversify";
import { AuthProviderParams } from "../auth/auth-provider";
import { BitbucketTokenHelper } from "../bitbucket/bitbucket-token-handler";

@injectable()
export class BitbucketServerApiFactory {

@inject(AuthProviderParams) protected readonly config: AuthProviderParams;
@inject(BitbucketTokenHelper) protected readonly tokenHelper: BitbucketTokenHelper;

/**
* Returns a Bitbucket API client for the given user.
* @param user The user the API client should be created for.
*/
public async create(user: User): Promise<APIClient> {
const token = await this.tokenHelper.getTokenWithScopes(user, []);
return this.createBitbucket(this.baseUrl, token);
}

protected createBitbucket(baseUrl: string, token: Token): APIClient {
return new Bitbucket({
baseUrl,
auth: {
token: token.value
}
});
}

protected get baseUrl(): string {
return `https://api.${this.config.host}/2.0`;
}
}

@injectable()
export class BasicAuthBitbucketServerApiFactory extends BitbucketServerApiFactory {
protected createBitbucket(baseUrl: string, token: Token): APIClient {

return new Bitbucket({
baseUrl,
auth: {
username: token.username!,
password: token.value
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* Copyright (c) 2021 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License-AGPL.txt in the project root for license information.
*/

import { AuthProviderInfo } from "@gitpod/gitpod-protocol";
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
import * as express from "express";
import { injectable } from "inversify";
import fetch from "node-fetch";
import { AuthUserSetup } from "../auth/auth-provider";
import { GenericAuthProvider } from "../auth/generic-auth-provider";
import { BitbucketServerOAuthScopes } from "./bitbucket-server-oauth-scopes";
import BitbucketServer = require("@atlassian/bitbucket-server")

@injectable()
export class BitbucketServerAuthProvider extends GenericAuthProvider {

get info(): AuthProviderInfo {
return {
...this.defaultInfo(),
scopes: BitbucketServerOAuthScopes.ALL,
requirements: {
default: BitbucketServerOAuthScopes.Requirements.DEFAULT,
publicRepo: BitbucketServerOAuthScopes.Requirements.DEFAULT,
privateRepo: BitbucketServerOAuthScopes.Requirements.DEFAULT,
},
}
}

/**
* Augmented OAuthConfig for Bitbucket
*/
protected get oauthConfig() {
const oauth = this.params.oauth!;
const scopeSeparator = " ";
return <typeof oauth>{
...oauth,
authorizationUrl: oauth.authorizationUrl || `https://${this.params.host}/rest/oauth2/latest/authorize`,
tokenUrl: oauth.tokenUrl || `https://${this.params.host}/rest/oauth2/latest/token`,
settingsUrl: oauth.settingsUrl || `https://${this.params.host}/plugins/servlet/oauth/users/access-tokens/`,
scope: BitbucketServerOAuthScopes.ALL.join(scopeSeparator),
scopeSeparator
};
}

protected get tokenUsername(): string {
return "x-token-auth";
}

authorize(req: express.Request, res: express.Response, next: express.NextFunction, scope?: string[]): void {
super.authorize(req, res, next, scope ? scope : BitbucketServerOAuthScopes.Requirements.DEFAULT);
}

protected readAuthUserSetup = async (accessToken: string, _tokenResponse: object) => {
try {

log.warn(`(${this.strategyName}) accessToken ${accessToken}`);

const fetchResult = await fetch(`https://${this.params.host}/plugins/servlet/applinks/whoami`, {
headers: {
"Authorization": `Bearer ${accessToken}`,
}
});
if (!fetchResult.ok) {
throw new Error(fetchResult.statusText);
}
const username = await fetchResult.text();
if (!username) {
throw new Error("username missing");
}

log.warn(`(${this.strategyName}) username ${username}`);

const options = {
baseUrl: `https://${this.params.host}`,
};
const client = new BitbucketServer(options);

client.authenticate({ type: "token", token: accessToken });
const result = await client.api.getUser({ userSlug: username });

const user = result.data;
// const headers = result.headers;

// const currentScopes = this.normalizeScopes((headers as any)["x-oauth-scopes"]
// .split(",")
// .map((s: string) => s.trim())
// );

// TODO: check if user.active === true?

return <AuthUserSetup>{
authUser: {
authId: `${user.id!}`,
authName: user.name!,
primaryEmail: user.emailAddress!,
name: user.displayName!,
// avatarUrl: user.links!.avatar!.href // TODO
},
currentScopes: BitbucketServerOAuthScopes.ALL, // TODO
}

} catch (error) {
log.error(`(${this.strategyName}) Reading current user info failed`, error, { accessToken, error });
throw error;
}
}

protected normalizeScopes(scopes: string[]) {
const set = new Set(scopes);
for (const item of set.values()) {
if (!(BitbucketServerOAuthScopes.Requirements.DEFAULT.includes(item))) {
set.delete(item);
}
}
return Array.from(set).sort();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2020 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License-AGPL.txt in the project root for license information.
*/

import { ContainerModule } from "inversify";
import { AuthProvider } from "../auth/auth-provider";
import { BitbucketFileProvider } from "../bitbucket/bitbucket-file-provider";
import { BitbucketLanguagesProvider } from "../bitbucket/bitbucket-language-provider";
import { BitbucketRepositoryProvider } from "../bitbucket/bitbucket-repository-provider";
import { BitbucketTokenHelper } from "../bitbucket/bitbucket-token-handler";
import { FileProvider, LanguagesProvider, RepositoryHost, RepositoryProvider } from "../repohost";
import { IContextParser } from "../workspace/context-parser";
import { BitbucketServerApiFactory } from "./bitbucket-api-factory";
import { BitbucketServerAuthProvider } from "./bitbucket-server-auth-provider";
import { BitbucketServerContextParser } from "./bitbucket-server-context-parser";

export const bitbucketServerContainerModule = new ContainerModule((bind, _unbind, _isBound, _rebind) => {
bind(RepositoryHost).toSelf().inSingletonScope();
bind(BitbucketServerApiFactory).toSelf().inSingletonScope();
bind(BitbucketFileProvider).toSelf().inSingletonScope();
bind(FileProvider).toService(BitbucketFileProvider);
bind(BitbucketServerContextParser).toSelf().inSingletonScope();
bind(BitbucketLanguagesProvider).toSelf().inSingletonScope();
bind(LanguagesProvider).toService(BitbucketLanguagesProvider);
bind(IContextParser).toService(BitbucketServerContextParser);
bind(BitbucketRepositoryProvider).toSelf().inSingletonScope();
bind(RepositoryProvider).toService(BitbucketRepositoryProvider);
bind(BitbucketServerAuthProvider).toSelf().inSingletonScope();
bind(AuthProvider).to(BitbucketServerAuthProvider).inSingletonScope();
bind(BitbucketTokenHelper).toSelf().inSingletonScope();
// bind(BitbucketTokenValidator).toSelf().inSingletonScope(); // TODO
// bind(IGitTokenValidator).toService(BitbucketTokenValidator);
});
Loading

0 comments on commit df307b3

Please sign in to comment.