Merge pull request #1 from skoobasteeve/python-package

Convert to Python package
This commit is contained in:
Ray Lyon
2023-08-05 11:16:13 -04:00
committed by GitHub
9 changed files with 267 additions and 216 deletions

165
.gitignore vendored Normal file
View File

@@ -0,0 +1,165 @@
sf_private_key.pem
test.py
auth_test.py
test/*
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

View File

@@ -1,24 +0,0 @@
FROM public.ecr.aws/lambda/python:3.9
# Install the function's dependencies using file requirements.txt
# from your project folder.
RUN yum install -y \
gcc \
gcc-c++ \
Cython \
make \
libxml2 \
libxslt \
xmlsec1 \
xmlsec1-devel \
xmlsec1-openssl \
libtool-ltdl-devel
COPY requirements.txt .
RUN pip3 install -r requirements.txt --target "${LAMBDA_TASK_ROOT}"
# Copy function code
COPY lambda_function/* ${LAMBDA_TASK_ROOT}/
# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "lambda_function.lambda_handler" ]

View File

@@ -1,139 +0,0 @@
#!/usr/bin/env python3
import base64
import json
import sys
import os
import requests
import xmlsec
import boto3
from lxml import etree
from datetime import datetime, timedelta
region = os.environ.get('AWS_REGION')
secret_id= os.environ.get('SECRET_ID')
template_file = 'sf_saml_template.xml'
private_keyfile = '/tmp/successfactors-private.pem'
def get_secret(region, secret_name, session):
client = session.client(
service_name='secretsmanager',
region_name=region
)
try:
get_secret_value_response = client.get_secret_value(
SecretId=secret_name
)
if 'SecretString' in get_secret_value_response:
secret = get_secret_value_response['SecretString']
except Exception as x:
print(x)
sys.exit(1)
return secret
def get_access_token(sf_url, company_id, client_id, assertion):
token_request = dict(
client_id=client_id,
company_id=company_id,
grant_type='urn:ietf:params:oauth:grant-type:saml2-bearer',
assertion=assertion
)
response = requests.post(f"{sf_url}/oauth/token", data=token_request)
token_data = response.json()
return token_data['access_token']
def generate_assertion(sf_root_url, user_id, client_id, template_file):
issue_instant = datetime.utcnow()
auth_instant = issue_instant
not_valid_before = issue_instant - timedelta(minutes=10)
not_valid_after = issue_instant + timedelta(minutes=10)
audience = 'www.successfactors.com'
context = dict(
issue_instant=issue_instant.isoformat(),
auth_instant=auth_instant.isoformat(),
not_valid_before=not_valid_before.isoformat(),
not_valid_after=not_valid_after.isoformat(),
sf_root_url=sf_root_url,
audience=audience,
user_id=user_id,
client_id=client_id,
session_id='mock_session_index',
)
saml_template = open(template_file).read()
return saml_template.format(**context)
def sign_assertion(xml_string, private_key):
key = xmlsec.Key.from_file(private_key, xmlsec.KeyFormat.PEM)
root = etree.fromstring(xml_string)
signature_node = xmlsec.tree.find_node(root, xmlsec.Node.SIGNATURE)
sign_context = xmlsec.SignatureContext()
sign_context.key = key
sign_context.sign(signature_node)
return etree.tostring(root)
def auth(sf_url, sf_company_id, sf_oauth_client_id,
sf_admin_user, sf_saml_private_key, template_file):
unsigned_assertion = generate_assertion(sf_url,
sf_admin_user,
sf_oauth_client_id,
template_file)
signed_assertion = sign_assertion(unsigned_assertion, sf_saml_private_key)
signed_assertion_b64 = base64.b64encode(signed_assertion).replace(b'\n', b'')
access_token = get_access_token(sf_url,
sf_company_id,
sf_oauth_client_id,
signed_assertion_b64)
return access_token
def lambda_handler(event, context):
session = boto3.session.Session()
print(event)
if event['rawPath'] == '/token':
body = json.loads(event['body'])
sf_url = body['odata_url']
sf_company_id = body['company_id']
sf_oauth_client_id = body['oauth_client_id']
sf_admin_user = body['admin_user']
private_key = get_secret(region,
secret_id,
session)
with open(private_keyfile, 'w') as f:
f.write(private_key)
token = auth(sf_url, sf_company_id, sf_oauth_client_id, sf_admin_user,
private_keyfile, template_file)
payload = {
"token": token
}
return {
'statusCode': 200,
'body': json.dumps(payload)
}

View File

@@ -1,3 +0,0 @@
requests
lxml
xmlsec

View File

@@ -1,49 +1,62 @@
# SuccessFactors + Python # SuccessFactors Auth
Authenticate with and use the SAP SuccessFactors API with Python. Authenticate with the SAP SuccessFactors API with OAuth2 and Python.
## Authentcation ## How to use
### How to use
1. Create an OAuth application in SuccessFactors. 1. Create an OAuth application in SuccessFactors.
2. Download the private key and copy the Client ID. 2. Download the private key and copy the Client ID.
3. Clone this repo and copy `sf_auth.py` and `sf_saml_template.xml` into your Python project directory. 3. Install the Python module:
4. Import `sf_auth` into your project. ``` shell
5. Install all the requirements listed in `requirements.txt` in this repo. pip install successfactors_auth
6. Call the `sf_auth.auth()` function in your Python project. You'll need to pass the following parameters: ```
4. Import `successfactors_auth` into your Python >=3.9 project.
5. Call the `successfactors_auth.auth()` function in your Python project. You'll need to pass the following parameters:
- `sf_url`: Base API url of your SuccessFactors instance, e.g. "https://api55.sapsf.eu". - `sf_url`: Base API url of your SuccessFactors instance, e.g. "https://api55.sapsf.eu".
- `sf_company_id`: SuccessFactors company ID. - `sf_company_id`: SuccessFactors company ID.
- `sf_oauth_client_id`: The Client ID for the OAuth application you created earlier. - `sf_oauth_client_id`: The Client ID for the OAuth application you created earlier.
- `sf_admin_user`: An admin user in SuccessFactors that has access to the OAuth application. - `sf_admin_user`: An admin user in SuccessFactors that has access to the OAuth application.
- `sf_saml_private_key`: Path to the private key file you downloaded when you created the OAuth application. - `sf_saml_private_key`: Path to the private key file you downloaded when you created the OAuth application.
- `template_file`: Path to the template file from this repo.
Example: ### Example
``` python
#!/usr/bin/env python
import requests ``` python
import sf_auth #!/usr/bin/env python
sf_url = 'https://your.base.url.com' import requests
sf_company_id = 'your-company-id' import successfactors_auth
sf_oauth_client_id = 'OAUTH-CLIENT-ID'
sf_admin_user = 'your_admin_user'
sf_saml_private_key = 'your_app_private_key.pem'
template_file = 'sf_saml_template.xml'
token = sf_auth.auth(sf_url, sf_company_id, sf_oauth_client_id, sf_admin_user, sf_saml_private_key, sf_saml_template) sf_url = 'https://your.base.url.com'
sf_company_id = 'your-company-id'
sf_oauth_client_id = 'your_app_client_id'
sf_admin_user = 'your_admin_user'
sf_saml_private_key = 'your_app_private_key.pem'
headers = { token = successfactors_auth.auth(
"Accept: application/json", sf_url,
f"Authorization: {token}" sf_company_id,
} sf_oauth_client_id,
sf_admin_user,
sf_saml_private_key,
sf_saml_template
)
request = requests.get(f"{sf_url}/User", headers=headers) headers = {
"Accept: application/json",
f"Authorization: {token}"
}
print(request.json()) request = requests.get(f"{sf_url}/User", headers=headers)
``` user = request.json()
### Using as an AWS Lambda function w/ API Gateway print(user)
```
Coming soon... ## Background
I wrote this module because I was forced to deal with the *horrific* SAP SuccessFactors API at my job, and I wanted to make sure other devs/sysadmins wouldn't have to feel the pain that I felt.
Once you get authenticated, getting the information you want is a whole new level of suffering. I hope to publish some more examples in the form of a blog post or docs in this repo.
## Contributing
All contributions welcome! Feel free to file an [issue](https://github.com/skoobasteeve/successfactors_auth/issues) or open a [pull request](https://github.com/skoobasteeve/successfactors_auth/pulls).

22
pyproject.toml Normal file
View File

@@ -0,0 +1,22 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "successfactors_auth"
version = "0.0.5"
authors = [
{ name="Ray Lyon", email="ray@raylyon.net" },
]
description = "Authenticate to the SuccessFactors API."
readme = "README.md"
requires-python = ">=3.9"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
]
[project.urls]
"Homepage" = "https://github.com/skoobasteeve/successfactors_auth"
"Bug Tracker" = "https://github.com/skoobasteeve/successfactors_auth/issues"

View File

View File

@@ -10,39 +10,44 @@ Bearer token.
Derived from: https://github.com/mtrdesign/python-saml-example Derived from: https://github.com/mtrdesign/python-saml-example
This script requires the following additional files: This module requires the following additional files:
-SAML template XML
-Private key file for a previously created SuccessFactors OAuth2 application -Private key file for a previously created SuccessFactors OAuth2 application
Required packages: Required packages:
pip install requests lxml xmlsec requests
lxml
xmlsec
Example: Example:
#!/usr/bin/env python3 #!/usr/bin/env python3
import sf_auth import successfactors_auth
token = sf_auth.auth( token = successfactors_auth.auth(
SF_URL, SF_URL,
SF_COMPANY_ID, SF_COMPANY_ID,
SF_OAUTH_CLIENT_ID, SF_OAUTH_CLIENT_ID,
SF_ADMIN_USER, SF_ADMIN_USER,
SF_OAUTH_PRIVATE_KEY_FILE, SF_OAUTH_PRIVATE_KEY_FILE,
SAML_TEMPLATE_FILE
) )
''' '''
import base64 import base64
from datetime import datetime, timedelta
from importlib import resources as impresources
import requests import requests
import xmlsec import xmlsec
from lxml import etree from lxml import etree
from datetime import datetime, timedelta
from . import templates
# Send POST request to SuccessFactors containing the generated def get_access_token(sf_url: str, company_id: str, client_id: str,
# SAML assertion and other details, then receive a token in response assertion: str) -> str:
def get_access_token(sf_url, company_id, client_id, assertion): """
Send POST request to SuccessFactors containing the generated
SAML assertion and other details, then receive a token in response
"""
# Request body # Request body
token_request = dict( token_request = dict(
client_id=client_id, client_id=client_id,
@@ -50,13 +55,17 @@ def get_access_token(sf_url, company_id, client_id, assertion):
grant_type='urn:ietf:params:oauth:grant-type:saml2-bearer', grant_type='urn:ietf:params:oauth:grant-type:saml2-bearer',
assertion=assertion assertion=assertion
) )
response = requests.post(f"{sf_url}/oauth/token", data=token_request) response = requests.post(f"{sf_url}/oauth/token", data=token_request,
timeout=15)
token_data = response.json() token_data = response.json()
return token_data['access_token'] return token_data['access_token']
# Generate SAML assertion from the template XML def generate_assertion(sf_root_url: str, user_id: str, client_id: str,
def generate_assertion(sf_root_url, user_id, client_id, template_file): template_file: str) -> str:
"""
Generate SAML assertion from the template XML
"""
# Calculate valid time values for the assertion's validity # Calculate valid time values for the assertion's validity
issue_instant = datetime.utcnow() issue_instant = datetime.utcnow()
auth_instant = issue_instant auth_instant = issue_instant
@@ -78,14 +87,16 @@ def generate_assertion(sf_root_url, user_id, client_id, template_file):
session_id='mock_session_index', session_id='mock_session_index',
) )
# Open the template file # Open the template file
saml_template = open(template_file).read() saml_template = open(template_file, encoding="utf-8").read()
# Fill the values into the template and return in # Fill the values into the template and return in
return saml_template.format(**context) return saml_template.format(**context)
# Sign the SAML assertion using a private key file def sign_assertion(xml_string: str, private_key: str) -> str:
def sign_assertion(xml_string, private_key): """
Sign the SAML assertion using a private key file
"""
# Import key file # Import key file
key = xmlsec.Key.from_file(private_key, xmlsec.KeyFormat.PEM) key = xmlsec.Key.from_file(private_key, xmlsec.KeyFormat.PEM)
@@ -102,8 +113,14 @@ def sign_assertion(xml_string, private_key):
return etree.tostring(root) return etree.tostring(root)
def auth(sf_url, sf_company_id, sf_oauth_client_id, def auth(sf_url: str, sf_company_id: str, sf_oauth_client_id: str,
sf_admin_user, sf_saml_private_key, template_file): sf_admin_user: str, sf_saml_private_key: str) -> str:
"""
Request an API access token by generating a signed SAML assertion
and using it to authenticate with SuccessFactors.
"""
template_file = impresources.files(templates) / 'sf_saml_template.xml'
# Generate SAML assertion XML from template file # Generate SAML assertion XML from template file
unsigned_assertion = generate_assertion(sf_url, unsigned_assertion = generate_assertion(sf_url,