Accessing an organization-restriced Google Sheet via API - python-3.x

I am writing a Python 3.7 script that needs to read data from a Google Spreadsheet.
The spreadsheet in question belongs to an Organization that my work Google Account is part of: let's just call it "Organization". The spreadsheet permissions are set as "Anyone at Organization with the link can view". This detail has been preventing my application from working.
I went to the credentials dashboard at https://console.cloud.google.com/apis/credentials while being authenticated with my account in Organization, and created a Service Account Key. Domain Wide Delegation is allowed, as per Using New Google API Console project getting unauthorized_client ,Client is unauthorized to retrieve access tokens using this method. I downloaded the JSON keyfile to /path/to/service_account_key.json.
Below I document my attempt with the gspread client library - https://github.com/burnash/gspread - however I had the exact same problem using google-api-python-client 1.7.4:
import gspread
from oauth2client.service_account import ServiceAccountCredentials
scopes = ['https://www.googleapis.com/auth/spreadsheets.readonly']
credentials = ServiceAccountCredentials.from_json_keyfile_name('/path/to/service_account_key.json', scopes)
gc = gspread.authorize(credentials)
# spreadhseet ID below obfuscated, it's actually the one you get from its URL
sheet = gc.open_by_key('12345-abcdef')
gspread's response has the same HTTP code and message as the plain Google API v4:
gspread.exceptions.APIError: {
"error": {
"code": 403,
"message": "The caller does not have permission",
"status": "PERMISSION_DENIED"
}
}
However if I change the permissions on the spreadsheet (which I won't be allowed to do on the actual one) to "Anyone with the link can view", thus removing Organization", everything works!
<Spreadsheet 'Your spreadsheet' id:12345-abcdef>
My rough guess was that the service account I created did not inherit from me the membership in Organization. However I found no option for ensuring that. I even asked the domain Administrator to create the service account for me from his Admin accounts (also with Domain-wide Delegation on): nothing.
It has been said at Google service account and sheets permissions that I must explicitly share the sheet with the email address of the service account. When I did that it worked, but I also got a warning message claiming that account was not in the G Suite domain of Organization (again, how could it not be?).
Many Thanks

Try delegating that account to a specific user of your organization.
it goes as
from oauth2client.service_account import ServiceAccountCredentials
from httplib2 import Http
scope = [
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/spreadsheets.readonly',
'https://www.googleapis.com/auth/spreadsheets'
]
creds = ServiceAccountCredentials.from_json_keyfile_dict('/path_to_service_account_json_key', scope)
delegated = creds.create_delegated('anyuser#org_domain.com')
delegated_http = delegated.authorize(Http())
spreadsheetservice = gspread.authorize(delegated)
This would make the service account delegated as the user.
now you may simply share that very sheet to the above mentioned user (anyuser#org_domain.com) or pass the sheet's owner email dynamically.

Related

Setting up an Application with Azure for use with Graph API outlook calendars

I'm aware that Graph API has a nice nuget package and I am confident on the code side of things, but my understanding is that I need to have the application set up in Azure and while there is a lot of documentation about this in general, I find it quite dense and I'm not confident I have the specifics down for how I need to set this portion up.
What I need my application to do is access an outlook calendar of a specific user that I own, read, search, add, delete and update calendar items. The integration assistant seems to suggest I need to configure a URI redirect and configure api permission. The default persmission is User.Read on graph API and if I try to add a permission, office 365 management seems like it might be the one I need except it specifically says just retrieving user information and nothing mentions outlook anywhere.
I need to know more or less the minimum steps in setting up the application in Azure to write a C# application that can make changes to outlook for a user.
need my application to do is access an outlook calendar of a specific user
Does it mean you need your app to have the abiltity to modify the callendar of any user you owned? If not, then it means you need your application to provide a sign in module and let users sign in, then the users can get authentication to call graph api and manage their own calendar, since this scenario, users give the delegate api permission, so they can't manage other users' calendar, so I don't think this is what you want.
If so, then you should use client credential flow to generate access token to call graph api. I think you know that when you want to call graph api, you have to get an access token which has correct permission first.
Ok, then let's come to the api permission, when you go to api document of the Calendar. You will see permissions like screenshot below:
Application permission type is suitable for client credential flow. And after viewing all the apis, you will find that they all need Calendars.ReadWrite except those not supporting Application type.
Then let's go to azure portal and reach Azure Active Directory. You need to create an Azure ad application and give the app Calendars.ReadWrite permission, then give the Admin consent.
Then you also need to create a client secret, pls go to Certificates & Secrets and add a new client secret, don't forget to copy the secret after you create it.
Now you've done all the steps. No need to set a redirect url, because you don't need to let the user to sign in your application. Let's see client credential flow document, it only require client_id, client_secret to generate access token.
Or in the code, you may use SDK like this :
using Azure.Identity;
using Microsoft.Graph;
public async Task<IActionResult> Index()
{
var scopes = new[] { "https://graph.microsoft.com/.default" };
var tenantId = "your_tenant_name.onmicrosoft.com";
var clientId = "azure_ad_app_id";
var clientSecret = "client_secret";
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret);
var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
var calendar = new Calendar{ Name = "Volunteer" };
var events = await graphClient.Users["user_id_which_is_needed_to_list_calendar_events"].Events.Request()
.Header("Prefer","outlook.timezone=\"Pacific Standard Time\"")
.Select("subject,body,bodyPreview,organizer,attendees,start,end,location")
.GetAsync();
return View();
}

How to programmatically access Sharepoint sites other than root from Office365-REST-Python-Client?

I need to programmatically access my organization's Office 365 Sharepoint. In particular, at the moment I need to be able to upload and download files from specific lists/paths.
I created an Azure AD application using this manual and granted Sites.FullControl.All permission (consent was granted by the admin as well).
I am using Office365-REST-Python-Client Python library for calling Sharepoint API, which seems to be a well-maintained library and accepted in the Sharepoint community.
I was able to authenticate as my app using a certificate I created:
from office365.sharepoint.client_context import ClientContext
base_url = 'https://my-company.sharepoint.com/'
cert_auth_data = dict(
tenant='61ad46cb-b256-451d-9b4c-b592d46a7dc1',
client_id='b3af34ed-1335-468b-a82b-d0cf6ec79683',
thumbprint='815B134CD6D15EB0F07D247940AA6EF96263859F',
cert_path='selfsigncert.pem',
)
ctx = ClientContext(base_url).with_client_certificate(**cert_auth_data)
res = ctx.web.get().execute_query()
print(res.properties['Title']) # Works
Now I need to access a specific site in the portal, and this is where I start having problems. If I put https://my-company.sharepoint.com/sites/mysite as URL, I receive the following error:
ValueError: {'error': 'invalid_resource', 'error_description': 'AADSTS500011: The resource principal named https://my-company.sharepoint.com/sites/mysite was not found in the tenant named My Company Limited. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You might have sent your authentication request to the wrong tenant.\r\nTrace ID: e8bbbe6c-f5ef-4d6a-8635-0848184e1801\r\nCorrelation ID: f6a57e34-886a-4247-8a4e-194f098876ed\r\nTimestamp: 2022-05-12 02:26:44Z', 'error_codes': [500011], 'timestamp': '2022-05-12 02:26:44Z', 'trace_id': 'e8bbbe6c-f5ef-4d6a-8635-0848184e1801', 'correlation_id': 'f6a57e34-886a-4247-8a4e-194f098876ed', 'error_uri': 'https://login.microsoftonline.com/error?code=500011'}
I assumed I can somehow reach the site from the ClientContext of the base URL, but I could not figure how to do it and if that is possible at all.
I tried to use Site API:
site = Site.from_url(site_url)
But I see that Site lacks with_client_certificate() (as opposed to ClientContext) and has only with_credentials() method. I added such a method similarly to ClientContext:
def with_client_certificate(self, tenant, client_id, thumbprint, cert_path, **kwargs):
self.context.with_client_certificate(tenant, client_id, thumbprint, cert_path, **kwargs)
return self
However in this case authorization fails with the same error as above.
Is my application lacking some permissions? If yes, how to fix that?
Am I using the library properly? I failed to find good examples in their repo for my case, but other resources suggest that using site URL is probably the right way [1], [2].

Search User Information Across different Microsoft Tenants

I want to be able to search for users across multiple tenants, and therefore my thoughts were to create a python script that runs on HTTP triggered Azure functions. This python script can authenticate to Microsoft Graph API for different tenants via service principals and then search for a user and return the data. is this a good idea or is there a better way of doing this?
Let's discuss on the achievement.
I find that one multi-tenant azure ad application is enough for querying users in different tenant through graph api. For example, there're 2 tenants, I created a multi-tenant application in azure ad app registration, after that I generated the client secret and add api permission of User.Read.All.
Now I have an app with its client id and secret in 'tenant_a'. Next, visit https://login.microsoftonline.com/{tenant_b}/adminconsent?client_id={client-id} in the browser, after sign in with the admin account in tenant_b, it will appear a 'permission' window to make consent the application have permission in tenant_b, after the consent, you will the the app created in tenant_a appears in the list of Enterprise applications in tenant_b.
Now we need to generate access token for different tenant to call graph api. It's necessary to generate access token for each tenant, because I tried to use common to replace the domain in the request(https://login.microsoftonline.com/common/oauth2/v2.0/token), it can generate access token successfully, but the token can't used in the api to query user information. The query user api needs user principal name as the input parameter. For example, I have a user which account is 'bob#tenant_b.onmicrosoft.com', use the account as the parameter is ok to get response, but if I use 'bob' as the parameter, it will return 'Resource xxx does not exist...'.
I'm not an expert in python, I only found a sample and tested successfully with it. Here's my code, it will execute loop query until the user be found. And if you wanna a function, you may create a http trigger base on it.
import sys
import json
import logging
import requests
import msal
config = json.load(open(sys.argv[1]))
authorityName = ["<tenant_a>.onmicrosoft.com","<tenant_b>.onmicrosoft.com"]
username = "userone#<tenant_a>.onmicrosoft.com"
for domainName in authorityName:
# Create a preferably long-lived app instance which maintains a token cache.
print("==============:"+config["authority"]+domainName)
app = msal.ConfidentialClientApplication(
"<client_id>", authority="https://login.microsoftonline.com/"+domainName,
client_credential="<client_secret>",
)
# The pattern to acquire a token looks like this.
result = None
# Firstly, looks up a token from cache
# Since we are looking for token for the current app, NOT for an end user,
# notice we give account parameter as None.
result = app.acquire_token_silent(["https://graph.microsoft.com/.default"], account=None)
if not result:
result = app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])
if "access_token" in result:
print("access token===:"+result['access_token'])
# Calling graph using the access token
graph_data = requests.get( # Use token to call downstream service
"https://graph.microsoft.com/v1.0/users/"+username,
headers={'Authorization': 'Bearer ' + result['access_token']}, ).json()
if "error" in graph_data:
print("error===="+json.dumps(graph_data, indent=2))
else:
print(json.dumps(graph_data, indent=2))
break
else:
print(result.get("error"))
print(result.get("error_description"))
print(result.get("correlation_id"))

