Office 365 API¶
ACE has a base class you can use to interact with Microsoft Graph
API. It can be imported by from saq import graph_api
. The graph API base class
utilizes the Microsoft-managed msal
library for authentication.
To use the graph_api.GraphApi
class you must provide a valid configuration.
The current base class is designed to faciliate client auth with
certificate-based authentication. Here is an example configuration:
[your_config_block_name]
tenant_id = uuid_from_o365
authority_base_url = https://login.microsoftonline.com
client_id = uuid_also_known_as_app_id
scopes = https://graph.microsoft.com/.default
thumbprint = certificate_thumbprint_as_defined_in_your_azure_ad_app
private_key_file = path/to/private/key/file
; -- make sure the slash at the end of the endpoint is present or it can cause some weird
; behavior
endpoint = https://graph.microsoft.com/v1.0/users/
Using the GraphAPI class¶
The saq.graph_api.GraphAPI
class comes with some helpful methods:
- initialize()
- When you instantiate the GraphAPI
class, it is just loading the
configuration in memory. The GraphAPI.initiailize
method begins
the requied I/O with Azure AD to get the authentication primed and
ready to go.
- You must call this function before attempting to make requests.
- This was split out so that you could instantiate the GraphAPI
class without requiring I/O. This helps for mocking in tests and
loading several versions of this class (maybe with different configuration
blocks for multiple O365 tenants).
- build_url(path)
- This function returns a url with the appropriate base url for
making HTTP requests to the Graph API endpoint/API.
- For example:
```python
email_user = 'admin@test.local'
email_folder = 'inbox'
api = saq.graph_api.GraphApi(graph_config)
path = f'/{user}/mailFolders/{folder}/messages'
url = api.build_url(path)
print(url)
# https://graph.microsoft.com/v1.0/users/admin@test.local/mailFolders/inbox/messages
```
request(endpoint, *args, method='get', proxies=None)
- This function is a helper function for making requests to Graph API.
- It uses the python requests library to faciliate requests but handles acquiring and injecting an authorizaiton token into the request headers.
-
**kwargs
is passed through to the request method. For example, if you have JSON body or paramaters you wish to send, you can pass it in as a keyword argument just like you would with the normalrequests
library: ```python # Lookup email message by message idemail_user = 'admin@test.local' email_folder = 'inbox' message_id = '12345abcde@test.local`
api = saq.graph_api.GraphApi(graph_config) path = f'/{user}/mailFolders/{folder}/messages' params = {"$filter": f"internetMessageId eq '{messaged_id}'"}
url = api.build_url(path)
response = api.request(url, method='get', params=params)
`` -
get_token()- In normal use, you should not need to access this method. - This method is automatically called in
GraphAPI.request` if there is not an existing token.
The saq.GraphAPI
base class was designed to be used with compositon
instead of inheritance whenever possible. For example, instead of inheriting directly from saq.GraphAPI
, add it as an attribute to your class. This
does not prevent inheriting from GraphAPI
, but future design descisions
will be made with composition in mind:
from saq import graph_api
class DoThingsWithGraphAPI:
def __init__(self, graph_config):
self.api = graph_api.GraphAPI(graph_config)
def initialize():
self.api.initialize()
def send_email():
# build your url
response = self.api.request(url, method='post', body=whatever)
return response
To help with composition, there is a helper function designed to help with this.
from saq import graph_api
class DoThingsWithGraphAPI:
def __init__(self, graph_config):
self.api = graph_api.get_graph_api_object(graph_config)
def initialize():
self.api.initialize()
def send_email():
# build your url
response = self.api.request(url, method='post', body=whatever)
return response
The get_graph_api_object
helper function also adds the support for two more configuration parameters named auth_ca_cert_path
and graph_ca_cert_path
to help facilitate requests to graph API through SSL decryption-enabled proxies and firewalls.
[your_graph_api_config_block]
... other GraphAPI configuration params
auth_ca_cert_path = /path/to/ca/cert.crt
graph_ca_cert_path = /path/to/ca/cert.crt
auth_ca_cert_path
and graph_ca_cert_path
are passed into the GraphAPI
object via the verify_auth
and verify_graph
keyword arguments:
verify_auth
- This determines if the requests to the authentication URL at Azure AD should verify the SSL certificate. For example, you may pass in the path to your CA bundle if your corporate proxy decrypts SSL destined for the Azure AD authentication endpoint.
verify_graph
- This determines if the requests to the Graph API endpoint should verify the SSL certificate. For example, you may pass in the path to your CA bundle if your corporate proxy decrypts SSL destined for the Graph API endpoint.
Another keyword argument made available through the GraphAPI object is
proxies
:
- This value is in the format of the proxies kwarg used by the
requests
library. This value is saved and will be used for each
request performed by the GraphAPI
object.
Helper functions¶
There are many helper functions available in the saq.graph_api
module.
If there is a function that directly uses the GraphAPI
class to perform a more complex function, but can be used in many scenarios--like finding a message based on message id or moving an email, feel free to add it to the saq.graph_api
Use dependency injection when writing helper functions so testing is easier and so that the functions can be used in a variety of cases--not just your code. As an example, we'll look at
For example, take a look at the following function to help get the mime content of an email:
from saq.extractors import RESULT_MESSAGE_NOT_FOUND, RESULT_MESSAGE_FOUND
def get_mime_content_by_o365_id(api, user, item_id, **kwargs):
"""Return the email mime content from Graph API."""
_request_func = kwargs.get('request_func') or api.request
url = api.build_url(f"{user}/messages/{item_id}/$value")
response = _request_func(url, method='get')
if response.status_code != 200:
return None, RESULT_MESSAGE_NOT_FOUND
return response.text, RESULT_MESSAGE_FOUND
Notice the dependency injection... you must pass in the API object. This allows more granular control of unit testing and helps keep the helper function from being tightly coupled to any one implementation of the api object itself. If the API object interface were to change, then a single change in this function would update all code that uses this function.