Skip to content

Commit 9994460

Browse files
committed
[bitbucket-server] add token validator
1 parent 335a4a7 commit 9994460

11 files changed

+250
-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: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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 { User } from "@gitpod/gitpod-protocol";
8+
import { skipIfEnvVarNotSet } from "@gitpod/gitpod-protocol/lib/util/skip-if";
9+
import { Container, ContainerModule } from "inversify";
10+
import { retries, suite, test, timeout } from "mocha-typescript";
11+
import { expect } from "chai";
12+
import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url";
13+
import { AuthProviderParams } from "../auth/auth-provider";
14+
import { BitbucketServerContextParser } from "./bitbucket-server-context-parser";
15+
import { BitbucketServerTokenHelper } from "./bitbucket-server-token-handler";
16+
import { TokenService } from "../user/token-service";
17+
import { Config } from "../config";
18+
import { TokenProvider } from "../user/token-provider";
19+
import { BitbucketServerApi } from "./bitbucket-server-api";
20+
import { HostContextProvider } from "../auth/host-context-provider";
21+
import { BitbucketServerRepositoryProvider } from "./bitbucket-server-repository-provider";
22+
23+
@suite(timeout(10000), retries(0), skipIfEnvVarNotSet("GITPOD_TEST_TOKEN_BITBUCKET_SERVER"))
24+
class TestBitbucketServerRepositoryProvider {
25+
protected service: BitbucketServerRepositoryProvider;
26+
protected user: User;
27+
28+
static readonly AUTH_HOST_CONFIG: Partial<AuthProviderParams> = {
29+
id: "MyBitbucketServer",
30+
type: "BitbucketServer",
31+
verified: true,
32+
description: "",
33+
icon: "",
34+
host: "bitbucket.gitpod-self-hosted.com",
35+
oauth: {
36+
callBackUrl: "",
37+
clientId: "not-used",
38+
clientSecret: "",
39+
tokenUrl: "",
40+
scope: "",
41+
authorizationUrl: "",
42+
},
43+
};
44+
45+
public before() {
46+
const container = new Container();
47+
container.load(
48+
new ContainerModule((bind, unbind, isBound, rebind) => {
49+
bind(BitbucketServerRepositoryProvider).toSelf().inSingletonScope();
50+
bind(BitbucketServerContextParser).toSelf().inSingletonScope();
51+
bind(AuthProviderParams).toConstantValue(TestBitbucketServerRepositoryProvider.AUTH_HOST_CONFIG);
52+
bind(BitbucketServerTokenHelper).toSelf().inSingletonScope();
53+
bind(TokenService).toConstantValue({
54+
createGitpodToken: async () => ({ token: { value: "foobar123-token" } }),
55+
} as any);
56+
bind(Config).toConstantValue({
57+
hostUrl: new GitpodHostUrl(),
58+
});
59+
bind(TokenProvider).toConstantValue(<TokenProvider>{
60+
getTokenForHost: async () => {
61+
return {
62+
value: process.env["GITPOD_TEST_TOKEN_BITBUCKET_SERVER"] || "undefined",
63+
scopes: [],
64+
};
65+
},
66+
getFreshPortAuthenticationToken: undefined as any,
67+
});
68+
bind(BitbucketServerApi).toSelf().inSingletonScope();
69+
bind(HostContextProvider).toConstantValue({});
70+
}),
71+
);
72+
this.service = container.get(BitbucketServerRepositoryProvider);
73+
this.user = {
74+
creationDate: "",
75+
id: "user1",
76+
identities: [
77+
{
78+
authId: "user1",
79+
authName: "AlexTugarev",
80+
authProviderId: "MyBitbucketServer",
81+
},
82+
],
83+
};
84+
}
85+
86+
@test async test_getRepo_ok() {
87+
const result = await this.service.getRepo(this.user, "JLDEC", "jldec-repo-march-30");
88+
expect(result).to.deep.include({
89+
webUrl: "https://bitbucket.gitpod-self-hosted.com/projects/JLDEC/repos/jldec-repo-march-30",
90+
cloneUrl: "https://bitbucket.gitpod-self-hosted.com/scm/jldec/jldec-repo-march-30.git",
91+
});
92+
}
93+
94+
@test async test_getBranch_ok() {
95+
const result = await this.service.getBranch(this.user, "JLDEC", "jldec-repo-march-30", "main");
96+
expect(result).to.deep.include({
97+
name: "main",
98+
});
99+
}
100+
101+
@test async test_getBranches_ok() {
102+
const result = await this.service.getBranches(this.user, "JLDEC", "jldec-repo-march-30");
103+
expect(result.length).to.be.gte(1);
104+
expect(result[0]).to.deep.include({
105+
name: "main",
106+
});
107+
}
108+
109+
@test async test_getBranches_ok_2() {
110+
try {
111+
await this.service.getBranches(this.user, "mil", "gitpod-large-image");
112+
expect.fail("this should not happen while 'mil/gitpod-large-image' has NO default branch configured.");
113+
} catch (error) {
114+
expect(error.message).to.include(
115+
"refs/heads/master is set as the default branch, but this branch does not exist",
116+
);
117+
}
118+
}
119+
120+
@test async test_getCommitInfo_ok() {
121+
const result = await this.service.getCommitInfo(this.user, "JLDEC", "jldec-repo-march-30", "test");
122+
expect(result).to.deep.include({
123+
author: "Alex Tugarev",
124+
});
125+
}
126+
}
127+
128+
module.exports = new TestBitbucketServerRepositoryProvider();
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 = false;
23+
let writeAccessToRepo = false;
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+
const username = await this.api.currentUsername(token);
38+
const userProfile = await this.api.getUserProfile(token, username);
39+
if (owner === userProfile.slug) {
40+
writeAccessToRepo = true;
41+
} else {
42+
let permission = await this.api.getPermission(token, {
43+
repoKind: repoKind as any,
44+
owner,
45+
username,
46+
repoName: repo,
47+
});
48+
if (permission && ["REPO_WRITE", "REPO_ADMIN", "PROJECT_ADMIN", ""].includes(permission)) {
49+
writeAccessToRepo = true;
50+
}
51+
}
52+
53+
return {
54+
found,
55+
isPrivateRepo,
56+
writeAccessToRepo,
57+
};
58+
}
59+
}

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();

0 commit comments

Comments
 (0)