Skip to content

[server/FGA] added make_admin permission #18371

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion components/gitpod-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,8 @@ export interface AdditionalUserData extends Partial<WorkspaceTimeoutSetting> {
// additional user profile data
profile?: ProfileDetails;
shouldSeeMigrationMessage?: boolean;

// fgaRelationshipsVersion is the version of the spicedb relationships
fgaRelationshipsVersion?: number;
// remembered workspace auto start options
workspaceAutostartOptions?: WorkspaceAutostartOption[];
}
Expand Down
4 changes: 4 additions & 0 deletions components/gitpod-protocol/src/teams-projects-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export interface Project {
}

export namespace Project {
export function is(data?: any): data is Project {
return typeof data === "object" && ["id", "name", "cloneUrl", "teamId"].every((p) => p in data);
}

export const create = (project: Omit<Project, "id" | "creationTime">): Project => {
return {
...project,
Expand Down
2 changes: 1 addition & 1 deletion components/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"start-testdb": "if netstat -tuln | grep ':23306 '; then echo 'Mysql is already running.'; else leeway run components/gitpod-db:init-testdb; fi",
"start-spicedb": "leeway run components/spicedb:start-spicedb",
"stop-spicedb": "leeway run components/spicedb:stop-spicedb",
"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",
"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",
"stop-redis": "docker stop test-redis || true",
"telepresence": "telepresence --swap-deployment server --method inject-tcp --run yarn start-inspect"
},
Expand Down
162 changes: 140 additions & 22 deletions components/server/src/authorization/authorizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,27 @@

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

import { Organization, Project, TeamMemberInfo, TeamMemberRole } from "@gitpod/gitpod-protocol";
import { Organization, Project, TeamMemberInfo, TeamMemberRole, User } from "@gitpod/gitpod-protocol";
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
import {
InstallationID,
InstallationRelation,
OrganizationPermission,
OrganizationRelation,
Permission,
ProjectPermission,
ProjectRelation,
Relation,
ResourceType,
UserPermission,
UserRelation,
} from "./definitions";
import { SpiceDBAuthorizer } from "./spicedb-authorizer";
import { BUILTIN_INSTLLATION_ADMIN_USER_ID } from "@gitpod/gitpod-db/lib";

export function createInitializingAuthorizer(spiceDbAuthorizer: SpiceDBAuthorizer): Authorizer {
const target = new Authorizer(spiceDbAuthorizer);
const initialized = target.addAdminRole(BUILTIN_INSTLLATION_ADMIN_USER_ID);
const initialized = target.addInstallationAdminRole(BUILTIN_INSTLLATION_ADMIN_USER_ID);
return new Proxy(target, {
get(target, propKey, receiver) {
const originalMethod = target[propKey as keyof typeof target];
Expand All @@ -39,6 +43,24 @@ export function createInitializingAuthorizer(spiceDbAuthorizer: SpiceDBAuthorize
});
}

export const installation = {
type: "installation",
id: InstallationID,
};

export type Resource = typeof installation | User | Organization | Project;
export namespace Resource {
export function getType(res: Resource): ResourceType {
return (res as any).type === "installation"
? "installation"
: User.is(res)
? "user"
: Project.is(res)
? "project"
: "organization";
}
}

export class Authorizer {
constructor(private authorizer: SpiceDBAuthorizer) {}

Expand Down Expand Up @@ -125,15 +147,52 @@ export class Authorizer {

// write operations below

async addUser(userId: string) {
public async removeAllRelationships(type: ResourceType, id: string) {
await this.authorizer.deleteRelationships(
v1.DeleteRelationshipsRequest.create({
relationshipFilter: {
resourceType: type,
optionalResourceId: id,
},
}),
);

// iterate over all resource types and remove by subject
for (const resourcetype of ["installation", "user", "organization", "project"] as ResourceType[]) {
await this.authorizer.deleteRelationships(
v1.DeleteRelationshipsRequest.create({
relationshipFilter: {
resourceType: resourcetype,
optionalResourceId: "",
optionalRelation: "",
optionalSubjectFilter: {
subjectType: type,
optionalSubjectId: id,
},
},
}),
);
}
}

async addUser(userId: string, owningOrgId?: string) {
const updates = [
v1.RelationshipUpdate.create({
operation: v1.RelationshipUpdate_Operation.TOUCH,
relationship: relationship(objectRef("user", userId), "self", subject("user", userId)),
}),
v1.RelationshipUpdate.create({
operation: v1.RelationshipUpdate_Operation.TOUCH,
relationship: relationship(
objectRef("user", userId),
"container",
owningOrgId ? subject("organization", owningOrgId) : subject("installation", InstallationID),
),
}),
];
await this.authorizer.writeRelationships(
v1.WriteRelationshipsRequest.create({
updates: [
v1.RelationshipUpdate.create({
operation: v1.RelationshipUpdate_Operation.TOUCH,
relationship: relationship(objectRef("user", userId), "self", subject("user", userId)),
}),
],
updates,
}),
);
}
Expand Down Expand Up @@ -197,17 +256,6 @@ export class Authorizer {
);
}

async deleteOrganization(orgID: string): Promise<void> {
await this.authorizer.deleteRelationships(
v1.DeleteRelationshipsRequest.create({
relationshipFilter: v1.RelationshipFilter.create({
resourceType: "organization",
optionalResourceId: orgID,
}),
}),
);
}

async addOrganization(org: Organization, members: TeamMemberInfo[], projects: Project[]): Promise<void> {
const updates: v1.RelationshipUpdate[] = [];

Expand Down Expand Up @@ -272,7 +320,7 @@ export class Authorizer {
);
}

async addAdminRole(userID: string) {
async addInstallationAdminRole(userID: string) {
await this.authorizer.writeRelationships(
v1.WriteRelationshipsRequest.create({
updates: [
Expand All @@ -289,7 +337,7 @@ export class Authorizer {
);
}

async removeAdminRole(userID: string) {
async removeInstallationAdminRole(userID: string) {
await this.authorizer.writeRelationships(
v1.WriteRelationshipsRequest.create({
updates: [
Expand Down Expand Up @@ -375,6 +423,76 @@ export class Authorizer {
}),
];
}

public async readRelationships(
inst: typeof installation,
relation: InstallationRelation,
target: Resource,
): Promise<Relationship[]>;
public async readRelationships(user: User, relation: UserRelation, target: Resource): Promise<Relationship[]>;
public async readRelationships(
org: Organization,
relation: OrganizationRelation,
target: Resource,
): Promise<Relationship[]>;
public async readRelationships(
project: Project,
relation: ProjectRelation,
target: Resource,
): Promise<Relationship[]>;
public async readRelationships(subject: Resource, relation?: Relation, object?: Resource): Promise<Relationship[]>;
public async readRelationships(subject: Resource, relation?: Relation, object?: Resource): Promise<Relationship[]> {
const relationShips = await this.authorizer.readRelationships({
consistency: v1.Consistency.create({
requirement: {
oneofKind: "fullyConsistent",
fullyConsistent: true,
},
}),
relationshipFilter: {
resourceType: Resource.getType(subject),
optionalResourceId: subject.id,
optionalRelation: relation || "",
optionalSubjectFilter: object && {
subjectType: Resource.getType(object),
optionalSubjectId: object?.id,
},
},
});
return relationShips
.map((rel) => {
const subject = rel.relationship?.subject?.object;
const object = rel.relationship?.resource;
const relation = rel.relationship?.relation;
if (!subject || !object || !relation) {
throw new Error("Invalid relationship");
}
return new Relationship(
object.objectType as ResourceType,
object.objectId!,
relation as Relation,
subject.objectType as ResourceType,
subject.objectId!,
);
})
.sort((a, b) => {
return a.toString().localeCompare(b.toString());
});
}
}

export class Relationship {
constructor(
public readonly subjectType: ResourceType,
public readonly subjectID: string,
public readonly relation: Relation,
public readonly objectType: ResourceType,
public readonly objectID: string,
) {}

public toString(): string {
return `${this.subjectType}:${this.subjectID}#${this.relation}@${this.objectType}:${this.objectID}`;
}
}

function objectRef(type: ResourceType, id: string): v1.ObjectReference {
Expand Down
2 changes: 1 addition & 1 deletion components/server/src/authorization/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type UserResourceType = "user";

export type UserRelation = "self" | "container";

export type UserPermission = "read_info" | "write_info" | "suspend";
export type UserPermission = "read_info" | "write_info" | "suspend" | "make_admin";

export type InstallationResourceType = "installation";

Expand Down
Loading