Skip to content

Commit 254a607

Browse files
committed
[server] add ScmService to be used by ScmServiceAPI (and WS API)
1 parent 3ff9173 commit 254a607

14 files changed

+426
-250
lines changed

components/gitpod-protocol/src/protocol.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -891,13 +891,6 @@ export interface GuessGitTokenScopesParams {
891891
host: string;
892892
repoUrl: string;
893893
gitCommand: string;
894-
currentToken: GitToken;
895-
}
896-
897-
export interface GitToken {
898-
token: string;
899-
user: string;
900-
scopes: string[];
901894
}
902895

903896
export interface GuessedGitTokenScopes {

components/gitpod-protocol/src/public-api-converter.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
EnvironmentVariableAdmission,
4444
UserEnvironmentVariable,
4545
} from "@gitpod/public-api/lib/gitpod/v1/envvar_pb";
46+
import { SCMToken, SuggestedRepository } from "@gitpod/public-api/lib/gitpod/v1/scm_pb";
4647
import { ContextURL } from "./context-url";
4748
import { ApplicationError, ErrorCode, ErrorCodes } from "./messaging/error";
4849
import {
@@ -57,6 +58,8 @@ import {
5758
WorkspaceInfo,
5859
UserEnvVarValue,
5960
ProjectEnvVar,
61+
Token,
62+
SuggestedRepository as SuggestedRepositoryProtocol,
6063
} from "./protocol";
6164
import {
6265
OrgMemberInfo,
@@ -542,4 +545,25 @@ export class PublicAPIConverter {
542545
return ""; // not allowed
543546
}
544547
}
548+
549+
toSCMToken(t: Token): SCMToken {
550+
return new SCMToken({
551+
username: t.username,
552+
value: t.value,
553+
refreshToken: t.refreshToken,
554+
expiryDate: t.expiryDate ? Timestamp.fromDate(new Date(t.expiryDate)) : undefined,
555+
updateDate: t.updateDate ? Timestamp.fromDate(new Date(t.updateDate)) : undefined,
556+
scopes: t.scopes,
557+
idToken: t.idToken,
558+
});
559+
}
560+
561+
toSuggestedRepository(r: SuggestedRepositoryProtocol): SuggestedRepository {
562+
return new SuggestedRepository({
563+
url: r.url,
564+
repoName: r.repositoryName,
565+
configurationId: r.projectId,
566+
configurationName: r.projectName,
567+
});
568+
}
545569
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* Copyright (c) 2023 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 { HandlerContext, ServiceImpl } from "@connectrpc/connect";
8+
import { SCMService as ScmServiceInterface } from "@gitpod/public-api/lib/gitpod/v1/scm_connect";
9+
import { inject, injectable } from "inversify";
10+
import { PublicAPIConverter } from "@gitpod/gitpod-protocol/lib/public-api-converter";
11+
import { ScmService } from "../scm/scm-service";
12+
import {
13+
GetSCMTokenRequest,
14+
GetSCMTokenResponse,
15+
GuessTokenScopesRequest,
16+
GuessTokenScopesResponse,
17+
SearchRepositoriesRequest,
18+
SearchRepositoriesResponse,
19+
ListSuggestedRepositoriesRequest,
20+
ListSuggestedRepositoriesResponse,
21+
} from "@gitpod/public-api/lib/gitpod/v1/scm_pb";
22+
import { ctxUserId } from "../util/request-context";
23+
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
24+
import { validate as uuidValidate } from "uuid";
25+
import { ProjectsService } from "../projects/projects-service";
26+
import { WorkspaceService } from "../workspace/workspace-service";
27+
import { PaginationResponse } from "@gitpod/public-api/lib/gitpod/v1/pagination_pb";
28+
29+
@injectable()
30+
export class ScmServiceAPI implements ServiceImpl<typeof ScmServiceInterface> {
31+
constructor(
32+
@inject(PublicAPIConverter) private readonly apiConverter: PublicAPIConverter,
33+
@inject(ScmService) private readonly scmService: ScmService,
34+
@inject(ProjectsService) private readonly projectService: ProjectsService,
35+
@inject(WorkspaceService) private readonly workspaceService: WorkspaceService,
36+
) {}
37+
38+
async getSCMToken(request: GetSCMTokenRequest, _: HandlerContext): Promise<GetSCMTokenResponse> {
39+
const userId = ctxUserId();
40+
try {
41+
const token = await this.scmService.getToken(userId, request);
42+
return new GetSCMTokenResponse({ token: this.apiConverter.toSCMToken(token) });
43+
} catch (error) {
44+
throw new ApplicationError(ErrorCodes.NOT_FOUND, "Token not found.");
45+
}
46+
}
47+
48+
async guessTokenScopes(request: GuessTokenScopesRequest, _: HandlerContext): Promise<GuessTokenScopesResponse> {
49+
const userId = ctxUserId();
50+
const { scopes, message } = await this.scmService.guessTokenScopes(userId, request);
51+
return new GuessTokenScopesResponse({
52+
scopes,
53+
message,
54+
});
55+
}
56+
57+
async searchRepositories(
58+
request: SearchRepositoriesRequest,
59+
_: HandlerContext,
60+
): Promise<SearchRepositoriesResponse> {
61+
const userId = ctxUserId();
62+
const repos = await this.scmService.searchRepositories(userId, {
63+
searchString: request.searchString,
64+
limit: request.limit,
65+
});
66+
return new SearchRepositoriesResponse({
67+
repositories: repos.map((r) => this.apiConverter.toSuggestedRepository(r)),
68+
});
69+
}
70+
71+
async listSuggestedRepositories(
72+
request: ListSuggestedRepositoriesRequest,
73+
_: HandlerContext,
74+
): Promise<ListSuggestedRepositoriesResponse> {
75+
const userId = ctxUserId();
76+
const { organizationId } = request;
77+
78+
if (!uuidValidate(organizationId)) {
79+
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "organizationId must be a valid UUID");
80+
}
81+
82+
const projectsPromise = this.projectService.getProjects(userId, organizationId);
83+
const workspacesPromise = this.workspaceService.getWorkspaces(userId, { organizationId });
84+
const repos = await this.scmService.listSuggestedRepositories(userId, { projectsPromise, workspacesPromise });
85+
return new ListSuggestedRepositoriesResponse({
86+
repositories: repos.map((r) => this.apiConverter.toSuggestedRepository(r)),
87+
pagination: new PaginationResponse({
88+
nextToken: "",
89+
total: repos.length,
90+
}),
91+
});
92+
}
93+
}

