Skip to content

Commit 345b001

Browse files
committed
introduced test decorator for checking delegated perms
1 parent 6c330f1 commit 345b001

File tree

8 files changed

+148
-44
lines changed

8 files changed

+148
-44
lines changed

examples/auth/with_adal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"""
77

88
from office365.graph_client import GraphClient
9-
from tests import test_tenant_name, test_client_id, test_username, test_password
9+
from tests import test_client_id, test_password, test_tenant_name, test_username
1010

1111

1212
def acquire_token():

office365/directory/groups/group.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,18 @@ def members(self):
267267
),
268268
)
269269

270+
@property
271+
def rejected_senders(self):
272+
"""
273+
The list of users or groups not allowed to create posts or calendar events in this group. Nullable
274+
"""
275+
return self.properties.get(
276+
"rejectedSenders",
277+
DirectoryObjectCollection(
278+
self.context, ResourcePath("rejectedSenders", self.resource_path)
279+
),
280+
)
281+
270282
@property
271283
def transitive_members(self):
272284
"""
@@ -425,6 +437,7 @@ def get_property(self, name, default_value=None):
425437
"groupTypes": self.group_types,
426438
"licenseProcessingState": self.license_processing_state,
427439
"permissionGrants": self.permission_grants,
440+
"rejectedSenders": self.rejected_senders,
428441
"transitiveMembers": self.transitive_members,
429442
"transitiveMemberOf": self.transitive_member_of,
430443
}

tests/booking/test_business.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,48 @@
11
from office365.booking.business.business import BookingBusiness
2+
from tests.decorators import requires_delegated_permission
23
from tests.graph_case import GraphTestCase
34

45

56
class TestBusiness(GraphTestCase):
67
business = None # type: BookingBusiness
78

9+
@requires_delegated_permission(
10+
"Bookings.Read.All",
11+
"Bookings.Manage.All",
12+
"Bookings.ReadWrite.All",
13+
"BookingsAppointment.ReadWrite.All",
14+
)
815
def test1_list_booking_business(self):
916
result = self.client.solutions.booking_businesses.get().execute_query()
1017
self.assertIsNotNone(result.resource_path)
1118

19+
@requires_delegated_permission("Bookings.Manage.All")
1220
def test2_create_booking_business(self):
1321
result = self.client.solutions.booking_businesses.add(
1422
"Fourth Coffee"
1523
).execute_query()
1624
self.assertIsNotNone(result.resource_path)
1725
self.__class__.business = result
1826

19-
# def test3_get_staff_availability(self):
27+
@requires_delegated_permission(
28+
"Bookings.Read.All",
29+
"Bookings.Manage.All",
30+
"Bookings.ReadWrite.All",
31+
"BookingsAppointment.ReadWrite.All",
32+
)
33+
def test3_ensure_created(self):
34+
result = self.__class__.business.get().execute_query_retry()
35+
self.assertIsNotNone(result.resource_path)
36+
37+
#def test4_get_staff_availability(self):
2038
# result = self.__class__.business.get_staff_availability().execute_query()
2139
# self.assertIsNotNone(result.resource_path)
2240

23-
def test3_get(self):
24-
result = self.__class__.business.get().execute_query()
25-
self.assertIsNotNone(result.resource_path)
26-
27-
def test4_publish(self):
41+
@requires_delegated_permission("Bookings.Manage.All")
42+
def test5_publish(self):
2843
result = self.__class__.business.publish().execute_query()
2944
self.assertIsNotNone(result.resource_path)
3045

31-
def test5_delete_booking_business(self):
46+
@requires_delegated_permission("Bookings.Manage.All")
47+
def test6_delete_booking_business(self):
3248
self.__class__.business.delete_object().execute_query()

