Skip to content

Commit 8d94dd8

Browse files
authored
Use User.fgaRelationshipsVersion instead of User.additionalAttributes.fgaRelationshipsVersion (#18838)
* [db] Add User.fgaRelationshipsVersion * [server] Use User.fgaRelationshipsVersion
1 parent d00f9c0 commit 8d94dd8

File tree

12 files changed

+122
-54
lines changed

12 files changed

+122
-54
lines changed

components/gitpod-db/src/typeorm/entity/db-user.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,10 @@ export class DBUser implements User {
103103
transformer: Transformer.MAP_EMPTY_STR_TO_UNDEFINED,
104104
})
105105
verificationPhoneNumber?: string;
106+
107+
@Column({
108+
default: 0,
109+
transformer: Transformer.MAP_ZERO_TO_UNDEFINED,
110+
})
111+
fgaRelationshipsVersion?: number;
106112
}

components/gitpod-db/src/typeorm/migration/1694775547603-AddOrgDefaultImageSetting.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { columnExists, tableExists } from "./helper/helper";
1010
export class AddOrgDefaultImageSetting1694775547603 implements MigrationInterface {
1111
public async up(queryRunner: QueryRunner): Promise<void> {
1212
if (await tableExists(queryRunner, "d_b_org_settings")) {
13-
if (!(await columnExists(queryRunner, "d_b_org_settings", "d_b_org_settings"))) {
13+
if (!(await columnExists(queryRunner, "d_b_org_settings", "defaultWorkspaceImage"))) {
1414
await queryRunner.query(
1515
"ALTER TABLE `d_b_org_settings` ADD COLUMN `defaultWorkspaceImage` varchar(255) NULL",
1616
);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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 { MigrationInterface, QueryRunner } from "typeorm";
8+
import { columnExists, indexExists } from "./helper/helper";
9+
10+
const TABLE_NAME = "d_b_user";
11+
const COLUMN_NAME = "fgaRelationshipsVersion";
12+
const INDEX_NAME = "ind_fgaRelationshipsVersion";
13+
14+
export class UserFgaRelationShipVersion1695906381289 implements MigrationInterface {
15+
public async up(queryRunner: QueryRunner): Promise<void> {
16+
if (!(await columnExists(queryRunner, TABLE_NAME, COLUMN_NAME))) {
17+
await queryRunner.query(
18+
`ALTER TABLE \`${TABLE_NAME}\` ADD COLUMN \`${COLUMN_NAME}\` int NOT NULL DEFAULT 0`,
19+
);
20+
}
21+
22+
if (!(await indexExists(queryRunner, TABLE_NAME, INDEX_NAME))) {
23+
await queryRunner.query(
24+
`ALTER TABLE \`${TABLE_NAME}\` ADD INDEX \`${INDEX_NAME}\` (${COLUMN_NAME}), ALGORITHM=INPLACE, LOCK=NONE`,
25+
);
26+
}
27+
}
28+
29+
public async down(queryRunner: QueryRunner): Promise<void> {
30+
if (await columnExists(queryRunner, TABLE_NAME, COLUMN_NAME)) {
31+
await queryRunner.query(`ALTER TABLE \`${TABLE_NAME}\` DROP COLUMN \`${COLUMN_NAME}\``);
32+
}
33+
34+
if (await indexExists(queryRunner, TABLE_NAME, INDEX_NAME)) {
35+
await queryRunner.query(`ALTER TABLE \`${TABLE_NAME}\` DROP INDEX \`${INDEX_NAME}\``);
36+
}
37+
}
38+
}

components/gitpod-db/src/typeorm/transformer.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,21 @@ export namespace Transformer {
2323
},
2424
};
2525

26+
export const MAP_ZERO_TO_UNDEFINED: ValueTransformer = {
27+
to(value: any): any {
28+
if (value === undefined) {
29+
return 0;
30+
}
31+
return value;
32+
},
33+
from(value: any): any {
34+
if (value === 0) {
35+
return undefined;
36+
}
37+
return value;
38+
},
39+
};
40+
2641
export const MAP_NULL_TO_UNDEFINED: ValueTransformer = {
2742
to(value: any): any {
2843
if (value === undefined) {

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,4 +647,19 @@ export class TypeORMUserDBImpl extends TransactionalDBImpl<UserDB> implements Us
647647
}
648648
return this.mapDBUserToUser(result);
649649
}
650+
651+
async findUserIdsNotYetMigratedToFgaVersion(fgaRelationshipsVersion: number, limit: number): Promise<string[]> {
652+
const userRepo = await this.getUserRepo();
653+
const ids = (await userRepo
654+
.createQueryBuilder("user")
655+
.select(["id"])
656+
.where({
657+
fgaRelationshipsVersion: Not(Equal(fgaRelationshipsVersion)),
658+
markedDeleted: Equal(false),
659+
})
660+
.orderBy("_lastModified", "DESC")
661+
.limit(limit)
662+
.getMany()) as Pick<DBUser, "id">[];
663+
return ids.map(({ id }) => id);
664+
}
650665
}

components/gitpod-db/src/user-db.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ export interface UserDB extends OAuthUserRepository, OAuthTokenRepository, Trans
150150
isBlockedPhoneNumber(phoneNumber: string): Promise<boolean>;
151151

152152
findOrgOwnedUser(organizationId: string, email: string): Promise<MaybeUser>;
153+
154+
findUserIdsNotYetMigratedToFgaVersion(fgaRelationshipsVersion: number, limit: number): Promise<string[]>;
153155
}
154156
export type PartialUserUpdate = Partial<Omit<User, "identities">> & Pick<User, "id">;
155157

components/gitpod-protocol/src/protocol.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ export interface User {
5858

5959
// The phone number used for the last phone verification.
6060
verificationPhoneNumber?: string;
61+
62+
// The FGA relationships version of this user
63+
fgaRelationshipsVersion?: number;
6164
}
6265

6366
export namespace User {
@@ -268,8 +271,6 @@ export interface AdditionalUserData extends Partial<WorkspaceTimeoutSetting> {
268271
// additional user profile data
269272
profile?: ProfileDetails;
270273
shouldSeeMigrationMessage?: boolean;
271-
// fgaRelationshipsVersion is the version of the spicedb relationships
272-
fgaRelationshipsVersion?: number;
273274
// remembered workspace auto start options
274275
workspaceAutostartOptions?: WorkspaceAutostartOption[];
275276
}

components/server/src/authorization/relationship-updater-job.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,40 +8,31 @@ import { injectable, inject } from "inversify";
88
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
99
import { Job } from "../jobs/runner";
1010
import { RelationshipUpdater } from "./relationship-updater";
11-
import { TypeORM, UserDB } from "@gitpod/gitpod-db/lib";
11+
import { UserDB } from "@gitpod/gitpod-db/lib";
1212

1313
@injectable()
1414
export class RelationshipUpdateJob implements Job {
1515
constructor(
1616
@inject(RelationshipUpdater) private relationshipUpdater: RelationshipUpdater,
1717
@inject(UserDB) private readonly userDB: UserDB,
18-
@inject(TypeORM) private readonly db: TypeORM,
1918
) {}
2019

2120
public name = "relationship-update-job";
2221
public frequencyMs = 1000 * 60 * 3; // 3m
2322

2423
public async run(): Promise<void> {
2524
try {
26-
const connection = await this.db.getConnection();
27-
const results = await connection.query(`
28-
SELECT id FROM d_b_user
29-
WHERE
30-
(additionalData->"$.fgaRelationshipsVersion" != ${RelationshipUpdater.version} OR
31-
additionalData->"$.fgaRelationshipsVersion" IS NULL) AND
32-
markedDeleted = 0
33-
ORDER BY _lastModified DESC
34-
LIMIT 50;`);
25+
const ids = await this.userDB.findUserIdsNotYetMigratedToFgaVersion(RelationshipUpdater.version, 50);
3526
const now = Date.now();
3627
let migrated = 0;
37-
for (const result of results) {
38-
const user = await this.userDB.findUserById(result.id);
28+
for (const userId of ids) {
29+
const user = await this.userDB.findUserById(userId);
3930
if (!user) {
4031
continue;
4132
}
4233
try {
4334
const resultingUser = await this.relationshipUpdater.migrate(user);
44-
if (resultingUser.additionalData?.fgaRelationshipsVersion === RelationshipUpdater.version) {
35+
if (resultingUser.fgaRelationshipsVersion === RelationshipUpdater.version) {
4536
migrated++;
4637
}
4738
} catch (error) {

components/server/src/authorization/relationship-updater.spec.db.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
WorkspaceDB,
1414
} from "@gitpod/gitpod-db/lib";
1515
import { resetDB } from "@gitpod/gitpod-db/lib/test/reset-db";
16-
import { AdditionalUserData, User, Workspace } from "@gitpod/gitpod-protocol";
16+
import { User, Workspace } from "@gitpod/gitpod-protocol";
1717
import { Experiments } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
1818
import * as chai from "chai";
1919
import { Container } from "inversify";
@@ -72,21 +72,21 @@ describe("RelationshipUpdater", async () => {
7272
let user = await userDB.newUser();
7373

7474
user = await migrator.migrate(user);
75-
expect(user.additionalData?.fgaRelationshipsVersion).to.not.be.undefined;
75+
expect(user.fgaRelationshipsVersion).to.not.be.undefined;
7676

7777
Experiments.configureTestingClient({
7878
centralizedPermissions: false,
7979
});
8080

8181
user = await migrator.migrate(user);
82-
expect(user.additionalData?.fgaRelationshipsVersion).to.be.undefined;
82+
expect(user.fgaRelationshipsVersion).to.be.undefined;
8383

8484
Experiments.configureTestingClient({
8585
centralizedPermissions: true,
8686
});
8787

8888
user = await migrator.migrate(user);
89-
expect(user.additionalData?.fgaRelationshipsVersion).to.not.be.undefined;
89+
expect(user.fgaRelationshipsVersion).to.not.be.undefined;
9090
});
9191

9292
it("should correctly update a simple user after it moves between org and installation level", async () => {
@@ -329,15 +329,18 @@ describe("RelationshipUpdater", async () => {
329329

330330
async function notExpected(relation: v1.Relationship): Promise<void> {
331331
const rs = await authorizer.find(relation);
332-
expect(rs).to.be.undefined;
332+
expect(
333+
rs,
334+
`Unexpected relation: ${rs?.subject?.object?.objectType}:${rs?.subject?.object?.objectId}#${rs?.relation}@${rs?.resource?.objectType}:${rs?.resource?.objectId}`,
335+
).to.be.undefined;
333336
}
334337

335338
async function migrate(user: User): Promise<User> {
336339
// reset fgaRelationshipsVersion to force update
337-
AdditionalUserData.set(user, { fgaRelationshipsVersion: undefined });
338-
user = await userDB.storeUser(user);
339-
user = await migrator.migrate(user);
340-
expect(user.additionalData?.fgaRelationshipsVersion).to.equal(RelationshipUpdater.version);
340+
user.fgaRelationshipsVersion = undefined;
341+
await userDB.updateUserPartial({ id: user.id, fgaRelationshipsVersion: user.fgaRelationshipsVersion });
342+
user = await migrator.migrate(user, true);
343+
expect(user.fgaRelationshipsVersion).to.equal(RelationshipUpdater.version);
341344
return user;
342345
}
343346
});

components/server/src/authorization/relationship-updater.ts

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import { ProjectDB, TeamDB, UserDB, WorkspaceDB } from "@gitpod/gitpod-db/lib";
8-
import { AdditionalUserData, Organization, User } from "@gitpod/gitpod-protocol";
8+
import { Organization, User } from "@gitpod/gitpod-protocol";
99
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
1010
import { inject, injectable } from "inversify";
1111
import { Authorizer, isFgaChecksEnabled, isFgaWritesEnabled } from "./authorizer";
@@ -38,14 +38,13 @@ export class RelationshipUpdater {
3838
* @param user
3939
* @returns
4040
*/
41-
public async migrate(user: User): Promise<User> {
41+
public async migrate(user: User, forceAwait: boolean = false): Promise<User> {
4242
const isEnabled = await isFgaWritesEnabled(user.id);
4343
if (!isEnabled) {
44-
if (user.additionalData?.fgaRelationshipsVersion !== undefined) {
44+
if (user.fgaRelationshipsVersion !== undefined) {
4545
log.info({ userId: user.id }, `User has been removed from FGA.`);
4646
// reset the fgaRelationshipsVersion to undefined, so the migration is triggered again when the feature is enabled
47-
AdditionalUserData.set(user, { fgaRelationshipsVersion: undefined });
48-
return await this.userDB.storeUser(user);
47+
await this.setFgaRelationshipsVersion(user, undefined);
4948
}
5049
return user;
5150
}
@@ -54,7 +53,7 @@ export class RelationshipUpdater {
5453
}
5554
const migrated = this.internalMigrate(user);
5655
// if checks are not enabled, we don't want to wait for the migration to finish
57-
if (!(await isFgaChecksEnabled(user.id))) {
56+
if (!forceAwait && !(await isFgaChecksEnabled(user.id))) {
5857
migrated.catch((err) => {
5958
log.error({ userId: user.id }, "Error while migrating user", err);
6059
});
@@ -83,7 +82,7 @@ export class RelationshipUpdater {
8382
return user;
8483
}
8584
log.info({ userId: user.id }, `Updating FGA relationships for user.`, {
86-
fromVersion: user?.additionalData?.fgaRelationshipsVersion,
85+
fromVersion: user?.fgaRelationshipsVersion,
8786
toVersion: RelationshipUpdater.version,
8887
});
8988
const orgs = await this.findAffectedOrganizations(user.id);
@@ -94,21 +93,15 @@ export class RelationshipUpdater {
9493
await this.updateOrganization(user.id, org);
9594
}
9695
await this.updateWorkspaces(user);
97-
AdditionalUserData.set(user, {
98-
fgaRelationshipsVersion: RelationshipUpdater.version,
99-
});
100-
await this.userDB.updateUserPartial({
101-
id: user.id,
102-
additionalData: user.additionalData,
103-
});
96+
await this.setFgaRelationshipsVersion(user, RelationshipUpdater.version);
10497
log.info({ userId: user.id }, `Finished updating relationships.`, {
10598
duration: new Date().getTime() - before,
10699
});
107100

108101
// let's double check the migration worked.
109102
if (!(await this.isMigrated(user))) {
110103
log.error({ userId: user.id }, `User migration failed.`, {
111-
markedMigrated: user.additionalData?.fgaRelationshipsVersion === RelationshipUpdater.version,
104+
markedMigrated: user.fgaRelationshipsVersion === RelationshipUpdater.version,
112105
});
113106
}
114107
return user;
@@ -119,18 +112,14 @@ export class RelationshipUpdater {
119112
}
120113

121114
private async isMigrated(user: User) {
122-
const isMigrated = user.additionalData?.fgaRelationshipsVersion === RelationshipUpdater.version;
115+
const isMigrated = user.fgaRelationshipsVersion === RelationshipUpdater.version;
123116
if (isMigrated) {
124117
const hasSelfRelationship = await this.authorizer.find(rel.user(user.id).self.user(user.id));
125118
if (!hasSelfRelationship) {
126119
log.warn({ userId: user.id }, `User is marked as migrated but doesn't have self relationship.`);
127120
//TODO(se) this is an extra safety net to detect
128121
// reset the fgaRelationshipsVersion to undefined, so the migration is triggered again when the feature is enabled
129-
AdditionalUserData.set(user, { fgaRelationshipsVersion: undefined });
130-
await this.userDB.updateUserPartial({
131-
id: user.id,
132-
additionalData: user.additionalData,
133-
});
122+
await this.setFgaRelationshipsVersion(user, undefined);
134123
return false;
135124
}
136125
}
@@ -205,4 +194,12 @@ export class RelationshipUpdater {
205194
projects.map((p) => p.id),
206195
);
207196
}
197+
198+
private async setFgaRelationshipsVersion(user: User, version: number | undefined): Promise<void> {
199+
user.fgaRelationshipsVersion = version;
200+
await this.userDB.updateUserPartial({
201+
id: user.id,
202+
fgaRelationshipsVersion: version,
203+
});
204+
}
208205
}

components/server/src/user/user-controller.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -254,13 +254,7 @@ export class UserController {
254254
}
255255

256256
// reset the FGA state
257-
await this.userService.updateUser(user.id, {
258-
id: user.id,
259-
additionalData: {
260-
...(user.additionalData || {}),
261-
fgaRelationshipsVersion: undefined,
262-
},
263-
});
257+
await this.userService.resetFgaVersion(user.id, user.id);
264258

265259
const redirectToUrl = this.getSafeReturnToParam(req) || this.config.hostUrl.toString();
266260

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,4 +220,10 @@ export class UserService {
220220
throw error;
221221
}
222222
}
223+
224+
async resetFgaVersion(subjectId: string, userId: string) {
225+
await this.authorizer.checkPermissionOnUser(subjectId, "write_info", userId);
226+
227+
await this.userDb.updateUserPartial({ id: userId, fgaRelationshipsVersion: undefined });
228+
}
223229
}

0 commit comments

Comments
 (0)