Skip to content

Commit 1010fde

Browse files
committed
[server] move FGA calls into AuthProviderService
1 parent e51d974 commit 1010fde

File tree

5 files changed

+169
-65
lines changed

5 files changed

+169
-65
lines changed

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.UpdateEntry): 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/server/src/auth/auth-provider-service.ts

Lines changed: 80 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,19 @@ import { oauthUrls as bbsUrls } from "../bitbucket-server/bitbucket-server-urls"
1616
import { oauthUrls as bbUrls } from "../bitbucket/bitbucket-urls";
1717
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
1818
import fetch from "node-fetch";
19+
import { Authorizer } from "../authorization/authorizer";
20+
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
21+
import { HostContextProvider } from "./host-context-provider";
1922

2023
@injectable()
2124
export class AuthProviderService {
22-
@inject(AuthProviderEntryDB)
23-
protected authProviderDB: AuthProviderEntryDB;
24-
25-
@inject(TeamDB)
26-
protected teamDB: TeamDB;
27-
28-
@inject(Config)
29-
protected readonly config: Config;
25+
constructor(
26+
@inject(AuthProviderEntryDB) private readonly authProviderDB: AuthProviderEntryDB,
27+
@inject(TeamDB) private readonly teamDB: TeamDB,
28+
@inject(Config) protected readonly config: Config,
29+
@inject(HostContextProvider) private readonly hostContexts: HostContextProvider,
30+
@inject(Authorizer) private readonly auth: Authorizer,
31+
) {}
3032

3133
/**
3234
* Returns all auth providers.
@@ -57,19 +59,59 @@ export class AuthProviderService {
5759
};
5860

5961
async getAuthProvidersOfUser(user: User | string): Promise<AuthProviderEntry[]> {
60-
const result = await this.authProviderDB.findByUserId(User.is(user) ? user.id : user);
62+
const userId = User.is(user) ? user.id : user;
63+
await this.auth.checkPermissionOnUser(userId, "read_info", userId);
64+
65+
const result = await this.authProviderDB.findByUserId(userId);
6166
return result;
6267
}
6368

64-
async getAuthProvidersOfOrg(organizationId: string): Promise<AuthProviderEntry[]> {
69+
async getAuthProvidersOfOrg(userId: string, organizationId: string): Promise<AuthProviderEntry[]> {
70+
await this.auth.checkPermissionOnOrganization(userId, "read_git_provider", organizationId);
6571
const result = await this.authProviderDB.findByOrgId(organizationId);
6672
return result;
6773
}
6874

69-
async deleteAuthProvider(authProvider: AuthProviderEntry): Promise<void> {
75+
async deleteAuthProviderOfUser(userId: string, authProviderId: string): Promise<void> {
76+
await this.auth.checkPermissionOnUser(userId, "write_info", userId);
77+
78+
const ownProviders = await this.getAuthProvidersOfUser(userId);
79+
const authProvider = ownProviders.find((p) => p.id === authProviderId);
80+
if (!authProvider) {
81+
throw new ApplicationError(ErrorCodes.NOT_FOUND, "User resource not found.");
82+
}
83+
7084
await this.authProviderDB.delete(authProvider);
7185
}
7286

87+
async deleteAuthProviderOfOrg(userId: string, organizationId: string, authProviderId: string): Promise<void> {
88+
await this.auth.checkPermissionOnOrganization(userId, "write_git_provider", organizationId);
89+
90+
// Find the matching auth provider we're attempting to delete
91+
const orgProviders = await this.getAuthProvidersOfOrg(userId, organizationId);
92+
const authProvider = orgProviders.find((p) => p.id === authProviderId && p.organizationId === organizationId);
93+
if (!authProvider) {
94+
throw new ApplicationError(ErrorCodes.NOT_FOUND, "Provider resource not found.");
95+
}
96+
97+
await this.authProviderDB.delete(authProvider);
98+
}
99+
100+
async getAuthProvider(userId: string, id: string): Promise<AuthProviderEntry | undefined> {
101+
const result = await this.authProviderDB.findById(id);
102+
if (!result) {
103+
return undefined;
104+
}
105+
106+
if (result.organizationId) {
107+
await this.auth.checkPermissionOnOrganization(userId, "write_git_provider", result.organizationId);
108+
} else {
109+
await this.auth.checkPermissionOnUser(userId, "write_info", userId);
110+
}
111+
112+
return result;
113+
}
114+
73115
async updateAuthProvider(
74116
entry: AuthProviderEntry.UpdateEntry | AuthProviderEntry.NewEntry,
75117
): Promise<AuthProviderEntry> {
@@ -109,20 +151,39 @@ export class AuthProviderService {
109151
return await this.authProviderDB.storeAuthProvider(authProvider as AuthProviderEntry, true);
110152
}
111153

112-
async createOrgAuthProvider(entry: AuthProviderEntry.NewOrgEntry): Promise<AuthProviderEntry> {
113-
const orgProviders = await this.authProviderDB.findByOrgId(entry.organizationId);
114-
const existing = orgProviders.find((p) => p.host === entry.host);
115-
if (existing) {
116-
throw new Error("Provider for this host already exists.");
154+
async createOrgAuthProvider(userId: string, newEntry: AuthProviderEntry.NewOrgEntry): Promise<AuthProviderEntry> {
155+
await this.auth.checkPermissionOnOrganization(userId, "write_git_provider", newEntry.organizationId);
156+
157+
// on creating we're are checking for already existing runtime providers
158+
const host = newEntry.host && newEntry.host.toLowerCase();
159+
160+
if (!(await this.isHostReachable(host))) {
161+
log.info(`Host could not be reached.`, { newEntry });
162+
throw new ApplicationError(ErrorCodes.BAD_REQUEST, `Host could not be reached.`);
117163
}
118164

119-
const authProvider = this.initializeNewProvider(entry);
165+
const hostContext = this.hostContexts.get(host);
166+
if (hostContext) {
167+
const builtInExists = hostContext.authProvider.params.ownerId === undefined;
168+
log.info(`Attempt to override an existing provider.`, { newEntry, builtInExists });
169+
throw new ApplicationError(ErrorCodes.CONFLICT, `Attempt to override an existing provider.`);
170+
}
120171

121-
return await this.authProviderDB.storeAuthProvider(authProvider as AuthProviderEntry, true);
172+
const orgProviders = await this.authProviderDB.findByOrgId(newEntry.organizationId);
173+
const existing = orgProviders.find((p) => p.host === host);
174+
if (existing) {
175+
log.info(`Attempt to override an existing provider.`, { newEntry });
176+
throw new ApplicationError(ErrorCodes.CONFLICT, `Provider for this host already exists.`);
177+
}
178+
179+
const authProvider = this.initializeNewProvider(newEntry);
180+
return await this.authProviderDB.storeAuthProvider(authProvider, true);
122181
}
123182

124-
async updateOrgAuthProvider(entry: AuthProviderEntry.UpdateOrgEntry): Promise<AuthProviderEntry> {
183+
async updateOrgAuthProvider(userId: string, entry: AuthProviderEntry.UpdateOrgEntry): Promise<AuthProviderEntry> {
125184
const { id, organizationId } = entry;
185+
await this.auth.checkPermissionOnOrganization(userId, "write_git_provider", organizationId);
186+
126187
// TODO can we change this to query for the provider by id and org instead of loading all from org?
127188
const existing = (await this.authProviderDB.findByOrgId(organizationId)).find((p) => p.id === id);
128189
if (!existing) {

components/server/src/auth/rate-limiter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,9 @@ const defaultFunctions: FunctionsConfig = {
195195
getIDToken: { group: "default", points: 1 },
196196
reportErrorBoundary: { group: "default", points: 1 },
197197
getOnboardingState: { group: "default", points: 1 },
198+
getAuthProvider: { group: "default", points: 1 },
199+
deleteAuthProvider: { group: "default", points: 1 },
200+
updateAuthProvider: { group: "default", points: 1 },
198201
};
199202

200203
function getConfig(config: RateLimiterConfig): RateLimiterConfig {

components/server/src/user/user-deletion-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export class UserDeletionService {
4545
const authProviders = await this.authProviderService.getAuthProvidersOfUser(user);
4646
for (const provider of authProviders) {
4747
try {
48-
await this.authProviderService.deleteAuthProvider(provider);
48+
await this.authProviderService.deleteAuthProviderOfUser(user.id, provider.id);
4949
} catch (error) {
5050
log.error({ userId: user.id }, "Failed to delete user's auth provider.", error);
5151
}

components/server/src/workspace/gitpod-server-impl.ts

Lines changed: 66 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2989,12 +2989,8 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
29892989
traceAPIParams(ctx, { params });
29902990

29912991
const user = await this.checkAndBlockUser("deleteOwnAuthProvider");
2992-
const ownProviders = await this.authProviderService.getAuthProvidersOfUser(user.id);
2993-
const authProvider = ownProviders.find((p) => p.id === params.id);
2994-
if (!authProvider) {
2995-
throw new ApplicationError(ErrorCodes.NOT_FOUND, "User resource not found.");
2996-
}
2997-
await this.authProviderService.deleteAuthProvider(authProvider);
2992+
2993+
await this.authProviderService.deleteAuthProviderOfUser(user.id, params.id);
29982994
}
29992995

30002996
async createOrgAuthProvider(
@@ -3018,36 +3014,19 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
30183014
if (!newProvider.organizationId || !uuidValidate(newProvider.organizationId)) {
30193015
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Invalid organizationId");
30203016
}
3021-
3022-
await this.guardWithFeatureFlag("orgGitAuthProviders", user, newProvider.organizationId);
3023-
3024-
await this.guardTeamOperation(newProvider.organizationId, "update");
3025-
await this.auth.checkPermissionOnOrganization(user.id, "write_git_provider", newProvider.organizationId);
3026-
30273017
if (!newProvider.host) {
30283018
throw new ApplicationError(
30293019
ErrorCodes.BAD_REQUEST,
30303020
"Must provider a host value when creating a new auth provider.",
30313021
);
30323022
}
30333023

3034-
try {
3035-
// on creating we're are checking for already existing runtime providers
3036-
const host = newProvider.host && newProvider.host.toLowerCase();
3037-
3038-
if (!(await this.authProviderService.isHostReachable(host))) {
3039-
log.debug(`Host could not be reached.`, { entry, newProvider });
3040-
throw new Error("Host could not be reached.");
3041-
}
3024+
await this.guardWithFeatureFlag("orgGitAuthProviders", user, newProvider.organizationId);
30423025

3043-
const hostContext = this.hostContextProvider.get(host);
3044-
if (hostContext) {
3045-
const builtInExists = hostContext.authProvider.params.ownerId === undefined;
3046-
log.debug(`Attempt to override existing auth provider.`, { entry, newProvider, builtInExists });
3047-
throw new Error("Provider for this host already exists.");
3048-
}
3026+
await this.guardTeamOperation(newProvider.organizationId, "update");
30493027

3050-
const result = await this.authProviderService.createOrgAuthProvider(newProvider);
3028+
try {
3029+
const result = await this.authProviderService.createOrgAuthProvider(user.id, newProvider);
30513030
return AuthProviderEntry.redact(result);
30523031
} catch (error) {
30533032
if (ApplicationError.hasErrorCode(error)) {
@@ -3081,10 +3060,9 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
30813060
await this.guardWithFeatureFlag("orgGitAuthProviders", user, providerUpdate.organizationId);
30823061

30833062
await this.guardTeamOperation(providerUpdate.organizationId, "update");
3084-
await this.auth.checkPermissionOnOrganization(user.id, "write_git_provider", providerUpdate.organizationId);
30853063

30863064
try {
3087-
const result = await this.authProviderService.updateOrgAuthProvider(providerUpdate);
3065+
const result = await this.authProviderService.updateOrgAuthProvider(user.id, providerUpdate);
30883066
return AuthProviderEntry.redact(result);
30893067
} catch (error) {
30903068
if (ApplicationError.hasErrorCode(error)) {
@@ -3106,10 +3084,9 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
31063084
await this.guardWithFeatureFlag("orgGitAuthProviders", user, params.organizationId);
31073085

31083086
await this.guardTeamOperation(params.organizationId, "get");
3109-
await this.auth.checkPermissionOnOrganization(user.id, "read_git_provider", params.organizationId);
31103087

31113088
try {
3112-
const result = await this.authProviderService.getAuthProvidersOfOrg(params.organizationId);
3089+
const result = await this.authProviderService.getAuthProvidersOfOrg(user.id, params.organizationId);
31133090
return result.map(AuthProviderEntry.redact.bind(AuthProviderEntry));
31143091
} catch (error) {
31153092
if (ApplicationError.hasErrorCode(error)) {
@@ -3125,24 +3102,76 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
31253102

31263103
const user = await this.checkAndBlockUser("deleteOrgAuthProvider");
31273104

3105+
// check for "orgGitAuthProviders" feature flag
31283106
const team = await this.getTeam(ctx, params.organizationId);
31293107
if (!team) {
31303108
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Invalid organizationId");
31313109
}
3132-
31333110
await this.guardWithFeatureFlag("orgGitAuthProviders", user, team.id);
31343111

31353112
await this.guardTeamOperation(params.organizationId || "", "update");
3136-
await this.auth.checkPermissionOnOrganization(user.id, "write_git_provider", params.organizationId);
31373113

3138-
// Find the matching auth provider we're attempting to delete
3139-
const orgProviders = await this.authProviderService.getAuthProvidersOfOrg(team.id);
3140-
const authProvider = orgProviders.find((p) => p.id === params.id && p.organizationId === params.organizationId);
3114+
await this.authProviderService.deleteAuthProviderOfOrg(user.id, params.organizationId, params.id);
3115+
}
3116+
3117+
async getAuthProvider(ctx: TraceContextWithSpan, id: string): Promise<AuthProviderEntry> {
3118+
traceAPIParams(ctx, { id });
3119+
3120+
const user = await this.checkAndBlockUser("getAuthProvider");
3121+
3122+
const result = await this.authProviderService.getAuthProvider(user.id, id);
3123+
if (!result) {
3124+
throw new ApplicationError(ErrorCodes.NOT_FOUND, "Provider resource not found.");
3125+
}
3126+
return result;
3127+
}
3128+
3129+
/**
3130+
* Delegates to `deleteOrgAuthProvider` or `deleteOwnAuthProvider` depending on the ownership
3131+
* of the specified auth provider.
3132+
*/
3133+
async deleteAuthProvider(ctx: TraceContextWithSpan, id: string): Promise<void> {
3134+
traceAPIParams(ctx, { id });
3135+
3136+
const user = await this.checkAndBlockUser("deleteAuthProvider");
3137+
3138+
const authProvider = await this.authProviderService.getAuthProvider(user.id, id);
31413139
if (!authProvider) {
31423140
throw new ApplicationError(ErrorCodes.NOT_FOUND, "Provider resource not found.");
31433141
}
31443142

3145-
await this.authProviderService.deleteAuthProvider(authProvider);
3143+
if (authProvider.organizationId) {
3144+
return this.deleteOrgAuthProvider(ctx, { id, organizationId: authProvider.organizationId });
3145+
} else {
3146+
return this.deleteOwnAuthProvider(ctx, { id });
3147+
}
3148+
}
3149+
3150+
/**
3151+
* Delegates to `updateOrgAuthProvider` or `updateOwnAuthProvider` depending on the ownership
3152+
* of the specified auth provider.
3153+
*/
3154+
async updateAuthProvider(
3155+
ctx: TraceContextWithSpan,
3156+
id: string,
3157+
entry: AuthProviderEntry.UpdateEntry,
3158+
): Promise<AuthProviderEntry> {
3159+
traceAPIParams(ctx, { id });
3160+
3161+
const user = await this.checkAndBlockUser("updateAuthProvider");
3162+
3163+
const authProvider = await this.authProviderService.getAuthProvider(user.id, id);
3164+
if (!authProvider) {
3165+
throw new ApplicationError(ErrorCodes.NOT_FOUND, "Provider resource not found.");
3166+
}
3167+
3168+
if (authProvider.organizationId) {
3169+
return this.updateOrgAuthProvider(ctx, {
3170+
entry: { ...entry, organizationId: authProvider.organizationId },
3171+
});
3172+
} else {
3173+
return this.updateOwnAuthProvider(ctx, { entry });
3174+
}
31463175
}
31473176

31483177
async getOnboardingState(ctx: TraceContext): Promise<GitpodServer.OnboardingState> {

0 commit comments

Comments
 (0)