Skip to content

Commit 0e00e3d

Browse files
authored
[public-api] Add AuthProviderService service (#19008)
* add Unauthenticated decorator for public-api * [server] add AuthProviderServiceAPI * [dashboard] add client facade (JsonRpcAuthProviderClient) * use uuidValidate * update UpdateAuthProviderResponse to return provider * return updated provider in UpdateAuthProviderResponse * handle pagination for ListAuthProvider(Description)s * add simple conversion tests for auth providers * relax param validation on updateAuthProvider allow to update clientId or clientSecret separately.
1 parent 8dcd0a5 commit 0e00e3d

17 files changed

+884
-172
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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 { PartialMessage } from "@bufbuild/protobuf";
8+
import { Code, ConnectError, PromiseClient } from "@connectrpc/connect";
9+
import { AuthProviderService } from "@gitpod/public-api/lib/gitpod/v1/authprovider_connect";
10+
import {
11+
CreateAuthProviderRequest,
12+
CreateAuthProviderResponse,
13+
DeleteAuthProviderRequest,
14+
DeleteAuthProviderResponse,
15+
GetAuthProviderRequest,
16+
GetAuthProviderResponse,
17+
ListAuthProviderDescriptionsRequest,
18+
ListAuthProviderDescriptionsResponse,
19+
ListAuthProvidersRequest,
20+
ListAuthProvidersResponse,
21+
UpdateAuthProviderRequest,
22+
UpdateAuthProviderResponse,
23+
} from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
24+
import { converter } from "./public-api";
25+
import { getGitpodService } from "./service";
26+
27+
export class JsonRpcAuthProviderClient implements PromiseClient<typeof AuthProviderService> {
28+
async createAuthProvider(request: PartialMessage<CreateAuthProviderRequest>): Promise<CreateAuthProviderResponse> {
29+
const ownerId = request.owner?.case === "ownerId" ? request.owner.value : undefined;
30+
const organizationId = request.owner?.case === "organizationId" ? request.owner.value : undefined;
31+
32+
if (!organizationId && !ownerId) {
33+
throw new ConnectError("organizationId or ownerId is required", Code.InvalidArgument);
34+
}
35+
if (!request.type) {
36+
throw new ConnectError("type is required", Code.InvalidArgument);
37+
}
38+
if (!request.host) {
39+
throw new ConnectError("host is required", Code.InvalidArgument);
40+
}
41+
42+
if (organizationId) {
43+
const result = await getGitpodService().server.createOrgAuthProvider({
44+
entry: {
45+
organizationId,
46+
host: request.host,
47+
type: converter.fromAuthProviderType(request.type),
48+
clientId: request.oauth2Config?.clientId,
49+
clientSecret: request.oauth2Config?.clientSecret,
50+
},
51+
});
52+
return new CreateAuthProviderResponse({ authProvider: converter.toAuthProvider(result) });
53+
}
54+
if (ownerId) {
55+
const result = await getGitpodService().server.updateOwnAuthProvider({
56+
entry: {
57+
host: request.host,
58+
ownerId,
59+
type: converter.fromAuthProviderType(request.type),
60+
clientId: request.oauth2Config?.clientId,
61+
clientSecret: request.oauth2Config?.clientSecret,
62+
},
63+
});
64+
return new CreateAuthProviderResponse({ authProvider: converter.toAuthProvider(result) });
65+
}
66+
67+
throw new ConnectError("organizationId or ownerId is required", Code.InvalidArgument);
68+
}
69+
70+
async getAuthProvider(request: PartialMessage<GetAuthProviderRequest>): Promise<GetAuthProviderResponse> {
71+
if (!request.authProviderId) {
72+
throw new ConnectError("authProviderId is required", Code.InvalidArgument);
73+
}
74+
75+
const provider = await getGitpodService().server.getAuthProvider(request.authProviderId);
76+
return new GetAuthProviderResponse({
77+
authProvider: converter.toAuthProvider(provider),
78+
});
79+
}
80+
81+
async listAuthProviders(request: PartialMessage<ListAuthProvidersRequest>): Promise<ListAuthProvidersResponse> {
82+
if (!request.id?.case) {
83+
throw new ConnectError("id is required", Code.InvalidArgument);
84+
}
85+
const organizationId = request.id.case === "organizationId" ? request.id.value : undefined;
86+
const userId = request.id.case === "userId" ? request.id.value : undefined;
87+
88+
if (!organizationId && !userId) {
89+
throw new ConnectError("organizationId or userId is required", Code.InvalidArgument);
90+
}
91+
92+
const authProviders = !!organizationId
93+
? await getGitpodService().server.getOrgAuthProviders({
94+
organizationId,
95+
})
96+
: await getGitpodService().server.getOwnAuthProviders();
97+
const response = new ListAuthProvidersResponse({
98+
authProviders: authProviders.map(converter.toAuthProvider),
99+
});
100+
return response;
101+
}
102+
103+
async listAuthProviderDescriptions(
104+
request: PartialMessage<ListAuthProviderDescriptionsRequest>,
105+
): Promise<ListAuthProviderDescriptionsResponse> {
106+
const aps = await getGitpodService().server.getAuthProviders();
107+
return new ListAuthProviderDescriptionsResponse({
108+
descriptions: aps.map((ap) => converter.toAuthProviderDescription(ap)),
109+
});
110+
}
111+
112+
async updateAuthProvider(request: PartialMessage<UpdateAuthProviderRequest>): Promise<UpdateAuthProviderResponse> {
113+
if (!request.authProviderId) {
114+
throw new ConnectError("authProviderId is required", Code.InvalidArgument);
115+
}
116+
const clientId = request?.clientId;
117+
const clientSecret = request?.clientSecret;
118+
if (!clientId || !clientSecret) {
119+
throw new ConnectError("clientId or clientSecret are required", Code.InvalidArgument);
120+
}
121+
122+
const entry = await getGitpodService().server.updateAuthProvider(request.authProviderId, {
123+
clientId,
124+
clientSecret,
125+
});
126+
return new UpdateAuthProviderResponse({
127+
authProvider: converter.toAuthProvider(entry),
128+
});
129+
}
130+
131+
async deleteAuthProvider(request: PartialMessage<DeleteAuthProviderRequest>): Promise<DeleteAuthProviderResponse> {
132+
if (!request.authProviderId) {
133+
throw new ConnectError("authProviderId is required", Code.InvalidArgument);
134+
}
135+
await getGitpodService().server.deleteAuthProvider(request.authProviderId);
136+
return new DeleteAuthProviderResponse();
137+
}
138+
}

components/dashboard/src/service/public-api.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { getMetricsInterceptor } from "@gitpod/public-api/lib/metrics";
2323
import { getExperimentsClient } from "../experiments/client";
2424
import { JsonRpcOrganizationClient } from "./json-rpc-organization-client";
2525
import { JsonRpcWorkspaceClient } from "./json-rpc-workspace-client";
26+
import { JsonRpcAuthProviderClient } from "./json-rpc-authprovider-client";
27+
import { AuthProviderService } from "@gitpod/public-api/lib/gitpod/v1/authprovider_connect";
2628

2729
const transport = createConnectTransport({
2830
baseUrl: `${window.location.protocol}//${window.location.host}/public-api`,
@@ -49,6 +51,8 @@ export const organizationClient = createServiceClient(
4951
// No jsonrcp client for the configuration service as it's only used in new UI of the dashboard
5052
export const configurationClient = createServiceClient(ConfigurationService);
5153

54+
export const authProviderClient = createServiceClient(AuthProviderService, new JsonRpcAuthProviderClient());
55+
5256
export async function listAllProjects(opts: { orgId: string }): Promise<ProtocolProject[]> {
5357
let pagination = {
5458
page: 1,

components/gitpod-protocol/src/protocol.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1577,14 +1577,16 @@ export namespace AuthProviderEntry {
15771577
clientId?: string;
15781578
clientSecret?: string;
15791579
};
1580-
export type UpdateEntry = Pick<AuthProviderEntry, "id" | "ownerId"> &
1581-
Pick<OAuth2Config, "clientId" | "clientSecret">;
1580+
export type UpdateEntry = Pick<AuthProviderEntry, "id" | "ownerId"> & {
1581+
clientId?: string;
1582+
clientSecret?: string;
1583+
};
15821584
export type NewOrgEntry = NewEntry & {
15831585
organizationId: string;
15841586
};
15851587
export type UpdateOrgEntry = Pick<AuthProviderEntry, "id"> & {
1586-
clientId: string;
1587-
clientSecret: string;
1588+
clientId?: string;
1589+
clientSecret?: string;
15881590
organizationId: string;
15891591
};
15901592
export type UpdateOAuth2Config = Pick<OAuth2Config, "clientId" | "clientSecret">;

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

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ import {
2121
PrebuildSettings,
2222
WorkspaceSettings,
2323
} from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
24+
import { AuthProviderEntry, AuthProviderInfo } from "./protocol";
25+
import {
26+
AuthProvider,
27+
AuthProviderDescription,
28+
AuthProviderType,
29+
} from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
2430

2531
describe("PublicAPIConverter", () => {
2632
const converter = new PublicAPIConverter();
@@ -721,4 +727,80 @@ describe("PublicAPIConverter", () => {
721727
expect(result).to.deep.equal(new WorkspaceSettings());
722728
});
723729
});
730+
731+
describe("toAuthProviderDescription", () => {
732+
const info: AuthProviderInfo = {
733+
authProviderId: "ap123",
734+
authProviderType: "GitHub",
735+
host: "localhost",
736+
verified: true,
737+
icon: "unused icon",
738+
description: "unused description",
739+
settingsUrl: "unused",
740+
ownerId: "unused",
741+
organizationId: "unused",
742+
};
743+
const description = new AuthProviderDescription({
744+
id: info.authProviderId,
745+
type: AuthProviderType.GITHUB,
746+
host: info.host,
747+
icon: info.icon,
748+
description: info.description,
749+
});
750+
it("should convert an auth provider info to a description", () => {
751+
const result = converter.toAuthProviderDescription(info);
752+
expect(result).to.deep.equal(description);
753+
});
754+
});
755+
756+
describe("toAuthProvider", () => {
757+
const entry: AuthProviderEntry = {
758+
id: "ap123",
759+
type: "GitHub",
760+
host: "localhost",
761+
status: "pending",
762+
ownerId: "userId",
763+
organizationId: "orgId123",
764+
oauth: {
765+
clientId: "clientId123",
766+
clientSecret: "should not appear in result",
767+
callBackUrl: "localhost/callback",
768+
authorizationUrl: "auth.service/authorize",
769+
tokenUrl: "auth.service/token",
770+
},
771+
};
772+
const provider = new AuthProvider({
773+
id: entry.id,
774+
type: AuthProviderType.GITHUB,
775+
host: entry.host,
776+
oauth2Config: {
777+
clientId: entry.oauth?.clientId,
778+
clientSecret: entry.oauth?.clientSecret,
779+
},
780+
owner: {
781+
case: "organizationId",
782+
value: entry.organizationId!,
783+
},
784+
});
785+
it("should convert an auth provider", () => {
786+
const result = converter.toAuthProvider(entry);
787+
expect(result).to.deep.equal(provider);
788+
});
789+
});
790+
791+
describe("toAuthProviderType", () => {
792+
const mapping: { [key: string]: number } = {
793+
GitHub: AuthProviderType.GITHUB,
794+
GitLab: AuthProviderType.GITLAB,
795+
Bitbucket: AuthProviderType.BITBUCKET,
796+
BitbucketServer: AuthProviderType.BITBUCKET_SERVER,
797+
Other: AuthProviderType.UNSPECIFIED,
798+
};
799+
it("should convert auth provider types", () => {
800+
for (const k of Object.getOwnPropertyNames(mapping)) {
801+
const result = converter.toAuthProviderType(k);
802+
expect(result).to.deep.equal(mapping[k]);
803+
}
804+
});
805+
});
724806
});

0 commit comments

Comments
 (0)