Skip to content

Commit a5ac3da

Browse files
committed
[server] move FGA calls into AuthProviderService
* split internal upsert method `updateAuthProvider` into create and update * refactor: move `getAuthProviders` logic from gitpod-server-impl to auth-provider-service * adding db tests for auth provider server * use redacted results in service
1 parent cf78ae0 commit a5ac3da

File tree

12 files changed

+715
-193
lines changed

12 files changed

+715
-193
lines changed

components/gitpod-db/src/typeorm/entity/db-auth-provider-entry.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ export class DBAuthProviderEntry implements AuthProviderEntry {
1818
@Column()
1919
ownerId: string;
2020

21-
@Column()
21+
@Column({
22+
...TypeORM.UUID_COLUMN_TYPE,
23+
default: "",
24+
transformer: Transformer.MAP_EMPTY_STR_TO_UNDEFINED,
25+
})
2226
organizationId?: string;
2327

2428
@Column("varchar")

components/gitpod-protocol/src/gitpod-service.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,31 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
8181
updateLoggedInUser(user: Partial<User>): Promise<User>;
8282
sendPhoneNumberVerificationToken(phoneNumber: string): Promise<{ verificationId: string }>;
8383
verifyPhoneNumberVerificationToken(phoneNumber: string, token: string, verificationId: string): Promise<boolean>;
84-
getAuthProviders(): Promise<AuthProviderInfo[]>;
85-
getOwnAuthProviders(): Promise<AuthProviderEntry[]>;
86-
updateOwnAuthProvider(params: GitpodServer.UpdateOwnAuthProviderParams): Promise<AuthProviderEntry>;
87-
deleteOwnAuthProvider(params: GitpodServer.DeleteOwnAuthProviderParams): Promise<void>;
8884
getConfiguration(): Promise<Configuration>;
8985
getToken(query: GitpodServer.GetTokenSearchOptions): Promise<Token | undefined>;
9086
getGitpodTokenScopes(tokenHash: string): Promise<string[]>;
9187
deleteAccount(): Promise<void>;
9288
getClientRegion(): Promise<string | undefined>;
9389

90+
// Auth Provider API
91+
getAuthProviders(): Promise<AuthProviderInfo[]>;
92+
// user-level
93+
getOwnAuthProviders(): Promise<AuthProviderEntry[]>;
94+
updateOwnAuthProvider(params: GitpodServer.UpdateOwnAuthProviderParams): Promise<AuthProviderEntry>;
95+
deleteOwnAuthProvider(params: GitpodServer.DeleteOwnAuthProviderParams): Promise<void>;
96+
// org-level
97+
createOrgAuthProvider(params: GitpodServer.CreateOrgAuthProviderParams): Promise<AuthProviderEntry>;
98+
updateOrgAuthProvider(params: GitpodServer.UpdateOrgAuthProviderParams): Promise<AuthProviderEntry>;
99+
getOrgAuthProviders(params: GitpodServer.GetOrgAuthProviderParams): Promise<AuthProviderEntry[]>;
100+
deleteOrgAuthProvider(params: GitpodServer.DeleteOrgAuthProviderParams): Promise<void>;
101+
// public-api compatibility
102+
/** @deprecated used for public-api compatibility only */
103+
getAuthProvider(id: string): Promise<AuthProviderEntry>;
104+
/** @deprecated used for public-api compatibility only */
105+
deleteAuthProvider(id: string): Promise<void>;
106+
/** @deprecated used for public-api compatibility only */
107+
updateAuthProvider(id: string, update: AuthProviderEntry.UpdateOAuth2Config): Promise<AuthProviderEntry>;
108+
94109
// Query/retrieve workspaces
95110
getWorkspaces(options: GitpodServer.GetWorkspacesOptions): Promise<WorkspaceInfo[]>;
96111
getWorkspaceOwner(workspaceId: string): Promise<UserInfo | undefined>;
@@ -167,10 +182,6 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
167182
deleteTeam(teamId: string): Promise<void>;
168183
getOrgSettings(orgId: string): Promise<OrganizationSettings>;
169184
updateOrgSettings(teamId: string, settings: Partial<OrganizationSettings>): Promise<OrganizationSettings>;
170-
createOrgAuthProvider(params: GitpodServer.CreateOrgAuthProviderParams): Promise<AuthProviderEntry>;
171-
updateOrgAuthProvider(params: GitpodServer.UpdateOrgAuthProviderParams): Promise<AuthProviderEntry>;
172-
getOrgAuthProviders(params: GitpodServer.GetOrgAuthProviderParams): Promise<AuthProviderEntry[]>;
173-
deleteOrgAuthProvider(params: GitpodServer.DeleteOrgAuthProviderParams): Promise<void>;
174185

175186
getDefaultWorkspaceImage(params: GetDefaultWorkspaceImageParams): Promise<GetDefaultWorkspaceImageResult>;
176187

components/gitpod-protocol/src/protocol.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1529,7 +1529,6 @@ export interface AuthProviderInfo {
15291529
readonly ownerId?: string;
15301530
readonly organizationId?: string;
15311531
readonly verified: boolean;
1532-
readonly isReadonly?: boolean;
15331532
readonly hiddenOnDashboard?: boolean;
15341533
readonly disallowLogin?: boolean;
15351534
readonly icon?: string;
@@ -1588,6 +1587,7 @@ export namespace AuthProviderEntry {
15881587
clientSecret: string;
15891588
organizationId: string;
15901589
};
1590+
export type UpdateOAuth2Config = Pick<OAuth2Config, "clientId" | "clientSecret">;
15911591
export function redact(entry: AuthProviderEntry): AuthProviderEntry {
15921592
return {
15931593
...entry,
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
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 { TypeORM } from "@gitpod/gitpod-db/lib";
8+
import { AuthProviderInfo, Organization, User } from "@gitpod/gitpod-protocol";
9+
import { Experiments } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
10+
import * as chai from "chai";
11+
import { Container } from "inversify";
12+
import "mocha";
13+
import { createTestContainer } from "../test/service-testing-container-module";
14+
import { resetDB } from "@gitpod/gitpod-db/lib/test/reset-db";
15+
import { UserService } from "../user/user-service";
16+
import { AuthProviderService } from "./auth-provider-service";
17+
import { Config } from "../config";
18+
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
19+
import { expectError } from "../test/expect-utils";
20+
import { AuthProviderEntry } from "@gitpod/gitpod-protocol";
21+
import { AuthProviderParams } from "./auth-provider";
22+
import { OrganizationService } from "../orgs/organization-service";
23+
24+
const expect = chai.expect;
25+
26+
describe("AuthProviderService", async () => {
27+
let service: AuthProviderService;
28+
let userService: UserService;
29+
let container: Container;
30+
let currentUser: User;
31+
let org: Organization;
32+
33+
const newEntry = () =>
34+
<AuthProviderEntry.NewEntry>{
35+
host: "github.com",
36+
ownerId: currentUser.id,
37+
type: "GitHub",
38+
clientId: "123",
39+
clientSecret: "secret-123",
40+
};
41+
const expectedEntry = () =>
42+
<Partial<AuthProviderEntry>>{
43+
host: "github.com",
44+
oauth: {
45+
authorizationUrl: "https://github.com/login/oauth/authorize",
46+
callBackUrl: "https://gitpod.io/auth/callback",
47+
clientId: "123",
48+
clientSecret: "redacted",
49+
tokenUrl: "https://github.com/login/oauth/access_token",
50+
},
51+
organizationId: undefined,
52+
type: "GitHub",
53+
status: "pending",
54+
ownerId: currentUser.id,
55+
};
56+
const expectedParams = () =>
57+
<Partial<AuthProviderParams>>{
58+
builtin: false,
59+
disallowLogin: false,
60+
verified: false,
61+
...expectedEntry(),
62+
oauth: { ...expectedEntry().oauth, clientSecret: "secret-123" },
63+
};
64+
65+
const newOrgEntry = () =>
66+
<AuthProviderEntry.NewOrgEntry>{
67+
host: "github.com",
68+
ownerId: currentUser.id,
69+
type: "GitHub",
70+
clientId: "123",
71+
clientSecret: "secret-123",
72+
organizationId: org.id,
73+
};
74+
const expectedOrgEntry = () =>
75+
<Partial<AuthProviderEntry>>{
76+
host: "github.com",
77+
oauth: {
78+
authorizationUrl: "https://github.com/login/oauth/authorize",
79+
callBackUrl: "https://gitpod.io/auth/callback",
80+
clientId: "123",
81+
clientSecret: "redacted",
82+
tokenUrl: "https://github.com/login/oauth/access_token",
83+
},
84+
organizationId: org.id,
85+
type: "GitHub",
86+
status: "pending",
87+
ownerId: currentUser.id,
88+
};
89+
const expectedOrgParams = () =>
90+
<Partial<AuthProviderParams>>{
91+
builtin: false,
92+
disallowLogin: true,
93+
verified: false,
94+
...expectedOrgEntry(),
95+
oauth: { ...expectedOrgEntry().oauth, clientSecret: "secret-123" },
96+
};
97+
98+
const addBuiltInProvider = (host: string = "github.com") => {
99+
const config = container.get<Config>(Config);
100+
config.builtinAuthProvidersConfigured = true;
101+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
102+
config.authProviderConfigs.push((<Partial<AuthProviderParams>>{
103+
host,
104+
id: "Public-GitHub",
105+
verified: true,
106+
}) as any);
107+
};
108+
109+
beforeEach(async () => {
110+
container = createTestContainer();
111+
Experiments.configureTestingClient({
112+
centralizedPermissions: true,
113+
});
114+
service = container.get(AuthProviderService);
115+
userService = container.get<UserService>(UserService);
116+
currentUser = await userService.createUser({
117+
identity: {
118+
authId: "gh-user-1",
119+
authName: "user",
120+
authProviderId: "public-github",
121+
},
122+
});
123+
const os = container.get<OrganizationService>(OrganizationService);
124+
org = await os.createOrganization(currentUser.id, "myorg");
125+
});
126+
127+
afterEach(async () => {
128+
// Clean-up database
129+
await resetDB(container.get(TypeORM));
130+
});
131+
132+
describe("createAuthProviderOfUser", async () => {
133+
it("should create user-level provider", async () => {
134+
const providersAtStart = await service.getAllAuthProviderParams();
135+
expect(providersAtStart).to.be.empty;
136+
137+
await service.createAuthProviderOfUser(currentUser.id, newEntry());
138+
139+
const providers = await service.getAllAuthProviderParams();
140+
expect(providers).to.have.lengthOf(1);
141+
expect(providers[0]).to.deep.include(expectedParams());
142+
});
143+
144+
it("should fail in case of conflict with built-in provider", async () => {
145+
addBuiltInProvider();
146+
147+
const providersAtStart = await service.getAllAuthProviderParams();
148+
expect(providersAtStart).to.be.empty;
149+
150+
await expectError(ErrorCodes.CONFLICT, service.createAuthProviderOfUser(currentUser.id, newEntry()));
151+
});
152+
it("should fail if host is not reachable", async () => {
153+
await expectError(
154+
ErrorCodes.BAD_REQUEST,
155+
service.createAuthProviderOfUser(currentUser.id, {
156+
...newEntry(),
157+
host: "please-dont-register-this-domain.com:666",
158+
}),
159+
);
160+
});
161+
it("should fail if trying to register same host", async () => {
162+
const providersAtStart = await service.getAllAuthProviderParams();
163+
expect(providersAtStart).to.be.empty;
164+
165+
await service.createAuthProviderOfUser(currentUser.id, newEntry());
166+
167+
await expectError(ErrorCodes.CONFLICT, service.createAuthProviderOfUser(currentUser.id, newEntry()));
168+
});
169+
});
170+
171+
describe("createOrgAuthProvider", async () => {
172+
it("should create org-level provider", async () => {
173+
const providersAtStart = await service.getAllAuthProviderParams();
174+
expect(providersAtStart).to.be.empty;
175+
176+
await service.createOrgAuthProvider(currentUser.id, newOrgEntry());
177+
178+
const providers = await service.getAllAuthProviderParams();
179+
expect(providers).to.have.lengthOf(1);
180+
expect(providers[0]).to.deep.include(expectedOrgParams());
181+
});
182+
it("should fail if host is not reachable", async () => {
183+
await expectError(
184+
ErrorCodes.BAD_REQUEST,
185+
service.createOrgAuthProvider(currentUser.id, {
186+
...newOrgEntry(),
187+
host: "please-dont-register-this-domain.com:666",
188+
}),
189+
);
190+
});
191+
it("should fail if trying to register same host", async () => {
192+
const providersAtStart = await service.getAllAuthProviderParams();
193+
expect(providersAtStart).to.be.empty;
194+
195+
await service.createOrgAuthProvider(currentUser.id, newOrgEntry());
196+
197+
await expectError(ErrorCodes.CONFLICT, service.createAuthProviderOfUser(currentUser.id, newOrgEntry()));
198+
});
199+
});
200+
describe("getAuthProvider", async () => {
201+
it("should find org-level provider", async () => {
202+
const providersAtStart = await service.getAllAuthProviderParams();
203+
expect(providersAtStart).to.be.empty;
204+
205+
const created = await service.createOrgAuthProvider(currentUser.id, newOrgEntry());
206+
207+
const retrieved = await service.getAuthProvider(currentUser.id, created.id);
208+
expect(retrieved).to.deep.include(expectedOrgEntry());
209+
});
210+
it("should find user-level provider", async () => {
211+
const providersAtStart = await service.getAllAuthProviderParams();
212+
expect(providersAtStart).to.be.empty;
213+
214+
const created = await service.createAuthProviderOfUser(currentUser.id, newEntry());
215+
216+
const retrieved = await service.getAuthProvider(currentUser.id, created.id);
217+
expect(retrieved).to.deep.include(expectedEntry());
218+
});
219+
it("should not find org-level provider for non-members", async () => {
220+
const providersAtStart = await service.getAllAuthProviderParams();
221+
expect(providersAtStart).to.be.empty;
222+
223+
const created = await service.createOrgAuthProvider(currentUser.id, newOrgEntry());
224+
225+
const nonMember = await userService.createUser({
226+
identity: {
227+
authId: "gh-user-2",
228+
authName: "user2",
229+
authProviderId: "public-github",
230+
},
231+
});
232+
233+
// expecting 404, as Orgs shall not be enumerable to non-members
234+
await expectError(ErrorCodes.NOT_FOUND, service.getAuthProvider(nonMember.id, created.id));
235+
});
236+
});
237+
238+
describe("getAuthProviderDescriptionsUnauthenticated", async () => {
239+
it("should find built-in provider", async () => {
240+
addBuiltInProvider();
241+
242+
const providers = await service.getAuthProviderDescriptionsUnauthenticated();
243+
expect(providers).to.has.lengthOf(1);
244+
expect(providers[0].authProviderId).to.be.equal("Public-GitHub");
245+
});
246+
it("should find only built-in providers but no user-level providers", async () => {
247+
addBuiltInProvider("localhost");
248+
249+
const created = await service.createAuthProviderOfUser(currentUser.id, newEntry());
250+
await service.markAsVerified({ userId: currentUser.id, id: created.id });
251+
252+
const providers = await service.getAuthProviderDescriptionsUnauthenticated();
253+
expect(providers).to.has.lengthOf(1);
254+
expect(providers[0].host).to.be.equal("localhost");
255+
});
256+
it("should find user-level providers if no built-in providers present", async () => {
257+
const created = await service.createAuthProviderOfUser(currentUser.id, newEntry());
258+
await service.markAsVerified({ userId: currentUser.id, id: created.id });
259+
260+
const providers = await service.getAuthProviderDescriptionsUnauthenticated();
261+
expect(providers).to.has.lengthOf(1);
262+
expect(providers[0]).to.deep.include(<Partial<AuthProviderInfo>>{
263+
authProviderId: created.id,
264+
authProviderType: created.type,
265+
host: created.host,
266+
});
267+
268+
const privateProperties: (keyof AuthProviderEntry)[] = ["oauth", "organizationId", "ownerId"];
269+
for (const privateProperty of privateProperties) {
270+
expect(providers[0]).to.not.haveOwnProperty(privateProperty);
271+
}
272+
});
273+
});
274+
275+
describe("getAuthProviderDescriptions", async () => {
276+
it("should find built-in provider", async () => {
277+
addBuiltInProvider();
278+
279+
const providers = await service.getAuthProviderDescriptions(currentUser);
280+
expect(providers).to.has.lengthOf(1);
281+
expect(providers[0].authProviderId).to.be.equal("Public-GitHub");
282+
});
283+
it("should find built-in providers and _own_ user-level providers", async () => {
284+
addBuiltInProvider("localhost");
285+
286+
const created = await service.createAuthProviderOfUser(currentUser.id, newEntry());
287+
await service.markAsVerified({ userId: currentUser.id, id: created.id });
288+
289+
const providers = await service.getAuthProviderDescriptions(currentUser);
290+
expect(providers).to.has.lengthOf(2);
291+
expect(providers[0].host).to.be.equal(created.host);
292+
expect(providers[1].host).to.be.equal("localhost");
293+
});
294+
it("should find user-level providers if no built-in providers present", async () => {
295+
const created = await service.createAuthProviderOfUser(currentUser.id, newEntry());
296+
await service.markAsVerified({ userId: currentUser.id, id: created.id });
297+
298+
const providers = await service.getAuthProviderDescriptions(currentUser);
299+
expect(providers).to.has.lengthOf(1);
300+
expect(providers[0]).to.deep.include(<Partial<AuthProviderInfo>>{
301+
authProviderId: created.id,
302+
authProviderType: created.type,
303+
host: created.host,
304+
organizationId: created.organizationId,
305+
ownerId: created.ownerId,
306+
});
307+
308+
const oauthProperty: keyof AuthProviderEntry = "oauth";
309+
expect(providers[0]).to.not.haveOwnProperty(oauthProperty);
310+
});
311+
});
312+
});

0 commit comments

Comments
 (0)