Skip to content

Commit c7dffeb

Browse files
committed
[bitbucket-server] add token validator
1 parent d020cb2 commit c7dffeb

11 files changed

+223
-47
lines changed

components/server/src/bitbucket-server/bitbucket-server-api.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,13 @@ export class BitbucketServerApi {
120120
}
121121

122122
async getPermission(
123-
user: User,
123+
userOrToken: User | string,
124124
params: { username: string; repoKind: BitbucketServer.RepoKind; owner: string; repoName?: string },
125125
): Promise<string | undefined> {
126126
const { username, repoKind, owner, repoName } = params;
127127
if (repoName) {
128128
const repoPermissions = await this.runQuery<BitbucketServer.Paginated<BitbucketServer.PermissionEntry>>(
129-
user,
129+
userOrToken,
130130
`/${repoKind}/${owner}/repos/${repoName}/permissions/users`,
131131
);
132132
const repoPermission = repoPermissions.values?.find((p) => p.user.name === username)?.permission;
@@ -136,7 +136,7 @@ export class BitbucketServerApi {
136136
}
137137
if (repoKind === "projects") {
138138
const projectPermissions = await this.runQuery<BitbucketServer.Paginated<BitbucketServer.PermissionEntry>>(
139-
user,
139+
userOrToken,
140140
`/${repoKind}/${owner}/permissions/users`,
141141
);
142142
const projectPermission = projectPermissions.values?.find((p) => p.user.name === username)?.permission;
@@ -149,11 +149,11 @@ export class BitbucketServerApi {
149149
}
150150

151151
async getRepository(
152-
user: User,
152+
userOrToken: User | string,
153153
params: { repoKind: "projects" | "users"; owner: string; repositorySlug: string },
154154
): Promise<BitbucketServer.Repository> {
155155
return this.runQuery<BitbucketServer.Repository>(
156-
user,
156+
userOrToken,
157157
`/${params.repoKind}/${params.owner}/repos/${params.repositorySlug}`,
158158
);
159159
}

components/server/src/bitbucket-server/bitbucket-server-container-module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import { ContainerModule } from "inversify";
88
import { AuthProvider } from "../auth/auth-provider";
99
import { FileProvider, LanguagesProvider, RepositoryHost, RepositoryProvider } from "../repohost";
1010
import { IContextParser } from "../workspace/context-parser";
11+
import { IGitTokenValidator } from "../workspace/git-token-validator";
1112
import { BitbucketServerApi } from "./bitbucket-server-api";
1213
import { BitbucketServerAuthProvider } from "./bitbucket-server-auth-provider";
1314
import { BitbucketServerContextParser } from "./bitbucket-server-context-parser";
1415
import { BitbucketServerFileProvider } from "./bitbucket-server-file-provider";
1516
import { BitbucketServerLanguagesProvider } from "./bitbucket-server-language-provider";
1617
import { BitbucketServerRepositoryProvider } from "./bitbucket-server-repository-provider";
1718
import { BitbucketServerTokenHelper } from "./bitbucket-server-token-handler";
19+
import { BitbucketServerTokenValidator } from "./bitbucket-server-token-validator";
1820

1921
export const bitbucketServerContainerModule = new ContainerModule((bind, _unbind, _isBound, _rebind) => {
2022
bind(RepositoryHost).toSelf().inSingletonScope();
@@ -30,4 +32,6 @@ export const bitbucketServerContainerModule = new ContainerModule((bind, _unbind
3032
bind(BitbucketServerAuthProvider).toSelf().inSingletonScope();
3133
bind(AuthProvider).to(BitbucketServerAuthProvider).inSingletonScope();
3234
bind(BitbucketServerTokenHelper).toSelf().inSingletonScope();
35+
bind(BitbucketServerTokenValidator).toSelf().inSingletonScope();
36+
bind(IGitTokenValidator).toService(BitbucketServerTokenValidator);
3337
});
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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 { skipIfEnvVarNotSet } from "@gitpod/gitpod-protocol/lib/util/skip-if";
8+
import { Container, ContainerModule } from "inversify";
9+
import { retries, suite, test, timeout } from "mocha-typescript";
10+
import { expect } from "chai";
11+
import { BitbucketServerApi } from "./bitbucket-server-api";
12+
import { BitbucketServerTokenValidator } from "./bitbucket-server-token-validator";
13+
import { AuthProviderParams } from "../auth/auth-provider";
14+
import { BitbucketServerTokenHelper } from "./bitbucket-server-token-handler";
15+
import { TokenProvider } from "../user/token-provider";
16+
17+
@suite(timeout(10000), retries(0), skipIfEnvVarNotSet("GITPOD_TEST_TOKEN_BITBUCKET_SERVER"))
18+
class TestBitbucketServerTokenValidator {
19+
protected validator: BitbucketServerTokenValidator;
20+
21+
static readonly AUTH_HOST_CONFIG: Partial<AuthProviderParams> = {
22+
id: "MyBitbucketServer",
23+
type: "BitbucketServer",
24+
verified: true,
25+
description: "",
26+
icon: "",
27+
host: "bitbucket.gitpod-self-hosted.com",
28+
oauth: {
29+
callBackUrl: "",
30+
clientId: "not-used",
31+
clientSecret: "",
32+
tokenUrl: "",
33+
scope: "",
34+
authorizationUrl: "",
35+
},
36+
};
37+
38+
public before() {
39+
const container = new Container();
40+
container.load(
41+
new ContainerModule((bind, unbind, isBound, rebind) => {
42+
bind(BitbucketServerTokenValidator).toSelf().inSingletonScope();
43+
bind(AuthProviderParams).toConstantValue(TestBitbucketServerTokenValidator.AUTH_HOST_CONFIG);
44+
bind(BitbucketServerTokenHelper).toSelf().inSingletonScope();
45+
// bind(TokenService).toConstantValue({
46+
// createGitpodToken: async () => ({ token: { value: "foobar123-token" } }),
47+
// } as any);
48+
// bind(Config).toConstantValue({
49+
// hostUrl: new GitpodHostUrl(),
50+
// });
51+
bind(TokenProvider).toConstantValue(<TokenProvider>{
52+
getTokenForHost: async () => {
53+
return {
54+
value: process.env["GITPOD_TEST_TOKEN_BITBUCKET_SERVER"] || "undefined",
55+
scopes: [],
56+
};
57+
},
58+
getFreshPortAuthenticationToken: undefined as any,
59+
});
60+
bind(BitbucketServerApi).toSelf().inSingletonScope();
61+
// bind(HostContextProvider).toConstantValue({});
62+
}),
63+
);
64+
this.validator = container.get(BitbucketServerTokenValidator);
65+
}
66+
67+
@test async test_checkWriteAccess_read_only() {
68+
const result = await this.validator.checkWriteAccess({
69+
host: "bitbucket.gitpod-self-hosted.com",
70+
owner: "mil",
71+
repo: "gitpod-large-image",
72+
repoKind: "projects",
73+
token: process.env["GITPOD_TEST_TOKEN_BITBUCKET_SERVER"]!,
74+
});
75+
expect(result).to.deep.equal({
76+
found: true,
77+
isPrivateRepo: true,
78+
writeAccessToRepo: false,
79+
});
80+
}
81+
82+
@test async test_checkWriteAccess_write_permissions() {
83+
const result = await this.validator.checkWriteAccess({
84+
host: "bitbucket.gitpod-self-hosted.com",
85+
owner: "alextugarev",
86+
repo: "yolo",
87+
repoKind: "users",
88+
token: process.env["GITPOD_TEST_TOKEN_BITBUCKET_SERVER"]!,
89+
});
90+
expect(result).to.deep.equal({
91+
found: true,
92+
isPrivateRepo: false,
93+
writeAccessToRepo: true,
94+
});
95+
}
96+
}
97+
98+
module.exports = new TestBitbucketServerTokenValidator();
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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 { inject, injectable } from "inversify";
8+
import { CheckWriteAccessResult, IGitTokenValidator, IGitTokenValidatorParams } from "../workspace/git-token-validator";
9+
import { BitbucketServerApi } from "./bitbucket-server-api";
10+
11+
@injectable()
12+
export class BitbucketServerTokenValidator implements IGitTokenValidator {
13+
@inject(BitbucketServerApi) protected readonly api: BitbucketServerApi;
14+
15+
async checkWriteAccess(params: IGitTokenValidatorParams): Promise<CheckWriteAccessResult> {
16+
const { token, owner, repo, repoKind } = params;
17+
if (!repoKind || !["users", "projects"].includes(repoKind)) {
18+
throw new Error("repo kind is missing");
19+
}
20+
21+
let found = false;
22+
let isPrivateRepo: boolean | undefined;
23+
let writeAccessToRepo: boolean | undefined;
24+
25+
try {
26+
const repository = await this.api.getRepository(token, {
27+
repoKind: repoKind as any,
28+
owner,
29+
repositorySlug: repo,
30+
});
31+
found = true;
32+
isPrivateRepo = !repository.public;
33+
} catch (error) {
34+
console.error(error);
35+
}
36+
37+
if (found) {
38+
writeAccessToRepo = false;
39+
const username = await this.api.currentUsername(token);
40+
const userProfile = await this.api.getUserProfile(token, username);
41+
if (owner === userProfile.slug) {
42+
writeAccessToRepo = true;
43+
} else {
44+
let permission = await this.api.getPermission(token, {
45+
repoKind: repoKind as any,
46+
owner,
47+
username,
48+
repoName: repo,
49+
});
50+
if (permission && ["REPO_WRITE", "REPO_ADMIN", "PROJECT_ADMIN", ""].includes(permission)) {
51+
writeAccessToRepo = true;
52+
}
53+
}
54+
}
55+
56+
return {
57+
found,
58+
isPrivateRepo,
59+
writeAccessToRepo,
60+
};
61+
}
62+
}

components/server/src/bitbucket/bitbucket-token-validator.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import { CheckWriteAccessResult, IGitTokenValidator, IGitTokenValidatorParams }
1111
@injectable()
1212
export class BitbucketTokenValidator implements IGitTokenValidator {
1313
async checkWriteAccess(params: IGitTokenValidatorParams): Promise<CheckWriteAccessResult> {
14-
const { token, host, repoFullName } = params;
14+
const { token, host, owner, repo } = params;
15+
const repoFullName = `${owner}/${repo}`;
1516

1617
const result: CheckWriteAccessResult = {
1718
found: false,

components/server/src/github/github-token-validator.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@ export class GitHubTokenValidator implements IGitTokenValidator {
1515
@inject(GitHubGraphQlEndpoint) githubGraphQLEndpoint: GitHubGraphQlEndpoint;
1616

1717
async checkWriteAccess(params: IGitTokenValidatorParams): Promise<CheckWriteAccessResult> {
18-
const { token, repoFullName } = params;
18+
const { token, owner, repo } = params;
19+
const repoFullName = `${owner}/${repo}`;
1920

2021
const parsedRepoName = this.parseGitHubRepoName(repoFullName);
2122
if (!parsedRepoName) {
2223
throw new Error(`Could not parse repo name: ${repoFullName}`);
2324
}
24-
let repo;
25+
let gitHubRepo;
2526
try {
26-
repo = await this.githubRestApi.run(token, (api) => api.repos.get(parsedRepoName));
27+
gitHubRepo = await this.githubRestApi.run(token, (api) => api.repos.get(parsedRepoName));
2728
} catch (error) {
2829
if (GitHubApiError.is(error) && error.response?.status === 404) {
2930
return { found: false };
@@ -32,12 +33,12 @@ export class GitHubTokenValidator implements IGitTokenValidator {
3233
return { found: false, error };
3334
}
3435

35-
const mayWritePrivate = GitHubResult.mayWritePrivate(repo);
36-
const mayWritePublic = GitHubResult.mayWritePublic(repo);
36+
const mayWritePrivate = GitHubResult.mayWritePrivate(gitHubRepo);
37+
const mayWritePublic = GitHubResult.mayWritePublic(gitHubRepo);
3738

38-
const isPrivateRepo = repo.data.private;
39-
let writeAccessToRepo = repo.data.permissions?.push;
40-
const inOrg = repo.data.owner?.type === "Organization";
39+
const isPrivateRepo = gitHubRepo.data.private;
40+
let writeAccessToRepo = gitHubRepo.data.permissions?.push;
41+
const inOrg = gitHubRepo.data.owner?.type === "Organization";
4142

4243
if (inOrg) {
4344
// if this repository belongs to an organization and Gitpod is not authorized,

components/server/src/gitlab/gitlab-token-validator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ export class GitLabTokenValidator implements IGitTokenValidator {
1414
let found = false;
1515
let isPrivateRepo: boolean | undefined;
1616
let writeAccessToRepo: boolean | undefined;
17-
const { token, host, repoFullName } = params;
17+
const { token, host, owner, repo } = params;
18+
const repoFullName = `${owner}/${repo}`;
1819

1920
try {
2021
const request = {
@@ -42,7 +43,6 @@ export class GitLabTokenValidator implements IGitTokenValidator {
4243
throw new Error(response.statusText);
4344
}
4445
} catch (e) {
45-
console.error(e);
4646
throw e;
4747
}
4848

components/server/src/repohost/repo-url.spec.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const expect = chai.expect;
1414
export class RepoUrlTest {
1515
@test public parseRepoUrl() {
1616
const testUrl = RepoURL.parseRepoUrl("https://gitlab.com/hello-group/my-cool-project.git");
17-
expect(testUrl).to.deep.equal({
17+
expect(testUrl).to.deep.include({
1818
host: "gitlab.com",
1919
owner: "hello-group",
2020
repo: "my-cool-project",
@@ -23,7 +23,7 @@ export class RepoUrlTest {
2323

2424
@test public parseSubgroupOneLevel() {
2525
const testUrl = RepoURL.parseRepoUrl("https://gitlab.com/hello-group/my-subgroup/my-cool-project.git");
26-
expect(testUrl).to.deep.equal({
26+
expect(testUrl).to.deep.include({
2727
host: "gitlab.com",
2828
owner: "hello-group/my-subgroup",
2929
repo: "my-cool-project",
@@ -34,7 +34,7 @@ export class RepoUrlTest {
3434
const testUrl = RepoURL.parseRepoUrl(
3535
"https://gitlab.com/hello-group/my-subgroup/my-sub-subgroup/my-cool-project.git",
3636
);
37-
expect(testUrl).to.deep.equal({
37+
expect(testUrl).to.deep.include({
3838
host: "gitlab.com",
3939
owner: "hello-group/my-subgroup/my-sub-subgroup",
4040
repo: "my-cool-project",
@@ -45,7 +45,7 @@ export class RepoUrlTest {
4545
const testUrl = RepoURL.parseRepoUrl(
4646
"https://gitlab.com/hello-group/my-subgroup/my-sub-subgroup/my-sub-sub-subgroup/my-cool-project.git",
4747
);
48-
expect(testUrl).to.deep.equal({
48+
expect(testUrl).to.deep.include({
4949
host: "gitlab.com",
5050
owner: "hello-group/my-subgroup/my-sub-subgroup/my-sub-sub-subgroup",
5151
repo: "my-cool-project",
@@ -56,12 +56,22 @@ export class RepoUrlTest {
5656
const testUrl = RepoURL.parseRepoUrl(
5757
"https://gitlab.com/hello-group/my-subgroup/my-sub-subgroup/my-sub-sub-subgroup/my-sub-sub-sub-subgroup/my-cool-project.git",
5858
);
59-
expect(testUrl).to.deep.equal({
59+
expect(testUrl).to.deep.include({
6060
host: "gitlab.com",
6161
owner: "hello-group/my-subgroup/my-sub-subgroup/my-sub-sub-subgroup/my-sub-sub-sub-subgroup",
6262
repo: "my-cool-project",
6363
});
6464
}
65+
66+
@test public parseScmCloneUrl() {
67+
const testUrl = RepoURL.parseRepoUrl("https://bitbucket.gitpod-self-hosted.com/scm/~jan/yolo.git");
68+
expect(testUrl).to.deep.include({
69+
host: "bitbucket.gitpod-self-hosted.com",
70+
repoKind: "users",
71+
owner: "jan",
72+
repo: "yolo",
73+
});
74+
}
6575
}
6676

6777
module.exports = new RepoUrlTest();

components/server/src/repohost/repo-url.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
* See License-AGPL.txt in the project root for license information.
55
*/
66

7-
import * as url from "url";
7+
import { URL } from "url";
88
export namespace RepoURL {
9-
export function parseRepoUrl(repoUrl: string): { host: string; owner: string; repo: string } | undefined {
10-
const u = url.parse(repoUrl);
9+
export function parseRepoUrl(
10+
repoUrl: string,
11+
): { host: string; owner: string; repo: string; repoKind?: string } | undefined {
12+
const u = new URL(repoUrl);
1113
const host = u.hostname || "";
1214
const path = u.pathname || "";
1315
const segments = path.split("/").filter((s) => !!s); // e.g. [ 'gitpod-io', 'gitpod.git' ]
@@ -19,12 +21,19 @@ export namespace RepoURL {
1921
if (segments.length > 2) {
2022
const endSegment = segments[segments.length - 1];
2123
let ownerSegments = segments.slice(0, segments.length - 1);
24+
let repoKind: string | undefined;
2225
if (ownerSegments[0] === "scm") {
2326
ownerSegments = ownerSegments.slice(1);
27+
repoKind = "projects";
28+
}
29+
30+
let owner = ownerSegments.join("/");
31+
if (owner.startsWith("~")) {
32+
repoKind = "users";
33+
owner = owner.substring(1);
2434
}
25-
const owner = ownerSegments.join("/");
2635
const repo = endSegment.endsWith(".git") ? endSegment.slice(0, -4) : endSegment;
27-
return { host, owner, repo };
36+
return { host, owner, repo, repoKind };
2837
}
2938
return undefined;
3039
}

0 commit comments

Comments
 (0)