From 1268e525c8722112a171627ab31114e2a4d241e7 Mon Sep 17 00:00:00 2001 From: vgrem Date: Sat, 8 Jul 2023 12:36:27 +0300 Subject: [PATCH] Examples & documentation updates, fix for SiteProperties type --- README.md | 7 +- examples/__init__.py | 10 -- examples/onenote/create_page.py | 6 +- .../connect_with_app_only_principal.py | 22 ++++- .../connect_with_client_certificate.py | 7 +- .../sharepoint/tenant/allow_custom_script.py | 19 +++- generator/import_metadata.py | 6 +- generator/metadata/MicrosoftGraph.xml | 99 ++++++++++++++++++- office365/runtime/auth/client_credential.py | 4 +- .../paths/entity.py => runtime/paths/key.py} | 8 +- office365/sharepoint/client_context.py | 17 +++- .../sharepoint/files/checked_out_file.py | 5 +- .../sharepoint/files/versions/version.py | 16 ++- office365/sharepoint/folders/collection.py | 4 +- office365/sharepoint/listitems/listitem.py | 4 +- .../deny_add_and_customize_pages_status.py | 3 + .../tenant/administration/sites/properties.py | 22 +++-- 17 files changed, 203 insertions(+), 56 deletions(-) rename office365/{sharepoint/internal/paths/entity.py => runtime/paths/key.py} (58%) diff --git a/README.md b/README.md index f03db3eb..617e704b 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,11 @@ pip install Office365-REST-Python-Client >pip install git+https://github.com/vgrem/Office365-REST-Python-Client.git >``` -# Authentication Credentials + +# Working with SharePoint API + + +## Authentication For the following examples, relevant credentials can be found in the Azure Portal. Steps to access: @@ -44,7 +48,6 @@ Steps to access: 5. In the application's "Overview" page, the client id can be found under "Application (client) id" 6. In the application's "Certificates & Secrets" page, the client secret can be found under the "Value" of the "Client Secrets." If there is no client secret yet, create one here. -# Working with SharePoint API The list of supported API versions: - [SharePoint 2013 REST API](https://msdn.microsoft.com/en-us/library/office/jj860569.aspx) and above diff --git a/examples/__init__.py b/examples/__init__.py index 3cbd4bbb..77311cff 100644 --- a/examples/__init__.py +++ b/examples/__init__.py @@ -20,16 +20,6 @@ sample_password = settings.get('user_credentials', "password") -def acquire_token_by_client_credentials(): - authority_url = 'https://login.microsoftonline.com/{0}'.format(sample_tenant_name) - app = msal.ConfidentialClientApplication( - authority=authority_url, - client_id=sample_client_id, - client_credential=settings.get('client_credentials', 'client_secret') - ) - return app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"]) - - def acquire_token_by_username_password(): authority_url = 'https://login.microsoftonline.com/{0}'.format(sample_tenant_name) app = msal.PublicClientApplication( diff --git a/examples/onenote/create_page.py b/examples/onenote/create_page.py index 5fbe9c93..fb27c5ee 100644 --- a/examples/onenote/create_page.py +++ b/examples/onenote/create_page.py @@ -1,5 +1,9 @@ -from examples import acquire_token_by_username_password +""" + +""" + from office365.graph_client import GraphClient +from tests.graph_case import acquire_token_by_username_password client = GraphClient(acquire_token_by_username_password) diff --git a/examples/sharepoint/connect_with_app_only_principal.py b/examples/sharepoint/connect_with_app_only_principal.py index 6efaf8c6..3113c9ee 100644 --- a/examples/sharepoint/connect_with_app_only_principal.py +++ b/examples/sharepoint/connect_with_app_only_principal.py @@ -1,11 +1,25 @@ """ -Example: SharePoint App-Only auth flow +There are two approaches for doing app-only for SharePoint: + + - Using an Azure AD application: this is the preferred method when using SharePoint Online because you can also + grant permissions to other Office 365 services (if needed) + you’ve a user interface (Azure portal) to maintain + your app principals. + + - Using a SharePoint App-Only principal: this method is older and only works for SharePoint access, + but is still relevant. This method is also the recommended model when you’re still working in SharePoint + on-premises since this model works in both SharePoint on-premises as SharePoint Online. + +Important: + Please safeguard the created client id/secret combination as would it be your administrator account. + Using this client id/secret one can read/update all data in your SharePoint Online environment! + +The example demonstrates how to use SharePoint App-Only principal (second option) + https://learn.microsoft.com/en-us/sharepoint/dev/solution-guidance/security-apponly-azureacs """ -from examples import sample_site_url, sample_client_id, sample_client_secret -from office365.runtime.auth.client_credential import ClientCredential from office365.sharepoint.client_context import ClientContext +from tests import test_site_url, test_client_id, test_client_secret -ctx = ClientContext(sample_site_url).with_credentials(ClientCredential(sample_client_id, sample_client_secret)) +ctx = ClientContext(test_site_url).with_client_credentials(test_client_id, test_client_secret) target_web = ctx.web.get().execute_query() print(target_web.url) diff --git a/examples/sharepoint/connect_with_client_certificate.py b/examples/sharepoint/connect_with_client_certificate.py index d0b5c938..22201f0b 100644 --- a/examples/sharepoint/connect_with_client_certificate.py +++ b/examples/sharepoint/connect_with_client_certificate.py @@ -1,5 +1,10 @@ """ -Example: Azure AD App-Only auth flow +When using SharePoint Online you can define applications in Azure AD and these applications can +be granted permissions to SharePoint, but also to all the other services in Office 365. +This model is the preferred model in case you're using SharePoint Online, if you're using SharePoint on-premises +you have to use the SharePoint Only model via based Azure ACS as described in here: + +Demonstrates how to use Azure AD App-Only auth flow https://learn.microsoft.com/en-us/sharepoint/dev/solution-guidance/security-apponly-azuread """ diff --git a/examples/sharepoint/tenant/allow_custom_script.py b/examples/sharepoint/tenant/allow_custom_script.py index 34ec13bc..2256f2cf 100644 --- a/examples/sharepoint/tenant/allow_custom_script.py +++ b/examples/sharepoint/tenant/allow_custom_script.py @@ -1,12 +1,25 @@ +""" +Allow or prevent custom script + + +Demonstrates how to determine whether custom script on SharePoint site is enabled and enable it if disabled + +https://learn.microsoft.com/en-us/sharepoint/allow-or-prevent-custom-script +""" + from office365.sharepoint.client_context import ClientContext +from office365.sharepoint.tenant.administration.deny_add_and_customize_pages_status import \ + DenyAddAndCustomizePagesStatus from tests import test_admin_site_url, test_admin_credentials, test_team_site_url client = ClientContext(test_admin_site_url).with_credentials(test_admin_credentials) site_props = client.tenant.get_site_properties_by_url(test_team_site_url, True).execute_query() -if site_props.deny_add_and_customize_pages: +if site_props.deny_add_and_customize_pages == DenyAddAndCustomizePagesStatus.Disabled: print("Enabling custom script on site: {0}...".format(test_team_site_url)) - site_props.deny_add_and_customize_pages = False + site_props.deny_add_and_customize_pages = DenyAddAndCustomizePagesStatus.Enabled site_props.update().execute_query() print("Done.") +elif site_props.deny_add_and_customize_pages == DenyAddAndCustomizePagesStatus.Enabled: + print("Skipping. Custom script has already been allowed on site: {0}".format(test_team_site_url)) else: - print("Custom script has already been allowed on site: {0}".format(test_team_site_url)) + print("Unknown status detected") diff --git a/generator/import_metadata.py b/generator/import_metadata.py index 611fd39d..6ade2f5e 100644 --- a/generator/import_metadata.py +++ b/generator/import_metadata.py @@ -1,10 +1,10 @@ from xml.dom import minidom from argparse import ArgumentParser -from examples import acquire_token_by_client_credentials from office365.graph_client import GraphClient from office365.sharepoint.client_context import ClientContext from tests import test_site_url, test_client_credentials +from tests.graph_case import acquire_token_by_client_credentials def export_to_file(path, content): @@ -15,9 +15,9 @@ def export_to_file(path, content): parser = ArgumentParser() parser.add_argument("-e", "--endpoint", dest="endpoint", - help="Import metadata endpoint", default="sharepoint") + help="Import metadata endpoint", default="microsoftgraph") parser.add_argument("-p", "--path", - dest="path", default="./metadata/SharePoint.xml", + dest="path", default="./metadata/MicrosoftGraph.xml", help="Import metadata endpoint") args = parser.parse_args() diff --git a/generator/metadata/MicrosoftGraph.xml b/generator/metadata/MicrosoftGraph.xml index adc5bdc5..79ef53fd 100644 --- a/generator/metadata/MicrosoftGraph.xml +++ b/generator/metadata/MicrosoftGraph.xml @@ -20276,6 +20276,10 @@ + + + + @@ -25218,6 +25222,7 @@ + @@ -25269,6 +25274,9 @@ + + + @@ -25280,6 +25288,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -25677,6 +25718,21 @@ + + + + + + + + + + + + + + + @@ -36154,6 +36210,19 @@ + + + + + + + + + + + + + @@ -36467,6 +36536,11 @@ + + + + + @@ -36607,9 +36681,18 @@ + + + + + + + + + + - @@ -37021,6 +37104,20 @@ + + + + + + + + + + + + + + diff --git a/office365/runtime/auth/client_credential.py b/office365/runtime/auth/client_credential.py index 9bfdf50d..698913e9 100644 --- a/office365/runtime/auth/client_credential.py +++ b/office365/runtime/auth/client_credential.py @@ -4,8 +4,8 @@ def __init__(self, client_id, client_secret): """ Client credentials - :type client_secret: str - :type client_id: str + :param str client_secret: + :param str client_id: """ self.clientId = client_id self.clientSecret = client_secret diff --git a/office365/sharepoint/internal/paths/entity.py b/office365/runtime/paths/key.py similarity index 58% rename from office365/sharepoint/internal/paths/entity.py rename to office365/runtime/paths/key.py index d2fd6127..2b80be00 100644 --- a/office365/sharepoint/internal/paths/entity.py +++ b/office365/runtime/paths/key.py @@ -1,12 +1,14 @@ from office365.runtime.paths.resource_path import ResourcePath -class EntityPath(ResourcePath): - """Path for addressing a single SharePoint entity""" +class KeyPath(ResourcePath): + """Path for addressing a single entity by key""" @property def segment(self): - if isinstance(self.key, int): + if self.key is None: + return "()" + elif isinstance(self.key, int): return "({0})".format(self.key) return "('{0}')".format(self.key) diff --git a/office365/sharepoint/client_context.py b/office365/sharepoint/client_context.py index 5d74f788..369eb620 100644 --- a/office365/sharepoint/client_context.py +++ b/office365/sharepoint/client_context.py @@ -1,6 +1,7 @@ import copy from office365.runtime.auth.authentication_context import AuthenticationContext +from office365.runtime.auth.client_credential import ClientCredential from office365.runtime.auth.user_credential import UserCredential from office365.runtime.client_runtime_context import ClientRuntimeContext from office365.runtime.compat import urlparse, get_absolute_url @@ -96,7 +97,8 @@ def with_access_token(self, token_func): def with_user_credentials(self, username, password, allow_ntlm=False, browser_mode=False): """ - Initializes a client to acquire a token via user credentials + Initializes a client to acquire a token via user credentials. + :param str username: Typically, a UPN in the form of an email address :param str password: The password @@ -109,6 +111,19 @@ def with_user_credentials(self, username, password, allow_ntlm=False, browser_mo browser_mode=browser_mode) return self + def with_client_credentials(self, client_id, client_secret): + """ + Initializes a client to acquire a token via client credentials (SharePoint App-Only) + + SharePoint App-Only is the older, but still very relevant, model of setting up app-principals. + This model works for both SharePoint Online and SharePoint 2013/2016/2019 on-premises + + :param str client_id: The OAuth client id of the calling application + :param str client_secret: Secret string that the application uses to prove its identity when requesting a token + """ + self.authentication_context.with_credentials(ClientCredential(client_id, client_secret)) + return self + def with_credentials(self, credentials): """ Initializes a client to acquire a token via user or client credentials diff --git a/office365/sharepoint/files/checked_out_file.py b/office365/sharepoint/files/checked_out_file.py index 669648ab..51002f13 100644 --- a/office365/sharepoint/files/checked_out_file.py +++ b/office365/sharepoint/files/checked_out_file.py @@ -1,7 +1,7 @@ from office365.runtime.paths.resource_path import ResourcePath +from office365.runtime.paths.key import KeyPath from office365.runtime.queries.service_operation import ServiceOperationQuery from office365.sharepoint.base_entity import BaseEntity -from office365.sharepoint.internal.paths.entity import EntityPath from office365.sharepoint.principal.users.user import User @@ -44,4 +44,5 @@ def set_property(self, name, value, persist_changes=True): super(CheckedOutFile, self).set_property(name, value, persist_changes) # fallback: create a new resource path if name == "CheckedOutById": - self._resource_path = EntityPath(value, self.parent_collection.resource_path) + self._resource_path = KeyPath(value, self.parent_collection.resource_path) + return self diff --git a/office365/sharepoint/files/versions/version.py b/office365/sharepoint/files/versions/version.py index e4efc2c8..43a443df 100644 --- a/office365/sharepoint/files/versions/version.py +++ b/office365/sharepoint/files/versions/version.py @@ -3,7 +3,7 @@ from office365.runtime.paths.resource_path import ResourcePath from office365.runtime.paths.service_operation import ServiceOperationPath from office365.sharepoint.base_entity import BaseEntity -from office365.sharepoint.internal.paths.entity import EntityPath +from office365.runtime.paths.key import KeyPath class FileVersion(BaseEntity): @@ -15,15 +15,13 @@ def download(self, file_object): :type file_object: typing.IO """ def _file_version_loaded(): - result = self.open_binary_stream() - - def _process_response(response): + def _save_file(return_type): """ - :type response: requests.Response + :type return_type:ClientResult """ - response.raise_for_status() - file_object.write(result.value) - self.context.after_execute(_process_response) + file_object.write(return_type.value) + self.open_binary_stream().after_execute(_save_file) + self.ensure_property("ID", _file_version_loaded) return self @@ -87,5 +85,5 @@ def set_property(self, key, value, persist_changes=True): super(FileVersion, self).set_property(key, value, persist_changes) if self._resource_path is None: if key == "ID": - self._resource_path = EntityPath(value, self.parent_collection.resource_path) + self._resource_path = KeyPath(value, self.parent_collection.resource_path) return self diff --git a/office365/sharepoint/folders/collection.py b/office365/sharepoint/folders/collection.py index b0cea7c0..4ff051c0 100644 --- a/office365/sharepoint/folders/collection.py +++ b/office365/sharepoint/folders/collection.py @@ -1,8 +1,8 @@ +from office365.runtime.paths.key import KeyPath from office365.runtime.paths.service_operation import ServiceOperationPath from office365.runtime.queries.service_operation import ServiceOperationQuery from office365.sharepoint.base_entity_collection import BaseEntityCollection from office365.sharepoint.folders.folder import Folder -from office365.sharepoint.internal.paths.entity import EntityPath from office365.sharepoint.types.resource_path import ResourcePath as SPResPath @@ -48,7 +48,7 @@ def add(self, name): :param str name: Specifies the Name of the folder. """ - return_type = Folder(self.context, EntityPath(name, self.resource_path)) + return_type = Folder(self.context, KeyPath(name, self.resource_path)) self.add_child(return_type) qry = ServiceOperationQuery(self, "Add", [name], None, None, return_type) self.context.add_query(qry) diff --git a/office365/sharepoint/listitems/listitem.py b/office365/sharepoint/listitems/listitem.py index aa86a43c..b7888677 100644 --- a/office365/sharepoint/listitems/listitem.py +++ b/office365/sharepoint/listitems/listitem.py @@ -2,6 +2,7 @@ from office365.runtime.client_result import ClientResult from office365.runtime.client_value_collection import ClientValueCollection +from office365.runtime.paths.key import KeyPath from office365.runtime.queries.service_operation import ServiceOperationQuery from office365.runtime.paths.resource_path import ResourcePath from office365.runtime.paths.service_operation import ServiceOperationPath @@ -13,7 +14,6 @@ from office365.sharepoint.fields.lookup_value import FieldLookupValue from office365.sharepoint.fields.multi_lookup_value import FieldMultiLookupValue from office365.sharepoint.fields.string_values import FieldStringValues -from office365.sharepoint.internal.paths.entity import EntityPath from office365.sharepoint.likes.liked_by_information import LikedByInformation from office365.sharepoint.listitems.compliance_info import ListItemComplianceInfo from office365.sharepoint.listitems.form_update_value import ListItemFormUpdateValue @@ -476,7 +476,7 @@ def set_property(self, name, value, persist_changes=True): # fallback: create a new resource path if self._resource_path is None and self.parent_collection is not None: if name == "Id": - self._resource_path = EntityPath(value, self.parent_collection.resource_path) + self._resource_path = KeyPath(value, self.parent_collection.resource_path) return self def _set_taxonomy_field_value(self, name, value): diff --git a/office365/sharepoint/tenant/administration/deny_add_and_customize_pages_status.py b/office365/sharepoint/tenant/administration/deny_add_and_customize_pages_status.py index f623e156..11e32f13 100644 --- a/office365/sharepoint/tenant/administration/deny_add_and_customize_pages_status.py +++ b/office365/sharepoint/tenant/administration/deny_add_and_customize_pages_status.py @@ -2,7 +2,10 @@ class DenyAddAndCustomizePagesStatus: """Represents the status of DenyAddAndCustomizePages on a site collection.""" Unknown = 0 + """The status of a site collection’s [DenyAddAndCustomizePages] is unknown.""" Disabled = 1 + """The status of a site collection where the [DenyAddAndCustomizePages] feature has been disabled.""" Enabled = 2 + """The status of a site collection where the [DenyAddAndCustomizePages] feature has been enabled.""" diff --git a/office365/sharepoint/tenant/administration/sites/properties.py b/office365/sharepoint/tenant/administration/sites/properties.py index 379569a8..b7bf3008 100644 --- a/office365/sharepoint/tenant/administration/sites/properties.py +++ b/office365/sharepoint/tenant/administration/sites/properties.py @@ -1,7 +1,7 @@ +from office365.runtime.paths.key import KeyPath from office365.runtime.paths.service_operation import ServiceOperationPath from office365.runtime.queries.service_operation import ServiceOperationQuery from office365.sharepoint.base_entity import BaseEntity -from office365.sharepoint.internal.paths.entity import EntityPath from office365.sharepoint.sites.site import Site from office365.sharepoint.tenant.administration.deny_add_and_customize_pages_status import \ DenyAddAndCustomizePagesStatus @@ -29,25 +29,26 @@ def update(self): site.set_property("__siteUrl", self.url) def _site_loaded(return_type): - self._resource_path = EntityPath(site.id, self.parent_collection.resource_path) + self._resource_path = KeyPath(site.id, self.parent_collection.resource_path) super(SiteProperties, self).update() self.context.load(site, after_loaded=_site_loaded) return self @property def deny_add_and_customize_pages(self): - enum_value = self.properties.get("DenyAddAndCustomizePages", None) - if enum_value is None: - return enum_value - return enum_value == DenyAddAndCustomizePagesStatus.Enabled + """ + Represents the status of the [DenyAddAndCustomizePages] feature on a site collection. + """ + return self.properties.get("DenyAddAndCustomizePages", DenyAddAndCustomizePagesStatus.Unknown) @deny_add_and_customize_pages.setter def deny_add_and_customize_pages(self, value): """ - :param bool value: + Sets the status of the [DenyAddAndCustomizePages] feature on a site collection. + + :param int value: """ - enum_value = DenyAddAndCustomizePagesStatus.Enabled if value else DenyAddAndCustomizePagesStatus.Disabled - self.set_property("DenyAddAndCustomizePages", enum_value) + self.set_property("DenyAddAndCustomizePages", value) @property def owner_login_name(self): @@ -59,6 +60,7 @@ def owner_login_name(self): @property def webs_count(self): """ + Gets the number of Web objects in the site. :rtype: int """ return self.properties.get('WebsCount', None) @@ -112,7 +114,7 @@ def sharing_capability(self): def sharing_capability(self, value): """ Sets the level of sharing for the site. - + :type value: int """ self.set_property('SharingCapability', value)