components/server/src/api/server.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ import { EnvironmentVariableServiceAPI } from "./envvar-service-api";
5151
import { Unauthenticated } from "./unauthenticated";
5252
import { SubjectId } from "../auth/subject-id";
5353
import { BearerAuth } from "../auth/bearer-authenticator";
54+
import { ScmServiceAPI } from "./scm-service-api";
55+
import { SCMService } from "@gitpod/public-api/lib/gitpod/v1/scm_connect";
5456

5557
decorate(injectable(), PublicAPIConverter);
5658

@@ -67,6 +69,7 @@ export class API {
6769
@inject(ConfigurationServiceAPI) private readonly configurationServiceApi: ConfigurationServiceAPI;
6870
@inject(AuthProviderServiceAPI) private readonly authProviderServiceApi: AuthProviderServiceAPI;
6971
@inject(EnvironmentVariableServiceAPI) private readonly envvarServiceApi: EnvironmentVariableServiceAPI;
72+
@inject(ScmServiceAPI) private readonly scmServiceAPI: ScmServiceAPI;
7073
@inject(StatsServiceAPI) private readonly tatsServiceApi: StatsServiceAPI;
7174
@inject(HelloServiceAPI) private readonly helloServiceApi: HelloServiceAPI;
7275
@inject(SessionHandler) private readonly sessionHandler: SessionHandler;
@@ -121,6 +124,7 @@ export class API {
121124
service(ConfigurationService, this.configurationServiceApi),
122125
service(AuthProviderService, this.authProviderServiceApi),
123126
service(EnvironmentVariableService, this.envvarServiceApi),
127+
service(SCMService, this.scmServiceAPI),
124128
]) {
125129
router.service(type, new Proxy(impl, this.interceptService(type)));
126130
}
@@ -372,6 +376,7 @@ export class API {
372376
bind(ConfigurationServiceAPI).toSelf().inSingletonScope();
373377
bind(AuthProviderServiceAPI).toSelf().inSingletonScope();
374378
bind(EnvironmentVariableServiceAPI).toSelf().inSingletonScope();
379+
bind(ScmServiceAPI).toSelf().inSingletonScope();
375380
bind(StatsServiceAPI).toSelf().inSingletonScope();
376381
bind(API).toSelf().inSingletonScope();
377382
}

components/server/src/api/teams.spec.db.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { ProjectsService } from "../projects/projects-service";
2929
import { AuthProviderService } from "../auth/auth-provider-service";
3030
import { BearerAuth } from "../auth/bearer-authenticator";
3131
import { EnvVarService } from "../user/env-var-service";
32+
import { ScmService } from "../scm/scm-service";
3233

3334
const expect = chai.expect;
3435

