8

Is it possible to upload a file to the Shared Documents library of a Microsoft SharePoint site with the Python OneDrive SDK?

This documentation says it should be (in the first sentence), but I can't make it work.

I'm able to authenticate (with Azure AD) and upload to a OneDrive folder, but when trying to upload to a SharePoint folder, I keep getting this error:

"Exception of type 'Microsoft.IdentityModel.Tokens.AudienceUriValidationFailedException' was thrown."

The code I'm using that returns an object with the error:

(...authentication...)
client = onedrivesdk.OneDriveClient('https://{tenant}.sharepoint.com/{site}/_api/v2.0/', auth, http)
client.item(path='/drive/special/documents').children['test.xlsx'].upload('test.xlsx')

where I'd like to upload on the web

I can successfully upload to https://{tenant}-my.sharepoint.com/_api/v2.0/ (notice the "-my" after the {tenant}) with the following code:

client = onedrivesdk.OneDriveClient('https://{tenant}-my.sharepoint.com/_api/v2.0/', auth, http)
returned_item = client.item(drive='me', id='root').children['test.xlsx'].upload('test.xlsx')

How could I upload the same file to a SharePoint site?

(Answers to similar questions (1,2,3,4) on Stack Overflow are either too vague or suggest using a different API. My question is if it's possible using the OneDrive Python SDK, and if so, how to do it.)


Update: Here is my full code and output. (Sensitive original data replaced with similarly formatted gibberish.)

import re
import onedrivesdk
from onedrivesdk.helpers.resource_discovery import ResourceDiscoveryRequest

# our domain (not the original)
redirect_uri = 'https://example.ourdomain.net/' 
# our client id (not the original)
client_id = "a1234567-1ab2-1234-a123-ab1234abc123"  
# our client secret (not the original)
client_secret = 'ABCaDEFGbHcd0e1I2fghJijkL3mn4M5NO67P8Qopq+r=' 
resource = 'https://api.office.com/discovery/'
auth_server_url = 'https://login.microsoftonline.com/common/oauth2/authorize'
auth_token_url = 'https://login.microsoftonline.com/common/oauth2/token'
http = onedrivesdk.HttpProvider()
auth = onedrivesdk.AuthProvider(http_provider=http, client_id=client_id, 
                                auth_server_url=auth_server_url, 
                                auth_token_url=auth_token_url)

should_authenticate_via_browser = False
try:
    # Look for a saved session. If not found, we'll have to 
    # authenticate by opening the browser.
    auth.load_session()
    auth.refresh_token()
except FileNotFoundError as e:
    should_authenticate_via_browser = True
    pass

if should_authenticate_via_browser:
    auth_url = auth.get_auth_url(redirect_uri)
    code = ''
    while not re.match(r'[a-zA-Z0-9_-]+', code):
        # Ask for the code
        print('Paste this URL into your browser, approve the app\'s access.')
        print('Copy the resulting URL and paste it below.')
        print(auth_url)
        code = input('Paste code here: ')
        # Parse code from URL if necessary
        if re.match(r'.*?code=([a-zA-Z0-9_-]+).*', code):
            code = re.sub(r'.*?code=([a-zA-Z0-9_-]*).*', r'\1', code)
    auth.authenticate(code, redirect_uri, client_secret, resource=resource)
    # If you have access to more than one service, you'll need to decide
    # which ServiceInfo to use instead of just using the first one, as below.
    service_info = ResourceDiscoveryRequest().get_service_info(auth.access_token)[0]
    auth.redeem_refresh_token(service_info.service_resource_id)
    auth.save_session()  # Save session into a local file.

# Doesn't work
client = onedrivesdk.OneDriveClient(
    'https://{tenant}.sharepoint.com/sites/{site}/_api/v2.0/', auth, http)
returned_item = client.item(path='/drive/special/documents')
                      .children['test.xlsx']
                      .upload('test.xlsx')
print(returned_item._prop_dict['error_description'])

# Works, uploads to OneDrive instead of SharePoint site
client2 = onedrivesdk.OneDriveClient(
    'https://{tenant}-my.sharepoint.com/_api/v2.0/', auth, http)
returned_item2 = client2.item(drive='me', id='root')
                        .children['test.xlsx']
                        .upload('test.xlsx')
print(returned_item2.web_url)

Output:

Exception of type 'Microsoft.IdentityModel.Tokens.AudienceUriValidationFailedException' was thrown.
https://{tenant}-my.sharepoint.com/personal/user_domain_net/_layouts/15/WopiFrame.aspx?sourcedoc=%1ABCDE2345-67F8-9012-3G45-6H78IJKL9M01%2N&file=test.xlsx&action=default
Community
  • 1
  • 1
Attila Tanyi
  • 4,904
  • 5
  • 27
  • 34
  • Can you provide the full traceback? Or specify which line was the source of the error the line starting with `client` or `returned_item`? – sytech Oct 13 '16 at 19:28
  • Also, have you [set the appropriate AuthScope](https://github.com/OneDrive/onedrive-api-docs/blob/master/site-images/AuthScopesForSharePoint.png?raw=true) for SharePoint Online? This is separate from your OneDrive permissions. – sytech Oct 13 '16 at 19:44
  • @Gator_Python - I added the full code. There is no error thrown, but the returned object contains the properties of the uploaded file in the successful case, and this error message in the unsuccessful case. The AuthScope is set like that (in fact, all the permissions are ticked). – Attila Tanyi Oct 14 '16 at 11:02
  • I noticed you did not specify the `drive` when you tried doing this for your sharepoint library. It may seem counterintuitive, but a sharepoint document library is considered a ***drive*** and is accessed the same way you access other OneDrive resources. Whether you're accessing OneDrive or Sharepoint, API actions should (pretty much) always address a drive resource. See [drive resources](https://dev.onedrive.com/resources/drive.htm) and [list drives](https://dev.onedrive.com/drives/list-drives.htm) – sytech Oct 17 '16 at 14:27
  • Thanks! I realized I can list drives with `onedrivesdk.OneDriveClient('https://{tenant}-my.sharepoint.com/_api/v2.0/', auth, http).drives.get()`. It lists one drive, with type 'business'. I assume this is the OneDrive storage. For the URL without `-my`, I can execute `onedrivesdk.OneDriveClient('https://{tenant}.sharepoint.com/_api/v2.0/', auth, http).drive.get()`, which returns an object with the same error message (AudienceUri...) as above. – Attila Tanyi Oct 18 '16 at 15:14
  • Interesting. Are you using One Drive for Business (Azure AD)? -- For the sake of debugging, can you try accessing the REST endpoints using `requests` instead? This way, you will receive an HTTP status code that will reveal a more precise cause of the error. Further, with OD for business, you can do [resource discovery](https://dev.onedrive.com/auth/aad_oauth.htm#step-3-discover-the-onedrive-for-business-resource-uri). I don't know why, but they say OD for business is not supported in the Python SDK, but the SDK itself suggests otherwise. – sytech Oct 18 '16 at 15:36
  • After looking into the source, apparently the SDK uses the same resource discovery you linked. I checked the returned list of resources before, and there was only one. Now I checked deeper in the SDK code, and the discovery actually returns 4 - `OneDrive MyFiles` v1.0, v2.0, `SharePoint RootSite` v1.0, and `Azure Directory` v1.0. The one I'd like to use is `SharePoint RootSite` v1.0. The problem is that the SDK filters for `v2.0` only, so it doesn't even return the `{tenant}.sharepoint.com/_api` one. (The message format is different - XML for v1, and JSON for v2.) I'll check if I can hack it. – Attila Tanyi Oct 19 '16 at 12:29
  • @sytech - I've finally found a workaround. Thank you for your help! – Attila Tanyi Oct 20 '16 at 12:02
  • Awesome! Glad that worked out! – sytech Oct 20 '16 at 12:05

1 Answers1

5

I finally found a solution, with the help of (SO user) sytech.

The answer to my original question is that using the original Python OneDrive SDK, it's not possible to upload a file to the Shared Documents folder of a SharePoint Online site (at the moment of writing this): when the SDK queries the resource discovery service, it drops all services whose service_api_version is not v2.0. However, I get the SharePoint service with v1.0, so it's dropped, although it could be accessed using API v2.0 too.

However, by extending the ResourceDiscoveryRequest class (in the OneDrive SDK), we can create a workaround for this. I managed to upload a file this way:

import json
import re
import onedrivesdk
import requests
from onedrivesdk.helpers.resource_discovery import ResourceDiscoveryRequest, \
    ServiceInfo

# our domain (not the original)
redirect_uri = 'https://example.ourdomain.net/' 
# our client id (not the original)
client_id = "a1234567-1ab2-1234-a123-ab1234abc123"  
# our client secret (not the original)
client_secret = 'ABCaDEFGbHcd0e1I2fghJijkL3mn4M5NO67P8Qopq+r=' 
resource = 'https://api.office.com/discovery/'
auth_server_url = 'https://login.microsoftonline.com/common/oauth2/authorize'
auth_token_url = 'https://login.microsoftonline.com/common/oauth2/token'

# our sharepoint URL (not the original)
sharepoint_base_url = 'https://{tenant}.sharepoint.com/'
# our site URL (not the original)
sharepoint_site_url = sharepoint_base_url + 'sites/{site}'

file_to_upload = 'C:/test.xlsx'
target_filename = 'test.xlsx'


class AnyVersionResourceDiscoveryRequest(ResourceDiscoveryRequest):

    def get_all_service_info(self, access_token, sharepoint_base_url):
        headers = {'Authorization': 'Bearer ' + access_token}
        response = json.loads(requests.get(self._discovery_service_url,
                                           headers=headers).text)
        service_info_list = [ServiceInfo(x) for x in response['value']]
        # Get all services, not just the ones with service_api_version 'v2.0'
        # Filter only on service_resource_id
        sharepoint_services = \
            [si for si in service_info_list
             if si.service_resource_id == sharepoint_base_url]
        return sharepoint_services


http = onedrivesdk.HttpProvider()
auth = onedrivesdk.AuthProvider(http_provider=http, client_id=client_id,
                                auth_server_url=auth_server_url,
                                auth_token_url=auth_token_url)

should_authenticate_via_browser = False
try:
    # Look for a saved session. If not found, we'll have to
    # authenticate by opening the browser.
    auth.load_session()
    auth.refresh_token()
except FileNotFoundError as e:
    should_authenticate_via_browser = True
    pass

if should_authenticate_via_browser:
    auth_url = auth.get_auth_url(redirect_uri)
    code = ''
    while not re.match(r'[a-zA-Z0-9_-]+', code):
        # Ask for the code
        print('Paste this URL into your browser, approve the app\'s access.')
        print('Copy the resulting URL and paste it below.')
        print(auth_url)
        code = input('Paste code here: ')
        # Parse code from URL if necessary
        if re.match(r'.*?code=([a-zA-Z0-9_-]+).*', code):
            code = re.sub(r'.*?code=([a-zA-Z0-9_-]*).*', r'\1', code)

    auth.authenticate(code, redirect_uri, client_secret, resource=resource)
    service_info = AnyVersionResourceDiscoveryRequest().\
        get_all_service_info(auth.access_token, sharepoint_base_url)[0]
    auth.redeem_refresh_token(service_info.service_resource_id)
    auth.save_session()

client = onedrivesdk.OneDriveClient(sharepoint_site_url + '/_api/v2.0/',
                                    auth, http)
# Get the drive ID of the Documents folder.
documents_drive_id = [x['id']
                      for x
                      in client.drives.get()._prop_list
                      if x['name'] == 'Documents'][0]
items = client.item(drive=documents_drive_id, id='root')
# Upload file
uploaded_file_info = items.children[target_filename].upload(file_to_upload)

Authenticating for a different service gives you a different token.

Attila Tanyi
  • 4,904
  • 5
  • 27
  • 34
  • The code looks good, but the code I'm writing needs to be automated, so I cannot copy and paste the "code" generated by browser each time, this "code" will expire very soon too. So, based on your code, I modified my code, but no matter it's v1.0 or v2.0, I still get empty response['value'].....May I ask, do you know what's wrong with my code? Here's my code: https://github.com/hanhanwu/Basic_But_Useful/blob/master/one_drive_buz_test.py – Cherry Wu Nov 01 '16 at 23:09
  • @CherryWu - The code above _is_ automated. `auth.save_session()` saves the authentication information into a file called `session.pickle`. `auth.load_session()` loads this. The way it works is that the first time you run this code, it will open a browser window where you need to log in and copy & paste the URL after redirection back into the Python console. This authentication token is then saved, and next time loaded without a browser opening. You can read more about this at the "Saving and Loading a Session" section [here](https://github.com/OneDrive/onedrive-sdk-python). – Attila Tanyi Nov 02 '16 at 10:21
  • Thank you very much Attila, today I don't have time to try this, but a quick question, the code we got from the browser has very short life span, does this mean, with the "Saving and Loading a Session" in your code, that "code" will no longer expire? – Cherry Wu Nov 02 '16 at 17:14
  • 1
    I don't know, I haven't run into the expiry yet. The short life span token is the _access token_, what you store here is also the _refresh token_. According to [this answer here](http://stackoverflow.com/questions/22043128/windows-azure-active-directory-expiration-of-refreshtoken), if used at least once in every 14 days, these Azure AD tokens should expire in 90 days. – Attila Tanyi Nov 02 '16 at 17:44