diff --git a/README.md b/README.md index 91ebc27..ba7b71b 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,15 @@ Nevermind, they just decided it would be cool and fun to hide the user informati | Yes | GRAPH_GRANT_TYPE | Should be 'client_credentials' | | Yes | GRAPH_SCOPES | Should typically be unless using more fine-grained permissions. | | No | SP_SITE | Base Site URL you're interacting with. Should be | +| No | SP_SCOPES | Scopes for sharepoint rest API. Should look like | +| No | SP_LOGIN_BASE_URL | Should be | +| No | SP_TENANT_ID | Tenant ID from app registration created in Azure. | +| No | SP_CLIENT_ID | Client ID from app registration created in Azure. | +| No | SP_GRANT_TYPE | client_credentials | +| No | SP_CERTIFICATE_PATH | Path to `.pfx` file | +| No | SP_CERTIFICATE_PASSWORD | Password for the `.pfx` file. | + +Most of the endpoints in grafap are just using the standard Microsoft Graph API which only requires a client ID and secret. The Sharepoint REST API, however requires using a client certificate. At least for the only endpoint being used thus far "ensure user". ### Get SharePoint Sites diff --git a/grafap/auth.py b/grafap/auth.py index ab370b9..2ad4989 100644 --- a/grafap/auth.py +++ b/grafap/auth.py @@ -1,16 +1,21 @@ +import base64 +import hashlib import os +import time +import uuid from datetime import datetime, timedelta +import jwt import requests +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.serialization import load_pem_private_key, pkcs12 +from OpenSSL import crypto class Decorators: """ Decorators class for handling token refreshing for Microsoft Graph and Sharepoint Rest API - - NOTE: I don't believe the SP auth is being done correctly. May be wrong endpoint - or wrong permissions, not sure. But subsequent requests to SP API fail. """ @staticmethod @@ -123,28 +128,69 @@ def get_sp_token(): raise Exception("Error, could not find SP_TENANT_ID in env") if "SP_CLIENT_ID" not in os.environ: raise Exception("Error, could not find SP_CLIENT_ID in env") - if "SP_CLIENT_SECRET" not in os.environ: - raise Exception("Error, could not find SP_CLIENT_SECRET in env") + # if "SP_CLIENT_SECRET" not in os.environ: + # raise Exception("Error, could not find SP_CLIENT_SECRET in env") + if "SP_CERTIFICATE_PATH" not in os.environ: + raise Exception("Error, could not find SP_CERTIFICATE_PATH in env") + if "SP_CERTIFICATE_PASSWORD" not in os.environ: + raise Exception("Error, could not find SP_CERTIFICATE_PASSWORD in env") if "SP_GRANT_TYPE" not in os.environ: raise Exception("Error, could not find SP_GRANT_TYPE in env") if "SP_SITE" not in os.environ: raise Exception("Error, could not find SP_SITE in env") - headers = { - "Accept": "application/json", - "Content-Type": "application/x-www-form-urlencoded", + # Load the certificate + with open(os.environ["SP_CERTIFICATE_PATH"], "rb") as cert_file: + cert_data = cert_file.read() + pfx = pkcs12.load_key_and_certificates( + cert_data, str.encode(os.environ["SP_CERTIFICATE_PASSWORD"]) + ) + + # Extract the private key and certificate + private_key = pfx[0] + certificate = pfx[1] + + private_key_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + # Compute the SHA-1 thumbprint of the certificate + cert_der = certificate.public_bytes(serialization.Encoding.DER) + thumbprint = hashlib.sha1(cert_der).digest() + thumbprint_b64 = ( + base64.urlsafe_b64encode(thumbprint).decode("utf-8").rstrip("=") + ) + + # JWT payload + payload = { + "aud": f"https://login.microsoftonline.com/{os.environ["GRAPH_TENANT_ID"]}/oauth2/v2.0/token", + "iss": os.environ["GRAPH_CLIENT_ID"], + "sub": os.environ["GRAPH_CLIENT_ID"], + "jti": str(uuid.uuid4()), + "exp": int(time.time()) + 600, } + # JWT header with x5t thumbprint + headers = {"x5t": thumbprint_b64} + + # Generate the JWT assertion + jwt_assertion = jwt.encode( + payload, private_key_pem, algorithm="RS256", headers=headers + ) + response = requests.post( os.environ["SP_LOGIN_BASE_URL"] + os.environ["SP_TENANT_ID"] - + "/oauth2/token", + + "/oauth2/v2.0/token", headers=headers, data={ "client_id": os.environ["SP_CLIENT_ID"], - "client_secret": os.environ["SP_CLIENT_SECRET"], "grant_type": os.environ["SP_GRANT_TYPE"], - "resource": os.environ["SP_SITE"], + "scope": os.environ["SP_SCOPES"], + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "client_assertion": jwt_assertion, }, timeout=30, ) @@ -165,5 +211,3 @@ def get_sp_token(): except Exception as e: print("Error, could not set os env expires at: ", e) raise Exception("Error, could not set os env expires at: " + str(e)) - - print(os.environ["SP_BEARER_TOKEN"]) diff --git a/grafap/users.py b/grafap/users.py index a8f31d1..230095c 100644 --- a/grafap/users.py +++ b/grafap/users.py @@ -169,3 +169,47 @@ def get_sp_user_info( # print("Status Code: ", response.status_code) # print("Error, could not get site user data: ", response.content) # raise Exception("Error, could not get site user data: " + str(response.content)) + + +@Decorators.refresh_sp_token +def ensure_sp_user(site_url: str, logon_name: str) -> dict: + """ + Users sharepoint REST API, not MS Graph API. Endpoint is only available + in the Sharepoint one. Ensure a user exists in given website. This is used + so that the user can be used in sharepoint lists in that site. If the user has + never interacted with the site or been picked in a People field, they are not + available in the Graph API to pick from. + """ + # Ensure the required environment variable is set + if "SP_BEARER_TOKEN" not in os.environ: + raise Exception("Error, could not find SP_BEARER_TOKEN in env") + + pass + + # Construct the URL for the ensure user endpoint + url = f"{site_url}/_api/web/ensureuser" + + # Make the POST request + response = requests.post( + url, + headers={ + "Authorization": "Bearer " + os.environ["SP_BEARER_TOKEN"], + "Accept": "application/json;odata=verbose;charset=utf-8", + "Content-Type": "application/json;odata=verbose;charset=utf-8", + }, + json={"logonName": logon_name}, + timeout=30, + ) + + # Check for errors in the response + if response.status_code != 200: + print( + f"Error {response.status_code}, could not ensure user: ", response.content + ) + raise Exception( + f"Error {response.status_code}, could not ensure user: " + + str(response.content) + ) + + # Return the JSON response + return response.json() diff --git a/requirements.txt b/requirements.txt index f229360..278bb71 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,4 @@ requests +pyopenssl +pyjwt +cryptography diff --git a/setup.py b/setup.py index 3f4bc68..aaa97cf 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name="grafap", - version="0.1.1", + version="0.1.2", description="Python package that acts as a wrapper for the Microsoft Graph API.", long_description=long_description, long_description_content_type="text/markdown", diff --git a/tests/test.py b/tests/test.py index 523f264..61a62b7 100644 --- a/tests/test.py +++ b/tests/test.py @@ -10,6 +10,9 @@ from grafap import * -sites = grafap.get_sp_sites() +res = grafap.ensure_sp_user( + "SITE URL", + "email@domain.com", +) pass