Skip to content

Commit 261329a

Browse files
committed
wip
1 parent 85d67a6 commit 261329a

File tree

4 files changed

+85
-28
lines changed

4 files changed

+85
-28
lines changed

components/server/src/authorization/spicedb-authorizer.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ import { TrustedValue } from "@gitpod/gitpod-protocol/lib/util/scrubbing";
1111
import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
1212
import { inject, injectable } from "inversify";
1313
import { observeSpicedbClientLatency, spicedbClientLatency } from "../prometheus-metrics";
14-
import { SpiceDBClient } from "./spicedb";
14+
import { SpiceDBClientProvider } from "./spicedb";
1515
// TODO(gpl) Change to "@grpc/grpc-js" once we can use 1.9.0 (or higher) everywhere
1616
import * as grpc from "@grpc/grpc-js_1_9_0";
1717

1818
@injectable()
1919
export class SpiceDBAuthorizer {
2020
constructor(
21-
@inject(SpiceDBClient)
22-
private client: SpiceDBClient,
21+
@inject(SpiceDBClientProvider)
22+
private readonly clientProvider: SpiceDBClientProvider,
2323
) {}
2424

2525
async check(
@@ -28,7 +28,8 @@ export class SpiceDBAuthorizer {
2828
userId: string;
2929
},
3030
): Promise<boolean> {
31-
if (!this.client) {
31+
const client = this.clientProvider.getClient();
32+
if (!client) {
3233
return true;
3334
}
3435
const timing = startTimer();
@@ -45,7 +46,7 @@ export class SpiceDBAuthorizer {
4546
const timer = spicedbClientLatency.startTimer();
4647
let error: Error | undefined;
4748
try {
48-
const response = await this.client.checkPermission(req, this.callOptions);
49+
const response = await client.checkPermission(req, this.callOptions);
4950
const permitted = response.permissionship === v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION;
5051

5152
return permitted;
@@ -62,15 +63,16 @@ export class SpiceDBAuthorizer {
6263
}
6364

6465
async writeRelationships(...updates: v1.RelationshipUpdate[]): Promise<v1.WriteRelationshipsResponse | undefined> {
65-
if (!this.client) {
66+
const client = this.clientProvider.getClient();
67+
if (!client) {
6668
return undefined;
6769
}
6870
const timing = startTimer();
6971

7072
const timer = spicedbClientLatency.startTimer();
7173
let error: Error | undefined;
7274
try {
73-
const response = await this.client.writeRelationships(
75+
const response = await client.writeRelationships(
7476
v1.WriteRelationshipsRequest.create({
7577
updates,
7678
}),
@@ -89,7 +91,7 @@ export class SpiceDBAuthorizer {
8991
}
9092

9193
async deleteRelationships(req: v1.DeleteRelationshipsRequest): Promise<v1.ReadRelationshipsResponse[]> {
92-
const client = this.client;
94+
const client = this.clientProvider.getClient();
9395
if (!client) {
9496
return [];
9597
}
@@ -125,10 +127,11 @@ export class SpiceDBAuthorizer {
125127
}
126128

127129
async readRelationships(req: v1.ReadRelationshipsRequest): Promise<v1.ReadRelationshipsResponse[]> {
128-
if (!this.client) {
130+
const client = this.clientProvider.getClient();
131+
if (!client) {
129132
return [];
130133
}
131-
return this.client.readRelationships(req, this.callOptions);
134+
return client.readRelationships(req, this.callOptions);
132135
}
133136

134137
/**
@@ -137,9 +140,9 @@ export class SpiceDBAuthorizer {
137140
* But the promisified client somehow does not have the same overloads. Thus we convince it here that options may be passed as 2nd argument.
138141
*/
139142
private get callOptions(): grpc.Metadata {
140-
return {
143+
return (<grpc.CallOptions>{
141144
deadline: Date.now() + 5000,
142-
} as any as grpc.Metadata;
145+
}) as any as grpc.Metadata;
143146
}
144147
}
145148
function startTimer() {

components/server/src/authorization/spicedb.ts

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,23 @@
55
*/
66

77
import { v1 } from "@authzed/authzed-node";
8-
import { defaultGRPCOptions } from "@gitpod/gitpod-protocol/lib/util/grpc";
98
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
9+
import * as grpc from "@grpc/grpc-js_1_9_0";
1010

11-
export const SpiceDBClient = Symbol("SpiceDBClient");
12-
export type SpiceDBClient = v1.ZedPromiseClientInterface | undefined;
11+
export const SpiceDBClientProvider = Symbol("SpiceDBClientProvider");
1312

14-
export function spicedbClientFromEnv(): SpiceDBClient {
13+
export interface SpiceDBClientConfig {
14+
address: string;
15+
token: string;
16+
}
17+
18+
export type SpiceDBClient = v1.ZedPromiseClientInterface;
19+
type Client = v1.ZedClientInterface & grpc.Client;
20+
export interface SpiceDBClientProvider {
21+
getClient(): SpiceDBClient | undefined;
22+
}
23+
24+
export function spiceDBConfigFromEnv(): SpiceDBClientConfig | undefined {
1525
const token = process.env["SPICEDB_PRESHARED_KEY"];
1626
if (!token) {
1727
log.error("[spicedb] No preshared key configured.");
@@ -23,11 +33,50 @@ export function spicedbClientFromEnv(): SpiceDBClient {
2333
log.error("[spicedb] No service address configured.");
2434
return undefined;
2535
}
36+
return {
37+
address,
38+
token,
39+
};
40+
}
41+
42+
function spicedbClientFromConfig(config: SpiceDBClientConfig | undefined): Client | undefined {
43+
if (!config) {
44+
log.error("[spicedb] No config based, so won't create a client.");
45+
return undefined;
46+
}
2647

27-
const clientOptions = {
28-
...defaultGRPCOptions,
48+
const clientOptions: grpc.ClientOptions = {
49+
"grpc.client_idle_timeout_ms": 60000,
50+
"grpc.max_reconnect_backoff_ms": 5000, // default: 12000
2951
};
3052

31-
return v1.NewClient(token, address, v1.ClientSecurity.INSECURE_PLAINTEXT_CREDENTIALS, undefined, clientOptions)
32-
.promises;
53+
return v1.NewClient(
54+
config.token,
55+
config.address,
56+
v1.ClientSecurity.INSECURE_PLAINTEXT_CREDENTIALS,
57+
undefined,
58+
clientOptions,
59+
) as Client;
60+
}
61+
62+
export class CachingSpiceDBClientProvider implements SpiceDBClientProvider {
63+
private client: Client | undefined;
64+
65+
constructor(private readonly clientConfig: SpiceDBClientConfig | undefined) {}
66+
67+
getClient(): SpiceDBClient | undefined {
68+
let client = this.client;
69+
if (!client) {
70+
client = spicedbClientFromConfig(this.clientConfig);
71+
} else if (client.getChannel().getConnectivityState(true) !== grpc.connectivityState.READY) {
72+
client.close();
73+
74+
log.warn("[spicedb] Lost connection to SpiceDB - reconnecting...");
75+
76+
client = spicedbClientFromConfig(this.clientConfig);
77+
}
78+
this.client = client;
79+
80+
return client?.promises;
81+
}
3382
}

components/server/src/container-module.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import { AuthJWT, SignInJWT } from "./auth/jwt";
4646
import { LoginCompletionHandler } from "./auth/login-completion-handler";
4747
import { VerificationService } from "./auth/verification-service";
4848
import { Authorizer, createInitializingAuthorizer } from "./authorization/authorizer";
49-
import { SpiceDBClient, spicedbClientFromEnv } from "./authorization/spicedb";
49+
import { CachingSpiceDBClientProvider, SpiceDBClientProvider, spiceDBConfigFromEnv } from "./authorization/spicedb";
5050
import { BillingModes } from "./billing/billing-mode";
5151
import { EntitlementService, EntitlementServiceImpl } from "./billing/entitlement-service";
5252
import { EntitlementServiceUBP } from "./billing/entitlement-service-ubp";
@@ -302,8 +302,11 @@ export const productionContainerModule = new ContainerModule(
302302
bind(IamSessionApp).toSelf().inSingletonScope();
303303

304304
// Authorization & Perms
305-
bind(SpiceDBClient)
306-
.toDynamicValue(() => spicedbClientFromEnv())
305+
bind(SpiceDBClientProvider)
306+
.toDynamicValue((ctx) => {
307+
const config = spiceDBConfigFromEnv();
308+
return new CachingSpiceDBClientProvider(config);
309+
})
307310
.inSingletonScope();
308311
bind(SpiceDBAuthorizer).toSelf().inSingletonScope();
309312
bind(Authorizer)

components/server/src/test/service-testing-container-module.ts

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

77
import * as grpc from "@grpc/grpc-js";
8-
import { v1 } from "@authzed/authzed-node";
98
import { IAnalyticsWriter, NullAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics";
109
import { IDEServiceClient, IDEServiceDefinition } from "@gitpod/ide-service-api/lib/ide.pb";
1110
import { UsageServiceDefinition } from "@gitpod/usage-api/lib/usage/v1/usage.pb";
@@ -14,7 +13,7 @@ import { v4 } from "uuid";
1413
import { AuthProviderParams } from "../auth/auth-provider";
1514
import { HostContextProvider, HostContextProviderFactory } from "../auth/host-context-provider";
1615
import { HostContextProviderImpl } from "../auth/host-context-provider-impl";
17-
import { SpiceDBClient } from "../authorization/spicedb";
16+
import { CachingSpiceDBClientProvider, SpiceDBClientProvider } from "../authorization/spicedb";
1817
import { Config } from "../config";
1918
import { StorageClient } from "../storage/storage-client";
2019
import { testContainer } from "@gitpod/gitpod-db/lib";
@@ -189,10 +188,13 @@ const mockApplyingContainerModule = new ContainerModule((bind, unbound, isbound,
189188
}))
190189
.inSingletonScope();
191190

192-
rebind(SpiceDBClient)
191+
rebind(SpiceDBClientProvider)
193192
.toDynamicValue(() => {
194-
const token = v4();
195-
return v1.NewClient(token, "localhost:50051", v1.ClientSecurity.INSECURE_PLAINTEXT_CREDENTIALS).promises;
193+
const config = {
194+
token: v4(),
195+
address: "localhost:50051",
196+
};
197+
return new CachingSpiceDBClientProvider(config);
196198
})
197199
.inSingletonScope();
198200
});

0 commit comments

Comments
 (0)