Skip to content

Commit 9977e60

Browse files
authored
Merge branch 'main' into schloerke/342-add-user-group-permission
2 parents 0dc78c2 + fed8325 commit 9977e60

File tree

7 files changed

+261
-12
lines changed

7 files changed

+261
-12
lines changed

.github/workflows/ci.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ jobs:
4141
matrix:
4242
CONNECT_VERSION:
4343
- preview
44+
- 2024.11.0
4445
- 2024.09.0
4546
- 2024.08.0
4647
- 2024.06.0

integration/Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ CONNECT_BOOTSTRAP_SECRETKEY ?= $(shell head -c 32 /dev/random | base64)
2222
help
2323

2424
# Versions
25-
CONNECT_VERSIONS := 2024.09.0 \
25+
CONNECT_VERSIONS := \
26+
2024.11.0 \
27+
2024.09.0 \
2628
2024.08.0 \
2729
2024.06.0 \
2830
2024.05.0 \
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from posit import connect
2+
from posit.connect.content import ContentItem
3+
4+
5+
class TestContentPermissions:
6+
content: ContentItem
7+
8+
@classmethod
9+
def setup_class(cls):
10+
cls.client = connect.Client()
11+
cls.content = cls.client.content.create(name="example")
12+
13+
cls.user_aron = cls.client.users.create(
14+
username="permission_aron",
15+
16+
password="permission_s3cur3p@ssword",
17+
)
18+
cls.user_bill = cls.client.users.create(
19+
username="permission_bill",
20+
21+
password="permission_s3cur3p@ssword",
22+
)
23+
24+
cls.group_friends = cls.client.groups.create(name="Friends")
25+
26+
@classmethod
27+
def teardown_class(cls):
28+
cls.content.delete()
29+
assert cls.client.content.count() == 0
30+
31+
cls.group_friends.delete()
32+
assert cls.client.groups.count() == 0
33+
34+
def test_permissions_add_destroy(self):
35+
assert self.client.groups.count() == 1
36+
assert self.client.users.count() == 3
37+
assert self.content.permissions.find() == []
38+
39+
# Add permissions
40+
self.content.permissions.create(
41+
principal_guid=self.user_aron["guid"],
42+
principal_type="user",
43+
role="viewer",
44+
)
45+
self.content.permissions.create(
46+
principal_guid=self.group_friends["guid"],
47+
principal_type="group",
48+
role="owner",
49+
)
50+
51+
def assert_permissions_match_guids(permissions, objs_with_guid):
52+
for permission, obj_with_guid in zip(permissions, objs_with_guid):
53+
assert permission["principal_guid"] == obj_with_guid["guid"]
54+
55+
# Prove they have been added
56+
assert_permissions_match_guids(
57+
self.content.permissions.find(),
58+
[self.user_aron, self.group_friends],
59+
)
60+
61+
# Remove permissions (and from some that isn't an owner)
62+
destroyed_permissions = self.content.permissions.destroy(self.user_aron, self.user_bill)
63+
assert_permissions_match_guids(
64+
destroyed_permissions,
65+
[self.user_aron],
66+
)
67+
68+
# Prove they have been removed
69+
assert_permissions_match_guids(
70+
self.content.permissions.find(),
71+
[self.group_friends],
72+
)
73+
74+
# Remove the last permission
75+
destroyed_permissions = self.content.permissions.destroy(self.group_friends)
76+
assert_permissions_match_guids(
77+
destroyed_permissions,
78+
[self.group_friends],
79+
)
80+
81+
# Prove they have been removed
82+
assert self.content.permissions.find() == []