Make use of the gmail api through a service account from the server side avoiding the OAUTH2 GUI

I have an application developed in python that uses the SMPT service to connect to a gmail account. This type of connection is typified as an "Access of insecure applications" lesssecureapps.
To remedy this I have set myself the task of updating my app, using the gmail api and for authentication to use a private key generated from a service account (without using G-Suit).
I have created a first proof of concept and it seems that it connects and authenticates correctly, but when trying to get the labels of the gmail account I get the following message:
<HttpError 400 when requesting https://gmail.googleapis.com/gmail/v1/users/me/labels?alt=json returned "Precondition check failed.">
I recap the steps I have followed to configure my google account:
I access Google Api Console and enable the use of the gmail api through the button that appears at the top: Enable apis and services
I access the credentials section, click on the top button: "create credentials" and select Service account
I create a service account and then generate a private key in json format
I add a small code snippet with my proof of concept and it that returns the error that I comment on the top:
from google.oauth2 import service_account
from googleapiclient.discovery import build
SCOPES = ['https://mail.google.com/','https://www.googleapis.com/auth/gmail.modify','https://www.googleapis.com/auth/gmail.readonly','https://www.googleapis.com/auth/gmail.labels','https://www.googleapis.com/auth/gmail.metadata','https://www.googleapis.com/auth/gmail.addons.current.message.metadata','https://www.googleapis.com/auth/gmail.addons.current.message.readonly']
SERVICE_ACCOUNT_FILE = '/home/user/keys/privatekey_from_service_account.json'
credentials = service_account.Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)
service = build('gmail', 'v1', credentials=credentials)
# Call the Gmail API
results = service.users().labels().list(userId='me').execute()
labels = results.get('labels', [])
if not labels:
print('No tienes labels.')
else:
print('Labels:')
for label in labels:
print(label['name'])
How can I solve my problem without using g-suite?
"Precondition check failed."
Means that you are not allowed to do what you are trying to do.
use a private key generated from a service account (without using G-Suit).
The Gmail api does not support service accounts for non gsuite domains. You can only use a service account with a Gsuite domain account and gsuite domain emails.
Services account wont work with normal google gmail accounts.

