Skip to content

Commit b41d9c6

Browse files
committed
[server] add AuthProviderServiceAPI
1 parent 7c2f6a0 commit b41d9c6

File tree

4 files changed

+302
-28
lines changed

4 files changed

+302
-28
lines changed

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

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

7-
import { Code, ConnectError } from "@connectrpc/connect";
87
import { Timestamp } from "@bufbuild/protobuf";
8+
import { Code, ConnectError } from "@connectrpc/connect";
9+
import {
10+
AuthProvider,
11+
AuthProviderDescription,
12+
AuthProviderType,
13+
OAuth2Config,
14+
} from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
15+
import {
16+
BranchMatchingStrategy,
17+
Configuration,
18+
PrebuildSettings,
19+
WorkspaceSettings,
20+
} from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
21+
import {
22+
Organization,
23+
OrganizationMember,
24+
OrganizationRole,
25+
OrganizationSettings,
26+
} from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
927
import {
1028
AdmissionLevel,
1129
EditorReference,
@@ -20,45 +38,35 @@ import {
2038
WorkspacePort_Protocol,
2139
WorkspaceStatus,
2240
} from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
23-
import {
24-
Organization,
25-
OrganizationMember,
26-
OrganizationRole,
27-
OrganizationSettings,
28-
} from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
29-
import {
30-
BranchMatchingStrategy,
31-
Configuration,
32-
PrebuildSettings,
33-
WorkspaceSettings,
34-
} from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
41+
import { ContextURL } from "./context-url";
3542
import { ApplicationError, ErrorCode, ErrorCodes } from "./messaging/error";
3643
import {
44+
AuthProviderEntry as AuthProviderProtocol,
45+
AuthProviderInfo,
3746
CommitContext,
3847
EnvVarWithValue,
48+
Workspace as ProtocolWorkspace,
3949
WithEnvvarsContext,
4050
WithPrebuild,
4151
WorkspaceContext,
4252
WorkspaceInfo,
43-
Workspace as ProtocolWorkspace,
4453
} from "./protocol";
54+
import {
55+
OrgMemberInfo,
56+
OrgMemberRole,
57+
OrganizationSettings as OrganizationSettingsProtocol,
58+
PrebuildSettings as PrebuildSettingsProtocol,
59+
Project,
60+
Organization as ProtocolOrganization,
61+
} from "./teams-projects-protocol";
62+
import { TrustedValue } from "./util/scrubbing";
4563
import {
4664
ConfigurationIdeConfig,
4765
PortProtocol,
4866
WorkspaceInstance,
4967
WorkspaceInstanceConditions,
5068
WorkspaceInstancePort,
5169
} from "./workspace-instance";
52-
import { ContextURL } from "./context-url";
53-
import { TrustedValue } from "./util/scrubbing";
54-
import {
55-
Organization as ProtocolOrganization,
56-
OrgMemberInfo,
57-
OrgMemberRole,
58-
OrganizationSettings as OrganizationSettingsProtocol,
59-
Project,
60-
PrebuildSettings as PrebuildSettingsProtocol,
61-
} from "./teams-projects-protocol";
6270

6371
const applicationErrorCode = "application-error-code";
6472
const applicationErrorData = "application-error-data";
@@ -427,4 +435,74 @@ export class PublicAPIConverter {
427435
}
428436
return result;
429437
}
438+
439+
toAuthProviderDescription(ap: AuthProviderInfo): AuthProviderDescription {
440+
const result = new AuthProviderDescription({
441+
id: ap.authProviderId,
442+
host: ap.host,
443+
type: this.toAuthProviderType(ap.authProviderType),
444+
});
445+
return result;
446+
}
447+
448+
toAuthProvider(ap: AuthProviderProtocol): AuthProvider {
449+
const result = new AuthProvider({
450+
id: ap.id,
451+
host: ap.host,
452+
type: this.toAuthProviderType(ap.type),
453+
verified: ap.status === "verified",
454+
settingsUrl: ap.oauth?.settingsUrl,
455+
scopes: ap.oauth?.scope?.split(ap.oauth?.scopeSeparator || " ") || [],
456+
});
457+
if (ap.organizationId) {
458+
result.owner = {
459+
case: "organizationId",
460+
value: ap.organizationId,
461+
};
462+
} else {
463+
result.owner = {
464+
case: "ownerId",
465+
value: ap.ownerId,
466+
};
467+
}
468+
result.oauth2Config = this.toOAuth2Config(ap);
469+
return result;
470+
}
471+
472+
toOAuth2Config(ap: AuthProviderProtocol): OAuth2Config {
473+
return new OAuth2Config({
474+
clientId: ap.oauth?.clientId,
475+
clientSecret: ap.oauth?.clientSecret,
476+
});
477+
}
478+
479+
toAuthProviderType(type: string): AuthProviderType {
480+
switch (type) {
481+
case "GitHub":
482+
return AuthProviderType.GITHUB;
483+
case "GitLab":
484+
return AuthProviderType.GITLAB;
485+
case "Bitbucket":
486+
return AuthProviderType.BITBUCKET;
487+
case "BitbucketServer":
488+
return AuthProviderType.BITBUCKET_SERVER;
489+
default:
490+
return AuthProviderType.UNSPECIFIED; // not allowed
491+
}
492+
}
493+
494+
fromAuthProviderType(type: AuthProviderType): string {
495+
switch (type) {
496+
case AuthProviderType.GITHUB:
497+
return "GitHub";
498+
case AuthProviderType.GITLAB:
499+
return "GitLab";
500+
case AuthProviderType.BITBUCKET:
501+
return "Bitbucket";
502+
case AuthProviderType.BITBUCKET_SERVER:
503+
return "BitbucketServer";
504+
default:
505+
return ""; // not allowed
506+
}
507+
}
430508
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
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 { Code, ConnectError, HandlerContext, ServiceImpl } from "@connectrpc/connect";
8+
import { AuthProviderService as AuthProviderServiceInterface } from "@gitpod/public-api/lib/gitpod/v1/authprovider_connect";
9+
import { inject, injectable } from "inversify";
10+
import { PublicAPIConverter } from "@gitpod/gitpod-protocol/lib/public-api-converter";
11+
import {
12+
CreateAuthProviderRequest,
13+
CreateAuthProviderResponse,
14+
GetAuthProviderRequest,
15+
GetAuthProviderResponse,
16+
ListAuthProvidersRequest,
17+
ListAuthProvidersResponse,
18+
ListAuthProviderDescriptionsRequest,
19+
ListAuthProviderDescriptionsResponse,
20+
UpdateAuthProviderRequest,
21+
UpdateAuthProviderResponse,
22+
DeleteAuthProviderRequest,
23+
DeleteAuthProviderResponse,
24+
} from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
25+
import { AuthProviderService } from "../auth/auth-provider-service";
26+
import { AuthProviderEntry, AuthProviderInfo } from "@gitpod/gitpod-protocol";
27+
import { Unauthenticated } from "./unauthenticated";
28+
29+
@injectable()
30+
export class AuthProviderServiceAPI implements ServiceImpl<typeof AuthProviderServiceInterface> {
31+
constructor(
32+
@inject(PublicAPIConverter) private readonly apiConverter: PublicAPIConverter,
33+
@inject(AuthProviderService) private readonly authProviderService: AuthProviderService,
34+
) {}
35+
36+
async createAuthProvider(
37+
request: CreateAuthProviderRequest,
38+
context: HandlerContext,
39+
): Promise<CreateAuthProviderResponse> {
40+
const ownerId = request.owner.case === "ownerId" ? request.owner.value : undefined;
41+
const organizationId = request.owner.case === "organizationId" ? request.owner.value : undefined;
42+
43+
if (!organizationId && !ownerId) {
44+
throw new ConnectError("organizationId or ownerId is required", Code.InvalidArgument);
45+
}
46+
47+
if (organizationId) {
48+
const result = await this.authProviderService.createOrgAuthProvider(context.user.id, {
49+
organizationId,
50+
host: request.host,
51+
ownerId: context.user.id,
52+
type: this.apiConverter.fromAuthProviderType(request.type),
53+
clientId: request.oauth2Config?.clientId,
54+
clientSecret: request.oauth2Config?.clientSecret,
55+
});
56+
57+
return new CreateAuthProviderResponse({ authProvider: this.apiConverter.toAuthProvider(result) });
58+
} else {
59+
const result = await this.authProviderService.createAuthProviderOfUser(context.user.id, {
60+
host: request.host,
61+
ownerId: context.user.id,
62+
type: this.apiConverter.fromAuthProviderType(request.type),
63+
clientId: request.oauth2Config?.clientId,
64+
clientSecret: request.oauth2Config?.clientSecret,
65+
});
66+
67+
return new CreateAuthProviderResponse({ authProvider: this.apiConverter.toAuthProvider(result) });
68+
}
69+
}
70+
async getAuthProvider(request: GetAuthProviderRequest, context: HandlerContext): Promise<GetAuthProviderResponse> {
71+
if (!request.authProviderId) {
72+
throw new ConnectError("authProviderId is required", Code.InvalidArgument);
73+
}
74+
75+
const authProvider = await this.authProviderService.getAuthProvider(context.user.id, request.authProviderId);
76+
if (!authProvider) {
77+
throw new ConnectError("Provider not found.", Code.NotFound);
78+
}
79+
80+
return new GetAuthProviderResponse({
81+
authProvider: this.apiConverter.toAuthProvider(authProvider),
82+
});
83+
}
84+
85+
async listAuthProviders(
86+
request: ListAuthProvidersRequest,
87+
context: HandlerContext,
88+
): Promise<ListAuthProvidersResponse> {
89+
const target = request.id;
90+
const ownerId = target.case === "userId" ? target.value : undefined;
91+
const organizationId = target.case === "organizationId" ? target.value : undefined;
92+
93+
if (!organizationId && !ownerId) {
94+
throw new ConnectError("organizationId or ownerId is required", Code.InvalidArgument);
95+
}
96+
97+
const authProviders = organizationId
98+
? await this.authProviderService.getAuthProvidersOfOrg(context.user.id, organizationId)
99+
: await this.authProviderService.getAuthProvidersOfUser(context.user.id);
100+
101+
const redacted = authProviders.map(AuthProviderEntry.redact.bind(AuthProviderEntry));
102+
103+
const result = new ListAuthProvidersResponse({
104+
authProviders: redacted.map((ap) => this.apiConverter.toAuthProvider(ap)),
105+
});
106+
return result;
107+
}
108+
109+
/**
110+
* Listing descriptions of auth providers doesn't require authentication.
111+
*/
112+
@Unauthenticated()
113+
async listAuthProviderDescriptions(
114+
_request: ListAuthProviderDescriptionsRequest,
115+
context: HandlerContext,
116+
): Promise<ListAuthProviderDescriptionsResponse> {
117+
const user = context.user;
118+
const aps = user
119+
? await this.authProviderService.getAuthProviderDescriptions(user)
120+
: await this.authProviderService.getAuthProviderDescriptionsUnauthenticated();
121+
122+
return new ListAuthProviderDescriptionsResponse({
123+
descriptions: aps.map((ap: AuthProviderInfo) => this.apiConverter.toAuthProviderDescription(ap)),
124+
});
125+
}
126+
127+
async updateAuthProvider(
128+
request: UpdateAuthProviderRequest,
129+
context: HandlerContext,
130+
): Promise<UpdateAuthProviderResponse> {
131+
if (!request.authProviderId) {
132+
throw new ConnectError("authProviderId is required", Code.InvalidArgument);
133+
}
134+
const clientId = request.clientId;
135+
const clientSecret = request.clientSecret;
136+
if (!clientId || typeof clientSecret === "undefined") {
137+
throw new ConnectError("clientId or clientSecret are required", Code.InvalidArgument);
138+
}
139+
140+
const authProvider = await this.authProviderService.getAuthProvider(context.user.id, request.authProviderId);
141+
if (!authProvider) {
142+
throw new ConnectError("Provider not found.", Code.NotFound);
143+
}
144+
145+
if (authProvider.organizationId) {
146+
await this.authProviderService.updateOrgAuthProvider(context.user.id, {
147+
id: request.authProviderId,
148+
organizationId: authProvider.organizationId,
149+
clientId: clientId,
150+
clientSecret: clientSecret,
151+
});
152+
} else {
153+
await this.authProviderService.updateAuthProviderOfUser(context.user.id, {
154+
id: request.authProviderId,
155+
ownerId: context.user.id,
156+
clientId: clientId,
157+
clientSecret: clientSecret,
158+
});
159+
}
160+
161+
return new UpdateAuthProviderResponse();
162+
}
163+
164+
async deleteAuthProvider(
165+
request: DeleteAuthProviderRequest,
166+
context: HandlerContext,
167+
): Promise<DeleteAuthProviderResponse> {
168+
if (!request.authProviderId) {
169+
throw new ConnectError("authProviderId is required", Code.InvalidArgument);
170+
}
171+
172+
const authProvider = await this.authProviderService.getAuthProvider(context.user.id, request.authProviderId);
173+
if (!authProvider) {
174+
throw new ConnectError("Provider not found.", Code.NotFound);
175+
}
176+
177+
if (authProvider.organizationId) {
178+
await this.authProviderService.deleteAuthProviderOfOrg(
179+
context.user.id,
180+
authProvider.organizationId,
181+
request.authProviderId,
182+
);
183+
} else {
184+
await this.authProviderService.deleteAuthProviderOfUser(context.user.id, request.authProviderId);
185+
}
186+
187+
return new DeleteAuthProviderResponse();
188+
}
189+
}

0 commit comments

Comments
 (0)