Skip to content

Commit 909514c

Browse files
committed
[server, db] Cleanup UpdateOrgSettings API handling
Tool: gitpod/catfood.gitpod.cloud
1 parent fd2825e commit 909514c

File tree

9 files changed

+228
-211
lines changed

9 files changed

+228
-211
lines changed

components/gitpod-db/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"prom-client": "^14.2.0",
4444
"reflect-metadata": "^0.1.13",
4545
"slugify": "^1.6.5",
46+
"ts-deepmerge": "^7.0.2",
4647
"the-big-username-blacklist": "^1.5.2",
4748
"typeorm": "0.2.38"
4849
},

components/gitpod-db/src/typeorm/team-db-impl.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { TypeORM } from "./typeorm";
3030
import { EncryptionService } from "@gitpod/gitpod-protocol/lib/encryption/encryption-service";
3131
import { DBOrgEnvVar } from "./entity/db-org-env-var";
3232
import { filter } from "../utils";
33+
import { merge } from "ts-deepmerge";
3334

3435
@injectable()
3536
export class TeamDBImpl extends TransactionalDBImpl<TeamDB> implements TeamDB {
@@ -394,19 +395,26 @@ export class TeamDBImpl extends TransactionalDBImpl<TeamDB> implements TeamDB {
394395
});
395396
}
396397

