Skip to content

Commit

Permalink
[public-api] Add AuthProviderService service (#19008)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
AlexTugarev authored Nov 13, 2023
1 parent 8dcd0a5 commit 0e00e3d
Show file tree
Hide file tree
Showing 17 changed files with 884 additions and 172 deletions.
138 changes: 138 additions & 0 deletions components/dashboard/src/service/json-rpc-authprovider-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/**
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License.AGPL.txt in the project root for license information.
*/

import { PartialMessage } from "@bufbuild/protobuf";
import { Code, ConnectError, PromiseClient } from "@connectrpc/connect";
import { AuthProviderService } from "@gitpod/public-api/lib/gitpod/v1/authprovider_connect";
import {
CreateAuthProviderRequest,
CreateAuthProviderResponse,
DeleteAuthProviderRequest,
DeleteAuthProviderResponse,
GetAuthProviderRequest,
GetAuthProviderResponse,
ListAuthProviderDescriptionsRequest,
ListAuthProviderDescriptionsResponse,
ListAuthProvidersRequest,
ListAuthProvidersResponse,
UpdateAuthProviderRequest,
UpdateAuthProviderResponse,
} from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
import { converter } from "./public-api";
import { getGitpodService } from "./service";

export class JsonRpcAuthProviderClient implements PromiseClient<typeof AuthProviderService> {
async createAuthProvider(request: PartialMessage<CreateAuthProviderRequest>): Promise<CreateAuthProviderResponse> {
const ownerId = request.owner?.case === "ownerId" ? request.owner.value : undefined;
const organizationId = request.owner?.case === "organizationId" ? request.owner.value : undefined;

if (!organizationId && !ownerId) {
throw new ConnectError("organizationId or ownerId is required", Code.InvalidArgument);
}
if (!request.type) {
throw new ConnectError("type is required", Code.InvalidArgument);
}
if (!request.host) {
throw new ConnectError("host is required", Code.InvalidArgument);
}

if (organizationId) {
const result = await getGitpodService().server.createOrgAuthProvider({
entry: {
organizationId,
host: request.host,
type: converter.fromAuthProviderType(request.type),
clientId: request.oauth2Config?.clientId,
clientSecret: request.oauth2Config?.clientSecret,
},
});
return new CreateAuthProviderResponse({ authProvider: converter.toAuthProvider(result) });
}
if (ownerId) {
const result = await getGitpodService().server.updateOwnAuthProvider({
entry: {
host: request.host,
ownerId,
type: converter.fromAuthProviderType(request.type),
clientId: request.oauth2Config?.clientId,
clientSecret: request.oauth2Config?.clientSecret,
},
});
return new CreateAuthProviderResponse({ authProvider: converter.toAuthProvider(result) });
}

throw new ConnectError("organizationId or ownerId is required", Code.InvalidArgument);
}

async getAuthProvider(request: PartialMessage<GetAuthProviderRequest>): Promise<GetAuthProviderResponse> {
if (!request.authProviderId) {
throw new ConnectError("authProviderId is required", Code.InvalidArgument);
}

const provider = await getGitpodService().server.getAuthProvider(request.authProviderId);
return new GetAuthProviderResponse({
authProvider: converter.toAuthProvider(provider),
});
}

async listAuthProviders(request: PartialMessage<ListAuthProvidersRequest>): Promise<ListAuthProvidersResponse> {
if (!request.id?.case) {
throw new ConnectError("id is required", Code.InvalidArgument);
}
const organizationId = request.id.case === "organizationId" ? request.id.value : undefined;
const userId = request.id.case === "userId" ? request.id.value : undefined;

if (!organizationId && !userId) {
throw new ConnectError("organizationId or userId is required", Code.InvalidArgument);
}

const authProviders = !!organizationId
? await getGitpodService().server.getOrgAuthProviders({
organizationId,
})
: await getGitpodService().server.getOwnAuthProviders();
const response = new ListAuthProvidersResponse({
authProviders: authProviders.map(converter.toAuthProvider),
});
return response;
}

async listAuthProviderDescriptions(
request: PartialMessage<ListAuthProviderDescriptionsRequest>,
): Promise<ListAuthProviderDescriptionsResponse> {
const aps = await getGitpodService().server.getAuthProviders();
return new ListAuthProviderDescriptionsResponse({
descriptions: aps.map((ap) => converter.toAuthProviderDescription(ap)),
});
}

async updateAuthProvider(request: PartialMessage<UpdateAuthProviderRequest>): Promise<UpdateAuthProviderResponse> {
if (!request.authProviderId) {
throw new ConnectError("authProviderId is required", Code.InvalidArgument);
}
const clientId = request?.clientId;
const clientSecret = request?.clientSecret;
if (!clientId || !clientSecret) {
throw new ConnectError("clientId or clientSecret are required", Code.InvalidArgument);
}

const entry = await getGitpodService().server.updateAuthProvider(request.authProviderId, {
clientId,
clientSecret,
});
return new UpdateAuthProviderResponse({
authProvider: converter.toAuthProvider(entry),
});
}

async deleteAuthProvider(request: PartialMessage<DeleteAuthProviderRequest>): Promise<DeleteAuthProviderResponse> {
if (!request.authProviderId) {
throw new ConnectError("authProviderId is required", Code.InvalidArgument);
}
await getGitpodService().server.deleteAuthProvider(request.authProviderId);
return new DeleteAuthProviderResponse();
}
}
4 changes: 4 additions & 0 deletions components/dashboard/src/service/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { getMetricsInterceptor } from "@gitpod/public-api/lib/metrics";
import { getExperimentsClient } from "../experiments/client";
import { JsonRpcOrganizationClient } from "./json-rpc-organization-client";
import { JsonRpcWorkspaceClient } from "./json-rpc-workspace-client";
import { JsonRpcAuthProviderClient } from "./json-rpc-authprovider-client";
import { AuthProviderService } from "@gitpod/public-api/lib/gitpod/v1/authprovider_connect";

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

export const authProviderClient = createServiceClient(AuthProviderService, new JsonRpcAuthProviderClient());

export async function listAllProjects(opts: { orgId: string }): Promise<ProtocolProject[]> {
let pagination = {
page: 1,
Expand Down
10 changes: 6 additions & 4 deletions components/gitpod-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1577,14 +1577,16 @@ export namespace AuthProviderEntry {
clientId?: string;
clientSecret?: string;
};
export type UpdateEntry = Pick<AuthProviderEntry, "id" | "ownerId"> &
Pick<OAuth2Config, "clientId" | "clientSecret">;
export type UpdateEntry = Pick<AuthProviderEntry, "id" | "ownerId"> & {
clientId?: string;
clientSecret?: string;
};
export type NewOrgEntry = NewEntry & {
organizationId: string;
};
export type UpdateOrgEntry = Pick<AuthProviderEntry, "id"> & {
clientId: string;
clientSecret: string;
clientId?: string;
clientSecret?: string;
organizationId: string;
};
export type UpdateOAuth2Config = Pick<OAuth2Config, "clientId" | "clientSecret">;
Expand Down
82 changes: 82 additions & 0 deletions components/gitpod-protocol/src/public-api-converter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ import {
PrebuildSettings,
WorkspaceSettings,
} from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
import { AuthProviderEntry, AuthProviderInfo } from "./protocol";
import {
AuthProvider,
AuthProviderDescription,
AuthProviderType,
} from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";

describe("PublicAPIConverter", () => {
const converter = new PublicAPIConverter();
Expand Down Expand Up @@ -721,4 +727,80 @@ describe("PublicAPIConverter", () => {
expect(result).to.deep.equal(new WorkspaceSettings());
});
});

describe("toAuthProviderDescription", () => {
const info: AuthProviderInfo = {
authProviderId: "ap123",
authProviderType: "GitHub",
host: "localhost",
verified: true,
icon: "unused icon",
description: "unused description",
settingsUrl: "unused",
ownerId: "unused",
organizationId: "unused",
};
const description = new AuthProviderDescription({
id: info.authProviderId,
type: AuthProviderType.GITHUB,
host: info.host,
icon: info.icon,
description: info.description,
});
it("should convert an auth provider info to a description", () => {
const result = converter.toAuthProviderDescription(info);
expect(result).to.deep.equal(description);
});
});

describe("toAuthProvider", () => {
const entry: AuthProviderEntry = {
id: "ap123",
type: "GitHub",
host: "localhost",
status: "pending",
ownerId: "userId",
organizationId: "orgId123",
oauth: {
clientId: "clientId123",
clientSecret: "should not appear in result",
callBackUrl: "localhost/callback",
authorizationUrl: "auth.service/authorize",
tokenUrl: "auth.service/token",
},
};
const provider = new AuthProvider({
id: entry.id,
type: AuthProviderType.GITHUB,
host: entry.host,
oauth2Config: {
clientId: entry.oauth?.clientId,
clientSecret: entry.oauth?.clientSecret,
},
owner: {
case: "organizationId",
value: entry.organizationId!,
},
});
it("should convert an auth provider", () => {
const result = converter.toAuthProvider(entry);
expect(result).to.deep.equal(provider);
});
});

describe("toAuthProviderType", () => {
const mapping: { [key: string]: number } = {
GitHub: AuthProviderType.GITHUB,
GitLab: AuthProviderType.GITLAB,
Bitbucket: AuthProviderType.BITBUCKET,
BitbucketServer: AuthProviderType.BITBUCKET_SERVER,
Other: AuthProviderType.UNSPECIFIED,
};
it("should convert auth provider types", () => {
for (const k of Object.getOwnPropertyNames(mapping)) {
const result = converter.toAuthProviderType(k);
expect(result).to.deep.equal(mapping[k]);
}
});
});
});
Loading

0 comments on commit 0e00e3d

Please sign in to comment.