Asking for user info anonymously Microsoft Graph

In an old application some people in my company were able to get info from Microsoft Graph without signing users in. I've tried to replicate this but I get unauthorized when trying to fetch users. I think the graph might have changed, or I'm doing something wrong in Azure when I register my app.
So in the Azure portal i have registered an application (web app), and granted it permissions to Azure ad and Microsoft graph to read all users full profiles.
Then I do a request
var client = new RestClient(string.Format("https://login.microsoftonline.com/{0}/oauth2/token", _tenant));
var request = new RestRequest();
request.Method = Method.POST;
request.AddParameter("tenant", _tenant);
request.AddParameter("client_id", _clientId);
request.AddParameter("client_secret", _secret);
request.AddParameter("grant_type", "client_credentials");
request.AddParameter("resource", "https://graph.microsoft.com");
request.AddParameter("scope", "Directory.Read.All");
I added the last row (scope) while testing. I still got a token without this but the result is same with or without it.
After I get a token I save it and do this request:
var testClient = new RestClient(string.Format("https://graph.microsoft.com/v1.0/users/{0}", "test#test.onmicrosoft.com")); //I use a real user here in my code ofc.
testRequest = new RestRequest();
testRequest.Method = Method.GET;
testRequest.AddParameter("Authorization", _token.Token);
var testResponse = testClient.Execute(testRequest);
However now I get an error saying unauthorized, Bearer access token is empty.
The errors point me to signing users in and doing the request, however I do not want to sign a user in. As far as i know this was possible before. Have Microsoft changed it to not allow anonymous requests?
If so, is it possible to not redirecting the user to a consent-page? The users are already signed in via Owin. However users may have different access and i want this app to be able to access everything from the azure ad, regardless of wich user is logged in. How is the correct way of doing this nowadays?
Or am I just missing something obvious? The app has been given access to azure and microsoft graph and an admin has granted permissions for the app.
Edit: just to clarify, i tried both "Authorization", "bearer " + _token.Token, and just _token.Token as in the snippet.
Yes, it's still possible to make requests to Graph without a user present using application permissions. You will need to have the tenant admin consent and approve your application.
Edit / answer: Adding the 'Authorization' as a header instead of a parameter did the trick. It works both with 'bearer token' and just 'token'

Resources