397-
public async setOrgSettings(orgId: string, settings: Partial<OrganizationSettings>): Promise<OrganizationSettings> {
398+
public async setOrgSettings(
399+
orgId: string,
400+
partialUpdate: Partial<OrganizationSettings>,
401+
): Promise<OrganizationSettings> {
398402
const repo = await this.getOrgSettingsRepo();
399-
const team = await repo.findOne({ where: { orgId, deleted: false } });
400-
if (!team) {
403+
const currentSettings = await repo.findOne({ where: { orgId, deleted: false } });
404+
if (!currentSettings) {
401405
return await repo.save({
402-
...settings,
406+
...partialUpdate,
403407
orgId,
404408
});
405409
}
406-
return await repo.save({
407-
...team,
408-
...settings,
409-
});
410+
// We want to deep-merge columns that are JSON shapes here.
411+
// We ignore fields set to undefined, and don't merge arrays to match our API semantics
412+
const settings = merge.withOptions(
413+
{ mergeArrays: false, allowUndefinedOverrides: false },
414+
currentSettings,
415+
partialUpdate,
416+
);
417+
return await repo.save(settings);
410418
}
411419

412420
public async hasActiveSSO(organizationId: string): Promise<boolean> {

components/gitpod-protocol/src/teams-projects-protocol.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -285,11 +285,14 @@ export interface OnboardingSettings {
285285
/**
286286
* the welcome message for new members of the organization
287287
*/
288-
welcomeMessage?: {
289-
featuredMemberId?: string;
290-
message?: string;
291-
footer?: string;
292-
};
288+
welcomeMessage?: WelcomeMessage;
289+
}
290+
291+
export interface WelcomeMessage {
292+
enabled?: boolean;
293+
featuredMemberId?: string;
294+
featuredMemberResolvedAvatarUrl?: string;
295+
message?: string;
293296
}
294297

295298
export type TeamMemberInfo = OrgMemberInfo;

components/public-api/typescript-common/src/public-api-converter.ts

Lines changed: 104 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ import {
5656
Organization as ProtocolOrganization,
5757
OrgMemberPermission,
5858
OrgMemberRole,
59+
RoleRestrictions,
60+
TimeoutSettings as TimeoutSettingsProtocol,
61+
OnboardingSettings as OnboardingSettingsProtocol,
62+
WelcomeMessage as WelcomeMessageProtocol,
5963
} from "@gitpod/gitpod-protocol/lib/teams-projects-protocol";
6064
import type { DeepPartial } from "@gitpod/gitpod-protocol/lib/util/deep-partial";
6165
import { parseGoDurationToMs } from "@gitpod/gitpod-protocol/lib/util/timeutil";
@@ -113,11 +117,15 @@ import {
113117
OnboardingState,
114118
} from "@gitpod/public-api/lib/gitpod/v1/installation_pb";
115119
import {
120+
OnboardingSettings,
121+
OnboardingSettings_WelcomeMessage,
116122
Organization,
117123
OrganizationMember,
118124
OrganizationPermission,
119125
OrganizationRole,
120126
OrganizationSettings,
127+
RoleRestrictionEntry,
128+
TimeoutSettings,
121129
} from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
122130
import {
123131
Prebuild,
@@ -1040,6 +1048,61 @@ export class PublicAPIConverter {
10401048
}
10411049
}
10421050

1051+
fromOrgMemberRoleString(role: string): OrgMemberRole {
1052+
switch (role) {
1053+
case "owner":
1054+
case "member":
1055+
case "collaborator":
1056+
return role;
1057+
default:
1058+
throw new Error("invalid org member role");
1059+
}
1060+
}
1061+
1062+
fromTimeoutSettings(timeoutSettings: TimeoutSettings): TimeoutSettingsProtocol {
1063+
const result: TimeoutSettingsProtocol = {
1064+
denyUserTimeouts: timeoutSettings.denyUserTimeouts,
1065+
};
1066+
if (timeoutSettings.inactivity) {
1067+
result.inactivity = this.toDurationString(timeoutSettings.inactivity);
1068+
}
1069+
return result;
1070+
}
1071+
1072+
fromRoleRestrictions(roleRestrictions: RoleRestrictionEntry[]): RoleRestrictions {
1073+
const result: RoleRestrictions = {};
1074+
for (const roleRestriction of roleRestrictions) {
1075+
const role = this.fromOrgMemberRole(roleRestriction.role);
1076+
const permissions = roleRestriction.permissions.map((p) => this.fromOrganizationPermission(p));
1077+
result[role] = permissions;
1078+
}
1079+
return result;
1080+
}
1081+
1082+
fromOnboardingSettings(onboardingSettings: OnboardingSettings): OnboardingSettingsProtocol {
1083+
const result: OnboardingSettingsProtocol = {
1084+
internalLink: onboardingSettings.internalLink,
1085+
};
1086+
1087+
if (onboardingSettings.welcomeMessage) {
1088+
result.welcomeMessage = this.fromWelcomeMessage(onboardingSettings.welcomeMessage);
1089+
}
1090+
1091+
if (onboardingSettings.updateRecommendedRepositories) {
1092+
result.recommendedRepositories = onboardingSettings.recommendedRepositories;
1093+
}
1094+
1095+
return result;
1096+
}
1097+
1098+
fromWelcomeMessage(welcomeMessage: OnboardingSettings_WelcomeMessage): WelcomeMessageProtocol {
1099+
return {
1100+
enabled: welcomeMessage.enabled,
1101+
message: welcomeMessage.message,
1102+
featuredMemberId: welcomeMessage.featuredMemberId,
1103+
};
1104+
}
1105+
10431106
fromWorkspaceSettings(settings?: DeepPartial<WorkspaceSettings>) {
10441107
const result: Partial<
10451108
Pick<
@@ -1132,26 +1195,52 @@ export class PublicAPIConverter {
11321195
pinnedEditorVersions: settings.pinnedEditorVersions || {},
11331196
restrictedEditorNames: settings.restrictedEditorNames || [],
11341197
defaultRole: settings.defaultRole || undefined,
1135-
timeoutSettings: {
1136-
inactivity: settings.timeoutSettings?.inactivity
1137-
? this.toDuration(settings.timeoutSettings?.inactivity)
1138-
: undefined,
1139-
denyUserTimeouts: settings.timeoutSettings?.denyUserTimeouts,
1140-
},
1141-
roleRestrictions: Object.entries(settings.roleRestrictions ?? {}).map(([role, permissions]) => ({
1142-
role: this.toOrgMemberRole(role as OrgMemberRole),
1143-
permissions: permissions.map((permission) => this.toOrganizationPermission(permission)),
1144-
})),
1198+
timeoutSettings: settings.timeoutSettings ? this.toTimeoutSettings(settings.timeoutSettings) : undefined,
1199+
roleRestrictions: settings.roleRestrictions
1200+
? this.toRoleRestrictions(settings.roleRestrictions)
1201+
: undefined,
11451202
maxParallelRunningWorkspaces: settings.maxParallelRunningWorkspaces ?? 0,
1146-
onboardingSettings: {
1147-
internalLink: settings?.onboardingSettings?.internalLink ?? undefined,
1148-
welcomeMessage: settings?.onboardingSettings?.welcomeMessage ?? undefined,
1149-
recommendedRepositories: settings?.onboardingSettings?.recommendedRepositories ?? [],
1150-
},
1203+
onboardingSettings: settings?.onboardingSettings
1204+
? this.toOnboardingSettings(settings.onboardingSettings)
1205+
: undefined,
11511206
annotateGitCommits: settings.annotateGitCommits ?? false,
11521207
});
11531208
}
11541209

1210+
toTimeoutSettings(settings: TimeoutSettingsProtocol): TimeoutSettings {
1211+
return new TimeoutSettings({
1212+
inactivity: settings.inactivity ? this.toDuration(settings.inactivity) : undefined,
1213+
denyUserTimeouts: settings.denyUserTimeouts,
1214+
});
1215+
}
1216+
1217+
toRoleRestrictions(roleRestrictions: RoleRestrictions): RoleRestrictionEntry[] {
1218+
return Object.entries(roleRestrictions ?? {}).map(
1219+
([role, permissions]) =>
1220+
new RoleRestrictionEntry({
1221+
role: this.toOrgMemberRole(role as OrgMemberRole),
1222+
permissions: permissions.map((permission) => this.toOrganizationPermission(permission)),
1223+
}),
1224+
);
1225+
}
1226+
1227+
toOnboardingSettings(settings: OnboardingSettingsProtocol): OnboardingSettings {
1228+
return new OnboardingSettings({
1229+
internalLink: settings.internalLink,
1230+
welcomeMessage: settings.welcomeMessage ? this.toWelcomeMessage(settings.welcomeMessage) : undefined,
1231+
recommendedRepositories: settings.recommendedRepositories,
1232+
});
1233+
}
1234+
1235+
toWelcomeMessage(settings: WelcomeMessageProtocol): OnboardingSettings_WelcomeMessage {
1236+
return new OnboardingSettings_WelcomeMessage({
1237+
enabled: settings.enabled,
1238+
message: settings.message,
1239+
featuredMemberId: settings.featuredMemberId,
1240+
featuredMemberResolvedAvatarUrl: settings.featuredMemberResolvedAvatarUrl,
1241+
});
1242+
}
1243+
11551244
toConfiguration(project: Project): Configuration {
11561245
const result = new Configuration();
11571246
result.id = project.id;

components/server/src/api/organization-service-api.ts

Lines changed: 5 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -46,22 +46,16 @@ import { validate as uuidValidate } from "uuid";
4646
import { ctxUserId } from "../util/request-context";
4747
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
4848
import { EntitlementService } from "../billing/entitlement-service";
49-
import { Config } from "../config";
50-
import { ProjectsService } from "../projects/projects-service";
5149

5250
@injectable()
5351
export class OrganizationServiceAPI implements ServiceImpl<typeof OrganizationServiceInterface> {
5452
constructor(
55-
@inject(Config)
56-
private readonly config: Config,
5753
@inject(OrganizationService)
5854
private readonly orgService: OrganizationService,
5955
@inject(PublicAPIConverter)
6056
private readonly apiConverter: PublicAPIConverter,
6157
@inject(EntitlementService)
6258
private readonly entitlementService: EntitlementService,
63-
@inject(ProjectsService)
64-
private readonly projectService: ProjectsService,
6559
) {}
6660

6761
async listOrganizationWorkspaceClasses(
@@ -188,7 +182,7 @@ export class OrganizationServiceAPI implements ServiceImpl<typeof OrganizationSe
188182
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "organizationId is required");
189183
}
190184

191-
const members = await this.orgService.listMembers(ctxUserId(), req.organizationId, true);
185+
const members = await this.orgService.listMembers(ctxUserId(), req.organizationId);
192186
//TODO pagination
193187
const response = new ListOrganizationMembersResponse();
194188
response.members = members.map((member) => this.apiConverter.toOrganizationMember(member));
@@ -218,7 +212,7 @@ export class OrganizationServiceAPI implements ServiceImpl<typeof OrganizationSe
218212
this.apiConverter.fromOrgMemberRole(req.role),
219213
);
220214
const member = await this.orgService
221-
.listMembers(ctxUserId(), req.organizationId, true)
215+
.listMembers(ctxUserId(), req.organizationId)
222216
.then((members) => members.find((member) => member.userId === req.userId));
223217
return new UpdateOrganizationMemberResponse({
224218
member: member && this.apiConverter.toOrganizationMember(member),
@@ -252,19 +246,6 @@ export class OrganizationServiceAPI implements ServiceImpl<typeof OrganizationSe
252246
const response = new GetOrganizationSettingsResponse();
253247
response.settings = this.apiConverter.toOrganizationSettings(settings);
254248

255-
// resolve the avatar URL for the featured member in the welcome message
256-
const onboardingWelcomeMessageFeaturedMemberId =
257-
response.settings.onboardingSettings?.welcomeMessage?.featuredMemberId;
258-
if (onboardingWelcomeMessageFeaturedMemberId) {
259-
const member = await this.orgService
260-
.getOrganizationMember(ctxUserId(), req.organizationId, onboardingWelcomeMessageFeaturedMemberId, true)
261-
.catch(() => undefined);
262-
if (member?.user.avatarUrl && response?.settings?.onboardingSettings?.welcomeMessage) {
263-
response.settings.onboardingSettings.welcomeMessage.featuredMemberResolvedAvatarUrl =
264-
member.user.avatarUrl;
265-
}
266-
}
267-
268249
return response;
269250
}
270251

@@ -354,30 +335,8 @@ export class OrganizationServiceAPI implements ServiceImpl<typeof OrganizationSe
354335
update.maxParallelRunningWorkspaces = req.maxParallelRunningWorkspaces;
355336
}
356337

357-
if (req.onboardingSettings && Object.keys(req.onboardingSettings).length > 0) {
358-
update.onboardingSettings = {};
359-
360-
if (!this.config.isDedicatedInstallation) {
361-
throw new ApplicationError(
362-
ErrorCodes.BAD_REQUEST,
363-
"onboardingSettings can only be set on enterprise installations",
364-
);
365-
}
366-
367-
if (req.onboardingSettings.welcomeMessage?.featuredMemberResolvedAvatarUrl) {
368-
throw new ApplicationError(
369-
ErrorCodes.BAD_REQUEST,
370-
"featuredMemberResolvedAvatarUrl is not allowed to be set",
371-
);
372-
}
373-
374-
if (req.onboardingSettings.internalLink) {
375-
if (req.onboardingSettings.internalLink.length > 255) {
376-
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "internalLink must be <= 255 characters long");
377-
}
378-
379-
update.onboardingSettings.internalLink = req.onboardingSettings.internalLink;
380-
}
338+
if (req.onboardingSettings) {
339+
update.onboardingSettings = this.apiConverter.fromOnboardingSettings(req.onboardingSettings);
381340

382341
if (
383342
!req.onboardingSettings.updateRecommendedRepositories &&
@@ -388,65 +347,8 @@ export class OrganizationServiceAPI implements ServiceImpl<typeof OrganizationSe
388347
"recommendedRepositories can only be set when updateRecommendedRepositories is true",
389348
);
390349
}
391-
392-
if (
393-
req.onboardingSettings.updateRecommendedRepositories &&
394-
req.onboardingSettings.recommendedRepositories
395-
) {
396-
if (req.onboardingSettings.recommendedRepositories.length > 3) {
397-
throw new ApplicationError(
398-
ErrorCodes.BAD_REQUEST,
399-
"there can't be more than 3 recommendedRepositories",
400-
);
401-
}
402-
for (const configurationId of req.onboardingSettings.recommendedRepositories) {
403-
if (!uuidValidate(configurationId)) {
404-
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "recommendedRepositories must be UUIDs");
405-
}
406-
407-
const project = await this.projectService.getProject(ctxUserId(), configurationId);
408-
if (!project) {
409-
throw new ApplicationError(ErrorCodes.BAD_REQUEST, `repository ${configurationId} not found`);
410-
}
411-
}
412-
413-
update.onboardingSettings.recommendedRepositories = req.onboardingSettings.recommendedRepositories;
414-
}
415-
416-
if (
417-
req.onboardingSettings.welcomeMessage &&
418-
Object.keys(req.onboardingSettings.welcomeMessage).length > 0
419-
) {
420-
if (req.onboardingSettings.welcomeMessage.featuredMemberId) {
421-
const member = await this.orgService
422-
.getOrganizationMember(
423-
ctxUserId(),
424-
req.organizationId,
425-
req.onboardingSettings.welcomeMessage.featuredMemberId,
426-
true,
427-
)
428-
.catch(() => undefined);
429-
if (!member) {
430-
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "featuredMemberId not found");
431-
}
432-
}
433-
434-
if (
435-
req.onboardingSettings.welcomeMessage.enabled &&
436-
req.onboardingSettings.welcomeMessage.message?.length === 0
437-
) {
438-
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "welcomeMessage must not be empty when enabled");
439-
}
440-
441-
update.onboardingSettings.welcomeMessage = req.onboardingSettings.welcomeMessage;
442-
}
443-
444-
const existingOnboardingSettings = await this.orgService.getSettings(ctxUserId(), req.organizationId);
445-
update.onboardingSettings = {
446-
...existingOnboardingSettings.onboardingSettings,
447-
...update.onboardingSettings,
448-
};
449350
}
351+
450352
if (req.annotateGitCommits !== undefined) {
451353
update.annotateGitCommits = req.annotateGitCommits;
452354
}

0 commit comments

Comments
 (0)