@@ -55,6 +56,7 @@ export class APITeamsServiceSpec {
5556
this.container.bind(ProjectsService).toConstantValue({} as ProjectsService);
5657
this.container.bind(AuthProviderService).toConstantValue({} as AuthProviderService);
5758
this.container.bind(EnvVarService).toConstantValue({} as EnvVarService);
59+
this.container.bind(ScmService).toConstantValue({} as ScmService);
5860

5961
// Clean-up database
6062
const typeorm = testContainer.get<TypeORM>(TypeORM);

components/server/src/auth/auth-provider-service.ts

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -86,44 +86,52 @@ export class AuthProviderService {
8686
return result.map(toPublic);
8787
}
8888

89+
async findAuthProviderDescription(user: User, host: string): Promise<AuthProviderInfo | undefined> {
90+
const provider =
91+
this.config.authProviderConfigs.find((p) => p.host.toLowerCase() === host?.toLowerCase()) ||
92+
(await this.getAllAuthProviderParams()).find((p) => p.host.toLowerCase() === host?.toLowerCase());
93+
return provider ? this.toInfo(provider) : undefined;
94+
}
95+
96+
// explicitly copy to avoid bleeding sensitive details
97+
private toInfo(ap: AuthProviderParams): AuthProviderInfo {
98+
return {
99+
authProviderId: ap.id,
100+
authProviderType: ap.type,
101+
ownerId: ap.ownerId,
102+
organizationId: ap.organizationId,
103+
verified: ap.verified,
104+
host: ap.host,
105+
icon: ap.icon,
106+
hiddenOnDashboard: ap.hiddenOnDashboard,
107+
disallowLogin: ap.disallowLogin,
108+
description: ap.description,
109+
scopes: getScopesOfProvider(ap),
110+
requirements: getRequiredScopes(ap),
111+
};
112+
}
113+
89114
async getAuthProviderDescriptions(user: User): Promise<AuthProviderInfo[]> {
90115
const { builtinAuthProvidersConfigured } = this.config;
91116

92117
const authProviders = [...(await this.getAllAuthProviderParams()), ...this.config.authProviderConfigs];
93118

94-
// explicitly copy to avoid bleeding sensitive details
95-
const toInfo = (ap: AuthProviderParams) =>
96-
<AuthProviderInfo>{
97-
authProviderId: ap.id,
98-
authProviderType: ap.type,
99-
ownerId: ap.ownerId,
100-
organizationId: ap.organizationId,
101-
verified: ap.verified,
102-
host: ap.host,
103-
icon: ap.icon,
104-
hiddenOnDashboard: ap.hiddenOnDashboard,
105-
disallowLogin: ap.disallowLogin,
106-
description: ap.description,
107-
scopes: getScopesOfProvider(ap),
108-
requirements: getRequiredScopes(ap),
109-
};
110-
111119
const result: AuthProviderInfo[] = [];
112120
for (const p of authProviders) {
113121
const identity = user.identities.find((i) => i.authProviderId === p.id);
114122
if (identity) {
115-
result.push(toInfo(p));
123+
result.push(this.toInfo(p));
116124
continue;
117125
}
118126
if (p.ownerId === user.id) {
119-
result.push(toInfo(p));
127+
result.push(this.toInfo(p));
120128
continue;
121129
}
122130
if (builtinAuthProvidersConfigured && !this.isBuiltIn(p)) {
123131
continue;
124132
}
125133
if (this.isNotHidden(p) && this.isVerified(p)) {
126-
result.push(toInfo(p));
134+
result.push(this.toInfo(p));
127135
}
128136
}
129137
return result;

components/server/src/container-module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ import { IncrementalWorkspaceService } from "./prebuilds/incremental-workspace-s
8686
import { PrebuildManager } from "./prebuilds/prebuild-manager";
8787
import { PrebuildStatusMaintainer } from "./prebuilds/prebuilt-status-maintainer";
8888
import { ProjectsService } from "./projects/projects-service";
89-
import { ScmService } from "./projects/scm-service";
9089
import { RedisMutex } from "./redis/mutex";
9190
import { Server } from "./server";
9291
import { SessionHandler } from "./session-handler";
@@ -128,6 +127,7 @@ import { WorkspaceStartController } from "./workspace/workspace-start-controller
128127
import { WorkspaceStarter } from "./workspace/workspace-starter";
129128
import { DefaultWorkspaceImageValidator } from "./orgs/default-workspace-image-validator";
130129
import { ContextAwareAnalyticsWriter } from "./analytics";
130+
import { ScmService } from "./scm/scm-service";
131131

132132
export const productionContainerModule = new ContainerModule(
133133
(bind, unbind, isBound, rebind, unbindAsync, onActivation, onDeactivation) => {

components/server/src/projects/projects-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messag
2828
import { URL } from "url";
2929
import { Authorizer, SYSTEM_USER } from "../authorization/authorizer";
3030
import { TransactionalContext } from "@gitpod/gitpod-db/lib/typeorm/transactional-db-impl";
31-
import { ScmService } from "./scm-service";
3231
import { daysBefore, isDateSmaller } from "@gitpod/gitpod-protocol/lib/util/timeutil";
32+
import { ScmService } from "../scm/scm-service";
3333

3434
const MAX_PROJECT_NAME_LENGTH = 100;
3535

components/server/src/projects/scm-service.ts

Lines changed: 0 additions & 29 deletions
This file was deleted.

0 commit comments

Comments
 (0)