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 normal requests library: ```python # Lookup email message by message id

      email_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 inGraphAPI.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.