Skip to content

Commit 54a8ffe

Browse files
committed
now with the missing fga files
1 parent 5a8e1dc commit 54a8ffe

File tree

2 files changed

+343
-0
lines changed

2 files changed

+343
-0
lines changed

descope/management/fga.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
from datetime import datetime, timezone
2+
from typing import Any, List, Optional
3+
4+
from descope._auth_base import AuthBase
5+
from descope.management.common import MgmtV1
6+
7+
8+
class FGA(AuthBase):
9+
def save_schema(self, schema: str):
10+
"""
11+
Create or update an FGA schema.
12+
Args:
13+
schema (str): the schema in the AuthZ 1.0 DSL
14+
model AuthZ 1.0
15+
16+
type user
17+
18+
type org
19+
relation member: user
20+
relation parent: org
21+
22+
type folder
23+
relation parent: folder
24+
relation owner: user | org#member
25+
relation editor: user
26+
relation viewer: user
27+
28+
permission can_create: owner | parent.owner
29+
permission can_edit: editor | can_create
30+
permission can_view: viewer | can_edit
31+
32+
type doc
33+
relation parent: folder
34+
relation owner: user | org#member
35+
relation editor: user
36+
relation viewer: user
37+
38+
permission can_create: owner | parent.owner
39+
permission can_edit: editor | can_create
40+
permission can_view: viewer | can_edit
41+
Raise:
42+
AuthException: raised if saving fails
43+
"""
44+
self._auth.do_post(
45+
MgmtV1.fga_save_schema,
46+
{"dsl": schema},
47+
pswd=self._auth.management_key,
48+
)
49+
50+
def create_relations(
51+
self,
52+
relations: List[dict],
53+
):
54+
"""
55+
Create the given relations based on the existing schema
56+
Args:
57+
relations (List[dict]): the relations to create. Each in the following format:
58+
{
59+
"resource": "id of the resource that has the relation",
60+
"resourceType": "the type of the resource (namespace)",
61+
"relation": "the relation definition for the relation",
62+
"target": "the target that has the relation - usually users or other resources",
63+
"targetType": "the type of the target (namespace) - can also be group#member for target sets"
64+
}
65+
Raise:
66+
AuthException: raised if create relations fails
67+
"""
68+
self._auth.do_post(
69+
MgmtV1.fga_create_relations,
70+
{
71+
"tuples": relations,
72+
},
73+
pswd=self._auth.management_key,
74+
)
75+
76+
def delete_relations(
77+
self,
78+
relations: List[dict],
79+
):
80+
"""
81+
Delete the given relations based on the existing schema
82+
Args:
83+
relations (List[dict]): the relations to create. Each in the format as specified above for (create_relations)
84+
Raise:
85+
AuthException: raised if delete relations fails
86+
"""
87+
self._auth.do_post(
88+
MgmtV1.fga_delete_relations,
89+
{
90+
"tuples": relations,
91+
},
92+
pswd=self._auth.management_key,
93+
)
94+
95+
def check(
96+
self,
97+
relations: List[dict],
98+
) -> List[dict]:
99+
"""
100+
Queries the given relations to see if they exist returning true if they do
101+
Args:
102+
relations (List[dict]): List of relation queries each in the format of:
103+
{
104+
"resource": "id of the resource that has the relation",
105+
"resourceType": "the type of the resource (namespace)",
106+
"relation": "the relation definition for the relation",
107+
"target": "the target that has the relation - usually users or other resources",
108+
"targetType": "the type of the target (namespace)"
109+
}
110+
111+
Return value (List[dict]):
112+
Return List in the format
113+
[
114+
{
115+
"allowed": True|False
116+
"relation": {
117+
"resource": "id of the resource that has the relation",
118+
"resourceType": "the type of the resource (namespace)",
119+
"relation": "the relation definition for the relation",
120+
"target": "the target that has the relation - usually users or other resources",
121+
"targetType": "the type of the target (namespace)"
122+
}
123+
}
124+
]
125+
Raise:
126+
AuthException: raised if query fails
127+
"""
128+
response = self._auth.do_post(
129+
MgmtV1.fga_check,
130+
{
131+
"tuples": relations,
132+
},
133+
pswd=self._auth.management_key,
134+
)
135+
return list(map(lambda tuple: {"relation": tuple["tuple"], "allowed": tuple["allowed"]}, response.json()["tuples"]))

