Skip to content

Commit 85d5695

Browse files
committed
[openfga] Setup component
1 parent bb8bfb3 commit 85d5695

File tree

23 files changed

+3140
-3
lines changed

23 files changed

+3140
-3
lines changed

components/openfga/model.openfga

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
model
2+
schema 1.1
3+
4+
type user
5+
type team
6+
relations
7+
define owner: [user]
8+
define member: [user] or owner
9+
10+
define grant_owner: owner
11+
define list_members: owner or member
12+
define add_members: owner
13+
define remove_members: owner
14+
define delete_team: owner
15+
16+
define project_creator: owner or member
17+
18+
type project
19+
relations
20+
define maintainer: [user,team#member]

components/openfga/scripts.hack

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
curl -X POST "openfga:8080/stores/01GP4CRKXESH1JE5E0SNHMZYG1/authorization-models" \
2+
-H "content-type: application/json" \
3+
-d '{
4+
"type_definitions": [
5+
{
6+
"type": "user",
7+
"relations": {}
8+
},
9+
{
10+
"type": "team",
11+
"relations": {
12+
"owner": {
13+
"this": {}
14+
},
15+
"member": {
16+
"union": {
17+
"child": [
18+
{
19+
"this": {}
20+
},
21+
{
22+
"computedUserset": {
23+
"object": "",
24+
"relation": "owner"
25+
}
26+
}
27+
]
28+
}
29+
},
30+
"grant_owner": {
31+
"computedUserset": {
32+
"object": "",
33+
"relation": "owner"
34+
}
35+
},
36+
"list_members": {
37+
"union": {
38+
"child": [
39+
{
40+
"computedUserset": {
41+
"object": "",
42+
"relation": "owner"
43+
}
44+
},
45+
{
46+
"computedUserset": {
47+
"object": "",
48+
"relation": "member"
49+
}
50+
}
51+
]
52+
}
53+
},
54+
"add_members": {
55+
"computedUserset": {
56+
"object": "",
57+
"relation": "owner"
58+
}
59+
},
60+
"remove_members": {
61+
"computedUserset": {
62+
"object": "",
63+
"relation": "owner"
64+
}
65+
},
66+
"delete_team": {
67+
"computedUserset": {
68+
"object": "",
69+
"relation": "owner"
70+
}
71+
},
72+
"project_creator": {
73+
"union": {
74+
"child": [
75+
{
76+
"computedUserset": {
77+
"object": "",
78+
"relation": "owner"
79+
}
80+
},
81+
{
82+
"computedUserset": {
83+
"object": "",
84+
"relation": "member"
85+
}
86+
}
87+
]
88+
}
89+
}
90+
},
91+
"metadata": {
92+
"relations": {
93+
"owner": {
94+
"directly_related_user_types": [
95+
{
96+
"type": "user"
97+
}
98+
]
99+
},
100+
"member": {
101+
"directly_related_user_types": [
102+
{
103+
"type": "user"
104+
}
105+
]
106+
},
107+
"grant_owner": {
108+
"directly_related_user_types": []
109+
},
110+
"list_members": {
111+
"directly_related_user_types": []
112+
},
113+
"add_members": {
114+
"directly_related_user_types": []
115+
},
116+
"remove_members": {
117+
"directly_related_user_types": []
118+
},
119+
"delete_team": {
120+
"directly_related_user_types": []
121+
},
122+
"project_creator": {
123+
"directly_related_user_types": []
124+
}
125+
}
126+
}
127+
},
128+
{
129+
"type": "project",
130+
"relations": {
131+
"maintainer": {
132+
"this": {}
133+
}
134+
},
135+
"metadata": {
136+
"relations": {
137+
"maintainer": {
138+
"directly_related_user_types": [
139+
{
140+
"type": "user"
141+
},
142+
{
143+
"type": "team",
144+
"relation": "member"
145+
}
146+
]
147+
}
148+
}
149+
}
150+
}
151+
],
152+
"schema_version": "1.1"
153+
}'
154+
155+
curl -X POST "openfga:8080/stores/01GP4CRKXESH1JE5E0SNHMZYG1/check" \
156+
-H "content-type: application/json" \
157+
-d '{"tuple_key":{"user":"user:milan","relation":"maintainer","object":"project:project1"}}'
158+
159+
# Response: {"allowed":true}
160+
b23f24d7-47f7-4366-a642-ce46b61b499e
161+
162+
curl -X GET "localhost:8080/stores/01GP4CRKXESH1JE5E0SNHMZYG1/changes" \
163+
-H "content-type: application/json"

components/server/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@improbable-eng/grpc-web-node-http-transport": "^0.14.0",
4444
"@jmondi/oauth2-server": "^2.6.1",
4545
"@octokit/rest": "18.6.1",
46+
"@openfga/sdk": "^0.2.0",
4647
"@probot/get-private-key": "^1.1.1",
4748
"@types/jaeger-client": "^3.18.3",
4849
"amqplib": "^0.8.0",

