18 Commits

Author SHA1 Message Date
Ray Lyon
51949082d2 Merge pull request #2 from skoobasteeve/0.0.10
update documentation
2023-08-08 11:40:36 -04:00
Ray Lyon
e6d16cb6b1 update documentation 2023-08-08 15:39:50 +00:00
Ray Lyon
7f175514e6 Update README.md 2023-08-07 17:09:50 -04:00
Ray Lyon
3fcd1ae4c6 Update python-publish.yml 2023-08-07 17:08:13 -04:00
Ray Lyon
416f287ddc fix workflow version tag 2023-08-07 17:04:22 -04:00
Ray Lyon
4090dec44d rev to 0.0.9 2023-08-07 20:56:49 +00:00
Ray Lyon
7302a4a4a3 rename functions for clarity 2023-08-07 20:56:38 +00:00
Ray Lyon
2a2e07f18a make imports simpler 2023-08-07 20:56:27 +00:00
Ray Lyon
556f4cb736 update README 2023-08-07 20:56:07 +00:00
Ray Lyon
761cfea9a6 Create python-publish.yml 2023-08-07 16:20:17 -04:00
Ray Lyon
5c0d96869d add dependencies 2023-08-05 15:53:50 +00:00
Ray Lyon
00e095c0e7 fix example code 2023-08-05 15:31:15 +00:00
Ray Lyon
283dc417f3 Merge pull request #1 from skoobasteeve/python-package
Convert to Python package
2023-08-05 11:16:13 -04:00
Ray Lyon
fc7b910a9d update repo link 2023-08-05 15:14:59 +00:00
Ray Lyon
d5dd13bd09 rev 2023-08-05 15:04:41 +00:00
Ray Lyon
62ffe0d491 update readme 2023-08-05 15:03:00 +00:00
Ray Lyon
75b0fedbd5 linting 2023-08-05 14:43:00 +00:00
Ray Lyon
ad9a5b7e0a include template in package 2023-08-05 14:27:34 +00:00
6 changed files with 144 additions and 58 deletions

40
.github/workflows/python-publish.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Upload Python Package
on:
release:
types: [published]
workflow_dispatch:
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}

View File

@@ -1,49 +1,73 @@
# 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 ## Dependencies
### How to use - xmlsec
- requests
## System Requirements
- libxml2 >= 2.9.1
- libxmlsec1 >= 1.2.18
## 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: ```
Depending on your OS, you may need to install additional system packages, see [xmlsec documentation](https://pypi.org/project/xmlsec/).
**Note for macOS users:** There is a bug that prevents you from installing xmlsec with Homebrew, currently tracked in a [Github issue](https://github.com/xmlsec/python-xmlsec/issues/254). There are some workaround you can try, but in the mean time it may be easier to install within a container or VM.
4. Import `successfactors_auth` into your Python >=3.9 project.
5. Call the `successfactors_auth.get_token()` 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 as sf
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 = 'app_private_key_file.pem'
headers = { token = sf.get_token(
"Accept: application/json", sf_company_id=company_id,
f"Authorization: {token}" sf_oauth_client_id=oauth_client_id,
} sf_admin_user=admin_api_user,
sf_saml_private_key=oauth_private_key,
sf_url=url
)
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).

View File

@@ -3,20 +3,25 @@ requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[project] [project]
name = "successfactors_auth" name = "successfactors-auth"
version = "0.0.2" version = "0.0.10"
authors = [ authors = [
{ name="Ray Lyon", email="ray@raylyon.net" }, { name="Ray Lyon", email="ray@raylyon.net" },
] ]
description = "Authenticate to the SuccessFactors API." description = "Authenticate to the SuccessFactors API."
readme = "README.md" readme = "README.md"
requires-python = ">=3.8" requires-python = ">=3.9"
classifiers = [ classifiers = [
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"License :: OSI Approved :: Apache Software License", "License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent", "Operating System :: OS Independent",
] ]
dependencies = [
"requests",
"lxml",
"xmlsec",
]
[project.urls] [project.urls]
"Homepage" = "https://github.com/skoobasteeve/successfactors-python" "Homepage" = "https://github.com/skoobasteeve/successfactors_auth"
"Bug Tracker" = "https://github.com/skoobasteeve/successfactors-python/issues" "Bug Tracker" = "https://github.com/skoobasteeve/successfactors_auth/issues"

View File

@@ -0,0 +1 @@
from .auth import get_token

View File

@@ -10,18 +10,20 @@ 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:
-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,
@@ -31,17 +33,21 @@ token = sf_auth.auth(
''' '''
import base64 import base64
from datetime import datetime, timedelta
from importlib import resources as impresources
import requests import requests
import xmlsec import xmlsec
import importlib.resources
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 request_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,
@@ -49,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
@@ -77,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)
@@ -101,10 +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 get_token(sf_url: str, sf_company_id: str, sf_oauth_client_id: str,
sf_admin_user, sf_saml_private_key): 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 = "sf_saml_template.xml" 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,
@@ -119,9 +135,9 @@ def auth(sf_url, sf_company_id, sf_oauth_client_id,
signed_assertion_b64 = base64.b64encode(signed_assertion).replace(b'\n', b'') signed_assertion_b64 = base64.b64encode(signed_assertion).replace(b'\n', b'')
# Request the API token from SuccessFactors via a POST request # Request the API token from SuccessFactors via a POST request
access_token = get_access_token(sf_url, access_token = request_token(sf_url,
sf_company_id, sf_company_id,
sf_oauth_client_id, sf_oauth_client_id,
signed_assertion_b64) signed_assertion_b64)
return access_token return access_token