tests/decorators.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from functools import lru_cache, wraps
2+
from typing import Any, Callable, TypeVar
3+
from unittest import TestCase
4+
5+
from office365.directory.applications.roles.collection import AppRoleCollection
6+
from office365.graph_client import GraphClient
7+
from office365.runtime.types.collections import StringCollection
8+
from tests import test_client_id
9+
10+
T = TypeVar("T", bound=Callable[..., Any])
11+
12+
13+
@lru_cache(maxsize=1)
14+
def _get_cached_permissions(client, client_id):
15+
# type: (GraphClient, str) -> AppRoleCollection
16+
"""Get and cache application permissions for a client"""
17+
resource = client.service_principals.get_by_name("Microsoft Graph")
18+
result = resource.get_application_permissions(client_id).execute_query()
19+
return result.value
20+
21+
22+
def requires_app_permission(app_role):
23+
# type: (str) -> Callable[[T], T]
24+
def decorator(test_method):
25+
# type: (T) -> T
26+
@wraps(test_method)
27+
def wrapper(self, *args, **kwargs):
28+
# type: (TestCase, *Any, **Any) -> Any
29+
client = getattr(self, "client", None)
30+
if not client:
31+
self.skipTest("No client available for permission check")
32+
33+
try:
34+
permissions = _get_cached_permissions(client, test_client_id)
35+
36+
if not any(role.value == app_role for role in permissions):
37+
self.skipTest(f"Required app permission '{app_role}' not granted")
38+
39+
return test_method(self, *args, **kwargs)
40+
41+
except Exception as e:
42+
self.skipTest(f"Permission check failed: {str(e)}")
43+
44+
return wrapper
45+
46+
return decorator
47+
48+
49+
@lru_cache(maxsize=1)
50+
def _get_cached_delegated_permissions(client, client_id):
51+
# type: (GraphClient, str) -> StringCollection
52+
"""Get and cache delegated permissions for a client"""
53+
resource = client.service_principals.get_by_name("Microsoft Graph")
54+
result = resource.get_delegated_permissions(client_id).execute_query()
55+
return result.value
56+
57+
58+
def requires_delegated_permission(*scopes):
59+
# type: (*str) -> Callable[[T], T]
60+
"""Decorator to verify delegated permissions before test execution"""
61+
62+
def decorator(test_method):
63+
# type: (T) -> T
64+
@wraps(test_method)
65+
def wrapper(self, *args, **kwargs):
66+
# type: (TestCase, *Any, **Any) -> Any
67+
client = getattr(self, "client", None)
68+
if not client:
69+
self.skipTest("No client available for permission check")
70+
71+
try:
72+
# Get permissions from cache or API
73+
granted_scopes = _get_cached_delegated_permissions(client, test_client_id)
74+
75+
if not any(scope in granted_scopes for scope in scopes):
76+
self.skipTest(
77+
f"Required delegated permission '{', '.join(scopes)}' not granted"
78+
)
79+
80+
return test_method(self, *args, **kwargs)
81+
82+
except Exception as e:
83+
self.skipTest(f"Permission check failed: {str(e)}")
84+
85+
return wrapper # type: ignore[return-value]
86+
87+
return decorator

tests/outlook/test_calendar.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from office365.outlook.calendar.calendar import Calendar
55
from office365.outlook.calendar.email_address import EmailAddress
66
from tests import create_unique_name, test_user_principal_name
7+
from tests.decorators import requires_delegated_permission
78
from tests.graph_case import GraphTestCase
89

910

@@ -21,10 +22,19 @@ def setUpClass(cls):
2122
def tearDownClass(cls):
2223
pass
2324

25+
@requires_delegated_permission(
26+
"Calendars.Read.Shared", "Calendars.ReadWrite.Shared"
27+
)
2428
def test1_find_my_meeting_times(self):
2529
result = self.client.me.find_meeting_times().execute_query()
2630
self.assertIsNotNone(result.value.meetingTimeSuggestions)
2731

32+
@requires_delegated_permission(
33+
"Calendars.ReadBasic",
34+
"Calendars.Read",
35+
"Calendars.ReadWrite",
36+
"Calendars.ReadWrite.Shared",
37+
)
2838
def test2_get_my_schedule(self):
2939
end_time = datetime.utcnow()
3040
start_time = end_time - timedelta(days=7)
@@ -35,10 +45,22 @@ def test2_get_my_schedule(self):
3545
).execute_query()
3646
self.assertIsNotNone(result.value)
3747

48+
@requires_delegated_permission(
49+
"Calendars.ReadBasic",
50+
"Calendars.ReadWrite",
51+
"Calendars.Read",
52+
"Calendars.ReadWrite.Shared",
53+
)
3854
def test3_list_my_cal_groups(self):
3955
cal_groups = self.client.me.calendar_groups.get().execute_query()
4056
self.assertIsNotNone(cal_groups.resource_path)
4157

58+
@requires_delegated_permission(
59+
"Calendars.ReadBasic",
60+
"Calendars.ReadWrite",
61+
"Calendars.Read",
62+
"Calendars.ReadWrite.Shared",
63+
)
4264
def test4_list_my_cal_permissions(self):
4365
result = self.client.me.calendar.calendar_permissions.get().execute_query()
4466
self.assertIsNotNone(result.resource_path)

tests/outlook/test_rooms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from office365.graph_client import GraphClient
44
from tests import test_client_id, test_client_secret, test_tenant
5-
from tests.test_decorators import requires_app_permission
5+
from tests.decorators import requires_app_permission
66

77

88
class TestRooms(TestCase):

tests/security/test_security.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from office365.graph_client import GraphClient
44
from tests import test_client_id, test_client_secret, test_tenant
5-
from tests.test_decorators import requires_app_permission
5+
from tests.decorators import requires_app_permission
66

77

88
class TestSecurity(TestCase):

tests/test_decorators.py

Lines changed: 0 additions & 34 deletions
This file was deleted.

0 commit comments

Comments
 (0)