From cffd268e672f8e5b6c2b43ff395536c6c3a4ed61 Mon Sep 17 00:00:00 2001 From: Ray Lyon <36998292+skoobasteeve@users.noreply.github.com> Date: Fri, 4 Aug 2023 21:30:04 +0000 Subject: [PATCH] initial working package --- .gitignore | 166 +++++++++++++++++- Lambda/Dockerfile | 24 --- Lambda/lambda_function.py | 139 --------------- Lambda/requirements.txt | 3 - pyproject.toml | 22 +++ src/successfactors_auth/__init__.py | 0 sf_auth.py => src/successfactors_auth/auth.py | 7 +- .../successfactors_auth/sf_saml_template.xml | 0 8 files changed, 191 insertions(+), 170 deletions(-) delete mode 100644 Lambda/Dockerfile delete mode 100644 Lambda/lambda_function.py delete mode 100644 Lambda/requirements.txt create mode 100644 pyproject.toml create mode 100644 src/successfactors_auth/__init__.py rename sf_auth.py => src/successfactors_auth/auth.py (97%) rename sf_saml_template.xml => src/successfactors_auth/sf_saml_template.xml (100%) diff --git a/.gitignore b/.gitignore index c59f099..f665a33 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,165 @@ -sf_private_key.pem \ No newline at end of file +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/ \ No newline at end of file diff --git a/Lambda/Dockerfile b/Lambda/Dockerfile deleted file mode 100644 index f03302f..0000000 --- a/Lambda/Dockerfile +++ /dev/null @@ -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" ] \ No newline at end of file diff --git a/Lambda/lambda_function.py b/Lambda/lambda_function.py deleted file mode 100644 index 6b36022..0000000 --- a/Lambda/lambda_function.py +++ /dev/null @@ -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) - } diff --git a/Lambda/requirements.txt b/Lambda/requirements.txt deleted file mode 100644 index 3e0594e..0000000 --- a/Lambda/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -requests -lxml -xmlsec \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0546fae --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "successfactors_auth" +version = "0.0.2" +authors = [ + { name="Ray Lyon", email="ray@raylyon.net" }, +] +description = "Authenticate to the SuccessFactors API." +readme = "README.md" +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", +] + +[project.urls] +"Homepage" = "https://github.com/skoobasteeve/successfactors-python" +"Bug Tracker" = "https://github.com/skoobasteeve/successfactors-python/issues" \ No newline at end of file diff --git a/src/successfactors_auth/__init__.py b/src/successfactors_auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_auth.py b/src/successfactors_auth/auth.py similarity index 97% rename from sf_auth.py rename to src/successfactors_auth/auth.py index 72447df..0013379 100644 --- a/sf_auth.py +++ b/src/successfactors_auth/auth.py @@ -11,7 +11,6 @@ Bearer token. Derived from: https://github.com/mtrdesign/python-saml-example This script requires the following additional files: --SAML template XML -Private key file for a previously created SuccessFactors OAuth2 application Required packages: @@ -28,13 +27,13 @@ token = sf_auth.auth( SF_OAUTH_CLIENT_ID, SF_ADMIN_USER, SF_OAUTH_PRIVATE_KEY_FILE, - SAML_TEMPLATE_FILE ) ''' import base64 import requests import xmlsec +import importlib.resources from lxml import etree from datetime import datetime, timedelta @@ -103,7 +102,9 @@ def sign_assertion(xml_string, private_key): def auth(sf_url, sf_company_id, sf_oauth_client_id, - sf_admin_user, sf_saml_private_key, template_file): + sf_admin_user, sf_saml_private_key): + + template_file = "sf_saml_template.xml" # Generate SAML assertion XML from template file unsigned_assertion = generate_assertion(sf_url, diff --git a/sf_saml_template.xml b/src/successfactors_auth/sf_saml_template.xml similarity index 100% rename from sf_saml_template.xml rename to src/successfactors_auth/sf_saml_template.xml