Skip to content

Commit

Permalink
[fga] add relationship update job (#18671)
Browse files Browse the repository at this point in the history
  • Loading branch information
svenefftinge authored Sep 7, 2023
1 parent b24ae94 commit 3a07121
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 15 deletions.
54 changes: 54 additions & 0 deletions components/server/src/authorization/relationship-updater-job.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* 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 { injectable, inject } from "inversify";
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
import { Job } from "../jobs/runner";
import { RelationshipUpdater } from "./relationship-updater";
import { TypeORM, UserDB } from "@gitpod/gitpod-db/lib";

@injectable()
export class RelationshipUpdateJob implements Job {
constructor(
@inject(RelationshipUpdater) private relationshipUpdater: RelationshipUpdater,
@inject(UserDB) private readonly userDB: UserDB,
@inject(TypeORM) private readonly db: TypeORM,
) {}

public name = "relationship-update-job";
public frequencyMs = 1000 * 60 * 60 * 1; // 1h

public async run(): Promise<void> {
try {
const connection = await this.db.getConnection();
const results = await connection.query(`
SELECT id FROM d_b_user
WHERE
(additionalData->"$.fgaRelationshipsVersion" != ${RelationshipUpdater.version} OR
additionalData->"$.fgaRelationshipsVersion" IS NULL) AND
markedDeleted = 0
ORDER BY _lastModified DESC
LIMIT 1000;`);
const now = Date.now();
for (const result of results) {
const user = await this.userDB.findUserById(result.id);
if (!user) {
continue;
}
try {
await this.relationshipUpdater.migrate(user);
} catch (error) {
log.error(RelationshipUpdateJob.name + ": error running relationship update job", error);
}
}
log.info(
RelationshipUpdateJob.name + ": updated " + results.length + " users in " + (Date.now() - now) + "ms",
);
} catch (error) {
log.error(RelationshipUpdateJob.name + ": error running relationship update job", error);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ describe("RelationshipUpdater", async () => {
AdditionalUserData.set(user, { fgaRelationshipsVersion: undefined });
user = await userDB.storeUser(user);
user = await migrator.migrate(user);
expect(user.additionalData?.fgaRelationshipsVersion).to.equal(migrator.version);
expect(user.additionalData?.fgaRelationshipsVersion).to.equal(RelationshipUpdater.version);
return user;
}
});
20 changes: 14 additions & 6 deletions components/server/src/authorization/relationship-updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { RedisMutex } from "../redis/mutex";

@injectable()
export class RelationshipUpdater {
public readonly version = 2;
public static readonly version = 2;

constructor(
@inject(UserDB) private readonly userDB: UserDB,
Expand All @@ -39,12 +39,20 @@ export class RelationshipUpdater {
* @returns
*/
public async migrate(user: User): Promise<User> {
const fgaEnabled = await getExperimentsClientForBackend().getValueAsync("centralizedPermissions", false, {
let isEnabled = await getExperimentsClientForBackend().getValueAsync("spicedb_relationship_updates", false, {
user: {
id: user.id,
},
});
if (!fgaEnabled) {
if (!isEnabled) {
// check the centralizedPermission featureflag
isEnabled = await getExperimentsClientForBackend().getValueAsync("centralizedPermissions", false, {
user: {
id: user.id,
},
});
}
if (!isEnabled) {
if (user.additionalData?.fgaRelationshipsVersion !== undefined) {
log.info({ userId: user.id }, `User has been removed from FGA.`);
// reset the fgaRelationshipsVersion to undefined, so the migration is triggered again when the feature is enabled
Expand All @@ -71,7 +79,7 @@ export class RelationshipUpdater {
}
log.info({ userId: user.id }, `Updating FGA relationships for user.`, {
fromVersion: user?.additionalData?.fgaRelationshipsVersion,
toVersion: this.version,
toVersion: RelationshipUpdater.version,
});
const orgs = await this.findAffectedOrganizations(user.id);

Expand All @@ -82,7 +90,7 @@ export class RelationshipUpdater {
}
await this.updateWorkspaces(user);
AdditionalUserData.set(user, {
fgaRelationshipsVersion: this.version,
fgaRelationshipsVersion: RelationshipUpdater.version,
});
await this.userDB.updateUserPartial({
id: user.id,
Expand All @@ -99,7 +107,7 @@ export class RelationshipUpdater {
}

private isMigrated(user: User) {
return user.additionalData?.fgaRelationshipsVersion === this.version;
return user.additionalData?.fgaRelationshipsVersion === RelationshipUpdater.version;
}

private async findAffectedOrganizations(userId: string): Promise<Organization[]> {
Expand Down
2 changes: 2 additions & 0 deletions components/server/src/container-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ import { SSHKeyService } from "./user/sshkey-service";
import { GitpodTokenService } from "./user/gitpod-token-service";
import { EnvVarService } from "./user/env-var-service";
import { ScmService } from "./projects/scm-service";
import { RelationshipUpdateJob } from "./authorization/relationship-updater-job";

export const productionContainerModule = new ContainerModule(
(bind, unbind, isBound, rebind, unbindAsync, onActivation, onDeactivation) => {
Expand Down Expand Up @@ -368,6 +369,7 @@ export const productionContainerModule = new ContainerModule(
bind(OTSGarbageCollector).toSelf().inSingletonScope();
bind(SnapshotsJob).toSelf().inSingletonScope();
bind(JobRunner).toSelf().inSingletonScope();
bind(RelationshipUpdateJob).toSelf().inSingletonScope();

// Redis
bind(Redis).toDynamicValue((ctx) => {
Expand Down
20 changes: 12 additions & 8 deletions components/server/src/jobs/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { TokenGarbageCollector } from "./token-gc";
import { WebhookEventGarbageCollector } from "./webhook-gc";
import { WorkspaceGarbageCollector } from "./workspace-gc";
import { SnapshotsJob } from "./snapshots";
import { RelationshipUpdateJob } from "../authorization/relationship-updater-job";

export const Job = Symbol("Job");

Expand All @@ -29,14 +30,16 @@ export interface Job {

@injectable()
export class JobRunner {
@inject(RedisMutex) protected mutex: RedisMutex;

@inject(DatabaseGarbageCollector) protected databaseGC: DatabaseGarbageCollector;
@inject(OTSGarbageCollector) protected otsGC: OTSGarbageCollector;
@inject(TokenGarbageCollector) protected tokenGC: TokenGarbageCollector;
@inject(WebhookEventGarbageCollector) protected webhookGC: WebhookEventGarbageCollector;
@inject(WorkspaceGarbageCollector) protected workspaceGC: WorkspaceGarbageCollector;
@inject(SnapshotsJob) protected snapshotsJob: SnapshotsJob;
constructor(
@inject(RedisMutex) private readonly mutex: RedisMutex,
@inject(DatabaseGarbageCollector) private readonly databaseGC: DatabaseGarbageCollector,
@inject(OTSGarbageCollector) private readonly otsGC: OTSGarbageCollector,
@inject(TokenGarbageCollector) private readonly tokenGC: TokenGarbageCollector,
@inject(WebhookEventGarbageCollector) private readonly webhookGC: WebhookEventGarbageCollector,
@inject(WorkspaceGarbageCollector) private readonly workspaceGC: WorkspaceGarbageCollector,
@inject(SnapshotsJob) private readonly snapshotsJob: SnapshotsJob,
@inject(RelationshipUpdateJob) private readonly relationshipUpdateJob: RelationshipUpdateJob,
) {}

public start(): DisposableCollection {
const disposables = new DisposableCollection();
Expand All @@ -48,6 +51,7 @@ export class JobRunner {
this.webhookGC,
this.workspaceGC,
this.snapshotsJob,
this.relationshipUpdateJob,
];

for (const job of jobs) {
Expand Down
1 change: 1 addition & 0 deletions components/usage-api/typescript/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"downlevelIteration": true,
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"target": "es6",
"jsx": "react",
"sourceMap": true,
Expand Down

0 comments on commit 3a07121

Please sign in to comment.