Skip to content

Commit 7c1dfce

Browse files
committed
Merge branch 'main' into schloerke/345-tags
* main: feat: Add `Permissions.delete(*permissions)` method (#339) ci: Add in recent docker integration version (#347)
2 parents 389dd24 + fed8325 commit 7c1dfce

File tree

7 files changed

+266
-13
lines changed

7 files changed

+266
-13
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+
email="permission_aron@example.com",
16+
password="permission_s3cur3p@ssword",
17+
)
18+
cls.user_bill = cls.client.users.create(
19+
username="permission_bill",
20+
email="permission_bill@example.com",
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
email="aron@example.com",
@@ -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: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22

33
from __future__ import annotations
44

5-
from typing import List, overload
5+
from typing import TYPE_CHECKING, List, overload
66

77
from requests.sessions import Session as Session
88

99
from .resources import Resource, ResourceParameters, Resources
1010

11+
if TYPE_CHECKING:
12+
from .groups import Group
13+
from .users import User
14+
1115

1216
class Permission(Resource):
13-
def delete(self) -> None:
14-
"""Delete the permission."""
17+
def destroy(self) -> None:
18+
"""Destroy the permission."""
1519
path = f"v1/content/{self['content_guid']}/permissions/{self['id']}"
1620
url = self.params.url + path
1721
self.params.session.delete(url)
@@ -137,3 +141,85 @@ def get(self, uid: str) -> Permission:
137141
url = self.params.url + path
138142
response = self.params.session.get(url)
139143
return Permission(self.params, **response.json())
144+
145+
def destroy(self, *permissions: str | Group | User | Permission) -> list[Permission]:
146+
"""Remove supplied content item permissions.
147+
148+
Removes all provided permissions from the content item's permissions. If a permission isn't
149+
found, it is silently ignored.
150+
151+
Parameters
152+
----------
153+
*permissions : str | Group | User | Permission
154+
The content item permissions to remove. If a `str` is received, it is compared against
155+
the `Permissions`'s `principal_guid`. If a `Group` or `User` is received, the associated
156+
`Permission` will be removed.
157+
158+
Returns
159+
-------
160+
list[Permission]
161+
The removed permissions. If a permission is not found, there is nothing to remove and
162+
it is not included in the returned list.
163+
164+
Examples
165+
--------
166+
```python
167+
from posit import connect
168+
169+
#### User-defined inputs ####
170+
# 1. specify the guid for the content item
171+
content_guid = "CONTENT_GUID_HERE"
172+
# 2. specify either the principal_guid or group name prefix
173+
principal_guid = "USER_OR_GROUP_GUID_HERE"
174+
group_name_prefix = "GROUP_NAME_PREFIX_HERE"
175+
############################
176+
177+
client = connect.Client()
178+
179+
# Remove a single permission by principal_guid
180+
client.content.get(content_guid).permissions.destroy(principal_guid)
181+
182+
# Remove by user (if principal_guid is a user)
183+
user = client.users.get(principal_guid)
184+
client.content.get(content_guid).permissions.destroy(user)
185+
186+
# Remove by group (if principal_guid is a group)
187+
group = client.groups.get(principal_guid)
188+
client.content.get(content_guid).permissions.destroy(group)
189+
190+
# Remove all groups with a matching prefix name
191+
groups = client.groups.find(prefix=group_name_prefix)
192+
client.content.get(content_guid).permissions.destroy(*groups)
193+
194+
# Confirm new permissions
195+
client.content.get(content_guid).permissions.find()
196+
```
197+
"""
198+
from .groups import Group
199+
from .users import User
200+
201+
if len(permissions) == 0:
202+
raise ValueError("Expected at least one `permission` to remove")
203+
204+
principal_guids: set[str] = set()
205+
206+
for arg in permissions:
207+
if isinstance(arg, str):
208+
principal_guid = arg
209+
elif isinstance(arg, (Group, User)):
210+
principal_guid: str = arg["guid"]
211+
elif isinstance(arg, Permission):
212+
principal_guid: str = arg["principal_guid"]
213+
else:
214+
raise TypeError(
215+
f"destroy() expected argument type 'str', 'User', 'Group', or 'Permission' but got '{type(arg).__name__}'",
216+
)
217+
principal_guids.add(principal_guid)
218+
219+
destroyed_permissions: list[Permission] = []
220+
for permission in self.find():
221+
if permission["principal_guid"] in principal_guids:
222+
permission.destroy()
223+
destroyed_permissions.append(permission)
224+
225+
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)