Skip to content

Commit

Permalink
Added ensure user endpoint, upped version
Browse files Browse the repository at this point in the history
Makes use of sharepoint REST API which requires a client certificate.
  • Loading branch information
JMaynor committed Aug 14, 2024
1 parent 76b8e44 commit b618b7e
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 15 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://graph.microsoft.com/.default> unless using more fine-grained permissions. |
| No | SP_SITE | Base Site URL you're interacting with. Should be <https://DOMAIN.sharepoint.com/> |
| No | SP_SCOPES | Scopes for sharepoint rest API. Should look like <https://{tenant name}.sharepoint.com/.default> |
| No | SP_LOGIN_BASE_URL | Should be <https://login.microsoftonline.com/> |
| 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

Expand Down
70 changes: 57 additions & 13 deletions grafap/auth.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
)
Expand All @@ -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"])
44 changes: 44 additions & 0 deletions grafap/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
requests
pyopenssl
pyjwt
cryptography
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 4 additions & 1 deletion tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

from grafap import *

sites = grafap.get_sp_sites()
res = grafap.ensure_sp_user(
"SITE URL",
"email@domain.com",
)

pass

0 comments on commit b618b7e

Please sign in to comment.