diff --git a/spiffworkflow-backend/migrations/versions/5c50ecf2c1cd_.py b/spiffworkflow-backend/migrations/versions/5c50ecf2c1cd_.py new file mode 100644 index 00000000..b02f0e76 --- /dev/null +++ b/spiffworkflow-backend/migrations/versions/5c50ecf2c1cd_.py @@ -0,0 +1,41 @@ +"""empty message + +Revision ID: 5c50ecf2c1cd +Revises: 64adf34a98db +Create Date: 2023-08-08 11:21:15.278625 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '5c50ecf2c1cd' +down_revision = '64adf34a98db' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('configuration', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('category', sa.String(length=255), nullable=True), + sa.Column('value', sa.JSON(), nullable=True), + sa.Column('updated_at_in_seconds', sa.Integer(), nullable=True), + sa.Column('created_at_in_seconds', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('configuration', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_configuration_category'), ['category'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('configuration', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_configuration_category')) + + op.drop_table('configuration') + # ### end Alembic commands ### diff --git a/spiffworkflow-backend/poetry.lock b/spiffworkflow-backend/poetry.lock index 506d1a7a..7a2d5cdc 100644 --- a/spiffworkflow-backend/poetry.lock +++ b/spiffworkflow-backend/poetry.lock @@ -1,9 +1,10 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. [[package]] name = "alembic" version = "1.10.3" description = "A database migration tool for SQLAlchemy." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -23,6 +24,7 @@ tz = ["python-dateutil"] name = "aniso8601" version = "9.0.1" description = "A library for parsing ISO 8601 strings." +category = "main" optional = false python-versions = "*" files = [ @@ -37,6 +39,7 @@ dev = ["black", "coverage", "isort", "pre-commit", "pyenchant", "pylint"] name = "apscheduler" version = "3.10.1" description = "In-process task scheduler with Cron-like capabilities" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -48,7 +51,7 @@ files = [ pytz = "*" setuptools = ">=0.7" six = ">=1.4.0" -tzlocal = ">=2.0,<3.dev0 || >=4.dev0" +tzlocal = ">=2.0,<3.0.0 || >=4.0.0" [package.extras] doc = ["sphinx", "sphinx-rtd-theme"] @@ -66,6 +69,7 @@ zookeeper = ["kazoo"] name = "attrs" version = "22.2.0" description = "Classes Without Boilerplate" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -84,6 +88,7 @@ tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy name = "bandit" version = "1.7.2" description = "Security oriented static analyser for python code." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -106,6 +111,7 @@ yaml = ["PyYAML"] name = "bcrypt" version = "4.0.1" description = "Modern password hashing for your software and your servers" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -140,6 +146,7 @@ typecheck = ["mypy"] name = "black" version = "22.12.0" description = "The uncompromising code formatter." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -174,6 +181,7 @@ uvloop = ["uvloop (>=0.15.2)"] name = "blinker" version = "1.6.2" description = "Fast, simple object-to-object and broadcast signaling" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -181,10 +189,23 @@ files = [ {file = "blinker-1.6.2.tar.gz", hash = "sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213"}, ] +[[package]] +name = "cachelib" +version = "0.10.2" +description = "A collection of cache libraries in the same API interface." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachelib-0.10.2-py3-none-any.whl", hash = "sha256:42d49f2fad9310dd946d7be73d46776bcd4d5fde4f49ad210cfdd447fbdfc346"}, + {file = "cachelib-0.10.2.tar.gz", hash = "sha256:593faeee62a7c037d50fc835617a01b887503f972fb52b188ae7e50e9cb69740"}, +] + [[package]] name = "certifi" version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -196,6 +217,7 @@ files = [ name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." +category = "main" optional = false python-versions = "*" files = [ @@ -272,6 +294,7 @@ pycparser = "*" name = "cfgv" version = "3.3.1" description = "Validate configuration and produce human readable error messages." +category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -283,6 +306,7 @@ files = [ name = "charset-normalizer" version = "3.1.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -367,6 +391,7 @@ files = [ name = "click" version = "8.1.3" description = "Composable command line interface toolkit" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -381,6 +406,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "clickclick" version = "20.10.2" description = "Click utility functions" +category = "main" optional = false python-versions = "*" files = [ @@ -396,6 +422,7 @@ PyYAML = ">=3.11" name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -407,6 +434,7 @@ files = [ name = "configparser" version = "5.3.0" description = "Updated configparser from stdlib for earlier Pythons." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -422,6 +450,7 @@ testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-chec name = "connexion" version = "2.14.1" description = "Connexion - API first applications with OpenAPI/Swagger and Flask" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -452,6 +481,7 @@ tests = ["MarkupSafe (>=0.23)", "aiohttp (>=2.3.10,<4)", "aiohttp-jinja2 (>=0.14 name = "coverage" version = "6.5.0" description = "Code coverage measurement for Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -517,6 +547,7 @@ toml = ["tomli"] name = "cryptography" version = "41.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -562,6 +593,7 @@ test-randomorder = ["pytest-randomly"] name = "dateparser" version = "1.1.8" description = "Date parsing library designed to parse dates from HTML pages" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -584,6 +616,7 @@ langdetect = ["langdetect"] name = "distlib" version = "0.3.6" description = "Distribution utilities" +category = "dev" optional = false python-versions = "*" files = [ @@ -595,6 +628,7 @@ files = [ name = "dparse" version = "0.6.2" description = "A parser for Python dependency files" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -614,6 +648,7 @@ pipenv = ["pipenv"] name = "exceptiongroup" version = "1.1.1" description = "Backport of PEP 654 (exception groups)" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -628,6 +663,7 @@ test = ["pytest (>=6)"] name = "execnet" version = "1.9.0" description = "execnet: rapid multi-Python deployment" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -642,6 +678,7 @@ testing = ["pre-commit"] name = "filelock" version = "3.11.0" description = "A platform independent file lock." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -657,6 +694,7 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.2)", "diff-cover (>=7.5)", "p name = "flask" version = "2.2.5" description = "A simple framework for building complex web applications." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -678,6 +716,7 @@ dotenv = ["python-dotenv"] name = "flask-admin" version = "1.6.1" description = "Simple and extensible admin interface framework for Flask" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -697,6 +736,7 @@ azure = ["azure-storage-blob"] name = "flask-bcrypt" version = "1.0.1" description = "Brcrypt hashing for Flask." +category = "main" optional = false python-versions = "*" files = [ @@ -712,6 +752,7 @@ Flask = "*" name = "flask-cors" version = "3.0.10" description = "A Flask extension adding a decorator for CORS support" +category = "main" optional = false python-versions = "*" files = [ @@ -727,6 +768,7 @@ Six = "*" name = "flask-jwt-extended" version = "4.4.4" description = "Extended JWT integration with Flask" +category = "main" optional = false python-versions = ">=3.7,<4" files = [ @@ -746,6 +788,7 @@ asymmetric-crypto = ["cryptography (>=3.3.1)"] name = "flask-mail" version = "0.9.1" description = "Flask extension for sending email" +category = "main" optional = false python-versions = "*" files = [ @@ -760,6 +803,7 @@ Flask = "*" name = "flask-marshmallow" version = "0.15.0" description = "Flask + marshmallow for beautiful APIs" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -782,6 +826,7 @@ tests = ["flask-sqlalchemy (>=3.0.0)", "marshmallow-sqlalchemy (>=0.28.2)", "moc name = "flask-migrate" version = "4.0.4" description = "SQLAlchemy database migrations for Flask applications using Alembic." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -794,10 +839,29 @@ alembic = ">=1.9.0" Flask = ">=0.9" Flask-SQLAlchemy = ">=1.0" +[[package]] +name = "flask-oauthlib" +version = "0.9.6" +description = "OAuthlib for Flask" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "Flask-OAuthlib-0.9.6.tar.gz", hash = "sha256:5bb79c8a8e670c2eb4cb553dfc3283b6c8d1202f674934676dc173cee94fe39c"}, + {file = "Flask_OAuthlib-0.9.6-py3-none-any.whl", hash = "sha256:a5c3b62959aa1922470a62b6ebf4273b75f1c29561a7eb4a69cde85d45a1d669"}, +] + +[package.dependencies] +cachelib = "*" +Flask = "*" +oauthlib = ">=1.1.2,<2.0.3 || >2.0.3,<2.0.4 || >2.0.4,<2.0.5 || >2.0.5,<3.0.0" +requests-oauthlib = ">=0.6.2,<1.2.0" + [[package]] name = "flask-restful" version = "0.3.9" description = "Simple framework for creating REST APIs" +category = "main" optional = false python-versions = "*" files = [ @@ -814,10 +878,27 @@ six = ">=1.3.0" [package.extras] docs = ["sphinx"] +[[package]] +name = "flask-session" +version = "0.5.0" +description = "Server-side session support for Flask" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Flask-Session-0.5.0.tar.gz", hash = "sha256:190875e6aebf2953c6803d42379ef3b934bc209ef8ef006f97aecb08f5aaeb86"}, + {file = "flask_session-0.5.0-py3-none-any.whl", hash = "sha256:1619bcbc16f04f64e90f8e0b17145ba5c9700090bb1294e889956c1282d58631"}, +] + +[package.dependencies] +cachelib = "*" +flask = ">=2.2" + [[package]] name = "flask-simple-crypt" version = "0.3.3" description = "Flask extension based on simple-crypt that allows simple, secure encryption and decryption for Python." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -833,6 +914,7 @@ pycryptodome = "*" name = "flask-sqlalchemy" version = "3.0.3" description = "Add SQLAlchemy support to your Flask application." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -848,6 +930,7 @@ SQLAlchemy = ">=1.4.18" name = "gitdb" version = "4.0.10" description = "Git Object Database" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -862,6 +945,7 @@ smmap = ">=3.0.1,<6" name = "gitpython" version = "3.1.31" description = "GitPython is a Python library used to interact with Git repositories" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -876,6 +960,7 @@ gitdb = ">=4.0.1,<5" name = "greenlet" version = "2.0.2" description = "Lightweight in-process concurrent programming" +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" files = [ @@ -949,6 +1034,7 @@ test = ["objgraph", "psutil"] name = "gunicorn" version = "20.1.0" description = "WSGI HTTP Server for UNIX" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -969,6 +1055,7 @@ tornado = ["tornado (>=0.2)"] name = "identify" version = "2.5.22" description = "File identification library for Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -983,6 +1070,7 @@ license = ["ukkonen"] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -994,6 +1082,7 @@ files = [ name = "inflection" version = "0.5.1" description = "A port of Ruby on Rails inflector to Python" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1005,6 +1094,7 @@ files = [ name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1016,6 +1106,7 @@ files = [ name = "itsdangerous" version = "2.1.2" description = "Safely pass data to untrusted environments and back." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1027,6 +1118,7 @@ files = [ name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1044,6 +1136,7 @@ i18n = ["Babel (>=2.7)"] name = "jsonschema" version = "4.17.3" description = "An implementation of JSON Schema validation for Python" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1063,6 +1156,7 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- name = "lxml" version = "4.9.2" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" files = [ @@ -1155,6 +1249,7 @@ source = ["Cython (>=0.29.7)"] name = "mako" version = "1.2.4" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1174,6 +1269,7 @@ testing = ["pytest"] name = "markupsafe" version = "2.1.2" description = "Safely add untrusted strings to HTML/XML markup." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1233,6 +1329,7 @@ files = [ name = "marshmallow" version = "3.19.0" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1253,6 +1350,7 @@ tests = ["pytest", "pytz", "simplejson"] name = "marshmallow-enum" version = "1.5.1" description = "Enum field for Marshmallow" +category = "main" optional = false python-versions = "*" files = [ @@ -1267,6 +1365,7 @@ marshmallow = ">=2.0.0" name = "marshmallow-sqlalchemy" version = "0.29.0" description = "SQLAlchemy integration with the marshmallow (de)serialization library" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1289,6 +1388,7 @@ tests = ["pytest", "pytest-lazy-fixture (>=0.6.2)"] name = "mypy" version = "1.2.0" description = "Optional static typing for Python" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1335,6 +1435,7 @@ reports = ["lxml"] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1346,6 +1447,7 @@ files = [ name = "mysqlclient" version = "2.2.0" description = "Python interface to MySQL" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1362,6 +1464,7 @@ files = [ name = "nodeenv" version = "1.7.0" description = "Node.js virtual environment builder" +category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ @@ -1372,10 +1475,29 @@ files = [ [package.dependencies] setuptools = "*" +[[package]] +name = "oauthlib" +version = "2.1.0" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "oauthlib-2.1.0-py2.py3-none-any.whl", hash = "sha256:d883b36b21a6ad813953803edfa563b1b579d79ca758fe950d1bc9e8b326025b"}, + {file = "oauthlib-2.1.0.tar.gz", hash = "sha256:ac35665a61c1685c56336bda97d5eefa246f1202618a1d6f34fccb1bdd404162"}, +] + +[package.extras] +rsa = ["cryptography"] +signals = ["blinker"] +signedtoken = ["cryptography", "pyjwt (>=1.0.0)"] +test = ["blinker", "cryptography", "mock", "nose", "pyjwt (>=1.0.0)", "unittest2"] + [[package]] name = "packaging" version = "21.3" description = "Core utilities for Python packages" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1390,6 +1512,7 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" name = "pathspec" version = "0.11.1" description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1401,6 +1524,7 @@ files = [ name = "pbr" version = "5.11.1" description = "Python Build Reasonableness" +category = "dev" optional = false python-versions = ">=2.6" files = [ @@ -1412,6 +1536,7 @@ files = [ name = "platformdirs" version = "3.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1427,6 +1552,7 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest- name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1442,6 +1568,7 @@ testing = ["pytest", "pytest-benchmark"] name = "pre-commit" version = "2.21.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1460,6 +1587,7 @@ virtualenv = ">=20.10.0" name = "pre-commit-hooks" version = "4.4.0" description = "Some out-of-the-box hooks for pre-commit." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1475,6 +1603,7 @@ tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} name = "prometheus-client" version = "0.16.0" description = "Python client for the Prometheus monitoring system." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1489,6 +1618,7 @@ twisted = ["twisted"] name = "prometheus-flask-exporter" version = "0.22.3" description = "Prometheus metrics exporter for Flask" +category = "main" optional = false python-versions = "*" files = [ @@ -1504,6 +1634,7 @@ prometheus-client = "*" name = "psycopg2" version = "2.9.6" description = "psycopg2 - Python-PostgreSQL Database Adapter" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1526,6 +1657,7 @@ files = [ name = "pycparser" version = "2.21" description = "C parser in Python" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1537,6 +1669,7 @@ files = [ name = "pycryptodome" version = "3.17" description = "Cryptographic library for Python" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1579,6 +1712,7 @@ files = [ name = "pygments" version = "2.15.1" description = "Pygments is a syntax highlighting package written in Python." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1593,6 +1727,7 @@ plugins = ["importlib-metadata"] name = "pyjwt" version = "2.6.0" description = "JSON Web Token implementation in Python" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1610,6 +1745,7 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] name = "pyparsing" version = "3.0.9" description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "main" optional = false python-versions = ">=3.6.8" files = [ @@ -1624,6 +1760,7 @@ diagrams = ["jinja2", "railroad-diagrams"] name = "pyrsistent" version = "0.19.3" description = "Persistent/Functional/Immutable data structures" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1660,6 +1797,7 @@ files = [ name = "pytest" version = "7.4.0" description = "pytest: simple powerful testing with Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1682,6 +1820,7 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pytest-flask" version = "1.2.0" description = "A set of py.test fixtures to test Flask applications." +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1701,6 +1840,7 @@ docs = ["Sphinx", "sphinx-rtd-theme"] name = "pytest-flask-sqlalchemy" version = "1.1.0" description = "A pytest plugin for preserving test isolation in Flask-SQlAlchemy using database transactions." +category = "dev" optional = false python-versions = "*" files = [ @@ -1722,6 +1862,7 @@ tests = ["psycopg2-binary", "pytest (>=6.0.1)", "pytest-postgresql (>=2.4.0,<4.0 name = "pytest-mock" version = "3.10.0" description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1739,6 +1880,7 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] name = "pytest-random-order" version = "1.1.0" description = "Randomise the order in which pytest tests are run with some control over the randomness" +category = "dev" optional = false python-versions = ">=3.5.0" files = [ @@ -1753,6 +1895,7 @@ pytest = ">=3.0.0" name = "pytest-xdist" version = "3.3.1" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1773,6 +1916,7 @@ testing = ["filelock"] name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -1787,6 +1931,7 @@ six = ">=1.5" name = "pytz" version = "2022.7.1" description = "World timezone definitions, modern and historical" +category = "main" optional = false python-versions = "*" files = [ @@ -1798,6 +1943,7 @@ files = [ name = "pytz-deprecation-shim" version = "0.1.0.post0" description = "Shims to make deprecation of pytz easier" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ @@ -1812,6 +1958,7 @@ tzdata = {version = "*", markers = "python_version >= \"3.6\""} name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1861,6 +2008,7 @@ files = [ name = "regex" version = "2023.3.23" description = "Alternative regular expression module, to replace re." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1930,6 +2078,7 @@ files = [ name = "requests" version = "2.31.0" description = "Python HTTP for Humans." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1947,10 +2096,30 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "requests-oauthlib" +version = "1.1.0" +description = "OAuthlib authentication support for Requests." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-oauthlib-1.1.0.tar.gz", hash = "sha256:eabd8eb700ebed81ba080c6ead96d39d6bdc39996094bd23000204f6965786b0"}, + {file = "requests_oauthlib-1.1.0-py2.py3-none-any.whl", hash = "sha256:be76f2bb72ca5525998e81d47913e09b1ca8b7957ae89b46f787a79e68ad5e61"}, +] + +[package.dependencies] +oauthlib = ">=2.1.0,<3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=2.1.0,<3.0.0)"] + [[package]] name = "restrictedpython" version = "6.1" description = "RestrictedPython is a defined subset of the Python language which allows to provide a program input into a trusted environment." +category = "main" optional = false python-versions = ">=3.6, <3.12" files = [ @@ -1966,6 +2135,7 @@ test = ["pytest", "pytest-mock"] name = "ruamel-yaml" version = "0.17.21" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +category = "dev" optional = false python-versions = ">=3" files = [ @@ -1984,6 +2154,7 @@ jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] name = "ruamel-yaml-clib" version = "0.2.7" description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1994,8 +2165,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win32.whl", hash = "sha256:763d65baa3b952479c4e972669f679fe490eee058d5aa85da483ebae2009d231"}, {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:1a6391a7cabb7641c32517539ca42cf84b87b667bad38b78d4d42dd23e957c81"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:9c7617df90c1365638916b98cdd9be833d31d337dbcd722485597b43c4a215bf"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_12_6_arm64.whl", hash = "sha256:721bc4ba4525f53f6a611ec0967bdcee61b31df5a56801281027a3a6d1c2daf5"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win32.whl", hash = "sha256:f6d3d39611ac2e4f62c3128a9eed45f19a6608670c5a2f4f07f24e8de3441d38"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:da538167284de58a52109a9b89b8f6a53ff8437dd6dc26d33b57bf6699153122"}, @@ -2030,6 +2200,7 @@ files = [ name = "ruff" version = "0.0.270" description = "An extremely fast Python linter, written in Rust." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2056,6 +2227,7 @@ files = [ name = "safety" version = "2.3.5" description = "Checks installed dependencies for known vulnerabilities and licenses." +category = "dev" optional = false python-versions = "*" files = [ @@ -2079,6 +2251,7 @@ gitlab = ["python-gitlab (>=1.3.0)"] name = "sentry-sdk" version = "1.19.1" description = "Python client for Sentry (https://sentry.io)" +category = "main" optional = false python-versions = "*" files = [ @@ -2120,6 +2293,7 @@ tornado = ["tornado (>=5)"] name = "setuptools" version = "65.7.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2136,6 +2310,7 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( name = "simplejson" version = "3.19.1" description = "Simple, fast, extensible JSON encoder/decoder for Python" +category = "main" optional = false python-versions = ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -2230,6 +2405,7 @@ files = [ name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -2241,6 +2417,7 @@ files = [ name = "smmap" version = "5.0.0" description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2252,6 +2429,7 @@ files = [ name = "spiff-element-units" version = "0.3.0" description = "" +category = "main" optional = false python-versions = ">=3.9" files = [ @@ -2272,6 +2450,7 @@ files = [ name = "SpiffWorkflow" version = "2.0.0rc0" description = "A workflow framework and BPMN/DMN Processor" +category = "main" optional = false python-versions = "*" files = [] @@ -2291,6 +2470,7 @@ resolved_reference = "0adfc8cbaec80d36f98a4136434e960f666fcfe2" name = "sqlalchemy" version = "2.0.9" description = "Database Abstraction Library" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2338,7 +2518,7 @@ files = [ ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\""} +greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} typing-extensions = ">=4.2.0" [package.extras] @@ -2368,6 +2548,7 @@ sqlcipher = ["sqlcipher3-binary"] name = "sqlalchemy-stubs" version = "0.4" description = "SQLAlchemy stubs and mypy plugin" +category = "main" optional = false python-versions = "*" files = [] @@ -2387,6 +2568,7 @@ resolved_reference = "d1176931684ce5b327539cc9567d4a1cd8ef1efd" name = "stevedore" version = "5.0.0" description = "Manage dynamic plugins for Python applications" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2401,6 +2583,7 @@ pbr = ">=2.0.0,<2.1.0 || >2.1.0" name = "swagger-ui-bundle" version = "0.0.9" description = "swagger_ui_bundle - swagger-ui files in a pip package" +category = "main" optional = false python-versions = "*" files = [ @@ -2415,6 +2598,7 @@ Jinja2 = ">=2.0" name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -2426,6 +2610,7 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2437,6 +2622,7 @@ files = [ name = "typeguard" version = "3.0.2" description = "Run-time type checker for Python" +category = "dev" optional = false python-versions = ">=3.7.4" files = [ @@ -2455,6 +2641,7 @@ test = ["mypy (>=0.991)", "pytest (>=7)"] name = "types-click" version = "7.1.8" description = "Typing stubs for click" +category = "dev" optional = false python-versions = "*" files = [ @@ -2466,6 +2653,7 @@ files = [ name = "types-dateparser" version = "1.1.4.9" description = "Typing stubs for dateparser" +category = "dev" optional = false python-versions = "*" files = [ @@ -2477,6 +2665,7 @@ files = [ name = "types-flask" version = "1.1.6" description = "Typing stubs for Flask" +category = "dev" optional = false python-versions = "*" files = [ @@ -2493,6 +2682,7 @@ types-Werkzeug = "*" name = "types-jinja2" version = "2.11.9" description = "Typing stubs for Jinja2" +category = "dev" optional = false python-versions = "*" files = [ @@ -2507,6 +2697,7 @@ types-MarkupSafe = "*" name = "types-markupsafe" version = "1.1.10" description = "Typing stubs for MarkupSafe" +category = "dev" optional = false python-versions = "*" files = [ @@ -2518,6 +2709,7 @@ files = [ name = "types-pytz" version = "2022.7.1.2" description = "Typing stubs for pytz" +category = "dev" optional = false python-versions = "*" files = [ @@ -2529,6 +2721,7 @@ files = [ name = "types-pyyaml" version = "6.0.12.9" description = "Typing stubs for PyYAML" +category = "dev" optional = false python-versions = "*" files = [ @@ -2540,6 +2733,7 @@ files = [ name = "types-requests" version = "2.28.11.17" description = "Typing stubs for requests" +category = "dev" optional = false python-versions = "*" files = [ @@ -2554,6 +2748,7 @@ types-urllib3 = "<1.27" name = "types-urllib3" version = "1.26.25.10" description = "Typing stubs for urllib3" +category = "dev" optional = false python-versions = "*" files = [ @@ -2565,6 +2760,7 @@ files = [ name = "types-werkzeug" version = "1.0.9" description = "Typing stubs for Werkzeug" +category = "dev" optional = false python-versions = "*" files = [ @@ -2576,6 +2772,7 @@ files = [ name = "typing-extensions" version = "4.5.0" description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2587,6 +2784,7 @@ files = [ name = "tzdata" version = "2023.3" description = "Provider of IANA time zone data" +category = "main" optional = false python-versions = ">=2" files = [ @@ -2598,6 +2796,7 @@ files = [ name = "tzlocal" version = "4.3" description = "tzinfo object for the local timezone" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2616,6 +2815,7 @@ devenv = ["black", "check-manifest", "flake8", "pyroma", "pytest (>=4.3)", "pyte name = "urllib3" version = "1.26.15" description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ @@ -2632,6 +2832,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] name = "virtualenv" version = "20.21.0" description = "Virtual Python Environment builder" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2652,6 +2853,7 @@ test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess name = "werkzeug" version = "2.3.4" description = "The comprehensive WSGI web application library." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2669,6 +2871,7 @@ watchdog = ["watchdog (>=2.3)"] name = "wtforms" version = "3.0.1" description = "Form validation and rendering for Python web development." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2686,6 +2889,7 @@ email = ["email-validator"] name = "xdoctest" version = "1.1.1" description = "A rewrite of the builtin doctest module" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2714,4 +2918,4 @@ tests-strict = ["codecov (==2.0.15)", "pytest (==4.6.0)", "pytest (==4.6.0)", "p [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.12" -content-hash = "43d6e84c8ef9e0ea48876bb490f8486b6e25bff2ada6981c739e03b35ea3da6c" +content-hash = "6cbc9666f9045835a75dc38cdd9e02c0ea53c061b57162e7744572661527aa61" diff --git a/spiffworkflow-backend/pyproject.toml b/spiffworkflow-backend/pyproject.toml index 3a6f2da9..47b779b4 100644 --- a/spiffworkflow-backend/pyproject.toml +++ b/spiffworkflow-backend/pyproject.toml @@ -73,6 +73,8 @@ spiff-element-units = "^0.3.0" # mysqlclient lib is deemed better than the mysql-connector-python lib by sqlalchemy # https://docs.sqlalchemy.org/en/20/dialects/mysql.html#module-sqlalchemy.dialects.mysql.mysqlconnector mysqlclient = "^2.2.0" +flask-session = "^0.5.0" +flask-oauthlib = "^0.9.6" [tool.poetry.dev-dependencies] pytest = "^7.1.2" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 398376c5..75dc48d7 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -1876,6 +1876,64 @@ paths: schema: $ref: "#/components/schemas/ServiceTask" + /authentication/configuration: + get: + tags: + - Authentications + operationId: spiffworkflow_backend.routes.service_tasks_controller.authentication_configuration + summary: Gets authentication configurations + responses: + "200": + description: Authentication configuration + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceTask" + put: + operationId: spiffworkflow_backend.routes.service_tasks_controller.authentication_configuration_update + summary: Save the authentication configuration + tags: + - Authentication + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceTask" + responses: + "200": + description: Authentication configuration updated successfully + content: + application/json: + schema: + $ref: "#/components/schemas/OkTrue" + + /authentication_begin/{service}/{auth_method}: + parameters: + - name: service + in: path + required: true + description: The name of the service + schema: + type: string + - name: auth_method + in: path + required: true + description: The method + schema: + type: string + get: + tags: + - Authentications + operationId: spiffworkflow_backend.routes.service_tasks_controller.authentication_begin + summary: Begin an external authentication + responses: + "200": + description: All authentications + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceTask" + /authentication_callback/{service}/{auth_method}: parameters: - name: service @@ -1892,13 +1950,37 @@ paths: type: string - name: response in: query - required: true + required: false description: The response schema: type: string - name: token in: query - required: true + required: false + description: The response + schema: + type: string + - name: code + in: query + required: false + description: The response + schema: + type: string + - name: code_challenge + in: query + required: false + description: The response + schema: + type: string + - name: code_challenge_method + in: query + required: false + description: The response + schema: + type: string + - name: state + in: query + required: false description: The response schema: type: string diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/load_database_models.py b/spiffworkflow-backend/src/spiffworkflow_backend/load_database_models.py index 2e301fd1..fbf3bbcb 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/load_database_models.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/load_database_models.py @@ -85,5 +85,8 @@ from spiffworkflow_backend.models.typeahead import ( from spiffworkflow_backend.models.task_draft_data import ( TaskDraftDataModel, ) # noqa: F401 +from spiffworkflow_backend.models.configuration import ( + ConfigurationModel, +) # noqa: F401 add_listeners() diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/configuration.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/configuration.py new file mode 100644 index 00000000..82abe6e1 --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/configuration.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass + +from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel +from spiffworkflow_backend.models.db import db + + +@dataclass +class ConfigurationModel(SpiffworkflowBaseDBModel): + __tablename__ = "configuration" + + id: int = db.Column(db.Integer, primary_key=True) + category: str = db.Column(db.String(255), index=True) + value: dict = db.Column(db.JSON) + updated_at_in_seconds: int = db.Column(db.Integer) + created_at_in_seconds: int = db.Column(db.Integer) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/service_tasks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/service_tasks_controller.py index eaff4e54..2ac7134f 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/service_tasks_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/service_tasks_controller.py @@ -9,7 +9,9 @@ from flask import redirect from flask import request from flask.wrappers import Response +from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.routes.user import verify_token +from spiffworkflow_backend.services.oauth_service import OAuthService from spiffworkflow_backend.services.secret_service import SecretService from spiffworkflow_backend.services.service_task_service import ServiceTaskService @@ -21,8 +23,11 @@ def service_task_list() -> flask.wrappers.Response: def authentication_list() -> flask.wrappers.Response: available_authentications = ServiceTaskService.authentication_list() + available_v2_authentications = OAuthService.authentication_list() + response_json = { "results": available_authentications, + "resultsV2": available_v2_authentications, "connector_proxy_base_url": current_app.config["SPIFFWORKFLOW_BACKEND_CONNECTOR_PROXY_URL"], "redirect_url": f"{current_app.config['SPIFFWORKFLOW_BACKEND_URL']}/v1.0/authentication_callback", } @@ -30,11 +35,44 @@ def authentication_list() -> flask.wrappers.Response: return Response(json.dumps(response_json), status=200, mimetype="application/json") +def authentication_configuration() -> flask.wrappers.Response: + config = OAuthService.authentication_configuration() + + return Response(json.dumps(config), status=200, mimetype="application/json") + + +def authentication_configuration_update(body: dict) -> flask.wrappers.Response: + OAuthService.update_authentication_configuration(body) + return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") + + +def authentication_begin( + service: str, + auth_method: str, +) -> werkzeug.wrappers.Response: + token = request.args.get("token") + verify_token(token, force_run=True) + if not OAuthService.supported_service(service): + raise ApiError("unknown_authentication_service", f"Unknown authentication service: {service}", status_code=400) + remote_app = OAuthService.remote_app(service, token) + callback = f"{current_app.config['SPIFFWORKFLOW_BACKEND_URL']}/v1.0/authentication_callback/{service}/oauth" + return remote_app.authorize(callback=callback, _external=True) # type: ignore + + def authentication_callback( service: str, auth_method: str, ) -> werkzeug.wrappers.Response: - verify_token(request.args.get("token"), force_run=True) - response = request.args["response"] - SecretService.update_secret(f"{service}/{auth_method}", response, g.user.id, create_if_not_exists=True) + if OAuthService.supported_service(service): + token = OAuthService.token_from_state(request.args.get("state")) + verify_token(token, force_run=True) + remote_app = OAuthService.remote_app(service, token) + response = remote_app.authorized_response() + SecretService.update_secret( + f"{service}_{auth_method}", response["access_token"], g.user.id, create_if_not_exists=True + ) + else: + verify_token(request.args.get("token"), force_run=True) + response = request.args["response"] + SecretService.update_secret(f"{service}/{auth_method}", response, g.user.id, create_if_not_exists=True) return redirect(f"{current_app.config['SPIFFWORKFLOW_BACKEND_URL_FOR_FRONTEND']}/admin/configuration") diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py index 2d61d2db..d39e79df 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py @@ -242,6 +242,7 @@ class AuthorizationService: authentication_exclusion_list = [ "status", "test_raise_error", + "authentication_begin", "authentication_callback", "github_webhook_receive", ] diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/configuration_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/configuration_service.py new file mode 100644 index 00000000..0e0adbc8 --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/configuration_service.py @@ -0,0 +1,29 @@ +import json +from typing import Any + +from spiffworkflow_backend.models.configuration import ConfigurationModel +from spiffworkflow_backend.models.db import db + + +class ConfigurationService: + @staticmethod + def configuration_for_category(category: str) -> Any: + config = db.session.query(ConfigurationModel).filter(ConfigurationModel.category == category).first() + try: + value = config.value if config is not None else "" + return json.loads(value) # type: ignore + except Exception: + return {} + + @staticmethod + def update_configuration_for_category(category: str, new_config: dict[str, Any]) -> None: + config = db.session.query(ConfigurationModel).filter(ConfigurationModel.category == category).first() + if config is None: + config = ConfigurationModel(category=category) + if "value" in new_config: + config.value = new_config["value"] + else: + config.value = {} + + db.session.add(config) + db.session.commit() diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/oauth_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/oauth_service.py new file mode 100644 index 00000000..a4d0827b --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/oauth_service.py @@ -0,0 +1,66 @@ +import base64 +import json +from typing import Any + +from flask import Flask +from flask import session +from flask_oauthlib.client import OAuth # type: ignore +from spiffworkflow_backend.exceptions.api_error import ApiError +from spiffworkflow_backend.services.configuration_service import ConfigurationService +from spiffworkflow_backend.services.secret_service import SecretService + + +class OAuthService: + @classmethod + def authentication_list( + cls, + ) -> list[dict[str, Any]]: + return [{"id": f"{k}/OAuth", "parameters": []} for k in cls.authentication_configuration().keys()] + + @staticmethod + def authentication_configuration() -> Any: + return ConfigurationService.configuration_for_category("oauth") + + @staticmethod + def update_authentication_configuration(config: dict[str, Any]) -> None: + try: + _ = json.loads(config["value"]) + except Exception as e: + raise ApiError( + error_code="invalid_authentication_configuration", + message=f"The authentication configuration is not valid JSON. {e}", + ) from e + + return ConfigurationService.update_configuration_for_category("oauth", config) + + @classmethod + def supported_service(cls, service: str) -> bool: + return service in cls.authentication_configuration() + + @classmethod + def remote_app(cls, service: str, token: str | None) -> Any: + config = cls.authentication_configuration()[service] + + for k in ["consumer_key", "consumer_secret"]: + if k in config: + config[k] = SecretService.resolve_possibly_secret_value(config[k]) + + if token is not None: + state = base64.urlsafe_b64encode(bytes(token, "utf-8")) + config["request_token_params"]["state"] = state + + app = Flask(__name__) + oauth = OAuth(app) + remote_app = oauth.remote_app(service, **config) + + @remote_app.tokengetter + def get_token(token=None): # type: ignore + return session[f"{service}_token"] + + return remote_app + + @staticmethod + def token_from_state(state: str | None) -> str | None: + if state is not None: + return base64.urlsafe_b64decode(state).decode("utf-8") + return None diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/secret_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/secret_service.py index 03453945..225ce93e 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/secret_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/secret_service.py @@ -1,3 +1,6 @@ +import re + +import sentry_sdk from flask import current_app from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.models.db import db @@ -110,3 +113,15 @@ class SecretService: message=f"Cannot delete secret with key: {key}. Resource does not exist.", status_code=404, ) + + @classmethod + def resolve_possibly_secret_value(cls, value: str) -> str: + if "SPIFF_SECRET:" in value: + spiff_secret_match = re.match(r".*SPIFF_SECRET:(?P\w+).*", value) + if spiff_secret_match is not None: + spiff_variable_name = spiff_secret_match.group("variable_name") + secret = cls.get_secret(spiff_variable_name) + with sentry_sdk.start_span(op="task", description="decrypt_secret"): + decrypted_value = cls._decrypt(secret.value) + return re.sub(r"\bSPIFF_SECRET:\w+", decrypted_value, value) + return value diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/service_task_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/service_task_service.py index 6bd19926..554f8bfa 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/service_task_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/service_task_service.py @@ -1,5 +1,4 @@ import json -import re from typing import Any import requests @@ -40,14 +39,7 @@ class ServiceTaskDelegate: with open(full_path) as f: return f.read() - if "SPIFF_SECRET:" in value: - spiff_secret_match = re.match(r".*SPIFF_SECRET:(?P\w+).*", value) - if spiff_secret_match is not None: - spiff_variable_name = spiff_secret_match.group("variable_name") - secret = SecretService.get_secret(spiff_variable_name) - with sentry_sdk.start_span(op="task", description="decrypt_secret"): - decrypted_value = SecretService._decrypt(secret.value) - return re.sub(r"\bSPIFF_SECRET:\w+", decrypted_value, value) + return SecretService.resolve_possibly_secret_value(value) return value diff --git a/spiffworkflow-frontend/src/components/AuthenticationConfiguration.tsx b/spiffworkflow-frontend/src/components/AuthenticationConfiguration.tsx new file mode 100644 index 00000000..455a6f8c --- /dev/null +++ b/spiffworkflow-frontend/src/components/AuthenticationConfiguration.tsx @@ -0,0 +1,43 @@ +import Editor from '@monaco-editor/react'; +import { useEffect, useState } from 'react'; +import { Button } from '@carbon/react'; +import HttpService from '../services/HttpService'; + +export default function AuthenticationConfiguration() { + const [authConfig, setAuthConfig] = useState(''); + + useEffect(() => { + HttpService.makeCallToBackend({ + path: '/authentication/configuration', + successCallback: (newAuthConfig: string) => { + setAuthConfig(JSON.stringify(newAuthConfig, null, 4)); + }, + }); + }, []); + + const saveAuthConfig = () => { + HttpService.makeCallToBackend({ + path: '/authentication/configuration', + successCallback: () => { window.location.reload() }, + httpMethod: 'PUT', + postBody: { value: authConfig }, + }); + }; + + return ( + <> + +
+
+

Local Configuration

+
+ setAuthConfig(value || '')} + /> + + ); +} diff --git a/spiffworkflow-frontend/src/routes/AuthenticationList.tsx b/spiffworkflow-frontend/src/routes/AuthenticationList.tsx index 1a4a030b..ffe05b6f 100644 --- a/spiffworkflow-frontend/src/routes/AuthenticationList.tsx +++ b/spiffworkflow-frontend/src/routes/AuthenticationList.tsx @@ -4,11 +4,16 @@ import { Table } from '@carbon/react'; import { AuthenticationItem } from '../interfaces'; import HttpService from '../services/HttpService'; import UserService from '../services/UserService'; +import { BACKEND_BASE_URL } from '../config'; +import AuthenticationConfiguration from '../components/AuthenticationConfiguration'; export default function AuthenticationList() { const [authenticationList, setAuthenticationList] = useState< AuthenticationItem[] | null >(null); + const [authenticationV2List, setAuthenticationV2List] = useState< + AuthenticationItem[] | null + >(null); const [connectProxyBaseUrl, setConnectProxyBaseUrl] = useState( null ); @@ -17,6 +22,7 @@ export default function AuthenticationList() { useEffect(() => { const processResult = (result: any) => { setAuthenticationList(result.results); + setAuthenticationV2List(result.resultsV2); setConnectProxyBaseUrl(result.connector_proxy_base_url); setRedirectUrl(result.redirect_url); }; @@ -27,7 +33,7 @@ export default function AuthenticationList() { }, []); const buildTable = () => { - if (authenticationList) { + if (authenticationList && authenticationV2List) { const rows = authenticationList.map((row) => { return ( @@ -43,6 +49,24 @@ export default function AuthenticationList() { {row.id} + Connector Proxy + + ); + }); + const rowsV2 = authenticationV2List.map((row) => { + return ( + + + + {row.id} + + + Local Configuration ); }); @@ -51,9 +75,13 @@ export default function AuthenticationList() { Id + Source - {rows} + + {rows} + {rowsV2} + ); } @@ -61,7 +89,12 @@ export default function AuthenticationList() { }; if (authenticationList) { - return <>{buildTable()}; + return ( + <> + {buildTable()} + {AuthenticationConfiguration()} + + ); } return
;