tests/management/test_fga.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
from unittest.mock import patch
2+
3+
from descope import AuthException, DescopeClient
4+
from descope.common import DEFAULT_TIMEOUT_SECONDS
5+
from descope.management.common import MgmtV1
6+
7+
from .. import common
8+
9+
10+
class TestFGA(common.DescopeTest):
11+
def setUp(self) -> None:
12+
super().setUp()
13+
self.dummy_project_id = "dummy"
14+
self.dummy_management_key = "key"
15+
self.public_key_dict = {
16+
"alg": "ES384",
17+
"crv": "P-384",
18+
"kid": "P2CtzUhdqpIF2ys9gg7ms06UvtC4",
19+
"kty": "EC",
20+
"use": "sig",
21+
"x": "pX1l7nT2turcK5_Cdzos8SKIhpLh1Wy9jmKAVyMFiOCURoj-WQX1J0OUQqMsQO0s",
22+
"y": "B0_nWAv2pmG_PzoH3-bSYZZzLNKUA0RoE2SH7DaS0KV4rtfWZhYd0MEr0xfdGKx0",
23+
}
24+
25+
def test_save_schema(self):
26+
client = DescopeClient(
27+
self.dummy_project_id,
28+
self.public_key_dict,
29+
False,
30+
self.dummy_management_key,
31+
)
32+
33+
# Test failed save_schema
34+
with patch("requests.post") as mock_post:
35+
mock_post.return_value.ok = False
36+
self.assertRaises(AuthException, client.mgmt.fga.save_schema, "")
37+
38+
# Test success flow
39+
with patch("requests.post") as mock_post:
40+
mock_post.return_value.ok = True
41+
self.assertIsNone(client.mgmt.fgs.save_schema("model AuthZ 1.0"))
42+
mock_post.assert_called_with(
43+
f"{common.DEFAULT_BASE_URL}{MgmtV1.fga_save_schema}",
44+
headers={
45+
**common.default_headers,
46+
"Authorization": f"Bearer {self.dummy_project_id}:{self.dummy_management_key}",
47+
},
48+
params=None,
49+
json={"dsl": "model AuthZ 1.0"},
50+
allow_redirects=False,
51+
verify=True,
52+
timeout=DEFAULT_TIMEOUT_SECONDS,
53+
)
54+
55+
def test_create_relations(self):
56+
client = DescopeClient(
57+
self.dummy_project_id,
58+
self.public_key_dict,
59+
False,
60+
self.dummy_management_key,
61+
)
62+
63+
# Test failed create_relations
64+
with patch("requests.post") as mock_post:
65+
mock_post.return_value.ok = False
66+
self.assertRaises(AuthException, client.mgmt.fga.create_relations, [])
67+
68+
# Test success flow
69+
with patch("requests.post") as mock_post:
70+
mock_post.return_value.ok = True
71+
self.assertIsNone(
72+
client.mgmt.fga.create_relations(
73+
[
74+
{
75+
"resource": "r",
76+
"resourceType": "rt",
77+
"relation": "rel",
78+
"target": "u",
79+
"targetType": "ty"
80+
}
81+
]
82+
)
83+
)
84+
mock_post.assert_called_with(
85+
f"{common.DEFAULT_BASE_URL}{MgmtV1.fga_create_relations}",
86+
headers={
87+
**common.default_headers,
88+
"Authorization": f"Bearer {self.dummy_project_id}:{self.dummy_management_key}",
89+
},
90+
params=None,
91+
json={
92+
"tuples": [
93+
{
94+
"resource": "r",
95+
"resourceType": "rt",
96+
"relation": "rel",
97+
"target": "u",
98+
"targetType": "ty"
99+
}
100+
]
101+
},
102+
allow_redirects=False,
103+
verify=True,
104+
timeout=DEFAULT_TIMEOUT_SECONDS,
105+
)
106+
107+
def test_delete_relations(self):
108+
client = DescopeClient(
109+
self.dummy_project_id,
110+
self.public_key_dict,
111+
False,
112+
self.dummy_management_key,
113+
)
114+
115+
# Test failed delete_relations
116+
with patch("requests.post") as mock_post:
117+
mock_post.return_value.ok = False
118+
self.assertRaises(AuthException, client.mgmt.fga.delete_relations, [])
119+
120+
# Test success flow
121+
with patch("requests.post") as mock_post:
122+
mock_post.return_value.ok = True
123+
self.assertIsNone(
124+
client.mgmt.authz.delete_relations(
125+
[
126+
{
127+
"resource": "r",
128+
"resourceType": "rt",
129+
"relation": "rel",
130+
"target": "u",
131+
"targetType": "ty"
132+
}
133+
]
134+
)
135+
)
136+
mock_post.assert_called_with(
137+
f"{common.DEFAULT_BASE_URL}{MgmtV1.fga_delete_relations}",
138+
headers={
139+
**common.default_headers,
140+
"Authorization": f"Bearer {self.dummy_project_id}:{self.dummy_management_key}",
141+
},
142+
params=None,
143+
json={
144+
"tuples": [
145+
{
146+
"resource": "r",
147+
"resourceType": "rt",
148+
"relation": "rel",
149+
"target": "u",
150+
"targetType": "ty"
151+
}
152+
]
153+
},
154+
allow_redirects=False,
155+
verify=True,
156+
timeout=DEFAULT_TIMEOUT_SECONDS,
157+
)
158+
159+
def test_check(self):
160+
client = DescopeClient(
161+
self.dummy_project_id,
162+
self.public_key_dict,
163+
False,
164+
self.dummy_management_key,
165+
)
166+
167+
# Test failed has_relations
168+
with patch("requests.post") as mock_post:
169+
mock_post.return_value.ok = False
170+
self.assertRaises(AuthException, client.mgmt.fga.check, [])
171+
172+
# Test success flow
173+
with patch("requests.post") as mock_post:
174+
mock_post.return_value.ok = True
175+
self.assertIsNotNone(
176+
client.mgmt.fga.check(
177+
[
178+
{
179+
"resource": "r",
180+
"resourceType": "rt",
181+
"relation": "rel",
182+
"target": "u",
183+
"targetType": "ty"
184+
}
185+
]
186+
)
187+
)
188+
mock_post.assert_called_with(
189+
f"{common.DEFAULT_BASE_URL}{MgmtV1.fga_check}",
190+
headers={
191+
**common.default_headers,
192+
"Authorization": f"Bearer {self.dummy_project_id}:{self.dummy_management_key}",
193+
},
194+
params=None,
195+
json={
196+
"relationQueries": [
197+
{
198+
"resource": "r",
199+
"relationDefinition": "rd",
200+
"namespace": "ns",
201+
"target": "u",
202+
}
203+
]
204+
},
205+
allow_redirects=False,
206+
verify=True,
207+
timeout=DEFAULT_TIMEOUT_SECONDS,
208+
)

0 commit comments

Comments
 (0)