components/server/src/perms.ts

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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 { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
8+
import { OpenFgaApi, TupleKey } from "@openfga/sdk";
9+
import { ResponseError } from "vscode-jsonrpc";
10+
11+
export const OpenFGA = new OpenFgaApi({
12+
apiScheme: "http", // Optional. Can be "http" or "https". Defaults to "https"
13+
apiHost: "openfga:8080", // required, define without the scheme (e.g. api.openfga.example instead of https://api.openfga.example)
14+
storeId: "01GP4CRKXESH1JE5E0SNHMZYG1",
15+
});
16+
17+
function tup(user: string, relation: string, object: string): TupleKey {
18+
return { user, relation, object };
19+
}
20+
21+
function user(id: string): string {
22+
return `user:${id}`;
23+
}
24+
25+
function team(id: string): string {
26+
return `team:${id}`;
27+
}
28+
29+
function proj(id: string): string {
30+
return `project:${id}`;
31+
}
32+
33+
export async function isTeamOwner(userID: string, teamID: string): Promise<boolean> {
34+
return (
35+
(
36+
await OpenFGA.check({
37+
tuple_key: tup(user(userID), "owner", team(teamID)),
38+
})
39+
).allowed || false
40+
);
41+
}
42+
43+
export async function isTeamMember(userID: string, teamID: string): Promise<boolean> {
44+
return (
45+
(
46+
await OpenFGA.check({
47+
tuple_key: tup(user(userID), "member", team(teamID)),
48+
})
49+
).allowed || false
50+
);
51+
}
52+
53+
export async function grantTeamOwner(userID: string, teamID: string) {
54+
const deletes: TupleKey[] = [];
55+
56+
const isMember = await isTeamMember(userID, teamID);
57+
if (isMember) {
58+
deletes.push(tup(user(userID), "member", team(teamID)));
59+
}
60+
61+
return await OpenFGA.write({
62+
writes: {
63+
tuple_keys: [tup(user(userID), "owner", team(teamID))],
64+
},
65+
deletes: {
66+
tuple_keys: deletes,
67+
},
68+
});
69+
}
70+
71+
export async function grantTeamMember(userID: string, teamID: string) {
72+
const deletes: TupleKey[] = [];
73+
74+
const isMember = await isTeamOwner(userID, teamID);
75+
if (isMember) {
76+
deletes.push(tup(user(userID), "owner", team(teamID)));
77+
}
78+
await OpenFGA.write({
79+
writes: {
80+
tuple_keys: [tup(user(userID), "member", team(teamID))],
81+
},
82+
deletes: {
83+
tuple_keys: deletes,
84+
},
85+
});
86+
}
87+
88+
export async function removeUserFromTeam(userID: string, teamID: string) {
89+
return OpenFGA.write({
90+
deletes: {
91+
tuple_keys: [tup(user(userID), "owner", team(teamID)), tup(user(userID), "member", team(teamID))],
92+
},
93+
});
94+
}
95+
96+
export async function canCreateProject(userID: string, teamID: string) {
97+
const response = await OpenFGA.check({
98+
tuple_key: tup(user(userID), "member", team(teamID)),
99+
});
100+
101+
if (!response.allowed) {
102+
throw newPermissionDenied(`user ${userID} is not allowed to create projects for team ${teamID}`);
103+
}
104+
return response.allowed || false;
105+
}
106+
107+
export async function canDeleteProject(userID: string, projectID: string) {
108+
const response = await OpenFGA.check({
109+
tuple_key: tup(user(userID), "maintainer", proj(projectID)),
110+
});
111+
112+
if (!response.allowed) {
113+
throw newPermissionDenied(`user ${userID} is not allowed to delete project ${projectID}`);
114+
}
115+
return response.allowed || false;
116+
}
117+
118+
export async function canAccessProject(userID: string, projectID: string): Promise<boolean> {
119+
const response = await OpenFGA.check({
120+
tuple_key: tup(user(userID), "maintainer", proj(projectID)),
121+
});
122+
123+
return response.allowed || false;
124+
}
125+
126+
export async function grantTeamProjectMaintainer(teamID: string, projectID: string) {
127+
return OpenFGA.write({
128+
writes: {
129+
tuple_keys: [
130+
{
131+
user: `team:${teamID}#member`,
132+
relation: "maintainer",
133+
object: proj(projectID),
134+
},
135+
],
136+
},
137+
});
138+
}
139+
140+
function newPermissionDenied(msg: string): ResponseError<string> {
141+
return new ResponseError(ErrorCodes.PERMISSION_DENIED, msg);
142+
}

0 commit comments

Comments
 (0)