integration/tests/posit/connect/test_users.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ class TestUser:
55
@classmethod
66
def setup_class(cls):
77
cls.client = client = connect.Client()
8+
9+
# Play nicely with other tests
10+
cls.existing_user_count = client.users.count()
11+
812
cls.aron = client.users.create(
913
username="aron",
1014
@@ -29,8 +33,8 @@ def test_lock(self):
2933
assert len(self.client.users.find(account_status="locked")) == 0
3034

3135
def test_count(self):
32-
# aron, bill, cole, and me
33-
assert self.client.users.count() == 4
36+
# aron, bill, cole, and me (and existing user)
37+
assert self.client.users.count() == 3 + self.existing_user_count
3438

3539
def test_find(self):
3640
assert self.client.users.find(prefix="aron") == [self.aron]

src/posit/connect/permissions.py

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414

1515

1616
class Permission(Resource):
17-
def delete(self) -> None:
18-
"""Delete the permission."""
17+
def destroy(self) -> None:
18+
"""Destroy the permission."""
1919
path = f"v1/content/{self['content_guid']}/permissions/{self['id']}"
2020
url = self.params.url + path
2121
self.params.session.delete(url)
@@ -215,3 +215,85 @@ def get(self, uid: str) -> Permission:
215215
url = self.params.url + path
216216
response = self.params.session.get(url)
217217
return Permission(self.params, **response.json())
218+
219+
def destroy(self, *permissions: str | Group | User | Permission) -> list[Permission]:
220+
"""Remove supplied content item permissions.
221+
222+
Removes all provided permissions from the content item's permissions. If a permission isn't
223+
found, it is silently ignored.
224+
225+
Parameters
226+
----------
227+
*permissions : str | Group | User | Permission
228+
The content item permissions to remove. If a `str` is received, it is compared against
229+
the `Permissions`'s `principal_guid`. If a `Group` or `User` is received, the associated
230+
`Permission` will be removed.
231+
232+
Returns
233+
-------
234+
list[Permission]
235+
The removed permissions. If a permission is not found, there is nothing to remove and
236+
it is not included in the returned list.
237+
238+
Examples
239+
--------
240+
```python
241+
from posit import connect
242+
243+
#### User-defined inputs ####
244+
# 1. specify the guid for the content item
245+
content_guid = "CONTENT_GUID_HERE"
246+
# 2. specify either the principal_guid or group name prefix
247+
principal_guid = "USER_OR_GROUP_GUID_HERE"
248+
group_name_prefix = "GROUP_NAME_PREFIX_HERE"
249+
############################
250+
251+
client = connect.Client()
252+
253+
# Remove a single permission by principal_guid
254+
client.content.get(content_guid).permissions.destroy(principal_guid)
255+
256+
# Remove by user (if principal_guid is a user)
257+
user = client.users.get(principal_guid)
258+
client.content.get(content_guid).permissions.destroy(user)
259+
260+
# Remove by group (if principal_guid is a group)
261+
group = client.groups.get(principal_guid)
262+
client.content.get(content_guid).permissions.destroy(group)
263+
264+
# Remove all groups with a matching prefix name
265+
groups = client.groups.find(prefix=group_name_prefix)
266+
client.content.get(content_guid).permissions.destroy(*groups)
267+
268+
# Confirm new permissions
269+
client.content.get(content_guid).permissions.find()
270+
```
271+
"""
272+
from .groups import Group
273+
from .users import User
274+
275+
if len(permissions) == 0:
276+
raise ValueError("Expected at least one `permission` to remove")
277+
278+
principal_guids: set[str] = set()
279+
280+
for arg in permissions:
281+
if isinstance(arg, str):
282+
principal_guid = arg
283+
elif isinstance(arg, (Group, User)):
284+
principal_guid: str = arg["guid"]
285+
elif isinstance(arg, Permission):
286+
principal_guid: str = arg["principal_guid"]
287+
else:
288+
raise TypeError(
289+
f"destroy() expected argument type 'str', 'User', 'Group', or 'Permission' but got '{type(arg).__name__}'",
290+
)
291+
principal_guids.add(principal_guid)
292+
293+
destroyed_permissions: list[Permission] = []
294+
for permission in self.find():
295+
if permission["principal_guid"] in principal_guids:
296+
permission.destroy()
297+
destroyed_permissions.append(permission)
298+
299+
return destroyed_permissions

tests/posit/connect/api.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@
22

33
import pyjson5 as json
44

5-
from posit.connect._json import Jsonifiable, JsonifiableDict, JsonifiableList
65

7-
8-
def load_mock(path: str) -> Jsonifiable:
6+
def load_mock(path: str):
97
"""
108
Load mock data from a file.
119
@@ -33,13 +31,13 @@ def load_mock(path: str) -> Jsonifiable:
3331
return json.loads((Path(__file__).parent / "__api__" / path).read_text())
3432

3533

36-
def load_mock_dict(path: str) -> JsonifiableDict:
34+
def load_mock_dict(path: str) -> dict:
3735
result = load_mock(path)
3836
assert isinstance(result, dict)
3937
return result
4038

4139

42-
def load_mock_list(path: str) -> JsonifiableList:
40+
def load_mock_list(path: str) -> list:
4341
result = load_mock(path)
4442
assert isinstance(result, list)
4543
return result

tests/posit/connect/test_permissions.py

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import random
22
import uuid
33

4+
import pytest
45
import requests
56
import responses
67
from responses import matchers
78

9+
from posit.connect.groups import Group
810
from posit.connect.permissions import Permission, Permissions
911
from posit.connect.resources import ResourceParameters
1012
from posit.connect.urls import Url
13+
from posit.connect.users import User
1114

1215
from .api import load_mock, load_mock_dict, load_mock_list
1316

1417

15-
class TestPermissionDelete:
18+
class TestPermissionDestroy:
1619
@responses.activate
1720
def test(self):
1821
# data
@@ -30,7 +33,7 @@ def test(self):
3033
permission = Permission(params, **fake_permission)
3134

3235
# invoke
33-
permission.delete()
36+
permission.destroy()
3437

3538
# assert
3639
assert mock_delete.call_count == 1
@@ -262,3 +265,80 @@ def test(self):
262265

263266
# assert
264267
assert permission == fake_permission
268+
269+
270+
class TestPermissionsDestroy:
271+
@responses.activate
272+
def test_destroy(self):
273+
# data
274+
permission_uid = "94"
275+
content_guid = "f2f37341-e21d-3d80-c698-a935ad614066"
276+
fake_permissions = load_mock_list(f"v1/content/{content_guid}/permissions.json")
277+
fake_followup_permissions = fake_permissions.copy()
278+
fake_followup_permissions.pop(0)
279+
fake_permission = load_mock_dict(
280+
f"v1/content/{content_guid}/permissions/{permission_uid}.json"
281+
)
282+
fake_user = load_mock_dict("v1/user.json")
283+
fake_group = load_mock_dict("v1/groups/6f300623-1e0c-48e6-a473-ddf630c0c0c3.json")
284+
285+
# behavior
286+
287+
# Used in internal for-loop
288+
mock_permissions_get = [
289+
responses.get(
290+
f"https://connect.example/__api__/v1/content/{content_guid}/permissions",
291+
json=fake_permissions,
292+
),
293+
responses.get(
294+
f"https://connect.example/__api__/v1/content/{content_guid}/permissions",
295+
json=fake_followup_permissions,
296+
),
297+
]
298+
# permission delete
299+
mock_permission_delete = responses.delete(
300+
f"https://connect.example/__api__/v1/content/{content_guid}/permissions/{permission_uid}",
301+
)
302+
303+
# setup
304+
params = ResourceParameters(requests.Session(), Url("https://connect.example/__api__"))
305+
permissions = Permissions(params, content_guid=content_guid)
306+
307+
# (Doesn't match any permissions, but that's okay)
308+
user_to_remove = User(params, **fake_user)
309+
group_to_remove = Group(params, **fake_group)
310+
permission_to_remove = Permission(params, **fake_permission)
311+
312+
# invoke
313+
destroyed_permission = permissions.destroy(
314+
fake_permission["principal_guid"],
315+
# Make sure duplicates are dropped
316+
fake_permission["principal_guid"],
317+
# Extract info from User, Group, Permission
318+
user_to_remove,
319+
group_to_remove,
320+
permission_to_remove,
321+
)
322+
323+
# Assert bad input value
324+
with pytest.raises(TypeError):
325+
permissions.destroy(
326+
42 # pyright: ignore[reportArgumentType]
327+
)
328+
with pytest.raises(ValueError):
329+
permissions.destroy()
330+
331+
# Assert values
332+
assert mock_permissions_get[0].call_count == 1
333+
assert mock_permissions_get[1].call_count == 0
334+
assert mock_permission_delete.call_count == 1
335+
assert len(destroyed_permission) == 1
336+
assert destroyed_permission[0] == fake_permission
337+
338+
# Invoking again is a no-op
339+
destroyed_permission = permissions.destroy(fake_permission["principal_guid"])
340+
341+
assert mock_permissions_get[0].call_count == 1
342+
assert mock_permissions_get[1].call_count == 1
343+
assert mock_permission_delete.call_count == 1
344+
assert len(destroyed_permission) == 0

0 commit comments

Comments
 (0)