From ec88bb87a125bea19376eac982c8dabc40eb63d3 Mon Sep 17 00:00:00 2001 From: William Ronchetti Date: Fri, 29 Sep 2023 11:24:06 -0400 Subject: [PATCH 01/37] branch off of david's 3.11 changes with my redis changes --- .github/workflows/main.yml | 5 +- Makefile | 8 +- .../docker/production/fourfront_any_alpha.ini | 4 +- development.ini.template | 1 + docs/source/index.rst | 2 +- poetry.lock | 281 ++++++- pyproject.toml | 6 +- pytest.ini | 2 + src/encoded/__init__.py | 14 +- src/encoded/authentication.py | 779 +++++++++++++++++- src/encoded/dev_servers.py | 1 - src/encoded/static/components/app.js | 4 +- src/encoded/static/components/index.js | 16 + .../navigation/components/AccountNav.js | 2 +- .../navigation/components/LoginSuccessView.js | 142 ++++ .../components/UserRegistrationView.js | 31 + src/encoded/tests/conftest.py | 37 + src/encoded/tests/conftest_settings.py | 1 + src/encoded/tests/test_redis.py | 18 + 19 files changed, 1321 insertions(+), 33 deletions(-) create mode 100644 src/encoded/static/components/navigation/components/LoginSuccessView.js create mode 100644 src/encoded/static/components/navigation/components/UserRegistrationView.js create mode 100644 src/encoded/tests/test_redis.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7be835cbe5..ca4560665f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -44,7 +44,7 @@ jobs: node-version: ${{ matrix.node_version }} check-latest: false - - name: Install/Link Postgres + - name: Install/Link Postgres and Redis if: ${{ matrix.test_type == 'NPM' || matrix.test_type == 'UNIT' }} run: | sudo apt-get install curl ca-certificates gnupg @@ -54,6 +54,9 @@ jobs: sudo apt-get install postgresql-14 postgresql-client-14 echo "/usr/lib/postgresql/14/bin" >> $GITHUB_PATH sudo ln -s /usr/lib/postgresql/14/bin/initdb /usr/local/bin/initdb + sudo apt install redis-server + sudo ln -s /usr/bin/redis-server /usr/local/bin/redis-server + sudo ln -s /usr/bin/redis-cli /usr/local/bin/redis-cli - name: Install Deps if: ${{ matrix.test_type == 'NPM' || matrix.test_type == 'UNIT' }} run: | diff --git a/Makefile b/Makefile index 401c8b87d8..d0d00af110 100644 --- a/Makefile +++ b/Makefile @@ -47,8 +47,13 @@ configure: # does any pre-requisite installs @#pip install poetry==1.1.9 # this version is known to work. -kmp 11-Mar-2021 # Pin to version 1.1.15 for now to avoid this error: # Because encoded depends on wheel (>=0.29.0) which doesn't match any versions, version solving failed. +<<<<<<< HEAD pip install wheel==0.40.0 pip install poetry==1.4.2 +======= + pip install wheel==0.37.1 + pip install poetry==1.3.2 +>>>>>>> redis pip install setuptools==57.5.0 # this version allows 2to3, any later will break -wrr 20-Sept-2021 poetry config virtualenvs.create false --local # do not create a virtualenv - the user should have already done this -wrr 20-Sept-2021 @@ -131,8 +136,9 @@ kibana-stop: kill: # kills back-end processes associated with the application. Use with care. pkill -f postgres & - pkill -f elasticsearch & + pkill -f opensearch & pkill -f moto_server & + pkill -f redis-server & clean-python: @echo -n "Are you sure? This will wipe all libraries installed on this virtualenv [y/N] " && read ans && [ $${ans:-N} = y ] diff --git a/deploy/docker/production/fourfront_any_alpha.ini b/deploy/docker/production/fourfront_any_alpha.ini index 414fd24e64..ae89ebdd0b 100644 --- a/deploy/docker/production/fourfront_any_alpha.ini +++ b/deploy/docker/production/fourfront_any_alpha.ini @@ -12,7 +12,6 @@ identity = ${IDENTITY} tibanna_output_bucket = ${TIBANNA_OUTPUT_BUCKET} application_bucket_prefix = ${APPLICATION_BUCKET_PREFIX} foursight_bucket_prefix = ${FOURSIGHT_BUCKET_PREFIX} -# blob_store_profile_name = encoded-4dn-files accession_factory = encoded.server_defaults.enc_accession elasticsearch.server = ${ES_SERVER} snovault.app_version = ask-pip @@ -31,6 +30,9 @@ elasticsearch.aws_auth = true production = true load_test_data = snovault.loadxl:load_${DATA_SET}_data sqlalchemy.url = postgresql://${RDS_USERNAME}:${RDS_PASSWORD}@${RDS_HOSTNAME}:${RDS_PORT}/${RDS_DB_NAME} +redis.server = ${REDIS_SERVER} +g.recaptcha.key = ${g.recaptcha.key} +g.recaptcha.secret = ${g.recaptcha.secret} [composite:indexer] use = config:base.ini#indexer diff --git a/development.ini.template b/development.ini.template index b0d477e18b..8e8f406168 100644 --- a/development.ini.template +++ b/development.ini.template @@ -6,6 +6,7 @@ [app:app] use = config:base.ini#app sqlalchemy.url = postgresql://postgres@localhost:5441/postgres?host=/tmp/snovault/pgdata +redis.server = redis://localhost:6379 blob_bucket = encoded-4dn-blobs metadata_bundles_bucket = metadata-bundles-fourfront-local-test load_test_only = true diff --git a/docs/source/index.rst b/docs/source/index.rst index d1af38abdc..9faec1725c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -54,7 +54,7 @@ Install or update dependencies:: $ brew install libevent libmagic libxml2 libxslt openssl postgresql graphviz nginx python3 $ brew install freetype libjpeg libtiff littlecms webp # Required by Pillow $ brew cask install adoptopenjdk8 - $ brew install opensearch node@16 + $ brew install opensearch node@16 redis NOTES: diff --git a/poetry.lock b/poetry.lock index dd4145014a..94965f97ac 100644 --- a/poetry.lock +++ b/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 = "apipkg" version = "3.0.2" description = "apipkg: namespace control and lazy-import mechanism" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -15,6 +16,7 @@ files = [ name = "arrow" version = "1.2.3" description = "Better dates & times for Python" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -29,6 +31,7 @@ python-dateutil = ">=2.7.0" name = "async-timeout" version = "4.0.3" description = "Timeout context manager for asyncio programs" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -40,6 +43,7 @@ files = [ name = "attrs" version = "23.1.0" description = "Classes Without Boilerplate" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -54,10 +58,30 @@ docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib- tests = ["attrs[tests-no-zope]", "zope-interface"] tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +[[package]] +name = "auth0-python" +version = "4.4.0" +description = "Auth0 Python SDK" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "auth0-python-4.4.0.tar.gz", hash = "sha256:61ff3f36643281d8a4417bfdb14ba440428a77e15ca523e719b6e2f24cd5c8ba"}, + {file = "auth0_python-4.4.0-py2.py3-none-any.whl", hash = "sha256:307330941f694ddb7a222de6e142bf31c6621bafa346e06a616e1160b9446c34"}, +] + +[package.dependencies] +pyjwt = {version = ">=2.6.0", extras = ["crypto"]} +requests = ">=2.14.0" + +[package.extras] +test = ["coverage", "pre-commit"] + [[package]] name = "aws-requests-auth" version = "0.4.3" description = "AWS signature version 4 signing process for the python requests module" +category = "main" optional = false python-versions = "*" files = [ @@ -72,6 +96,7 @@ requests = ">=0.14.0" name = "aws-xray-sdk" version = "0.95" description = "The AWS X-Ray SDK for Python (the SDK) enables Python developers to record and emit information from within their applications to the AWS X-Ray service." +category = "dev" optional = false python-versions = "*" files = [ @@ -88,6 +113,7 @@ wrapt = "*" name = "awscli" version = "1.29.57" description = "Universal Command Line Environment for AWS." +category = "main" optional = false python-versions = ">= 3.7" files = [ @@ -107,6 +133,7 @@ s3transfer = ">=0.7.0,<0.8.0" name = "beautifulsoup4" version = "4.12.2" description = "Screen-scraping library" +category = "main" optional = false python-versions = ">=3.6.0" files = [ @@ -125,6 +152,7 @@ lxml = ["lxml"] name = "blinker" version = "1.6.2" description = "Fast, simple object-to-object and broadcast signaling" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -136,6 +164,7 @@ files = [ name = "boto" version = "2.49.0" description = "Amazon Web Services Library" +category = "dev" optional = false python-versions = "*" files = [ @@ -147,6 +176,7 @@ files = [ name = "boto3" version = "1.28.57" description = "The AWS SDK for Python" +category = "main" optional = false python-versions = ">= 3.7" files = [ @@ -166,6 +196,7 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] name = "boto3-stubs" version = "1.28.57" description = "Type annotations for boto3 1.28.57 generated with mypy-boto3-builder 7.19.0" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -544,6 +575,7 @@ xray = ["mypy-boto3-xray (>=1.28.0,<1.29.0)"] name = "botocore" version = "1.31.57" description = "Low-level, data-driven core of boto 3." +category = "main" optional = false python-versions = ">= 3.7" files = [ @@ -561,13 +593,14 @@ crt = ["awscrt (==0.16.26)"] [[package]] name = "botocore-stubs" -version = "1.31.56" +version = "1.31.57" description = "Type annotations and code completion for botocore" +category = "dev" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "botocore_stubs-1.31.56-py3-none-any.whl", hash = "sha256:e8f7273f57c09ef47f8bdc89765b151f43a46b3c29c52fb58e9982115ed14d84"}, - {file = "botocore_stubs-1.31.56.tar.gz", hash = "sha256:e1510bd361acf755ecace42ee984281adfea6f96695ed22a08a4ceeabfe66f5f"}, + {file = "botocore_stubs-1.31.57-py3-none-any.whl", hash = "sha256:bce87eb261f6ad9a43ae1d6946fa48582a99685642a9edb9a56b50ac113b3177"}, + {file = "botocore_stubs-1.31.57.tar.gz", hash = "sha256:e28f3ca7a6279f01dcec4663980be80aa93dc4eb2efcc2396859ceb647623b83"}, ] [package.dependencies] @@ -578,6 +611,7 @@ typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.9\""} name = "certifi" version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -589,6 +623,7 @@ files = [ name = "cffi" version = "1.16.0" description = "Foreign Function Interface for Python calling C code." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -653,6 +688,7 @@ pycparser = "*" name = "chardet" version = "5.2.0" description = "Universal encoding detector for Python 3" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -664,6 +700,7 @@ files = [ name = "charset-normalizer" version = "3.2.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 = [ @@ -748,6 +785,7 @@ files = [ name = "click" version = "8.1.7" description = "Composable command line interface toolkit" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -762,6 +800,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "codacy-coverage" version = "1.3.11" description = "Codacy coverage reporter for Python" +category = "dev" optional = false python-versions = "*" files = [ @@ -780,6 +819,7 @@ test = ["coverage", "nosetests"] name = "codeguru-profiler-agent" version = "1.2.4" description = "The Python agent to be used for Amazon CodeGuru Profiler" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -794,6 +834,7 @@ boto3 = ">=1.14.0" name = "colorama" version = "0.3.3" description = "Cross-platform colored terminal text." +category = "main" optional = false python-versions = "*" files = [ @@ -804,6 +845,7 @@ files = [ name = "coverage" version = "6.5.0" description = "Code coverage measurement for Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -869,6 +911,7 @@ toml = ["tomli"] name = "coveralls" version = "3.3.1" description = "Show coverage stats online via coveralls.io" +category = "dev" optional = false python-versions = ">= 3.5" files = [ @@ -877,7 +920,7 @@ files = [ ] [package.dependencies] -coverage = ">=4.1,<6.0.dev0 || >6.1,<6.1.1 || >6.1.1,<7.0" +coverage = ">=4.1,<6.0.0 || >6.1,<6.1.1 || >6.1.1,<7.0" docopt = ">=0.6.1" requests = ">=1.0.0" @@ -888,6 +931,7 @@ yaml = ["PyYAML (>=3.10)"] name = "cryptography" version = "39.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -933,6 +977,7 @@ tox = ["tox"] name = "dcicsnovault" version = "10.0.4.1b25" description = "Storage support for 4DN Data Portals." +category = "main" optional = false python-versions = ">=3.8.1,<3.12" files = [ @@ -989,6 +1034,7 @@ xlrd = ">=1.0.0,<2.0.0" name = "dcicutils" version = "7.12.0.2b9" description = "Utility package for interacting with the 4DN Data Portal and other 4DN resources" +category = "main" optional = false python-versions = ">=3.8,<3.12" files = [ @@ -1022,6 +1068,7 @@ webtest = ">=2.0.34,<3.0.0" name = "docker" version = "4.4.4" description = "A Python library for the Docker Engine API." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1043,6 +1090,7 @@ tls = ["cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=17.5.0)"] name = "docopt" version = "0.6.2" description = "Pythonic argument parser, that will make you smile" +category = "dev" optional = false python-versions = "*" files = [ @@ -1053,6 +1101,7 @@ files = [ name = "docutils" version = "0.16" description = "Docutils -- Python Documentation Utilities" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1064,6 +1113,7 @@ files = [ name = "ecdsa" version = "0.18.0" description = "ECDSA cryptographic signature library (pure python)" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1082,6 +1132,7 @@ gmpy2 = ["gmpy2"] name = "elasticsearch" version = "7.13.4" description = "Python client for Elasticsearch" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" files = [ @@ -1103,6 +1154,7 @@ requests = ["requests (>=2.4.0,<3.0.0)"] name = "elasticsearch-dsl" version = "7.4.1" description = "Python client for Elasticsearch" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1122,6 +1174,7 @@ develop = ["coverage (<5.0.0)", "mock", "pytest (>=3.0.0)", "pytest-cov", "pytes name = "exceptiongroup" version = "1.1.3" description = "Backport of PEP 654 (exception groups)" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1136,6 +1189,7 @@ test = ["pytest (>=6)"] name = "execnet" version = "1.4.1" description = "execnet: rapid multi-Python deployment" +category = "main" optional = false python-versions = "*" files = [ @@ -1150,6 +1204,7 @@ apipkg = ">=1.4" name = "flake8" version = "6.1.0" description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" optional = false python-versions = ">=3.8.1" files = [ @@ -1166,6 +1221,7 @@ pyflakes = ">=3.1.0,<3.2.0" name = "flaky" version = "3.7.0" description = "Plugin for nose or pytest that automatically reruns flaky tests." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1177,6 +1233,7 @@ files = [ name = "flask" version = "2.3.3" description = "A simple framework for building complex web applications." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1200,6 +1257,7 @@ dotenv = ["python-dotenv"] name = "fqdn" version = "1.5.1" description = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern bowsers" +category = "main" optional = false python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4" files = [ @@ -1211,6 +1269,7 @@ files = [ name = "future" version = "0.18.3" description = "Clean single-source support for Python 3 and 2" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1221,6 +1280,7 @@ files = [ name = "gitdb" version = "4.0.10" description = "Git Object Database" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1235,6 +1295,7 @@ smmap = ">=3.0.1,<6" name = "gitpython" version = "3.1.37" description = "GitPython is a Python library used to interact with Git repositories" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1252,6 +1313,7 @@ test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mypy", "pre-commit" 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 = [ @@ -1260,6 +1322,7 @@ files = [ {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, + {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c"}, {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, @@ -1268,6 +1331,7 @@ files = [ {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, + {file = "greenlet-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, @@ -1297,6 +1361,7 @@ files = [ {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, + {file = "greenlet-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, @@ -1305,6 +1370,7 @@ files = [ {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, + {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417"}, {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, @@ -1325,6 +1391,7 @@ test = ["objgraph", "psutil"] name = "html5lib" version = "1.1" description = "HTML parser based on the WHATWG HTML specification" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1346,6 +1413,7 @@ lxml = ["lxml"] name = "humanfriendly" version = "1.44.9" description = "Human friendly output for text interfaces using Python" +category = "main" optional = false python-versions = "*" files = [ @@ -1357,6 +1425,7 @@ files = [ name = "hupper" version = "1.5" description = "Integrated process monitor for developing and reloading daemons." +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" files = [ @@ -1372,6 +1441,7 @@ testing = ["mock", "pytest", "pytest-cov", "watchdog"] name = "idna" version = "2.10" description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1383,6 +1453,7 @@ files = [ name = "importlib-metadata" version = "6.8.0" description = "Read metadata from Python packages" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1402,6 +1473,7 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs name = "importlib-resources" version = "6.1.0" description = "Read resources from Python packages" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1420,6 +1492,7 @@ testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1431,6 +1504,7 @@ files = [ name = "isodate" version = "0.6.0" description = "An ISO 8601 date/time/duration parser and formatter" +category = "main" optional = false python-versions = "*" files = [ @@ -1445,6 +1519,7 @@ six = "*" name = "isoduration" version = "20.11.0" description = "Operations with ISO 8601 durations" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1459,6 +1534,7 @@ arrow = ">=0.15.0" name = "itsdangerous" version = "2.1.2" description = "Safely pass data to untrusted environments and back." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1470,6 +1546,7 @@ files = [ name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1487,6 +1564,7 @@ i18n = ["Babel (>=2.7)"] name = "jmespath" version = "0.9.0" description = "JSON Matching Expressions" +category = "main" optional = false python-versions = "*" files = [ @@ -1498,6 +1576,7 @@ files = [ name = "jsondiff" version = "1.1.1" description = "Diff JSON and JSON-like structures in Python" +category = "dev" optional = false python-versions = "*" files = [ @@ -1508,6 +1587,7 @@ files = [ name = "jsonpickle" version = "3.0.2" description = "Python library for serializing any arbitrary object graph into JSON" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1524,6 +1604,7 @@ testing-libs = ["simplejson", "ujson"] name = "jsonpointer" version = "2.4" description = "Identify specific nodes in a JSON document (RFC 6901)" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" files = [ @@ -1535,6 +1616,7 @@ files = [ name = "jsonschema" version = "4.19.1" description = "An implementation of JSON Schema validation for Python" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1566,6 +1648,7 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- name = "jsonschema-specifications" version = "2023.7.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1581,6 +1664,7 @@ referencing = ">=0.28.0" name = "lorem-text" version = "2.1" description = "Dummy lorem ipsum text generator" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1595,6 +1679,7 @@ Click = ">=7.0" name = "markdown" version = "3.4.4" description = "Python implementation of John Gruber's Markdown." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1613,6 +1698,7 @@ testing = ["coverage", "pyyaml"] name = "markupsafe" version = "2.1.3" description = "Safely add untrusted strings to HTML/XML markup." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1636,6 +1722,16 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -1672,6 +1768,7 @@ files = [ name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1679,10 +1776,26 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +[[package]] +name = "mirakuru" +version = "2.5.1" +description = "Process executor (not only) for tests." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mirakuru-2.5.1-py3-none-any.whl", hash = "sha256:0a16f897841741f8cd784f790e54d74e61456ba36be9cb9de731b49e2e7a45dc"}, + {file = "mirakuru-2.5.1.tar.gz", hash = "sha256:5a60d641fa92c8bfcd383f6e52f7a0bf3f081da0467fc6e3e6a3f6b3e3e47a7b"}, +] + +[package.dependencies] +psutil = {version = ">=4.0.0", markers = "sys_platform != \"cygwin\""} + [[package]] name = "mock" version = "5.1.0" description = "Rolling backport of unittest.mock for all Pythons" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1699,6 +1812,7 @@ test = ["pytest", "pytest-cov"] name = "moto" version = "1.3.7" description = "A library that allows your python tests to easily mock out the boto library" +category = "dev" optional = false python-versions = "*" files = [ @@ -1733,6 +1847,7 @@ server = ["flask"] name = "netaddr" version = "0.9.0" description = "A network address manipulation library for Python" +category = "main" optional = false python-versions = "*" files = [ @@ -1744,6 +1859,7 @@ files = [ name = "opensearch-py" version = "2.3.1" description = "Python client for OpenSearch" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" files = [ @@ -1768,6 +1884,7 @@ kerberos = ["requests-kerberos"] name = "packaging" version = "23.1" description = "Core utilities for Python packages" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1779,6 +1896,7 @@ files = [ name = "passlib" version = "1.7.4" description = "comprehensive password hashing framework supporting over 30 schemes" +category = "main" optional = false python-versions = "*" files = [ @@ -1796,6 +1914,7 @@ totp = ["cryptography"] name = "pastedeploy" version = "1.5.2" description = "Load, configure, and compose WSGI applications and servers" +category = "main" optional = false python-versions = "*" files = [ @@ -1810,6 +1929,7 @@ paste = ["Paste"] name = "pbkdf2" version = "1.3" description = "PKCS#5 v2.0 PBKDF2 Module" +category = "main" optional = false python-versions = "*" files = [ @@ -1820,6 +1940,7 @@ files = [ name = "pillow" version = "9.5.0" description = "Python Imaging Library (Fork)" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1899,6 +2020,7 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa name = "pip-licenses" version = "3.5.5" description = "Dump the software license list of Python packages installed with pip." +category = "dev" optional = false python-versions = "~=3.7" files = [ @@ -1916,6 +2038,7 @@ test = ["docutils", "pytest-cov", "pytest-pycodestyle", "pytest-runner"] name = "pipdeptree" version = "2.13.0" description = "Command line utility to show dependency tree of packages." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1931,6 +2054,7 @@ test = ["covdefaults (>=2.3)", "diff-cover (>=7.7)", "pip (>=23.2)", "pytest (>= name = "pkgutil-resolve-name" version = "1.3.10" description = "Resolve a name to an object." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1942,6 +2066,7 @@ files = [ name = "plaster" version = "1.0" description = "A loader interface around multiple config file formats." +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" files = [ @@ -1960,6 +2085,7 @@ testing = ["pytest", "pytest-cov"] name = "plaster-pastedeploy" version = "0.6" description = "A loader implementing the PasteDeploy syntax to be used by plaster." +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" files = [ @@ -1978,6 +2104,7 @@ testing = ["pytest", "pytest-cov"] name = "pluggy" version = "1.3.0" description = "plugin and hook calling mechanisms for python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1989,10 +2116,23 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "port-for" +version = "0.7.1" +description = "Utility that helps with local TCP ports management. It can find an unused TCP localhost port and remember the association." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "port-for-0.7.1.tar.gz", hash = "sha256:e0f3b8d7ec8bdc388c14d88bdbab8d6441c8081ed2bdec7847b38ca6d1563f23"}, + {file = "port_for-0.7.1-py3-none-any.whl", hash = "sha256:8abdaa1a7810281b0cecf718a6319da5f8538fdae3a5101d1e9afd54da0baf8c"}, +] + [[package]] name = "psutil" version = "5.9.5" description = "Cross-platform lib for process and system monitoring in Python." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -2019,6 +2159,7 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] name = "psycopg2-binary" version = "2.9.8" description = "psycopg2 - Python-PostgreSQL Database Adapter" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2088,6 +2229,7 @@ files = [ name = "ptable" version = "0.9.2" description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format" +category = "dev" optional = false python-versions = "*" files = [ @@ -2098,6 +2240,7 @@ files = [ name = "py" version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -2107,13 +2250,14 @@ files = [ [[package]] name = "pyaml" -version = "23.9.6" +version = "23.9.7" description = "PyYAML-based module to produce a bit more pretty and readable YAML-serialized data" +category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "pyaml-23.9.6-py3-none-any.whl", hash = "sha256:9dcc67922b7278f3680e573324b2e8a8d2f86c5d09bf640cba83735fb1663e97"}, - {file = "pyaml-23.9.6.tar.gz", hash = "sha256:2b2c39017b718a127bef9f96bc55f89414d960876668d69880aae66f4ba98957"}, + {file = "pyaml-23.9.7-py3-none-any.whl", hash = "sha256:fdb4c111b676d2381d1aa88c378fcde46c167575dfd688e656977a77075b692c"}, + {file = "pyaml-23.9.7.tar.gz", hash = "sha256:581ea4e99f0e308864407e04c03c609241aefa3a15dfba8964da7644baf3b217"}, ] [package.dependencies] @@ -2126,6 +2270,7 @@ anchors = ["unidecode"] name = "pyasn1" version = "0.5.0" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ @@ -2137,6 +2282,7 @@ files = [ name = "pybrowserid" version = "0.10.0" description = "Python library for the BrowserID Protocol" +category = "main" optional = false python-versions = "*" files = [ @@ -2150,6 +2296,7 @@ requests = "*" name = "pycodestyle" version = "2.11.0" description = "Python style guide checker" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2161,6 +2308,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 = [ @@ -2172,6 +2320,7 @@ files = [ name = "pycryptodome" version = "3.19.0" description = "Cryptographic library for Python" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -2213,6 +2362,7 @@ files = [ name = "pyflakes" version = "3.1.0" description = "passive checker of Python programs" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2224,6 +2374,7 @@ files = [ name = "pyjwt" version = "2.8.0" description = "JSON Web Token implementation in Python" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2231,6 +2382,9 @@ files = [ {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, ] +[package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} + [package.extras] crypto = ["cryptography (>=3.4.0)"] dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] @@ -2241,6 +2395,7 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] name = "pyopenssl" version = "23.2.0" description = "Python wrapper module around the OpenSSL library" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2259,6 +2414,7 @@ test = ["flaky", "pretend", "pytest (>=3.0.1)"] name = "pyparsing" version = "2.4.7" description = "Python parsing module" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -2270,6 +2426,7 @@ files = [ name = "pyramid" version = "1.10.4" description = "The Pyramid Web Framework, a Pylons project" +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" files = [ @@ -2296,6 +2453,7 @@ testing = ["coverage", "nose", "virtualenv", "webtest (>=1.3.1)", "zope.componen name = "pyramid-multiauth" version = "0.9.0" description = "pyramid_multiauth" +category = "main" optional = false python-versions = "*" files = [ @@ -2310,6 +2468,7 @@ pyramid = "*" name = "pyramid-retry" version = "1.0" description = "An execution policy for Pyramid that supports retrying requests after certain failure exceptions." +category = "main" optional = false python-versions = "*" files = [ @@ -2329,6 +2488,7 @@ testing = ["WebTest", "pytest", "pytest-cov"] name = "pyramid-tm" version = "2.5" description = "A package which allows Pyramid requests to join the active transaction" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2348,6 +2508,7 @@ testing = ["WebTest", "coverage (>=5.0)", "pytest", "pytest-cov"] name = "pyramid-translogger" version = "0.1" description = "access log logger tween (almost stolen from Paste.translogger)" +category = "main" optional = false python-versions = "*" files = [ @@ -2361,6 +2522,7 @@ setuptools = "*" name = "pytest" version = "7.4.2" description = "pytest: simple powerful testing with Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2383,6 +2545,7 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pytest-cov" version = "4.1.0" description = "Pytest plugin for measuring coverage." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2401,6 +2564,7 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale name = "pytest-instafail" version = "0.5.0" description = "pytest plugin to show failures instantly" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2415,6 +2579,7 @@ pytest = ">=5" name = "pytest-mock" version = "3.11.1" description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2428,10 +2593,32 @@ pytest = ">=5.0" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] +[[package]] +name = "pytest-redis" +version = "2.4.0" +description = "Redis fixtures and fixture factories for Pytest." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-redis-2.4.0.tar.gz", hash = "sha256:8a07520abed3cd341e8da1793059aa5717b02e56c43e7c76435db682cede10aa"}, + {file = "pytest_redis-2.4.0-py3-none-any.whl", hash = "sha256:3cf00ad3f7241e38ce6f1bcb66af11b91956a889f1e216cfc026e81aa638a4e7"}, +] + +[package.dependencies] +mirakuru = "*" +port-for = ">=0.6.0" +pytest = ">=6.2.0" +redis = "*" + +[package.extras] +tests = ["mock", "pytest-cov", "pytest-xdist"] + [[package]] name = "pytest-timeout" version = "2.1.0" description = "pytest plugin to abort hanging tests" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2446,6 +2633,7 @@ pytest = ">=5.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 = [ @@ -2466,6 +2654,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 = [ @@ -2480,6 +2669,7 @@ six = ">=1.5" name = "python-jose" version = "2.0.2" description = "JOSE implementation in Python" +category = "dev" optional = false python-versions = "*" files = [ @@ -2501,6 +2691,7 @@ pycrypto = ["pycrypto (>=2.6.0,<2.7.0)"] name = "python-magic" version = "0.4.27" description = "File type identification using libmagic" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -2512,6 +2703,7 @@ files = [ name = "pytz" version = "2023.3.post1" description = "World timezone definitions, modern and historical" +category = "main" optional = false python-versions = "*" files = [ @@ -2523,6 +2715,7 @@ files = [ name = "pywin32" version = "227" description = "Python for Window Extensions" +category = "main" optional = false python-versions = "*" files = [ @@ -2544,6 +2737,7 @@ files = [ name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2552,6 +2746,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -2559,8 +2754,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -2577,6 +2779,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -2584,6 +2787,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -2593,6 +2797,7 @@ files = [ name = "rdflib" version = "4.2.2" description = "RDFLib is a Python library for working with RDF, a simple yet powerful language for representing information." +category = "main" optional = false python-versions = "*" files = [ @@ -2612,6 +2817,7 @@ sparql = ["SPARQLWrapper"] name = "rdflib-jsonld" version = "0.6.0" description = "rdflib extension adding JSON-LD parser and serializer" +category = "main" optional = false python-versions = "*" files = [ @@ -2626,6 +2832,7 @@ rdflib = "*" name = "redis" version = "4.6.0" description = "Python client for Redis database and key-value store" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2644,6 +2851,7 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" name = "referencing" version = "0.30.2" description = "JSON Referencing + Python" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2659,6 +2867,7 @@ rpds-py = ">=0.7.0" name = "repoze-debug" version = "1.1" description = "Forensic debugging WSGI middleware" +category = "dev" optional = false python-versions = "*" files = [ @@ -2677,6 +2886,7 @@ testing = ["WebOb", "coverage", "nose"] name = "requests" version = "2.31.0" description = "Python HTTP for Humans." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2698,6 +2908,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "responses" version = "0.23.3" description = "A utility library for mocking out the `requests` Python library." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2718,6 +2929,7 @@ tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asy name = "rfc3339-validator" version = "0.1.4" description = "A pure python RFC3339 validator" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -2732,6 +2944,7 @@ six = "*" name = "rfc3986" version = "1.5.0" description = "Validating URI References per RFC 3986" +category = "main" optional = false python-versions = "*" files = [ @@ -2746,6 +2959,7 @@ idna2008 = ["idna"] name = "rfc3986-validator" version = "0.1.1" description = "Pure python rfc3986 validator" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -2757,6 +2971,7 @@ files = [ name = "rpds-py" version = "0.10.3" description = "Python bindings to Rust's persistent data structures (rpds)" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2863,6 +3078,7 @@ files = [ name = "rsa" version = "3.3" description = "Pure-Python RSA implementation" +category = "main" optional = false python-versions = "*" files = [ @@ -2877,6 +3093,7 @@ pyasn1 = ">=0.1.3" name = "rutter" version = "0.4" description = "Py3k-compatible fork of Paste's urlmap" +category = "main" optional = false python-versions = "*" files = [ @@ -2894,6 +3111,7 @@ testing = ["WebTest", "coverage", "pytest", "pytest-cov"] name = "s3transfer" version = "0.7.0" description = "An Amazon S3 Transfer Manager" +category = "main" optional = false python-versions = ">= 3.7" files = [ @@ -2911,6 +3129,7 @@ crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] name = "semantic-version" version = "2.10.0" description = "A library implementing the 'SemVer' scheme." +category = "main" optional = false python-versions = ">=2.7" files = [ @@ -2926,6 +3145,7 @@ doc = ["Sphinx", "sphinx-rtd-theme"] name = "sentry-sdk" version = "1.31.0" description = "Python client for Sentry (https://sentry.io)" +category = "main" optional = false python-versions = "*" files = [ @@ -2971,6 +3191,7 @@ tornado = ["tornado (>=5)"] name = "setuptools" version = "68.2.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2987,6 +3208,7 @@ testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jar 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 = [ @@ -3081,6 +3303,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 = [ @@ -3092,6 +3315,7 @@ files = [ name = "smmap" version = "5.0.1" description = "A pure Python implementation of a sliding window memory map manager" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3103,6 +3327,7 @@ files = [ name = "soupsieve" version = "2.5" description = "A modern CSS selector implementation for Beautiful Soup." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -3114,6 +3339,7 @@ files = [ name = "sparqlwrapper" version = "1.8.5" description = "SPARQL Endpoint interface to Python" +category = "main" optional = false python-versions = "*" files = [ @@ -3132,6 +3358,7 @@ keepalive = ["keepalive (>=0.5)"] name = "sqlalchemy" version = "1.4.41" description = "Database Abstraction Library" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ @@ -3179,7 +3406,7 @@ files = [ ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (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 = "python_version >= \"3\" and platform_machine == \"aarch64\" or python_version >= \"3\" and platform_machine == \"ppc64le\" or python_version >= \"3\" and platform_machine == \"x86_64\" or python_version >= \"3\" and platform_machine == \"amd64\" or python_version >= \"3\" and platform_machine == \"AMD64\" or python_version >= \"3\" and platform_machine == \"win32\" or python_version >= \"3\" and platform_machine == \"WIN32\""} [package.extras] aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] @@ -3206,6 +3433,7 @@ sqlcipher = ["sqlcipher3-binary"] name = "structlog" version = "19.2.0" description = "Structured Logging for Python" +category = "main" optional = false python-versions = "*" files = [ @@ -3226,6 +3454,7 @@ tests = ["coverage", "freezegun (>=0.2.8)", "pretend", "pytest (>=3.3.0)", "pyth name = "submit4dn" version = "0.9.7" description = "Tools for data wrangling and submission to data.4dnucleome.org" +category = "main" optional = false python-versions = "*" files = [ @@ -3253,6 +3482,7 @@ test = ["pytest (>=3.0.1)", "pytest-cov", "pytest-mock", "tox (>=2.5.0)"] name = "subprocess-middleware" version = "0.3" description = "Subprocess WSGI middleware and Pyramid tween." +category = "main" optional = false python-versions = "*" files = [ @@ -3269,6 +3499,7 @@ test = ["WebTest", "pyramid", "pytest"] name = "supervisor" version = "4.2.5" description = "A system for controlling process state under UNIX" +category = "main" optional = false python-versions = "*" files = [ @@ -3286,6 +3517,7 @@ testing = ["pytest", "pytest-cov"] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -3297,6 +3529,7 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -3308,6 +3541,7 @@ files = [ name = "tqdm" version = "4.66.1" description = "Fast, Extensible Progress Meter" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3328,6 +3562,7 @@ telegram = ["requests"] name = "transaction" version = "3.1.0" description = "Transaction management for Python" +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" files = [ @@ -3347,6 +3582,7 @@ testing = ["coverage", "mock", "nose"] name = "translationstring" version = "1.3" description = "Utility library for i18n relied on by various Repoze and Pyramid packages" +category = "main" optional = false python-versions = "*" files = [ @@ -3358,6 +3594,7 @@ files = [ name = "types-awscrt" version = "0.19.1" description = "Type annotations and code completion for awscrt" +category = "dev" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -3369,6 +3606,7 @@ files = [ name = "types-pyyaml" version = "6.0.12.12" description = "Typing stubs for PyYAML" +category = "dev" optional = false python-versions = "*" files = [ @@ -3380,6 +3618,7 @@ files = [ name = "types-s3transfer" version = "0.7.0" description = "Type annotations and code completion for s3transfer" +category = "dev" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -3391,6 +3630,7 @@ files = [ name = "typing-extensions" version = "4.8.0" description = "Backported and Experimental Type Hints for Python 3.8+" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -3402,6 +3642,7 @@ files = [ name = "uptime" version = "3.0.1" description = "Cross-platform uptime library" +category = "main" optional = false python-versions = "*" files = [ @@ -3412,6 +3653,7 @@ files = [ name = "uri-template" version = "1.3.0" description = "RFC 6570 URI Template Processor" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3426,6 +3668,7 @@ dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake name = "urllib3" version = "1.26.16" 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 = [ @@ -3442,6 +3685,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] name = "venusian" version = "1.2.0" description = "A library for deferring decorator actions" +category = "main" optional = false python-versions = "*" files = [ @@ -3457,6 +3701,7 @@ testing = ["coverage", "pytest", "pytest-cov"] name = "waitress" version = "2.1.2" description = "Waitress WSGI server" +category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -3472,6 +3717,7 @@ testing = ["coverage (>=5.0)", "pytest", "pytest-cover"] name = "webcolors" version = "1.13" description = "A library for working with the color formats defined by HTML and CSS." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3487,6 +3733,7 @@ tests = ["pytest", "pytest-cov"] name = "webencodings" version = "0.5.1" description = "Character encoding aliases for legacy web content" +category = "main" optional = false python-versions = "*" files = [ @@ -3498,6 +3745,7 @@ files = [ name = "webob" version = "1.8.7" description = "WSGI request and response object" +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*" files = [ @@ -3513,6 +3761,7 @@ testing = ["coverage", "pytest (>=3.1.0)", "pytest-cov", "pytest-xdist"] name = "websocket-client" version = "1.6.3" description = "WebSocket client for Python with low level API options" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -3529,6 +3778,7 @@ test = ["websockets"] name = "webtest" version = "2.0.35" description = "Helper to test WSGI applications" +category = "main" optional = false python-versions = "*" files = [ @@ -3550,6 +3800,7 @@ tests = ["PasteDeploy", "WSGIProxy2", "coverage", "mock", "nose (<1.3.0)", "pyqu name = "werkzeug" version = "2.3.7" description = "The comprehensive WSGI web application library." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -3567,6 +3818,7 @@ watchdog = ["watchdog (>=2.3)"] name = "wheel" version = "0.40.0" description = "A built-package format for Python" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3581,6 +3833,7 @@ test = ["pytest (>=6.0.0)"] name = "wrapt" version = "1.15.0" description = "Module for decorators, wrappers and monkey patching." +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -3665,6 +3918,7 @@ files = [ name = "wsgiproxy2" version = "0.4.2" description = "UNKNOWN" +category = "main" optional = false python-versions = "*" files = [ @@ -3679,6 +3933,7 @@ webob = "*" name = "xlrd" version = "1.2.0" description = "Library for developers to extract data from Microsoft Excel (tm) spreadsheet files" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -3690,6 +3945,7 @@ files = [ name = "xlwt" version = "1.2.0" description = "Library to create spreadsheet files compatible with MS Excel 97/2000/XP/2003 XLS files, on any platform, with Python 2.6, 2.7, 3.3+" +category = "main" optional = false python-versions = "*" files = [ @@ -3701,6 +3957,7 @@ files = [ name = "xmltodict" version = "0.13.0" description = "Makes working with XML feel like you are working with JSON" +category = "dev" optional = false python-versions = ">=3.4" files = [ @@ -3712,6 +3969,7 @@ files = [ name = "zipp" version = "3.17.0" description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -3727,6 +3985,7 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p name = "zope-deprecation" version = "4.4.0" description = "Zope Deprecation Infrastructure" +category = "main" optional = false python-versions = "*" files = [ @@ -3745,6 +4004,7 @@ test = ["zope.testrunner"] name = "zope-interface" version = "5.5.2" description = "Interfaces for Python" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -3798,6 +4058,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] name = "zope-sqlalchemy" version = "1.6" description = "Minimal Zope/SQLAlchemy transaction integration" +category = "main" optional = false python-versions = "*" files = [ @@ -3817,4 +4078,4 @@ test = ["zope.testing"] [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<3.12" -content-hash = "147b7a50d718cf992c500ee2080886d56da6da7d7ce813aae8a8e709c0885d4b" +content-hash = "e195fe7db8310b0d28ff545a14d377a1d41108ac5a2a83e2fbdc67a701cd3934" diff --git a/pyproject.toml b/pyproject.toml index 4eacbdb7d7..b18f062535 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] # Note: Various modules refer to this system as "encoded", not "fourfront". name = "encoded" -version = "6.4.4.2b2" # TODO: To become 7.0.0 +version = "8.0.0" # TODO: To become 8.0.0 description = "4DN-DCIC Fourfront" authors = ["4DN-DCIC Team "] license = "MIT" @@ -98,7 +98,7 @@ simplejson = "^3.17.0" SPARQLWrapper = "^1.8.5" SQLAlchemy = "1.4.41" # latest stable version we can take - Will Jan 13 2023 structlog = ">=19.2.0,<20" -submit4dn = "0.9.7" +submit4dn = "0.9.7" # XXX: this version is likely not 3.11 compliant but will allow locking for now subprocess_middleware = ">=0.3,<1" # Useful for picking apart pyproject.toml toml = ">=0.10.1,<1" @@ -120,6 +120,7 @@ xlwt = "1.2.0" codeguru-profiler-agent = "^1.2.4" supervisor = "^4.2.4" wheel = "0.40.0" +auth0-python = "^4.0.0" [tool.poetry.dev-dependencies] @@ -152,6 +153,7 @@ pytest = "^7.2.1" pytest-cov = ">=2.2.1" pytest-instafail = ">=0.3.0" pytest-mock = ">=0.11.0" +pytest-redis = "^2.0.0" pytest-timeout = ">=1.0.0" pytest-xdist = ">=1.14" "repoze.debug" = ">=1.0.2" diff --git a/pytest.ini b/pytest.ini index 32a94e40d2..295ac3a811 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,8 @@ [pytest] timeout_func_only = true +redis_exec = /usr/local/bin/redis-server addopts = + --basetemp=/tmp/pytest -p encoded.tests.datafixtures -p snovault.tests.serverfixtures --instafail diff --git a/src/encoded/__init__.py b/src/encoded/__init__.py index ea87e9a431..dcbc861a48 100644 --- a/src/encoded/__init__.py +++ b/src/encoded/__init__.py @@ -8,6 +8,7 @@ import pkg_resources import sentry_sdk import subprocess +from pyramid.session import SignedCookieSessionFactory from codeguru_profiler_agent import Profiler from dcicutils.ecs_utils import ECSUtils @@ -182,8 +183,8 @@ def main(global_config, **local_config): settings['auth0.options'] = { 'auth': { 'sso': False, - 'redirect': False, - 'responseType': 'token', + 'redirect': True, + 'responseType': 'code', 'params': { 'scope': 'openid email', 'prompt': 'select_account' @@ -202,8 +203,8 @@ def main(global_config, **local_config): settings['ga4.secret'] = settings.get('ga4.secret', os.environ.get('GA4Secret')) # set google reCAPTCHA keys # TODO propagate from GAC - settings['g.recaptcha.key'] = os.environ.get('reCaptchaKey') - settings['g.recaptcha.secret'] = os.environ.get('reCaptchaSecret') + settings['g.recaptcha.key'] = settings.get('g.recaptcha.key', os.environ.get('reCaptchaKey')) + settings['g.recaptcha.secret'] = settings.get('g.recaptcha.secret', os.environ.get('reCaptchaSecret')) # enable invalidation scope settings[INVALIDATION_SCOPE_ENABLED] = True @@ -222,6 +223,8 @@ def main(global_config, **local_config): config.include('pyramid_multiauth') # must be before calling set_authorization_policy # Override default authz policy set by pyramid_multiauth config.set_authorization_policy(LocalRolesAuthorizationPolicy()) + + # This creates a session factory (from definition in Snovault/app.py) config.include(session) # must include, as tm.attempts was removed from pyramid_tm @@ -251,6 +254,9 @@ def main(global_config, **local_config): config.include('snovault.elasticsearch') config.include('.search') + if 'redis.server' in config.registry.settings: + config.include('snovault.redis') + # this contains fall back url, so make sure it comes just before static_resoruces config.include('.types.page') config.include(static_resources) diff --git a/src/encoded/authentication.py b/src/encoded/authentication.py index a41d682846..506b2bdf52 100644 --- a/src/encoded/authentication.py +++ b/src/encoded/authentication.py @@ -1,10 +1,771 @@ -from snovault.authentication import ( - Auth0AuthenticationPolicy, - basic_auth_check, - BasicAuthAuthenticationPolicy, - CRYPT_CONTEXT, - generate_password, - generate_user, - NamespacedAuthenticationPolicy, - session_properties +import base64 +import os +from operator import itemgetter +import jwt +import json +import logging + +from passlib.context import CryptContext +from urllib.parse import urlencode +from pyramid.authentication import ( + BasicAuthAuthenticationPolicy as _BasicAuthAuthenticationPolicy, + CallbackAuthenticationPolicy ) +import requests +from pyramid.path import ( + DottedNameResolver, + caller_package, +) +from pyramid.security import ( + NO_PERMISSION_REQUIRED, +) +from pyramid.httpexceptions import ( + HTTPForbidden, + HTTPUnauthorized, + HTTPTemporaryRedirect +) +from pyramid.view import ( + view_config, +) +from snovault import ( + ROOT, + COLLECTIONS +) +from snovault.validation import ValidationFailure +from snovault.calculated import calculate_properties +from snovault.validators import no_validate_item_content_post +from snovault.crud_views import collection_add as sno_collection_add +from snovault.schema_utils import validate_request +from snovault.util import debug_log +from snovault.redis.interfaces import REDIS +from dcicutils.redis_tools import RedisSessionToken + + +logger = logging.getLogger(__name__) + + +CRYPT_CONTEXT = __name__ + ':crypt_context' + + +JWT_ENCODING_ALGORITHM = 'HS256' + +# Might need to keep a list of previously used algorithms here, not just the one we use now. +# Decryption algorithm used to default to a long list, but more recent versions of jwt library +# say we should stop assuming that. +# +# In case it goes away, as far as I can tell, the default for decoding from their +# default_algorithms() method used to be what we've got in JWT_ALL_ALGORITHMS here. +# -kmp 15-May-2020 + +JWT_ALL_ALGORITHMS = ['ES512', 'RS384', 'HS512', 'ES256', 'none', + 'RS256', 'PS512', 'ES384', 'HS384', 'ES521', + 'PS384', 'HS256', 'PS256', 'RS512'] + +# Probably we could get away with fewer, but I think not as few as just our own encoding algorithm, +# so for now I believe the above list was the default, and this just rearranges it to prefer the one +# we use for encoding. -kmp 19-Jan-2021 + +JWT_DECODING_ALGORITHMS = [JWT_ENCODING_ALGORITHM] + + +CONTENT_TYPE = "Content-Type" +JSON_CONTENT_TYPE = "application/json" +STANDARD_HEADERS = {CONTENT_TYPE: JSON_CONTENT_TYPE} + + +def includeme(config): + config.include('.edw_hash') + setting_prefix = 'passlib.' + passlib_settings = { + k[len(setting_prefix):]: v + for k, v in config.registry.settings.items() + if k.startswith(setting_prefix) + } + if not passlib_settings: + passlib_settings = {'schemes': 'edw_hash, unix_disabled'} + crypt_context = CryptContext(**passlib_settings) + config.registry[CRYPT_CONTEXT] = crypt_context + + # basic login route + config.add_route('login', '/login') + config.add_route('logout', '/logout') + config.add_route('me', '/me') + config.add_route('impersonate-user', '/impersonate-user') + config.add_route('session-properties', '/session-properties') + config.add_route('create-unauthorized-user', '/create-unauthorized-user') + config.add_route('callback', '/callback') + config.scan(__name__) + + +def redis_is_active(request): + """ Quick helper to standardize detecting whether redis is in use """ + return 'redis.server' in request.registry.settings + + +@view_config(route_name='callback', request_method='GET', permission=NO_PERMISSION_REQUIRED) +def callback(context, request): + """ /callback for Fourfront that will result in a session token + Note that this sets jwtToken as to not break the front-end + """ + if not redis_is_active(request): + raise HTTPForbidden('Calls to /callback are not allowed when Redis not in use - check your ini file') + auth0_code = request.params.get('code', None) + if not auth0_code: + raise HTTPForbidden('No code sent back from Auth0') + is_https = request.scheme == "https" + + # Acquire Auth0 configuration + registry = request.registry + auth0_domain = registry.settings.get('auth0.domain') + auth0_client = registry.settings.get('auth0.client') + auth0_secret = registry.settings.get('auth0.secret') + if not (auth0_code and auth0_domain and auth0_client and auth0_secret): + raise HTTPForbidden('Auth0 not configured, no callback possible') + + # Create auth0 payload, send and get JWT back + auth0_redirect_uri = f'{request.host_url}' + auth0_payload = { + 'grant_type': 'authorization_code', + 'client_id': auth0_client, + 'client_secret': auth0_secret, + 'code': auth0_code, + 'redirect_uri': auth0_redirect_uri + } + auth0_post_url = f'https://{auth0_domain}/oauth/token' + auth0_payload_json = json.dumps(auth0_payload) + auth0_headers = STANDARD_HEADERS + auth0_response = requests.post(auth0_post_url, data=auth0_payload_json, headers=auth0_headers) + auth0_response_json = auth0_response.json() + auth0_jwt = auth0_response_json.get('id_token') + if not auth0_jwt: + raise LoginDenied('No JWT returned from Auth0, check Auth0 configuration') + + # Generate a session from Redis + redis_handler = registry[REDIS] + env_name = registry.settings['env.name'] + redis_session_token = RedisSessionToken( + namespace=env_name, + jwt=auth0_jwt + ) + + # Check that the user exists in our database, if they do not, redirect them to /registration + email = Auth0AuthenticationPolicy.get_token_info(auth0_jwt, request).get('email', '').lower() + if not email: + raise LoginDenied('No email extracted from JWT, not possible to continue') + try: + Auth0AuthenticationPolicy.get_user_info(request, email, redis_session_token.get_session_token()) + except HTTPUnauthorized: + # in this case return a different response that the UI can interpret to pull up the registration modal + resp_json = { + '@type': ['registration'], + '@context': '/callback', + 'title': 'registration', + '@graph': [ + email # this is needed by the front-end to render the UserRegistrationModal + ] + } + except Exception as e: + raise LoginDenied(f'Unknown error encountered trying to extract user from DB {str(e)}') + else: + resp_json = { + '@type': ['callback'], + '@context': '/callback', + 'title': 'callback' + } + + # Give a session token unconditionally so we can retrieve JWT later on + # in the registration scenario (if an unknown user) or make auth'd requests + # as an existing user + redis_session_token.store_session_token(redis_handler=redis_handler) + request.response.set_cookie( + 'jwtToken', # note that although we are setting jwtToken, it is NOT a JWT when going through this route + value=redis_session_token.get_session_token(), + domain=request.domain, + path='/', + httponly=True, + samesite='lax', + overwrite=True, + secure=is_https + ) + return resp_json + + +class NamespacedAuthenticationPolicy(object): + """ Wrapper for authentication policy classes + + As userids are included in the list of principals, it seems good practice + to namespace them to avoid clashes. + + Constructor Arguments + + ``namespace`` + + The namespace used (string). + + ``base`` + + The base authentication policy (class or dotted name). + + Remaining arguments are passed to the ``base`` constructor. + + Example + + To make a ``REMOTE_USER`` 'admin' be 'user.admin' + + .. code-block:: python + + policy = NamespacedAuthenticationPolicy('user', + 'pyramid.authentication.RemoteUserAuthenticationPolicy') + """ + + def __new__(cls, namespace, base, *args, **kw): + # Dotted name support makes it easy to configure with pyramid_multiauth + name_resolver = DottedNameResolver(caller_package()) + base = name_resolver.maybe_resolve(base) + # Dynamically create a subclass + name = 'Namespaced_%s_%s' % (namespace, base.__name__) + klass = type(name, (cls, base), {'_namespace_prefix': namespace + '.'}) + return super(NamespacedAuthenticationPolicy, klass).__new__(klass) + + def __init__(self, namespace, base, *args, **kw): + super(NamespacedAuthenticationPolicy, self).__init__(*args, **kw) + + def unauthenticated_userid(self, request): + cls = super(NamespacedAuthenticationPolicy, self) + userid = super(NamespacedAuthenticationPolicy, self) \ + .unauthenticated_userid(request) + if userid is not None: + userid = self._namespace_prefix + userid + return userid + + def remember(self, request, principal, **kw): + if not principal.startswith(self._namespace_prefix): + return [] + principal = principal[len(self._namespace_prefix):] + return super(NamespacedAuthenticationPolicy, self) \ + .remember(request, principal, **kw) + + +class BasicAuthAuthenticationPolicy(_BasicAuthAuthenticationPolicy): + def __init__(self, check, *args, **kw): + # Dotted name support makes it easy to configure with pyramid_multiauth + name_resolver = DottedNameResolver(caller_package()) + check = name_resolver.maybe_resolve(check) + super(BasicAuthAuthenticationPolicy, self).__init__(check, *args, **kw) + + +class LoginDenied(HTTPUnauthorized): + title = 'Login Failure' + + def __init__(self, domain=None, *args, **kwargs): + super(LoginDenied, self).__init__(*args, **kwargs) + if not self.headers.get('WWW-Authenticate') and domain: + # headers['WWW-Authenticate'] might be set in constructor thru headers + self.headers['WWW-Authenticate'] = "Bearer realm=\"{}\"; Basic realm=\"{}\"".format(domain, domain) + + +_fake_user = object() + + +class Auth0AuthenticationPolicy(CallbackAuthenticationPolicy): + + login_path = '/login' + method = 'POST' + + def unauthenticated_userid(self, request): + ''' + So basically this is used to do a login, instead of the actual + login view... not sure why, but yeah.. + ''' + # we will cache it for the life of this request, cause pyramids does traversal + cached = getattr(request, '_auth0_authenticated', _fake_user) + if cached is not _fake_user: + return cached + + # try to find the token in the request (should be in the header) + id_token = get_jwt(request) + if not id_token: + # can I thrown an 403 here? + # print('Missing assertion.', 'unauthenticated_userid', request) + return None + + if redis_is_active(request): + session_token = RedisSessionToken.from_redis( + redis_handler=request.registry[REDIS], + namespace=request.registry.settings['env.name'], + token=id_token + ) + if not session_token: + return None + jwt_info = session_token.decode_jwt( + audience=request.registry.settings['auth0.client'], + secret=request.registry.settings['auth0.secret'] + ) + else: + jwt_info = self.get_token_info(id_token, request) + if not jwt_info: + return None + + email = request._auth0_authenticated = jwt_info['email'].lower() + + # At this point, email has been authenticated with their Auth0 provider, but we don't know yet if this email is in our database. + # If not authenticated (not in our DB), request.user_info will throw an HTTPUnauthorized error. + def get_user_info(request): + # This indirection is necessary, otherwise needed parameters don't make it + return self.get_token_info(request, email, id_token) + + request.set_property(get_user_info, "user_info", True) + return email + + @staticmethod + def get_user_info(request, email, id_token): + """ + Previously an inner method, redefined here so can be used outside, but can only be used within a route + Allow access basic user credentials from request obj after authenticating & saving request + """ + user_props = request.embed('/session-properties', as_user=email) # Performs an authentication against DB for user. + if not user_props.get('details'): + raise HTTPUnauthorized( + title="Could not find user info for {}".format(email), + headers={'WWW-Authenticate': "Bearer realm=\"{}\"; Basic realm=\"{}\"".format(request.domain, request.domain) } + ) + user_props['id_token'] = id_token + return user_props + + @staticmethod + def get_token_info(token, request): + ''' + Given a jwt get token info from auth0, handle retrying and whatnot. + This is only called if we receive a Bearer token in Authorization header. + ''' + try: + + # lets see if we have an auth0 token or our own + registry = request.registry + auth0_client = registry.settings.get('auth0.client') + auth0_secret = registry.settings.get('auth0.secret') + if auth0_client and auth0_secret: + # leeway accounts for clock drift between us and auth0 + payload = jwt.decode(token, auth0_secret, + algorithms=JWT_DECODING_ALGORITHMS, + audience=auth0_client, leeway=30) + if 'email' in payload and payload.get('email_verified') is True: + request.set_property(lambda r: False, 'auth0_expired') + return payload + + else: # we don't have the key, let auth0 do the work for us + user_url = "https://{domain}/tokeninfo".format(domain='hms-dbmi.auth0.com') + resp = requests.post(user_url, {'id_token':token}) + payload = resp.json() + if 'email' in payload and payload.get('email_verified') is True: + request.set_property(lambda r: False, 'auth0_expired') + return payload + + except (ValueError, jwt.exceptions.InvalidTokenError, jwt.exceptions.InvalidKeyError) as e: + # Catch errors from decoding JWT + print('Invalid JWT assertion : %s (%s)', (e, type(e).__name__)) + request.set_property(lambda r: True, 'auth0_expired') # Allow us to return 403 code &or unset cookie in renderers.py + return None + + print("didn't get email or email is not verified") + return None + + +def get_jwt_from_auth_header(request): + if "Authorization" in request.headers: + try: + # Ensure this is a JWT token, not basic auth. + # Per https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication and + # https://tools.ietf.org/html/rfc6750, JWT is introduced by 'bearer', as in + # Authorization: Bearer something.something.something + # rather than, for example, the 'basic' key information, which as discussed in + # https://tools.ietf.org/html/rfc7617 is base64 encoded and looks like: + # Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== + # See also https://jwt.io/introduction/ for other info specific to JWT. + [ auth_type, auth_data ] = request.headers['Authorization'].strip().split(' ', 1) + if auth_type.lower() == 'bearer': + return auth_data.strip() # The spec says exactly one space, but then a token, so spaces don't matter + except Exception: + return None + return None + + +def get_jwt(request): + + # First try to obtain JWT from headers + token = get_jwt_from_auth_header(request) + + # If the JWT is not in the headers, get it from cookies + if not token: + token = request.cookies.get('jwtToken') + + return token + + +@view_config(route_name='login', request_method='POST', permission=NO_PERMISSION_REQUIRED) +@debug_log +def login(context, request): + ''' + Save JWT as httpOnly cookie + ''' + + # Allow providing token thru Authorization header as well as POST request body. + # Should be about equally secure if using HTTPS. + request_token = get_jwt_from_auth_header(request) + if request_token is None: + request_token = request.json_body.get("id_token", None) + + is_https = request.scheme == "https" + + + # The below 'check' is disabled to provide less feedback than possible + # to make it slightly harder for brute force attacks. + + # is_valid_token = Auth0AuthenticationPolicy.get_token_info(request_token, request) + # if not is_valid_token: + # # Doesn't check if User exists in system, just if token is validly signed, + # # to allow for unregistered users to still have cookie stored so + # # they can go through registration process. + # # (This check is not strictly needed for anything, just provides more feedback faster.. maybe should be removed?) + # raise LoginDenied(domain=request.domain) + + + request.response.set_cookie( + "jwtToken", + value=request_token, + domain=request.domain, + path="/", + httponly=True, + samesite="lax", + overwrite=True, + secure=is_https + ) + + return { "saved_cookie" : True } + + +@view_config(route_name='logout', + permission=NO_PERMISSION_REQUIRED, http_cache=0) +@debug_log +def logout(context, request): + """ + This endpoint is called by the front-end upon executing a logout. It will delete + the session token passed to it from Redis storage, which has the effect of logging + out the user internally. If this isn't done there is no harm as the session token + will expire after 3 hours anyway. + + We do not send the logout signal to Auth0 at this time, since we handle it on our end. + It may be undesirable to send this signal as it will log the user out of all sessions + tied to that account. + """ + # Delete Redis Session Token, if in use + registry = request.registry + session_token = get_jwt(request) + if redis_is_active(request): + redis_handler = registry[REDIS] + env_name = registry.settings['env.name'] + redis_session_token = RedisSessionToken.from_redis( + redis_handler=redis_handler, + namespace=env_name, + token=session_token + ) + if redis_session_token: + redis_session_token.delete_session_token(redis_handler=redis_handler) + + # Tell the browser to delete the cookie + request.response.set_cookie( + name='jwtToken', + value=None, + domain=request.domain, + max_age=0, + path='/', + overwrite=True + ) + + # previously returned 401 - I think 200 (or even 204) is "more correct" - Will March 28 2023 + request.response.status_code = 200 + request.response.headers['WWW-Authenticate'] = ( + "Bearer realm=\"{}\", title=\"Session Expired\"; Basic realm=\"{}\"" + .format(request.domain, request.domain) + ) + return { "deleted_cookie" : True } + + # TODO: NEED DO THIS CLIENTSIDE SO IT UNSETS USER'S COOKIE - MUST BE THRU REDIRECT NOT AJAX + # (we don't do this - i.e. we don't bother to log user out of all of Auth0 session, just out of + # own web app) + # Note that in the Redis factor using Auth0 we are sticking with this behavior, but a callout may + # be necessary to RAS when that system is integrated - Will March 28 2023 + + # call auth0 to logout - + # auth0_logout_url = "https://{domain}/v2/logout" \ + # .format(domain='hms-dbmi.auth0.com') + + # requests.get(auth0_logout_url) + + # if asbool(request.params.get('redirect', True)): + # raise HTTPFound(location=request.resource_path(request.root)) + + # return {} + + +@view_config(route_name='me', request_method='GET', permission=NO_PERMISSION_REQUIRED) +@debug_log +def me(context, request): + '''Alias /users/''' + for principal in request.effective_principals: + if principal.startswith('userid.'): + break + else: + raise HTTPForbidden(title="Not logged in.") + + namespace, userid = principal.split('.', 1) + + # return { "uuid" : userid } # Uncomment and delete below code to just grab UUID. + + request.response.status_code = 307 # Prevent from creating 301 redirects which are then cached permanently by browser + properties = request.embed('/users/' + userid, as_user=userid) + return properties + + +def get_basic_properties_for_user(request, userid): + user = request.registry[COLLECTIONS]['user'][userid] + user_dict = user.__json__(request) + + # Only include certain/applicable fields from profile + include_detail_fields = ['email', 'first_name', 'last_name', 'groups', 'timezone', 'status'] + user_actions = calculate_properties(user, request, category='user_action') + + properties = { + #'user': request.embed(request.resource_path(user)), + 'details' : { p:v for p, v in user_dict.items() if p in include_detail_fields }, + 'user_actions' : [ v for k, v in sorted(user_actions.items(), key=itemgetter(0)) ] + } + + # add uuid to user details + properties['details']['uuid'] = userid + + return properties + + +@view_config(route_name='session-properties', request_method='GET', + permission=NO_PERMISSION_REQUIRED) +@debug_log +def session_properties(context, request): + for principal in request.effective_principals: + if principal.startswith('userid.'): + break + else: + raise LoginDenied(domain=request.domain) + + namespace, userid = principal.split('.', 1) + properties = get_basic_properties_for_user(request, userid) + + #if 'auth.userid' in request.session: + # properties['auth.userid'] = request.session['auth.userid'] + return properties + + +def basic_auth_check(username, password, request): + # We may get called before the context is found and the root set + root = request.registry[ROOT] + collection = root['access-keys'] + try: + access_key = collection[username] + except KeyError: + return None + + properties = access_key.properties + hash = properties['secret_access_key_hash'] + + crypt_context = request.registry[CRYPT_CONTEXT] + valid = crypt_context.verify(password, hash) + if not valid: + return None + + #valid, new_hash = crypt_context.verify_and_update(password, hash) + #if new_hash: + # replace_user_hash(user, new_hash) + + return [] + + +@view_config(route_name='impersonate-user', request_method='POST', + validators=[no_validate_item_content_post], + permission='impersonate') +@debug_log +def impersonate_user(context, request): + """As an admin, impersonate a different user.""" + + userid = request.validated['userid'] + users = request.registry[COLLECTIONS]['user'] + + try: + user = users[userid] + except KeyError: + raise ValidationFailure('body', ['userid'], 'User not found.') + + if user.properties.get('status') != 'current': + raise ValidationFailure('body', ['userid'], 'User is not enabled.') + + user_properties = get_basic_properties_for_user(request, userid) + # pop off impersonate user action if not admin + user_properties['user_actions'] = [x for x in user_properties['user_actions'] if (x['id'] and x['id'] != 'impersonate')] + # make a key + registry = request.registry + auth0_client = registry.settings.get('auth0.client') + auth0_secret = registry.settings.get('auth0.secret') + if not(auth0_client and auth0_secret): + raise HTTPForbidden(title="No keys to impersonate user") + + jwt_contents = { + 'email': userid, + 'email_verified': True, + 'aud': auth0_client, + } + + id_token = jwt.encode( + jwt_contents, + auth0_secret, + algorithm=JWT_ENCODING_ALGORITHM + ) + + if redis_is_active(request): + redis_session_token = RedisSessionToken.from_redis( + redis_handler=request.registry[REDIS], + namespace=request.registry.settings['env.name'], + token=id_token + ) + if not redis_session_token: + raise HTTPForbidden('Unable to generate session token for impersonate user') + + is_https = request.scheme == "https" + + request.response.set_cookie( + "jwtToken", + value=redis_session_token.get_session_token(), # noQA HTTPForbidden raised if not set + domain=request.domain, + path="/", + httponly=True, + samesite="strict", + overwrite=True, + secure=is_https + ) + + return user_properties + + +def generate_user(): + """ Generate a random user name with 64 bits of entropy + Used to generate access_key + """ + # Take a random 5 char binary string (80 bits of + # entropy) and encode it as upper cased base32 (8 chars) + random_bytes = os.urandom(5) + user = base64.b32encode(random_bytes).decode('ascii').rstrip('=').upper() + return user + + +def generate_password(): + """ Generate a password with 80 bits of entropy + """ + # Take a random 10 char binary string (80 bits of + # entropy) and encode it as lower cased base32 (16 chars) + random_bytes = os.urandom(10) + password = base64.b32encode(random_bytes).decode('ascii').rstrip('=').lower() + return password + + +@view_config(route_name='create-unauthorized-user', request_method='POST', + permission=NO_PERMISSION_REQUIRED) +@debug_log +def create_unauthorized_user(context, request): + """ + Endpoint to create an unauthorized user, which will have no lab or award. + Requires a reCAPTCHA response, which is propogated from the front end + registration form. This is so the endpoint cannot be abused. + Given a user properties in the request body, will validate those and also + validate the reCAPTCHA response using the reCAPTCHA server. If all checks + are succesful, POST a new user + + Args: + request: Request object + + Returns: + dictionary User creation response from collection_add + + Raises: + LoginDenied, HTTPForbidden, or ValidationFailure + """ + recaptcha_resp = request.json.get('g-recaptcha-response') + if not recaptcha_resp: + raise LoginDenied() + + registry = request.registry + + # old method for retrieving auth'd email - request object should have _auth0_authenticated set + # NOTE: it is not obvious to me how this works... probably should be looked into - Will March 29 2023 + if not redis_is_active(request): + email = "" + if hasattr(request, "_auth0_authenticated"): + email = request._auth0_authenticated # equal to: jwt_info['email'].lower() + + # new method for retrieving auth'd email - request should have transmitted a session token + # from which we can get the JWT and the email they auth'd with + else: + id_token = get_jwt(request) + redis_handler = registry[REDIS] + env_name = registry.settings['env.name'] + redis_session_token = RedisSessionToken.from_redis( + redis_handler=redis_handler, + namespace=env_name, + token=id_token + ) + jwt_info = redis_session_token.decode_jwt( + audience=request.registry.settings['auth0.client'], + secret=request.registry.settings['auth0.secret'] + ) + email = jwt_info.get('email', '').lower() + + user_props = request.json + user_props_email = user_props.get("email", "").lower() + if user_props_email != email: + raise HTTPUnauthorized( + title="Provided email {} not validated with Auth0. Try logging in again.".format(user_props_email), + headers={'WWW-Authenticate': "Bearer realm=\"{}\"; Basic realm=\"{}\"".format(request.domain, request.domain) } + ) + + del user_props['g-recaptcha-response'] + user_props['was_unauthorized'] = True + user_props['email'] = user_props_email # lowercased + user_coll = request.registry[COLLECTIONS]['User'] + request.remote_user = 'EMBED' # permission = import_items + + # validate the User json + validate_request(user_coll.type_info.schema, request, user_props) + if request.errors: + raise ValidationFailure('body', 'create_unauthorized_user', 'Cannot validate request') + + # validate recaptcha_resp + # https://developers.google.com/recaptcha/docs/verify + recap_url = 'https://www.google.com/recaptcha/api/siteverify' + recap_secret = request.registry.settings['g.recaptcha.secret'] + recap_values = { + 'secret': recap_secret, + 'response': recaptcha_resp + } + data = urlencode(recap_values).encode() + headers = {"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"} + recap_res = requests.get(recap_url, params=data, headers=headers).json() + if recap_res['success']: + sno_res = sno_collection_add(user_coll, request, False) # POST User + if sno_res.get('status') == 'success': + return sno_res + else: + raise HTTPForbidden(title="Could not create user. Try logging in again.") + else: + # error with re-captcha + logger.error(f'Recaptcha used {recap_secret[0:10]} Error from captcha {recap_res}') + raise HTTPUnauthorized( + title="Invalid reCAPTCHA. Try logging in again.", + headers={'WWW-Authenticate': "Bearer realm=\"{}\"; Basic realm=\"{}\"".format(request.domain, request.domain) } + ) diff --git a/src/encoded/dev_servers.py b/src/encoded/dev_servers.py index bbea47d0eb..e4b2512748 100644 --- a/src/encoded/dev_servers.py +++ b/src/encoded/dev_servers.py @@ -1,5 +1,4 @@ from snovault import dev_servers from . import project_defs - dev_servers.main() diff --git a/src/encoded/static/components/app.js b/src/encoded/static/components/app.js index c2be699495..c978c3f23a 100644 --- a/src/encoded/static/components/app.js +++ b/src/encoded/static/components/app.js @@ -1294,9 +1294,9 @@ class ContentRenderer extends React.PureComponent { } else if (status) { // error catching content = ; } else if (context) { // What should occur (success) - + console.log('context:', context); var ContentView = (contentViews || globalContentViews).lookup(context, currentAction); - + console.log('content view', ContentView); if (!ContentView){ // Handle the case where context is not loaded correctly content = ; } else if (currentAction && _.contains(['edit', 'add', 'create'], currentAction)) { // Handle content edit + create action permissions diff --git a/src/encoded/static/components/index.js b/src/encoded/static/components/index.js index 0b3754bce1..c94e471107 100644 --- a/src/encoded/static/components/index.js +++ b/src/encoded/static/components/index.js @@ -6,6 +6,15 @@ * Here we import all of our Content Views (Page Views) and register them * to `globals.content_views` so that they may be picked up and routed to in * the root `App` component. + * + * VERY IMPORTANT: The routing mechanism dispatching from the back-end is based on the + * "title" field ie: if the back-end returns JSON with title=login-success that will trigger + * the rendering of the LoginSuccessView. In addition, there is some weirdness with + * "npm run dev-quick" where it will not reload new components into the system, nor + * can they be imported directly from SPC in this file - so you need to override all components + * from SPC in this repo before configuring the dispatch below. See LoginSuccessView.js and + * authentication.py for a basic illustration of how it works. - Will March 17 2023 + * */ import { content_views } from './globals'; @@ -52,6 +61,10 @@ import PublicationSearchView from './browse/PublicationSearchView'; import SubscriptionsView from './browse/SubscriptionsView'; import FileSearchView from './browse/FileSearchView'; +// auth related views +import LoginSuccessView from './navigation/components/LoginSuccessView'; +import UserRegistrationView from './navigation/components/UserRegistrationView'; + content_views.register(StaticPage, 'StaticPage'); content_views.register(DirectoryPage, 'DirectoryPage'); @@ -111,6 +124,9 @@ content_views.register(SearchView, 'PublicationSearchResults', 'mul content_views.register(SubscriptionsView, 'Submissions'); // TODO: Rename 'Submissions' to 'Subscriptions' on back-end (?) content_views.register(FileSearchView, 'FileSearchResults'); +content_views.register(LoginSuccessView, 'callback'); +content_views.register(UserRegistrationView, 'registration'); + // Fallback for anything we haven't registered content_views.fallback = function () { return FallbackView; diff --git a/src/encoded/static/components/navigation/components/AccountNav.js b/src/encoded/static/components/navigation/components/AccountNav.js index e8dcac6c9d..da0d962e76 100644 --- a/src/encoded/static/components/navigation/components/AccountNav.js +++ b/src/encoded/static/components/navigation/components/AccountNav.js @@ -7,7 +7,6 @@ import Nav from 'react-bootstrap/esm/Nav'; import { JWT, isServerSide, object, console, memoizedUrlParse } from '@hms-dbmi-bgm/shared-portal-components/es/components/util'; import { LoginController, LogoutController } from '@hms-dbmi-bgm/shared-portal-components/es/components/navigation/components/LoginController'; - import { LoginNavItem } from './LoginNavItem'; import { BigDropdownNavItem, BigDropdownIntroductionWrapper } from './BigDropdown'; @@ -51,6 +50,7 @@ const auth0Options = { export const AccountNav = React.memo(function AccountNav(props){ const { session, updateAppSessionState, schemas, ...passProps } = props; const { windowWidth, href } = passProps; + updateAppSessionState(); // call this in an attempt before rendering the component if (!session) { // Render login button return (