From d254f9a67c1b6afa077e2cc464c9108adff4ce32 Mon Sep 17 00:00:00 2001 From: vgrem Date: Sun, 5 Sep 2021 15:07:42 +0300 Subject: [PATCH] SharePoint API: get_user_effective_permissions method fix and example (#407) File and Folder properties --- .../files/print_file_permissions.py | 19 +++ .../contenttypes/content_type_collection.py | 4 +- office365/sharepoint/files/file.py | 108 +++++++++++++++++- .../sharepoint/files/file_version_event.py | 6 + office365/sharepoint/folders/folder.py | 13 ++- office365/sharepoint/forms/form.py | 18 +++ office365/sharepoint/forms/form_collection.py | 9 +- office365/sharepoint/lists/list.py | 35 +++--- .../permissions/base_permissions.py | 3 +- .../information_rights_management_settings.py | 1 + .../permissions/securable_object.py | 23 ++-- office365/sharepoint/webs/web.py | 51 +++++++-- 12 files changed, 246 insertions(+), 44 deletions(-) create mode 100644 examples/sharepoint/files/print_file_permissions.py create mode 100644 office365/sharepoint/files/file_version_event.py diff --git a/examples/sharepoint/files/print_file_permissions.py b/examples/sharepoint/files/print_file_permissions.py new file mode 100644 index 00000000..aaf2aba3 --- /dev/null +++ b/examples/sharepoint/files/print_file_permissions.py @@ -0,0 +1,19 @@ +from pprint import pprint + +from office365.sharepoint.client_context import ClientContext +from office365.sharepoint.permissions.permission_kind import PermissionKind +from tests import test_team_site_url, test_user_principal_name_alt, test_user_credentials + +client = ClientContext(test_team_site_url).with_credentials(test_user_credentials) +file_url = "/sites/team/Shared Documents/report #123.csv" + +# user = client.web.site_users.get_by_email(test_user_principal_name_alt).get().execute_query() +target_user = client.web.site_users.get_by_email(test_user_principal_name_alt) +target_file = client.web.get_file_by_server_relative_path(file_url) +result = target_file.listItemAllFields.get_user_effective_permissions(target_user).execute_query() +pprint(result.value.permission_levels) # print all permission levels + +# verify whether user has Reader role to a file +if result.value.has(PermissionKind.OpenItems): + print("User has access to read a file") + diff --git a/office365/sharepoint/contenttypes/content_type_collection.py b/office365/sharepoint/contenttypes/content_type_collection.py index 3c65bb37..cd86bf3e 100644 --- a/office365/sharepoint/contenttypes/content_type_collection.py +++ b/office365/sharepoint/contenttypes/content_type_collection.py @@ -52,11 +52,11 @@ def add_available_content_type(self, contentTypeId): self.context.add_query(qry) return ct - def add_existing_content_type(self, contentType): + def add_existing_content_type(self, content_type): """Adds an existing content type to the collection. The name of the given content type MUST NOT be the same as any of the content types in the collection. A reference to the SP.ContentType that was added is returned. - :param ContentType contentType: Specifies the content type to be added to the collection + :param ContentType content_type: Specifies the content type to be added to the collection """ pass diff --git a/office365/sharepoint/files/file.py b/office365/sharepoint/files/file.py index 32ecbffe..e3df2f10 100644 --- a/office365/sharepoint/files/file.py +++ b/office365/sharepoint/files/file.py @@ -4,11 +4,14 @@ from office365.runtime.queries.service_operation_query import ServiceOperationQuery from office365.runtime.resource_path import ResourcePath from office365.runtime.resource_path_service_operation import ResourcePathServiceOperation +from office365.sharepoint.base_entity_collection import BaseEntityCollection +from office365.sharepoint.files.file_version_event import FileVersionEvent from office365.sharepoint.internal.download_file import create_download_file_query from office365.sharepoint.base_entity import BaseEntity from office365.sharepoint.directory.user import User from office365.sharepoint.files.file_version_collection import FileVersionCollection from office365.sharepoint.listitems.listitem import ListItem +from office365.sharepoint.permissions.information_rights_management_settings import InformationRightsManagementSettings from office365.sharepoint.webparts.limited_webpart_manager import LimitedWebPartManager from office365.sharepoint.types.resource_path import ResourcePath as SPResPath @@ -48,6 +51,38 @@ def from_url(abs_url): file = ctx.web.get_file_by_server_relative_url(file_relative_url) return file + def get_image_preview_uri(self, width, height, client_type=None): + """ + :param int width: + :param int height: + :param str client_type: + """ + result = ClientResult(self.context) + payload = { + "width": width, + "height": height, + "clientType": client_type + } + qry = ServiceOperationQuery(self, "GetImagePreviewUri", None, payload, None, result) + self.context.add_query(qry) + return result + + def get_image_preview_url(self, width, height, client_type=None): + """ + :param int width: + :param int height: + :param str client_type: + """ + result = ClientResult(self.context) + payload = { + "width": width, + "height": height, + "clientType": client_type + } + qry = ServiceOperationQuery(self, "GetImagePreviewUrl", None, payload, None, result) + self.context.add_query(qry) + return result + def recycle(self): """Moves the file to the Recycle Bin and returns the identifier of the new Recycle Bin item.""" @@ -201,6 +236,51 @@ def get_limited_webpart_manager(self, scope): self.resource_path )) + def open_binary_stream(self): + """Opens the file as a stream.""" + return_stream = ClientResult(self.context) + qry = ServiceOperationQuery(self, "OpenBinaryStream", None, None, None, return_stream) + self.context.add_query(qry) + return return_stream + + def save_binary_stream(self, stream): + """Saves the file.""" + qry = ServiceOperationQuery(self, "SaveBinaryStream", None, {"file": stream}) + self.context.add_query(qry) + return self + + def get_upload_status(self, upload_id): + payload = { + "uploadId": upload_id, + } + qry = ServiceOperationQuery(self, "GetUploadStatus", None, payload) + self.context.add_query(qry) + return self + + def upload_with_checksum(self, upload_id, checksum, stream): + """ + :param str upload_id: + :param str checksum: + :param bytes stream: + """ + return_type = File(self.context) + payload = { + "uploadId": upload_id, + "checksum": checksum, + "stream": stream + } + qry = ServiceOperationQuery(self, "UploadWithChecksum", None, payload, None, return_type) + self.context.add_query(qry) + return return_type + + def cancel_upload(self, upload_id): + payload = { + "uploadId": upload_id, + } + qry = ServiceOperationQuery(self, "CancelUpload", None, payload) + self.context.add_query(qry) + return self + def start_upload(self, upload_id, content): """Starts a new chunk upload session and uploads the first fragment. @@ -324,6 +404,7 @@ def _construct_download_request(request): """ request.stream = True request.method = HttpMethod.Get + self.context.before_execute(_construct_download_request) def _process_download_response(response): @@ -337,6 +418,7 @@ def _process_download_response(response): if callable(chunk_downloaded): chunk_downloaded(bytes_read) file_object.write(chunk) + self.context.after_execute(_process_download_response) self.context.add_query(qry) @@ -344,6 +426,27 @@ def _process_download_response(response): self.ensure_property("ServerRelativeUrl", _download_as_stream) return self + @property + def checked_out_by_user(self): + """Gets an object that represents the user who has checked out the file.""" + return self.properties.get('CheckedOutByUser', + User(self.context, ResourcePath("CheckedOutByUser", self.resource_path))) + + @property + def version_events(self): + return self.properties.get("VersionEvents", + BaseEntityCollection(self.context, + FileVersionEvent, + ResourcePath("VersionEvents", self.resource_path))) + + @property + def information_rights_management_settings(self): + return self.properties.get('InformationRightsManagementSettings', + InformationRightsManagementSettings(self.context, + ResourcePath( + "InformationRightsManagementSettings", + self.resource_path))) + @property def listItemAllFields(self): """Gets a value that specifies the list item fields values for the list item corresponding to the file.""" @@ -387,7 +490,7 @@ def server_relative_path(self): """Gets the server-relative Path of the list folder. :rtype: SPResPath or None """ - return self.properties.get("ServerRelativePath", SPResPath(None)) + return self.properties.get("ServerRelativePath", SPResPath()) @property def length(self): @@ -482,6 +585,9 @@ def unique_id(self): def get_property(self, name, default_value=None): if default_value is None: property_mapping = { + "CheckedOutByUser": self.checked_out_by_user, + "VersionEvents": self.version_events, + "InformationRightsManagementSettings": self.information_rights_management_settings, "LockedByUser": self.locked_by_user, "ModifiedBy": self.modified_by } diff --git a/office365/sharepoint/files/file_version_event.py b/office365/sharepoint/files/file_version_event.py new file mode 100644 index 00000000..60620d00 --- /dev/null +++ b/office365/sharepoint/files/file_version_event.py @@ -0,0 +1,6 @@ +from office365.sharepoint.base_entity import BaseEntity + + +class FileVersionEvent(BaseEntity): + """""" + pass diff --git a/office365/sharepoint/folders/folder.py b/office365/sharepoint/folders/folder.py index 52994f02..4500cf6c 100644 --- a/office365/sharepoint/folders/folder.py +++ b/office365/sharepoint/folders/folder.py @@ -189,6 +189,7 @@ def _move_folder(): @property def storage_metrics(self): + """""" return self.properties.get("StorageMetrics", StorageMetrics(self.context, ResourcePath("StorageMetrics", self.resource_path))) @@ -227,6 +228,15 @@ def name(self): """ return self.properties.get("Name", None) + @property + def is_wopi_enabled(self): + return self.properties.get("IsWOPIEnabled", None) + + @property + def prog_id(self): + """Gets the identifier (ID) of the application in which the folder was created.""" + return self.properties.get("ProgID", None) + @property def unique_id(self): """Gets the unique ID of the folder. @@ -293,7 +303,8 @@ def get_property(self, name, default_value=None): "UniqueContentTypeOrder": self.unique_content_type_order, "ListItemAllFields": self.list_item_all_fields, "ParentFolder": self.parent_folder, - "ServerRelativePath": self.server_relative_path + "ServerRelativePath": self.server_relative_path, + "StorageMetrics": self.storage_metrics } default_value = property_mapping.get(name, None) return super(Folder, self).get_property(name, default_value) diff --git a/office365/sharepoint/forms/form.py b/office365/sharepoint/forms/form.py index 2ebb5339..c7825e6b 100644 --- a/office365/sharepoint/forms/form.py +++ b/office365/sharepoint/forms/form.py @@ -3,3 +3,21 @@ class Form(BaseEntity): """A form provides a display and editing interface for a single list item.""" + + @property + def form_type(self): + """ + Gets the type of the form. + + :rtype: str or None + """ + return self.properties.get("FormType", None) + + @property + def server_relative_url(self): + """ + Gets the server-relative URL of the form. + + :rtype: str or None + """ + return self.properties.get("ServerRelativeUrl", None) diff --git a/office365/sharepoint/forms/form_collection.py b/office365/sharepoint/forms/form_collection.py index beefafc0..b0adf11c 100644 --- a/office365/sharepoint/forms/form_collection.py +++ b/office365/sharepoint/forms/form_collection.py @@ -1,3 +1,4 @@ +from office365.runtime.resource_path_service_operation import ResourcePathServiceOperation from office365.sharepoint.base_entity_collection import BaseEntityCollection from office365.sharepoint.forms.form import Form @@ -7,5 +8,9 @@ class FormCollection(BaseEntityCollection): def __init__(self, context, resource_path=None): super(FormCollection, self).__init__(context, Form, resource_path) - def get_by_page_type(self): - pass + def get_by_id(self, _id): + """Gets the form with the specified ID.""" + return Form(self.context, ResourcePathServiceOperation("GetById", [_id], self.resource_path)) + + def get_by_page_type(self, form_type): + return Form(self.context, ResourcePathServiceOperation("GetByPageType", [form_type], self.resource_path)) diff --git a/office365/sharepoint/lists/list.py b/office365/sharepoint/lists/list.py index 0835b3ec..4c3accc0 100644 --- a/office365/sharepoint/lists/list.py +++ b/office365/sharepoint/lists/list.py @@ -176,33 +176,33 @@ def get_list_item_changes_since_token(self, query): self.context.add_query(qry) return result - def save_as_template(self, fileName, name, description, saveData): + def save_as_template(self, file_name, name, description, save_data): """ Saves the list as a template in the list template gallery and includes the option of saving with or without the data that is contained in the current list. - :param bool saveData: true to save the data of the original list along with the list template; otherwise, false. + :param bool save_data: true to save the data of the original list along with the list template; otherwise, false. :param str description: A string that contains the description for the list template. :param str name: A string that contains the title for the list template. - :param str fileName: A string that contains the file name for the list template with an .stp extension. + :param str file_name: A string that contains the file name for the list template with an .stp extension. :return: """ payload = { - "strFileName": fileName, + "strFileName": file_name, "strName": name, "strDescription": description, - "bSaveData": saveData + "bSaveData": save_data } qry = ServiceOperationQuery(self, "saveAsTemplate", None, payload, None, None) self.context.add_query(qry) return self - def get_item_by_unique_id(self, uniqueId): + def get_item_by_unique_id(self, unique_id): """ Returns the list item with the specified ID. - :param str uniqueId:""" - item = ListItem(self.context, ResourcePathServiceOperation("getItemByUniqueId", [uniqueId], self.resource_path)) + :param str unique_id:""" + item = ListItem(self.context, ResourcePathServiceOperation("getItemByUniqueId", [unique_id], self.resource_path)) return item def get_web_dav_url(self, source_url): @@ -478,16 +478,15 @@ def parent_web_path(self): return self.properties.get('ParentWebPath', None) def get_property(self, name, default_value=None): - if name == "UserCustomActions": - default_value = self.user_custom_actions - elif name == "ParentWeb": - default_value = self.parent_web - elif name == "RootFolder": - default_value = self.root_folder - elif name == "ContentTypes": - default_value = self.content_types - elif name == "DefaultView": - default_value = self.default_view + if default_value is None: + property_mapping = { + "ContentTypes": self.content_types, + "DefaultView": self.default_view, + "ParentWeb": self.parent_web, + "RootFolder": self.root_folder, + "UserCustomActions": self.user_custom_actions + } + default_value = property_mapping.get(name, None) return super(List, self).get_property(name, default_value) def set_property(self, name, value, persist_changes=True): diff --git a/office365/sharepoint/permissions/base_permissions.py b/office365/sharepoint/permissions/base_permissions.py index 80b5a0fd..8c7fabeb 100644 --- a/office365/sharepoint/permissions/base_permissions.py +++ b/office365/sharepoint/permissions/base_permissions.py @@ -35,7 +35,6 @@ def set(self, perm): def has(self, perm): """Determines whether the current instance has the specified permission. - """ if perm == PermissionKind.EmptyMask: return True @@ -56,7 +55,7 @@ def clear_all(self): self.Low = 0 self.High = 0 - def to_json(self): + def to_json(self, json_format=None): return {'Low': str(self.High), 'High': str(self.Low)} @property diff --git a/office365/sharepoint/permissions/information_rights_management_settings.py b/office365/sharepoint/permissions/information_rights_management_settings.py index a9a2307d..f4474748 100644 --- a/office365/sharepoint/permissions/information_rights_management_settings.py +++ b/office365/sharepoint/permissions/information_rights_management_settings.py @@ -2,6 +2,7 @@ class InformationRightsManagementSettings(BaseEntity): + """Represents the Information Rights Management (IRM) settings of a list in Microsoft SharePoint Foundation.""" @property def policy_title(self): diff --git a/office365/sharepoint/permissions/securable_object.py b/office365/sharepoint/permissions/securable_object.py index c8b3c903..398df009 100644 --- a/office365/sharepoint/permissions/securable_object.py +++ b/office365/sharepoint/permissions/securable_object.py @@ -5,6 +5,7 @@ from office365.sharepoint.permissions.base_permissions import BasePermissions from office365.sharepoint.permissions.role_assignment import RoleAssignment from office365.sharepoint.permissions.roleAssignmentCollection import RoleAssignmentCollection +from office365.sharepoint.principal.user import User class SecurableObject(BaseEntity): @@ -14,7 +15,7 @@ def get_role_assignment(self, principal): """ :param office365.sharepoint.principal.principal.Principal principal: Specifies the user or group of the - role assignment. + role assignment. :return: RoleAssignment """ @@ -83,26 +84,34 @@ def break_role_inheritance(self, copy_role_assignments=True, clear_sub_scopes=Tr "copyRoleAssignments": copy_role_assignments, "clearSubscopes": clear_sub_scopes } - qry = ServiceOperationQuery(self, "breakRoleInheritance", None, payload, None, None) + qry = ServiceOperationQuery(self, "BreakRoleInheritance", None, payload, None, None) self.context.add_query(qry) return self def reset_role_inheritance(self): """Resets the role inheritance for the securable object and inherits role assignments from the parent securable object.""" - qry = ServiceOperationQuery(self, "resetRoleInheritance", None, None, None, None) + qry = ServiceOperationQuery(self, "ResetRoleInheritance", None, None, None, None) self.context.add_query(qry) return self - def get_user_effective_permissions(self, user_name): + def get_user_effective_permissions(self, user_or_name): """ Returns the user permissions for this list. - :param str user_name: Specifies the user login name. + :param str or User user_or_name: Specifies the user login name or User object. """ result = ClientResult(self.context, BasePermissions()) - qry = ServiceOperationQuery(self, "getUserEffectivePermissions", [user_name], None, None, result) - self.context.add_query(qry) + + if isinstance(user_or_name, User): + def _user_loaded(): + next_qry = ServiceOperationQuery(self, "GetUserEffectivePermissions", [user_or_name.login_name], + None, None, result) + self.context.add_query(next_qry) + user_or_name.ensure_property("LoginName", _user_loaded) + else: + qry = ServiceOperationQuery(self, "GetUserEffectivePermissions", [user_or_name], None, None, result) + self.context.add_query(qry) return result @property diff --git a/office365/sharepoint/webs/web.py b/office365/sharepoint/webs/web.py index b4cdd6ca..65924def 100644 --- a/office365/sharepoint/webs/web.py +++ b/office365/sharepoint/webs/web.py @@ -61,6 +61,13 @@ def __init__(self, context, resource_path=None): super(Web, self).__init__(context, resource_path) self._web_url = None + def get_push_notification_subscriber(self, device_app_instance_id): + return_type = PushNotificationSubscriber(self.context) + qry = ServiceOperationQuery(self, "GetPushNotificationSubscriber", [device_app_instance_id], None, + None, return_type) + self.context.add_query(qry) + return return_type + def get_push_notification_subscribers_by_user(self, user_or_username): """ @@ -369,7 +376,7 @@ def ensure_user(self, login_name): """ target_user = User(self.context) self.site_users.add_child(target_user) - qry = ServiceOperationQuery(self, "ensureUser", [login_name], None, None, target_user) + qry = ServiceOperationQuery(self, "EnsureUser", [login_name], None, None, target_user) self.context.add_query(qry) return target_user @@ -388,7 +395,7 @@ def does_user_have_permissions(self, permission_mask): :type permission_mask: BasePermissions """ result = ClientResult(self.context) - qry = ServiceOperationQuery(self, "doesUserHavePermissions", permission_mask, None, None, result) + qry = ServiceOperationQuery(self, "DoesUserHavePermissions", permission_mask, None, None, result) self.context.add_query(qry) return result @@ -398,7 +405,7 @@ def get_folder_by_id(self, unique_id): :type unique_id: str """ folder = Folder(self.context) - qry = ServiceOperationQuery(self, "getFolderById", [unique_id], None, None, folder) + qry = ServiceOperationQuery(self, "GetFolderById", [unique_id], None, None, folder) self.context.add_query(qry) return folder @@ -484,6 +491,15 @@ def get_file_by_guest_url(self, guest_url): self.context.add_query(qry) return return_type + def get_file_by_wopi_frame_url(self, wopi_frame_url): + """ + :param str wopi_frame_url: + """ + return_type = File(self.context) + qry = ServiceOperationQuery(self, "GetFileByWOPIFrameUrl", [wopi_frame_url], None, None, return_type) + self.context.add_query(qry) + return return_type + def get_folder_by_guest_url(self, guest_url): """ :type guest_url: str @@ -615,8 +631,8 @@ def get_sharing_link_kind(context, file_url): return result @staticmethod - def share_object(context, url, peoplePickerInput, - roleValue=None, + def share_object(context, url, people_picker_input, + role_value=None, groupId=0, propagateAcl=False, sendEmail=True, includeAnonymousLinkInEmail=False, emailSubject=None, emailBody=None, useSimplifiedRoles=True): @@ -627,8 +643,8 @@ def share_object(context, url, peoplePickerInput, :param office365.sharepoint.client_context.ClientContext context: SharePoint context :param str url: The URL of the website with the path of an object in SharePoint query string parameters. - :param str roleValue: The sharing role value for the type of permission to grant on the object. - :param str peoplePickerInput: A string of JSON representing users in people picker format. + :param str role_value: The sharing role value for the type of permission to grant on the object. + :param str people_picker_input: A string of JSON representing users in people picker format. :param int groupId: The ID of the group to be added. Zero if not adding to a permissions group. :param bool propagateAcl: A flag to determine if permissions SHOULD be pushed to items with unique permissions. :param bool sendEmail: A flag to determine if an email notification SHOULD be sent (if email is configured). @@ -644,8 +660,8 @@ def share_object(context, url, peoplePickerInput, payload = { "url": url, "groupId": groupId, - "peoplePickerInput": peoplePickerInput, - "roleValue": roleValue, + "peoplePickerInput": people_picker_input, + "roleValue": role_value, "includeAnonymousLinkInEmail": includeAnonymousLinkInEmail, "propagateAcl": propagateAcl, "sendEmail": sendEmail, @@ -694,8 +710,7 @@ def get_list_item(self, str_url): for example, "/sites/MySite/Shared Documents/MyDocument.docx". :return: ListItem """ - return_item = ListItem(self.context, ResourcePathServiceOperation("GetListItem", [str_url], self.resource_path)) - return return_item + return ListItem(self.context, ResourcePathServiceOperation("GetListItem", [str_url], self.resource_path)) def get_catalog(self, type_catalog): """Gets the list template gallery, site template gallery, or Web Part gallery for the Web site. @@ -704,6 +719,20 @@ def get_catalog(self, type_catalog): """ return List(self.context, ResourcePathServiceOperation("getCatalog", [type_catalog], self.resource_path)) + def page_context_info(self, include_odb_settings, emit_navigation_info): + """ + :param bool include_odb_settings: + :param bool emit_navigation_info: + """ + return_type = ClientResult(self.context) + payload = { + "includeODBSettings": include_odb_settings, + "emitNavigationInfo": emit_navigation_info + } + qry = ServiceOperationQuery(self, "PageContextInfo", None, payload, None, return_type) + self.context.add_query(qry) + return return_type + @property def allow_rss_feeds(self): """Gets a Boolean value that specifies whether the site collection allows RSS feeds.