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 authored and roboquat committed Feb 16, 2022
1 parent 0f95d44 commit e205b48
Show file tree
Hide file tree
Showing 13 changed files with 690 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
118 changes: 118 additions & 0 deletions components/server/src/bitbucket-server/bitbucket-server-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* Copyright (c) 2022 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 fetch from 'node-fetch';
import { User } from "@gitpod/gitpod-protocol";
import { inject, injectable } from "inversify";
import { AuthProviderParams } from "../auth/auth-provider";
import { BitbucketServerTokenHelper } from './bitbucket-server-token-handler';

@injectable()
export class BitbucketServerApi {

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

public async runQuery<T>(user: User, urlPath: string): Promise<T> {
const token = (await this.tokenHelper.getTokenWithScopes(user, [])).value;
const fullUrl = `${this.baseUrl}${urlPath}`;
const response = await fetch(fullUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw Error(response.statusText);
}
const result = await response.json();
return result as T;
}

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

getRepository(user: User, params: { kind: "projects" | "users", userOrProject: string; repositorySlug: string; }): Promise<BitbucketServer.Repository> {
return this.runQuery<BitbucketServer.Repository>(user, `/${params.kind}/${params.userOrProject}/repos/${params.repositorySlug}`);
}

getCommits(user: User, params: { kind: "projects" | "users", userOrProject: string, repositorySlug: string, q?: { limit: number } }): Promise<BitbucketServer.Paginated<BitbucketServer.Commit>> {
return this.runQuery<BitbucketServer.Paginated<BitbucketServer.Commit>>(user, `/${params.kind}/${params.userOrProject}/repos/${params.repositorySlug}/commits`);
}

getDefaultBranch(user: User, params: { kind: "projects" | "users", userOrProject: string, repositorySlug: string }): Promise<BitbucketServer.Branch> {
//https://bitbucket.gitpod-self-hosted.com/rest/api/1.0/users/jldec/repos/test-repo/default-branch
return this.runQuery<BitbucketServer.Branch>(user, `/${params.kind}/${params.userOrProject}/repos/${params.repositorySlug}/default-branch`);
}
}


export namespace BitbucketServer {
export interface Repository {
id: number;
slug: string;
name: string;
public: boolean;
links: {
clone: {
href: string;
name: string;
}[]
}
project: Project;
}

export interface Project {
key: string;
owner?: User;
id: number;
name: string;
public: boolean;
}

export interface Branch {
"id": string,
"displayId": string,
"type": "BRANCH" | string,
"latestCommit": string,
"isDefault": boolean
}

export interface User {
"name": string,
"emailAddress": string,
"id": number,
"displayName": string,
"active": boolean,
"slug": string,
"type": string,
"links": {
"self": [
{
"href": string
}
]
}
}

export interface Commit {
"id": string,
"displayId": string,
"author": BitbucketServer.User
}

export interface Paginated<T> {
isLastPage?: boolean;
limit?: number;
size?: number;
start?: number;
values?: T[];
[k: string]: any;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* Copyright (c) 2022 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 * as BitbucketServer from "@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 {
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;

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

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

} 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,33 @@
/**
* 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 { FileProvider, LanguagesProvider, RepositoryHost, RepositoryProvider } from "../repohost";
import { IContextParser } from "../workspace/context-parser";
import { BitbucketServerApi } from "./bitbucket-server-api";
import { BitbucketServerAuthProvider } from "./bitbucket-server-auth-provider";
import { BitbucketServerContextParser } from "./bitbucket-server-context-parser";
import { BitbucketServerFileProvider } from "./bitbucket-server-file-provider";
import { BitbucketServerLanguagesProvider } from "./bitbucket-server-language-provider";
import { BitbucketServerRepositoryProvider } from "./bitbucket-server-repository-provider";
import { BitbucketServerTokenHelper } from "./bitbucket-server-token-handler";

export const bitbucketServerContainerModule = new ContainerModule((bind, _unbind, _isBound, _rebind) => {
bind(RepositoryHost).toSelf().inSingletonScope();
bind(BitbucketServerApi).toSelf().inSingletonScope();
bind(BitbucketServerFileProvider).toSelf().inSingletonScope();
bind(FileProvider).toService(BitbucketServerFileProvider);
bind(BitbucketServerContextParser).toSelf().inSingletonScope();
bind(BitbucketServerLanguagesProvider).toSelf().inSingletonScope();
bind(LanguagesProvider).toService(BitbucketServerLanguagesProvider);
bind(IContextParser).toService(BitbucketServerContextParser);
bind(BitbucketServerRepositoryProvider).toSelf().inSingletonScope();
bind(RepositoryProvider).toService(BitbucketServerRepositoryProvider);
bind(BitbucketServerAuthProvider).toSelf().inSingletonScope();
bind(AuthProvider).to(BitbucketServerAuthProvider).inSingletonScope();
bind(BitbucketServerTokenHelper).toSelf().inSingletonScope();
});
Loading

0 comments on commit e205b48

Please sign in to comment.