Skip to content

Commit a744dbd

Browse files
committed
[server] relationship updates
1 parent ff48768 commit a744dbd

File tree

12 files changed

+438
-39
lines changed

12 files changed

+438
-39
lines changed

components/gitpod-protocol/src/protocol.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,8 @@ export interface AdditionalUserData extends Partial<WorkspaceTimeoutSetting> {
268268
// additional user profile data
269269
profile?: ProfileDetails;
270270
shouldSeeMigrationMessage?: boolean;
271-
271+
// fgaRelationshipsVersion is the version of the spicedb relationships
272+
fgaRelationshipsVersion?: number;
272273
// remembered workspace auto start options
273274
workspaceAutostartOptions?: WorkspaceAutostartOption[];
274275
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ export interface Project {
4040
}
4141

4242
export namespace Project {
43+
export function is(data?: any): data is Project {
44+
return typeof data === "object" && ["id", "name", "cloneUrl", "teamId"].every((p) => p in data);
45+
}
46+
4347
export const create = (project: Omit<Project, "id" | "creationTime">): Project => {
4448
return {
4549
...project,

components/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"start-testdb": "if netstat -tuln | grep ':23306 '; then echo 'Mysql is already running.'; else leeway run components/gitpod-db:init-testdb; fi",
2626
"start-spicedb": "leeway run components/spicedb:start-spicedb",
2727
"stop-spicedb": "leeway run components/spicedb:stop-spicedb",
28-
"start-redis": "if netstat -tuln | grep ':6379 '; then echo 'Redis is already running.'; else docker run --rm --name test-redis -p 6379:6379 -d redis; fi",
28+
"start-redis": "if netstat -tuln | grep ':6379 '; then echo 'Redis is already running.'; else docker run --rm --name test-redis -p 6379:6379 -d redis; while ! nc -z localhost 6379; do sleep 0.1; done; echo 'Redis is ready.'; fi",
2929
"stop-redis": "docker stop test-redis || true",
3030
"telepresence": "telepresence --swap-deployment server --method inject-tcp --run yarn start-inspect"
3131
},

components/server/src/authorization/authorizer.ts

Lines changed: 139 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,27 @@
66

77
import { v1 } from "@authzed/authzed-node";
88

9-
import { Organization, Project, TeamMemberInfo, TeamMemberRole } from "@gitpod/gitpod-protocol";
9+
import { Organization, Project, TeamMemberInfo, TeamMemberRole, User } from "@gitpod/gitpod-protocol";
1010
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
1111
import {
1212
InstallationID,
13+
InstallationRelation,
1314
OrganizationPermission,
15+
OrganizationRelation,
1416
Permission,
1517
ProjectPermission,
18+
ProjectRelation,
1619
Relation,
1720
ResourceType,
1821
UserPermission,
22+
UserRelation,
1923
} from "./definitions";
2024
import { SpiceDBAuthorizer } from "./spicedb-authorizer";
2125
import { BUILTIN_INSTLLATION_ADMIN_USER_ID } from "@gitpod/gitpod-db/lib";
2226

2327
export function createInitializingAuthorizer(spiceDbAuthorizer: SpiceDBAuthorizer): Authorizer {
2428
const target = new Authorizer(spiceDbAuthorizer);
25-
const initialized = target.addAdminRole(BUILTIN_INSTLLATION_ADMIN_USER_ID);
29+
const initialized = target.addInstallationAdminRole(BUILTIN_INSTLLATION_ADMIN_USER_ID);
2630
return new Proxy(target, {
2731
get(target, propKey, receiver) {
2832
const originalMethod = target[propKey as keyof typeof target];
@@ -39,6 +43,24 @@ export function createInitializingAuthorizer(spiceDbAuthorizer: SpiceDBAuthorize
3943
});
4044
}
4145

46+
export const installation = {
47+
type: "installation",
48+
id: InstallationID,
49+
};
50+
51+
export type Resource = typeof installation | User | Organization | Project;
52+
export namespace Resource {
53+
export function getType(res: Resource): ResourceType {
54+
return (res as any).type === "installation"
55+
? "installation"
56+
: User.is(res)
57+
? "user"
58+
: Project.is(res)
59+
? "project"
60+
: "organization";
61+
}
62+
}
63+
4264
export class Authorizer {
4365
constructor(private authorizer: SpiceDBAuthorizer) {}
4466

@@ -125,15 +147,52 @@ export class Authorizer {
125147

126148
// write operations below
127149

128-
async addUser(userId: string) {
150+
public async removeAllRelationships(type: ResourceType, id: string) {
151+
await this.authorizer.deleteRelationships(
152+
v1.DeleteRelationshipsRequest.create({
153+
relationshipFilter: {
154+
resourceType: type,
155+
optionalResourceId: id,
156+
},
157+
}),
158+
);
159+
160+
// iterate over all resource types and remove by subject
161+
for (const resourcetype of ["installation", "user", "organization", "project"] as ResourceType[]) {
162+
await this.authorizer.deleteRelationships(
163+
v1.DeleteRelationshipsRequest.create({
164+
relationshipFilter: {
165+
resourceType: resourcetype,
166+
optionalResourceId: "",
167+
optionalRelation: "",
168+
optionalSubjectFilter: {
169+
subjectType: type,
170+
optionalSubjectId: id,
171+
},
172+
},
173+
}),
174+
);
175+
}
176+
}
177+
178+
async addUser(userId: string, owningOrgId?: string) {
179+
const updates = [
180+
v1.RelationshipUpdate.create({
181+
operation: v1.RelationshipUpdate_Operation.TOUCH,
182+
relationship: relationship(objectRef("user", userId), "self", subject("user", userId)),
183+
}),
184+
v1.RelationshipUpdate.create({
185+
operation: v1.RelationshipUpdate_Operation.TOUCH,
186+
relationship: relationship(
187+
objectRef("user", userId),
188+
"container",
189+
owningOrgId ? subject("organization", owningOrgId) : subject("installation", InstallationID),
190+
),
191+
}),
192+
];
129193
await this.authorizer.writeRelationships(
130194
v1.WriteRelationshipsRequest.create({
131-
updates: [
132-
v1.RelationshipUpdate.create({
133-
operation: v1.RelationshipUpdate_Operation.TOUCH,
134-
relationship: relationship(objectRef("user", userId), "self", subject("user", userId)),
135-
}),
136-
],
195+
updates,
137196
}),
138197
);
139198
}
@@ -197,17 +256,6 @@ export class Authorizer {
197256
);
198257
}
199258

200-
async deleteOrganization(orgID: string): Promise<void> {
201-
await this.authorizer.deleteRelationships(
202-
v1.DeleteRelationshipsRequest.create({
203-
relationshipFilter: v1.RelationshipFilter.create({
204-
resourceType: "organization",
205-
optionalResourceId: orgID,
206-
}),
207-
}),
208-
);
209-
}
210-
211259
async addOrganization(org: Organization, members: TeamMemberInfo[], projects: Project[]): Promise<void> {
212260
const updates: v1.RelationshipUpdate[] = [];
213261

@@ -272,7 +320,7 @@ export class Authorizer {
272320
);
273321
}
274322

275-
async addAdminRole(userID: string) {
323+
async addInstallationAdminRole(userID: string) {
276324
await this.authorizer.writeRelationships(
277325
v1.WriteRelationshipsRequest.create({
278326
updates: [
@@ -375,6 +423,76 @@ export class Authorizer {
375423
}),
376424
];
377425
}
426+
427+
public async readRelationships(
428+
inst: typeof installation,
429+
relation: InstallationRelation,
430+
target: Resource,
431+
): Promise<Relationship[]>;
432+
public async readRelationships(user: User, relation: UserRelation, target: Resource): Promise<Relationship[]>;
433+
public async readRelationships(
434+
org: Organization,
435+
relation: OrganizationRelation,
436+
target: Resource,
437+
): Promise<Relationship[]>;
438+
public async readRelationships(
439+
project: Project,
440+
relation: ProjectRelation,
441+
target: Resource,
442+
): Promise<Relationship[]>;
443+
public async readRelationships(subject: Resource, relation?: Relation, object?: Resource): Promise<Relationship[]>;
444+
public async readRelationships(subject: Resource, relation?: Relation, object?: Resource): Promise<Relationship[]> {
445+
const relationShips = await this.authorizer.readRelationships({
446+
consistency: v1.Consistency.create({
447+
requirement: {
448+
oneofKind: "fullyConsistent",
449+
fullyConsistent: true,
450+
},
451+
}),
452+
relationshipFilter: {
453+
resourceType: Resource.getType(subject),
454+
optionalResourceId: subject.id,
455+
optionalRelation: relation || "",
456+
optionalSubjectFilter: object && {
457+
subjectType: Resource.getType(object),
458+
optionalSubjectId: object?.id,
459+
},
460+
},
461+
});
462+
return relationShips
463+
.map((rel) => {
464+
const subject = rel.relationship?.subject?.object;
465+
const object = rel.relationship?.resource;
466+
const relation = rel.relationship?.relation;
467+
if (!subject || !object || !relation) {
468+
throw new Error("Invalid relationship");
469+
}
470+
return new Relationship(
471+
object.objectType as ResourceType,
472+
object.objectId!,
473+
relation as Relation,
474+
subject.objectType as ResourceType,
475+
subject.objectId!,
476+
);
477+
})
478+
.sort((a, b) => {
479+
return a.toString().localeCompare(b.toString());
480+
});
481+
}
482+
}
483+
484+
export class Relationship {
485+
constructor(
486+
public readonly subjectType: ResourceType,
487+
public readonly subjectID: string,
488+
public readonly relation: Relation,
489+
public readonly objectType: ResourceType,
490+
public readonly objectID: string,
491+
) {}
492+
493+
public toString(): string {
494+
return `${this.subjectType}:${this.subjectID}#${this.relation}@${this.objectType}:${this.objectID}`;
495+
}
378496
}
379497

380498
function objectRef(type: ResourceType, id: string): v1.ObjectReference {

0 commit comments

Comments
 (0)