From 0c55b8f2405bae02f40fba64019dc217923b1b10 Mon Sep 17 00:00:00 2001 From: Midnighter Date: Thu, 30 Apr 2020 11:09:41 +0200 Subject: [PATCH 01/11] refactor: merge new cookiecutter --- .travis.yml | 44 ++++---- Dockerfile | 54 +++++++--- Makefile | 153 ++++++++++++++++++--------- deployment/production/deployment.yml | 1 - deployment/staging/deployment.yml | 1 - docker-compose.yml | 9 +- pyproject.toml | 3 + scripts/deploy.sh | 6 +- scripts/verify_license_headers.sh | 15 ++- setup.cfg | 75 ++++++++----- 10 files changed, 241 insertions(+), 120 deletions(-) create mode 100644 pyproject.toml diff --git a/.travis.yml b/.travis.yml index 6513a7b..aab478a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ -sudo: required -language: minimal +os: linux +language: shell git: depth: 2 @@ -10,32 +10,38 @@ branches: - devel services: - - docker +- docker env: global: - - IMAGE_REPO=gcr.io/dd-decaf-cfbf6/iam - - IMAGE_TAG=travis-ci-test - - IMAGE=${IMAGE_REPO}:${IMAGE_TAG} + - IMAGE=gcr.io/dd-decaf-cfbf6/iam + - BRANCH=${TRAVIS_BRANCH} + - BUILD_COMMIT=${TRAVIS_COMMIT} + - SHORT_COMMIT=${TRAVIS_COMMIT:0:7} + - BUILD_TIMESTAMP=$(date --utc --iso-8601=seconds) + - BUILD_DATE=$(date --utc --iso-8601=date) + - BUILD_TAG=${BRANCH}_${BUILD_DATE}_${SHORT_COMMIT} + +before_install: +- make setup install: - - docker build -t ${IMAGE} . - - make setup +- make build +- make build-travis +- make post-build +- make start script: - - make flake8 - - make isort - - make license - - make safety - - make test-travis +- make style +- make safety +# Run the tests and report coverage (see https://docs.codecov.io/docs/testing-with-docker). +- docker-compose exec -e ENVIRONMENT=testing web pytest --cov=iam --cov-report=term --cov-report=xml +- bash <(curl -s https://codecov.io/bash) before_deploy: - - ./scripts/install_gcloud.sh - - ./scripts/install_kubectl.sh - - docker tag ${IMAGE} ${IMAGE_REPO}:${TRAVIS_COMMIT::12} - - docker tag ${IMAGE} ${IMAGE_REPO}:${TRAVIS_BRANCH} - - docker push ${IMAGE_REPO}:${TRAVIS_COMMIT::12} - - docker push ${IMAGE_REPO}:${TRAVIS_BRANCH} +- ./scripts/install_gcloud.sh +- ./scripts/install_kubectl.sh +- if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then make push; fi deploy: provider: script diff --git a/Dockerfile b/Dockerfile index bd8317e..689d0d5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,59 @@ -FROM dddecaf/postgres-base:master +# Copyright (c) 2018-2020 Novo Nordisk Foundation Center for Biosustainability, +# Technical University of Denmark. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. -ENV APP_USER=giraffe +ARG BASE_TAG=alpine -ARG UID=1000 -ARG GID=1000 +FROM dddecaf/postgres-base:${BASE_TAG} + +ARG BASE_TAG=alpine +ARG BUILD_COMMIT +ARG BUILD_TIMESTAMP + +LABEL dk.dtu.biosustain.iam.alpine.vendor="Novo Nordisk Foundation \ +Center for Biosustainability, Technical University of Denmark" \ + maintainer="niso@biosustain.dtu.dk" \ + dk.dtu.biosustain.iam.alpine.build.base-tag="${BASE_TAG}" \ + dk.dtu.biosustain.iam.alpine.build.commit="${BUILD_COMMIT}" \ + dk.dtu.biosustain.iam.alpine.build.timestamp="${BUILD_TIMESTAMP}" + +ARG CWD="/app" + +ENV PYTHONPATH="${CWD}/src" -ARG CWD=/app -ENV PYTHONPATH=${CWD}/src WORKDIR "${CWD}" -RUN addgroup -g "${GID}" -S "${APP_USER}" && \ - adduser -u "${UID}" -G "${APP_USER}" -S "${APP_USER}" +COPY requirements ./requirements/ # Install openssh to be able to generate rsa keys RUN apk add --update --no-cache openssh # Install python dependencies COPY requirements ./requirements + RUN set -eux \ # build-base is required to build grpcio->firebase-admin && apk add --no-cache --virtual .build-deps build-base linux-headers \ - && pip-sync requirements/requirements.txt \ + && pip install -r requirements/requirements.txt \ + && rm -rf /root/.cache/pip \ # Remove build dependencies to reduce layer size. && apk del .build-deps -# Install the codebase -COPY . "${CWD}/" -RUN chown -R "${APP_USER}:${APP_USER}" "${CWD}" +COPY . ./ + +RUN chown -R "${APP_USER}:${APP_USER}" . + +EXPOSE 8000 + +CMD ["gunicorn", "-c", "gunicorn.py", "iam.wsgi:app"] diff --git a/Makefile b/Makefile index 9449aac..acc0288 100644 --- a/Makefile +++ b/Makefile @@ -1,33 +1,82 @@ -.PHONY: setup network keypair databases lock build start qa style test \ - test-travis flake8 isort isort-save license stop clean logs safety -SHELL:=/bin/bash +.PHONY: setup post-build lock own build push start qa style safety test qc stop \ + clean logs +################################################################################ +# Variables # +################################################################################ + +IMAGE ?= gcr.io/dd-decaf-cfbf6/iam +BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) +BUILD_COMMIT ?= $(shell git rev-parse HEAD) +SHORT_COMMIT ?= $(shell git rev-parse --short HEAD) +BUILD_TIMESTAMP ?= $(shell date --utc --iso-8601=seconds) +BUILD_DATE ?= $(shell date --utc --iso-8601=date) +BUILD_TAG ?= ${BRANCH}_${BUILD_DATE}_${SHORT_COMMIT} ################################################################################# # COMMANDS # ################################################################################# -## Run all initialization targets. You must only run this once. -setup: network keypair databases - ## Create the docker bridge network if necessary. network: docker network inspect DD-DeCaF >/dev/null 2>&1 || \ docker network create DD-DeCaF -## Build local docker images. +## Run all initialization targets. +setup: network + +## Generate the compiled requirements files. +lock: + docker pull dddecaf/tag-spy:latest + $(eval LATEST_BASE_TAG := $(shell docker run --rm dddecaf/tag-spy:latest tag-spy dddecaf/postgres-base alpine dk.dtu.biosustain.postgres-base.alpine.build.timestamp)) + $(file >LATEST_BASE_TAG, $(LATEST_BASE_TAG)) + $(eval COMPILER_TAG := $(subst alpine,alpine-compiler,$(LATEST_BASE_TAG))) + $(info ************************************************************) + $(info * Compiling service dependencies on the basis of:) + $(info * dddecaf/postgres-base:$(COMPILER_TAG)) + $(info ************************************************************) + docker pull dddecaf/postgres-base:$(COMPILER_TAG) + docker run --rm --mount \ + "source=$(CURDIR)/requirements,target=/opt/requirements,type=bind" \ + dddecaf/postgres-base:$(COMPILER_TAG) \ + pip-compile --allow-unsafe --verbose --generate-hashes --upgrade \ + /opt/requirements/requirements.in + +## Change file ownership from root to local user. +own: + sudo chown "$(shell id --user --name):$(shell id --group --name)" . + +## Build the Docker image for deployment. +build-travis: + $(eval LATEST_BASE_TAG := $(shell cat LATEST_BASE_TAG)) + $(info ************************************************************) + $(info * Building the service on the basis of:) + $(info * dddecaf/postgres-base:$(LATEST_BASE_TAG)) + $(info * Today is $(shell date --utc --iso-8601=date).) + $(info * Please re-run `make lock` if you want to check for and) + $(info * depend on a later version.) + $(info ************************************************************) + docker pull dddecaf/postgres-base:$(LATEST_BASE_TAG) + docker build --build-arg BASE_TAG=$(LATEST_BASE_TAG) \ + --build-arg BUILD_COMMIT=$(BUILD_COMMIT) \ + --build-arg BUILD_TIMESTAMP=$(BUILD_TIMESTAMP) \ + --tag $(IMAGE):$(BRANCH) \ + --tag $(IMAGE):$(BUILD_TAG) \ + . + +## Build the local docker-compose image. build: - docker-compose build + $(eval LATEST_BASE_TAG := $(shell cat LATEST_BASE_TAG)) + BASE_TAG=$(LATEST_BASE_TAG) docker-compose build -## Recompile requirements and store pinned dependencies with hashes. -pip-compile: - docker run --rm -v `pwd`/requirements:/build dddecaf/postgres-base:compiler \ - pip-compile --generate-hashes --upgrade \ - --output-file /build/requirements.txt /build/requirements.in +## Push local Docker images to their registries. +push: + docker push $(IMAGE):$(BRANCH) + docker push $(IMAGE):$(BUILD_TAG) ## Start all services in the background. start: - docker-compose up -d + docker-compose up --force-recreate -d ## Create RSA keypair used for signing JWTs. keypair: @@ -43,45 +92,51 @@ databases: docker-compose stop # note: not migrating iam_test db; tests will create and tear down tables -## Run all QA targets. -qa: style safety test - -## Run all style related targets. -style: flake8 isort license +## Run all post-build initialization targets. You must only run this once. +post-build: keypair databases -## Run flake8. -flake8: - docker-compose run --rm web flake8 src/iam tests +## Apply all quality assurance (QA) tools. +qa: + docker-compose exec -e ENVIRONMENT=testing web \ + isort --recursive src tests + docker-compose exec -e ENVIRONMENT=testing web \ + black src tests -## Check Python package import order. isort: - docker-compose run --rm web isort --check-only --recursive src/iam tests + docker-compose exec -e ENVIRONMENT=testing web \ + isort --check-only --diff --recursive src tests + +black: + docker-compose exec -e ENVIRONMENT=testing web \ + black --check --diff src tests -## Sort imports and write changes to files. -isort-save: - docker-compose run --rm web isort --recursive src/iam tests +flake8: + docker-compose exec -e ENVIRONMENT=testing web \ + flake8 src tests -## Verify source code license headers. license: - ./scripts/verify_license_headers.sh src/iam tests + docker-compose exec -e ENVIRONMENT=testing web \ + ./scripts/verify_license_headers.sh src tests -## Check for known vulnerabilities in python dependencies. +## Run all style checks. +style: isort black flake8 license + +## Check installed dependencies for vulnerabilities. safety: - docker-compose run --rm web safety check + docker-compose exec -e ENVIRONMENT=testing web \ + safety check --full-report -## Run the tests. +## Run the test suite. test: - docker-compose run --rm -e ENVIRONMENT=testing web pytest --cov=src/iam - -## Run the tests and report coverage (see https://docs.codecov.io/docs/testing-with-docker). -shared := /tmp/coverage -test-travis: - mkdir "$(shared)" - docker-compose run --rm -e ENVIRONMENT=testing \ - -v "$(shared):$(shared)" web \ - pytest --cov-report xml:$(shared)/coverage.xml --cov-report term \ - --cov=src/iam - bash <(curl -s https://codecov.io/bash) -f "$(shared)/coverage.xml" + docker-compose exec -e ENVIRONMENT=testing web \ + pytest --cov=iam --cov-report=term + +## Run all quality control (QC) tools. +qc: style safety test + +## Check the gunicorn configuration. +gunicorn: + docker-compose run --rm web gunicorn --check-config -c gunicorn.py iam.wsgi:app ## Stop all services. stop: @@ -95,13 +150,14 @@ clean: logs: docker-compose logs --tail="all" -f -################################################################################# -# Self Documenting Commands # -################################################################################# +################################################################################ +# Self Documenting Commands # +################################################################################ .DEFAULT_GOAL := show-help -# Inspired by +# Inspired by +# # sed script explained: # /^##/: # * save line in hold space @@ -154,4 +210,5 @@ show-help: } \ printf "\n"; \ }' \ - | more $(shell test $(shell uname) = Darwin && echo '--no-init --raw-control-chars') + | more $(shell test $(shell uname) = Darwin \ + && echo '--no-init --raw-control-chars') diff --git a/deployment/production/deployment.yml b/deployment/production/deployment.yml index d36e06f..01c2f98 100644 --- a/deployment/production/deployment.yml +++ b/deployment/production/deployment.yml @@ -201,7 +201,6 @@ spec: readOnly: true - name: prometheus-client mountPath: /prometheus-client - command: ["gunicorn", "-c", "gunicorn.py", "iam.wsgi:app"] readinessProbe: httpGet: path: /iam/healthz diff --git a/deployment/staging/deployment.yml b/deployment/staging/deployment.yml index ee42e49..9339714 100644 --- a/deployment/staging/deployment.yml +++ b/deployment/staging/deployment.yml @@ -201,7 +201,6 @@ spec: readOnly: true - name: prometheus-client mountPath: /prometheus-client - command: ["gunicorn", "-c", "gunicorn.py", "iam.wsgi:app"] readinessProbe: httpGet: path: /iam/healthz diff --git a/docker-compose.yml b/docker-compose.yml index 78efc43..9adfc76 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,10 @@ services: build: context: . dockerfile: Dockerfile + args: + - BASE_TAG=${BASE_TAG} + - BUILD_COMMIT=${BUILD_COMMIT:-unknown} + - BUILD_TIMESTAMP=${BUILD_TIMESTAMP:-unknown} image: gcr.io/dd-decaf-cfbf6/iam:${IMAGE_TAG:-latest} networks: default: @@ -20,9 +24,9 @@ services: - postgres environment: - ENVIRONMENT=${ENVIRONMENT:-development} - - SCRIPT_NAME=${SCRIPT_NAME} - FLASK_APP=src/iam/wsgi.py - - ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-*} + - SCRIPT_NAME=${SCRIPT_NAME} + - ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-http://localhost:4200} - SENTRY_DSN=${SENTRY_DSN} - BASIC_AUTH_USERNAME=${BASIC_AUTH_USERNAME:-admin} - BASIC_AUTH_PASSWORD=${BASIC_AUTH_PASSWORD} @@ -41,7 +45,6 @@ services: - FIREBASE_PRIVATE_KEY=${FIREBASE_PRIVATE_KEY} - prometheus_multiproc_dir=/prometheus-client - SENDGRID_API_KEY=${SENDGRID_API_KEY} - command: gunicorn -c gunicorn.py iam.wsgi:app postgres: image: postgres:9.6-alpine diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..61a0cef --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.black] +line-length = 80 +python-version = ['py36'] diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 7754be7..d175c84 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash -# Copyright 2018 Novo Nordisk Foundation Center for Biosustainability, DTU. +# Copyright 2018-2020 Novo Nordisk Foundation Center for Biosustainability, DTU. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -25,4 +25,4 @@ else exit 0 fi -kubectl set image deployment/${DEPLOYMENT} web=${IMAGE_REPO}:${TRAVIS_COMMIT::12} +kubectl set image deployment/${DEPLOYMENT} web=${IMAGE}:${BUILD_TAG} diff --git a/scripts/verify_license_headers.sh b/scripts/verify_license_headers.sh index 150c5c4..502b428 100755 --- a/scripts/verify_license_headers.sh +++ b/scripts/verify_license_headers.sh @@ -1,13 +1,13 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh -# Copyright (c) 2018, Novo Nordisk Foundation Center for Biosustainability, +# Copyright (c) 2020, Novo Nordisk Foundation Center for Biosustainability, # Technical University of Denmark. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -15,17 +15,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -set -eu +set -u PATTERN="Novo Nordisk Foundation Center for Biosustainability" RET=0 for file in $(find $@ -name '*.py') do - grep "${PATTERN}" ${file} >/dev/null - if [[ $? != 0 ]] - then - echo "Source code file ${file} seems to be missing a license header" + grep "${PATTERN}" "${file}" >/dev/null + if [ $? -ne 0 ]; then + echo "Source file '${file}' seems to be missing a license header." RET=1 fi done diff --git a/setup.cfg b/setup.cfg index 286259b..14c206d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,39 +15,64 @@ description = Identity and Access Management long_description = file: README.md keywords = -[options] -zip_safe = True -python_requires = >=3.6 -include_package_data = True -packages = find: +################################################################################ +# Testing tools configuration # +################################################################################ -[options.package_data] -iam = +[tool:pytest] +testpaths = + tests +markers = + raises + +[coverage:paths] +source = + src/iam -[wheel] -universal = 1 +[coverage:run] +branch = true +parallel = true -[bdist_wheel] -universal = 1 +[coverage:report] +exclude_lines = +# Have to re-enable the standard pragma + pragma: no cover +precision = 2 [flake8] -ignore = D100,D101,D102,D103,D104,D105,D106,D107 max-line-length = 80 -exclude = - __init__.py - docs - -[aliases] -test = pytest - -[tool:pytest] -testpaths = tests - -[pydocstyle] -match_dir = src/iam +exclude = __init__.py +ignore = +# We do not require docstrings on all public functions and classes. We +# encourage them but ultimately it is decided during review. + D100 + D101 + D102 + D103 + D104 + D105 + D106 + D107 +# The following conflict with `black` which is the more pedantic. + E203 + W503 + D202 [isort] +skip = __init__.py line_length = 80 indent = 4 -multi_line_output = 4 +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true lines_after_imports = 2 +known_first_party = iam +known_third_party = + flask + flask_apispec + flask_cors + marshmallow + pytest + raven + werkzeug From 25e8757bf02cb91d26c155a929e7a7463bf33dfa Mon Sep 17 00:00:00 2001 From: Midnighter Date: Thu, 30 Apr 2020 11:14:00 +0200 Subject: [PATCH 02/11] chore: run dependency lock --- LATEST_BASE_TAG | 1 + requirements/requirements.txt | 289 +++++++++++++++++----------------- 2 files changed, 148 insertions(+), 142 deletions(-) create mode 100644 LATEST_BASE_TAG diff --git a/LATEST_BASE_TAG b/LATEST_BASE_TAG new file mode 100644 index 0000000..6ed1dfc --- /dev/null +++ b/LATEST_BASE_TAG @@ -0,0 +1 @@ + alpine_2020-04-29_ae14192 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index ff2dd19..e0b6593 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --generate-hashes --output-file=/build/requirements.txt /build/requirements.in +# pip-compile --allow-unsafe --generate-hashes /opt/requirements/requirements.in # alembic==1.4.2 \ --hash=sha256:035ab00497217628bf5d0be82d664d8713ab13d37b630084da8e1f98facf4dbf \ @@ -14,7 +14,7 @@ apispec==3.3.0 \ appdirs==1.4.3 \ --hash=sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92 \ --hash=sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e \ - # via -r /opt/sql-requirements.txt, black, virtualenv + # via -r /opt/sql-requirements.txt, black attrs==19.3.0 \ --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \ --hash=sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72 \ @@ -41,58 +41,55 @@ cachetools==4.1.0 \ certifi==2020.4.5.1 \ --hash=sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304 \ --hash=sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519 \ - # via -r /opt/sql-requirements.txt, pipenv, requests + # via -r /opt/sql-requirements.txt, requests chardet==3.0.4 \ --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \ --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 \ # via -r /opt/sql-requirements.txt, requests -click==7.1.1 \ - --hash=sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc \ - --hash=sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a \ +click==7.1.2 \ + --hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a \ + --hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc \ # via -r /opt/sql-requirements.txt, black, flask, safety -coverage==5.0.4 \ - --hash=sha256:03f630aba2b9b0d69871c2e8d23a69b7fe94a1e2f5f10df5049c0df99db639a0 \ - --hash=sha256:046a1a742e66d065d16fb564a26c2a15867f17695e7f3d358d7b1ad8a61bca30 \ - --hash=sha256:0a907199566269e1cfa304325cc3b45c72ae341fbb3253ddde19fa820ded7a8b \ - --hash=sha256:165a48268bfb5a77e2d9dbb80de7ea917332a79c7adb747bd005b3a07ff8caf0 \ - --hash=sha256:1b60a95fc995649464e0cd48cecc8288bac5f4198f21d04b8229dc4097d76823 \ - --hash=sha256:1f66cf263ec77af5b8fe14ef14c5e46e2eb4a795ac495ad7c03adc72ae43fafe \ - --hash=sha256:2e08c32cbede4a29e2a701822291ae2bc9b5220a971bba9d1e7615312efd3037 \ - --hash=sha256:3844c3dab800ca8536f75ae89f3cf566848a3eb2af4d9f7b1103b4f4f7a5dad6 \ - --hash=sha256:408ce64078398b2ee2ec08199ea3fcf382828d2f8a19c5a5ba2946fe5ddc6c31 \ - --hash=sha256:443be7602c790960b9514567917af538cac7807a7c0c0727c4d2bbd4014920fd \ - --hash=sha256:4482f69e0701139d0f2c44f3c395d1d1d37abd81bfafbf9b6efbe2542679d892 \ - --hash=sha256:4a8a259bf990044351baf69d3b23e575699dd60b18460c71e81dc565f5819ac1 \ - --hash=sha256:513e6526e0082c59a984448f4104c9bf346c2da9961779ede1fc458e8e8a1f78 \ - --hash=sha256:5f587dfd83cb669933186661a351ad6fc7166273bc3e3a1531ec5c783d997aac \ - --hash=sha256:62061e87071497951155cbccee487980524d7abea647a1b2a6eb6b9647df9006 \ - --hash=sha256:641e329e7f2c01531c45c687efcec8aeca2a78a4ff26d49184dce3d53fc35014 \ - --hash=sha256:65a7e00c00472cd0f59ae09d2fb8a8aaae7f4a0cf54b2b74f3138d9f9ceb9cb2 \ - --hash=sha256:6ad6ca45e9e92c05295f638e78cd42bfaaf8ee07878c9ed73e93190b26c125f7 \ - --hash=sha256:73aa6e86034dad9f00f4bbf5a666a889d17d79db73bc5af04abd6c20a014d9c8 \ - --hash=sha256:7c9762f80a25d8d0e4ab3cb1af5d9dffbddb3ee5d21c43e3474c84bf5ff941f7 \ - --hash=sha256:85596aa5d9aac1bf39fe39d9fa1051b0f00823982a1de5766e35d495b4a36ca9 \ - --hash=sha256:86a0ea78fd851b313b2e712266f663e13b6bc78c2fb260b079e8b67d970474b1 \ - --hash=sha256:8a620767b8209f3446197c0e29ba895d75a1e272a36af0786ec70fe7834e4307 \ - --hash=sha256:922fb9ef2c67c3ab20e22948dcfd783397e4c043a5c5fa5ff5e9df5529074b0a \ - --hash=sha256:9fad78c13e71546a76c2f8789623eec8e499f8d2d799f4b4547162ce0a4df435 \ - --hash=sha256:a37c6233b28e5bc340054cf6170e7090a4e85069513320275a4dc929144dccf0 \ - --hash=sha256:c3fc325ce4cbf902d05a80daa47b645d07e796a80682c1c5800d6ac5045193e5 \ - --hash=sha256:cda33311cb9fb9323958a69499a667bd728a39a7aa4718d7622597a44c4f1441 \ - --hash=sha256:db1d4e38c9b15be1521722e946ee24f6db95b189d1447fa9ff18dd16ba89f732 \ - --hash=sha256:eda55e6e9ea258f5e4add23bcf33dc53b2c319e70806e180aecbff8d90ea24de \ - --hash=sha256:f372cdbb240e09ee855735b9d85e7f50730dcfb6296b74b95a3e5dea0615c4c1 \ +coverage==5.1 \ + --hash=sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a \ + --hash=sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355 \ + --hash=sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65 \ + --hash=sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7 \ + --hash=sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9 \ + --hash=sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1 \ + --hash=sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0 \ + --hash=sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55 \ + --hash=sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c \ + --hash=sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6 \ + --hash=sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef \ + --hash=sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019 \ + --hash=sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e \ + --hash=sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0 \ + --hash=sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf \ + --hash=sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24 \ + --hash=sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2 \ + --hash=sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c \ + --hash=sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4 \ + --hash=sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0 \ + --hash=sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd \ + --hash=sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04 \ + --hash=sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e \ + --hash=sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730 \ + --hash=sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2 \ + --hash=sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768 \ + --hash=sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796 \ + --hash=sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7 \ + --hash=sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a \ + --hash=sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489 \ + --hash=sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052 \ # via -r /opt/sql-requirements.txt, pytest-cov decorator==4.4.2 \ --hash=sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760 \ --hash=sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7 \ # via -r /opt/sql-requirements.txt, ipython, traitlets -distlib==0.3.0 \ - --hash=sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21 \ - # via -r /opt/sql-requirements.txt, virtualenv -dparse==0.5.0 \ - --hash=sha256:14fed5efc5e98c0a81dfe100c4c2ea0a4c189104e9a9d18b5cfd342a163f97be \ - --hash=sha256:db349e53f6d03c8ee80606c49b35f515ed2ab287a8e1579e2b4bdf52b12b1530 \ +dparse==0.5.1 \ + --hash=sha256:a1b5f169102e1c894f9a7d5ccf6f9402a836a5d24be80a986c7ce9eaed78f367 \ + --hash=sha256:e953a25e44ebb60a5c6efc2add4420c177f1d8404509da88da9729202f306994 \ # via -r /opt/sql-requirements.txt, safety ecdsa==0.15 \ --hash=sha256:867ec9cf6df0b03addc8ef66b56359643cb5d0c1dc329df76ba7ecfe256c8061 \ @@ -102,14 +99,10 @@ entrypoints==0.3 \ --hash=sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19 \ --hash=sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451 \ # via -r /opt/sql-requirements.txt, flake8 -filelock==3.0.12 \ - --hash=sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59 \ - --hash=sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836 \ - # via -r /opt/sql-requirements.txt, virtualenv -firebase-admin==4.0.1 \ - --hash=sha256:b93fb42ab4b5938f4ad43029bd65f80d17d9d5c48f5c81daa8f01b383783ff62 \ - --hash=sha256:cadbcc6aef11c77b37a83e87c236f4e84a787d046a5ff579bc6f7703d2e1afc8 \ - # via -r /build/requirements.in +firebase-admin==4.1.0 \ + --hash=sha256:0ce190793a4fc73a73dc6301cab98c9abdd8c045ada490310beea7eba92a6c53 \ + --hash=sha256:86f753b69e2534bdc925c49e64ba97704a9cdf201374a480c883f4a69c7f46a6 \ + # via -r /opt/requirements/requirements.in flake8-bugbear==20.1.4 \ --hash=sha256:a3ddc03ec28ba2296fc6f89444d1c946a6b76460f859795b35b77d4920a51b63 \ --hash=sha256:bd02e4b009fb153fe6072c31c52aeab5b133d508095befb2ffcf3b41c4823162 \ @@ -148,46 +141,47 @@ flask==1.1.2 \ --hash=sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060 \ --hash=sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557 \ # via -r /opt/sql-requirements.txt, flask-admin, flask-apispec, flask-basicauth, flask-cors, flask-migrate, flask-sqlalchemy, raven -gevent==1.4.0 \ - --hash=sha256:0774babec518a24d9a7231d4e689931f31b332c4517a771e532002614e270a64 \ - --hash=sha256:0e1e5b73a445fe82d40907322e1e0eec6a6745ca3cea19291c6f9f50117bb7ea \ - --hash=sha256:0ff2b70e8e338cf13bedf146b8c29d475e2a544b5d1fe14045aee827c073842c \ - --hash=sha256:107f4232db2172f7e8429ed7779c10f2ed16616d75ffbe77e0e0c3fcdeb51a51 \ - --hash=sha256:14b4d06d19d39a440e72253f77067d27209c67e7611e352f79fe69e0f618f76e \ - --hash=sha256:1b7d3a285978b27b469c0ff5fb5a72bcd69f4306dbbf22d7997d83209a8ba917 \ - --hash=sha256:1eb7fa3b9bd9174dfe9c3b59b7a09b768ecd496debfc4976a9530a3e15c990d1 \ - --hash=sha256:2711e69788ddb34c059a30186e05c55a6b611cb9e34ac343e69cf3264d42fe1c \ - --hash=sha256:28a0c5417b464562ab9842dd1fb0cc1524e60494641d973206ec24d6ec5f6909 \ - --hash=sha256:3249011d13d0c63bea72d91cec23a9cf18c25f91d1f115121e5c9113d753fa12 \ - --hash=sha256:44089ed06a962a3a70e96353c981d628b2d4a2f2a75ea5d90f916a62d22af2e8 \ - --hash=sha256:4bfa291e3c931ff3c99a349d8857605dca029de61d74c6bb82bd46373959c942 \ - --hash=sha256:50024a1ee2cf04645535c5ebaeaa0a60c5ef32e262da981f4be0546b26791950 \ - --hash=sha256:53b72385857e04e7faca13c613c07cab411480822ac658d97fd8a4ddbaf715c8 \ - --hash=sha256:74b7528f901f39c39cdbb50cdf08f1a2351725d9aebaef212a29abfbb06895ee \ - --hash=sha256:7d0809e2991c9784eceeadef01c27ee6a33ca09ebba6154317a257353e3af922 \ - --hash=sha256:896b2b80931d6b13b5d9feba3d4eebc67d5e6ec54f0cf3339d08487d55d93b0e \ - --hash=sha256:8d9ec51cc06580f8c21b41fd3f2b3465197ba5b23c00eb7d422b7ae0380510b0 \ - --hash=sha256:9f7a1e96fec45f70ad364e46de32ccacab4d80de238bd3c2edd036867ccd48ad \ - --hash=sha256:ab4dc33ef0e26dc627559786a4fba0c2227f125db85d970abbf85b77506b3f51 \ - --hash=sha256:d1e6d1f156e999edab069d79d890859806b555ce4e4da5b6418616322f0a3df1 \ - --hash=sha256:d752bcf1b98174780e2317ada12013d612f05116456133a6acf3e17d43b71f05 \ - --hash=sha256:e5bcc4270671936349249d26140c267397b7b4b1381f5ec8b13c53c5b53ab6e1 \ +gevent==20.4.0 \ + --hash=sha256:0b84a8d6f088b29a74402728681c9f11864b95e49f5587a666e6fbf5c683e597 \ + --hash=sha256:1ef086264e846371beb5742ebaeb148dc96adf72da2ff350ae5603421cdc2ad9 \ + --hash=sha256:2070c65896f89a85b39f49427d6132f7abd047129fc4da88b3670f0ba13b0cf7 \ + --hash=sha256:2fbe0bc43d8c5540153f06eece6235dda14e5f99bdd9183838396313100815d7 \ + --hash=sha256:32813de352918fb652a3db805fd6e08e0a1666a1a9304eef95938c9c426f9573 \ + --hash=sha256:38c45d8a3b647f56f8a68769a8ac4953be84a84735c7c7a4d7ca62022bd54036 \ + --hash=sha256:3b4c4d99f87c0d04b825879c5a91fbfa2b66da7c25b8689e9bdd9f4741d5f80d \ + --hash=sha256:42cae3be36b7458f411bd589c66aaba27e4e611ec3d3621e37fd732fe383f9b6 \ + --hash=sha256:4572dc7907a0ac3c39b9f0898dbdf390ae3250baaae5f7395661fb844e2e23be \ + --hash=sha256:6088bedd8b6bcdb815be322304a5d1c028ffa837d84e93b349928dadac62f354 \ + --hash=sha256:8a9aba59a3268f20c7b584119215bdc589cb81500d93dad4dab428eb02f72944 \ + --hash=sha256:8cca7ffd58559f8d51e5605ad73afcc6f348f9747d2fa539b336e70851b69b79 \ + --hash=sha256:956e82a5d0e90f8d71efe4cecccde602cfb657cd866c58bb953c9c30ca1b3d77 \ + --hash=sha256:b0aea12de542f8fcd6882087bdd5b4d7dc8bb316d28181f6b012dd0b91583285 \ + --hash=sha256:b46399f6c9eccc2e6de1dc1057d362be840443e5439b06cce8b01d114ba1a7ec \ + --hash=sha256:c0b38a654c8fde5b9d9bd27ea3261aeefe36bc9244b170b6d3b11d72a2163bdb \ + --hash=sha256:c516cc5d70c3faf07f271d50930d144339c69fb80f3cac9b687aa964e518535e \ + --hash=sha256:c7a62d51c6dca84f91a91b940037523c926a516f0568f47dc1386bd1682cf4e9 \ + --hash=sha256:cea28f958bc4206ae092043e0775cd7a2bb2536bcbece292732c6484c1076c01 \ + --hash=sha256:d56f36eb98532d2bccc51cb0964c31e9fbd9b2282074c297dc9b006b047e2966 \ + --hash=sha256:de6c0cbcb890d0a79323961d3b593a0f2f54dcb9fe38ee5167f2d514e69e3c8c \ + --hash=sha256:e0990009e7c1624f9a0f3335df1ab8d45678241c852659ac645b70ed8229097c \ + --hash=sha256:e7d23d5f32c9db6ae49c4b58585618dcafd6ad0babae251c9c8297afebc4744b \ + --hash=sha256:ee39caf14d66e619709cdfe3962bc68a234518e43ea8c811c0d67a864bc7c196 \ # via -r /opt/sql-requirements.txt google-api-core[grpc]==1.17.0 \ --hash=sha256:c0e430658ed6be902d7ba7095fb0a9cac810270d71bf7ac4484e76c300407aae \ --hash=sha256:e4082a0b479dc2dee2f8d7b80ea8b5d0184885b773caab15ab1836277a01d689 \ # via firebase-admin, google-api-python-client, google-cloud-core, google-cloud-firestore -google-api-python-client==1.8.0 \ - --hash=sha256:0f5b42a14e2d2f7dee40f2e4514531dbe95ebde9c2173b1c4040a65c427e7900 \ - --hash=sha256:5032ad1af5046889649b3848f2e871889fbb6ae440198a549fe1699581300386 \ +google-api-python-client==1.8.2 \ + --hash=sha256:8dd35a3704650c2db44e6cf52abdaf9de71f409c93c56bbe48a321ab5e14ebad \ + --hash=sha256:bf482c13fb41a6d01770f9d62be6b33fdcd41d68c97f2beb9be02297bdd9e725 \ # via firebase-admin google-auth-httplib2==0.0.3 \ --hash=sha256:098fade613c25b4527b2c08fa42d11f3c2037dda8995d86de0745228e965d445 \ --hash=sha256:f1c437842155680cf9918df9bc51c1182fda41feef88c34004bd1978c8157e08 \ # via google-api-python-client -google-auth==1.14.0 \ - --hash=sha256:050f1713142fa57d4b34f4fd4a998210e330f6a29c84c6ce359b928cc11dc8ad \ - --hash=sha256:9813eaae335c45e8a1b5d274610fa961ac8aa650568d1cfb005b2c07da6bde6c \ +google-auth==1.14.1 \ + --hash=sha256:0c41a453b9a8e77975bfa436b8daedac00aed1c545d84410daff8272fff40fbb \ + --hash=sha256:e63b2210e03c4ed829063b72c4af0c4b867c2788efb3210b6b9439b488bd3afd \ # via google-api-core, google-api-python-client, google-auth-httplib2, google-cloud-storage google-cloud-core==1.3.0 \ --hash=sha256:6ae5c62931e8345692241ac1939b85a10d6c38dc9e2854bdbacb7e5ac3033229 \ @@ -197,9 +191,9 @@ google-cloud-firestore==1.6.2 \ --hash=sha256:31bfbc47865ae5933ffd24dad27c672b91ca0fc0ae0f9c4bddf7c2aaf9aa2edb \ --hash=sha256:5ad4835c3a0f6350bcbbc42fd70e90f7568fca289fdb5e851888df394c4ebf80 \ # via firebase-admin -google-cloud-storage==1.27.0 \ - --hash=sha256:3af167094142a61b1bda3489da4a724e55f2703b236431b27f71c9936d94f8d8 \ - --hash=sha256:62d5efa529fb39ae01504698b7053f2a009877d0d4b3c8f297e3e68c8c38a117 \ +google-cloud-storage==1.28.0 \ + --hash=sha256:07998ac15de406e7b7d72c98713d598e060399858a699eeb2ca45dc7f22a7af9 \ + --hash=sha256:35ecd0b00d4b4147c666d73fa2a5c0c7d9a7fe0fe430a4f544d428f5dc68b544 \ # via firebase-admin google-resumable-media==0.5.0 \ --hash=sha256:2a8fd188afe1cbfd5998bf20602f76b0336aa892de88fe842a806b9a3ed78d2a \ @@ -265,13 +259,13 @@ grpcio==1.28.1 \ --hash=sha256:f80d10bdf1a306f7063046321fd4efc7732a606acdd4e6259b8a37349079b704 \ --hash=sha256:f83b0c91796eb42865451a20e82246011078ba067ea0744f7301e12a94ae2e1b \ # via google-api-core -gunicorn==19.9.0 \ - --hash=sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471 \ - --hash=sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3 \ +gunicorn==20.0.4 \ + --hash=sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626 \ + --hash=sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c \ # via -r /opt/sql-requirements.txt -httplib2==0.17.2 \ - --hash=sha256:396ef66a170f76d5b2103f6c474da8aa3ff0c3c34c546323885e9de7e9eb08cd \ - --hash=sha256:eb7a6b137ae31e61c5f429083c5bebb71fe5fd1958e7f3d5c39b21b11cd4b290 \ +httplib2==0.17.3 \ + --hash=sha256:39dd15a333f67bfb70798faa9de8a6e99c819da6ad82b77f9a259a5c7b1225a2 \ + --hash=sha256:6d9722decd2deacd486ef10c5dd5e2f120ca3ba8736842b90509afcdc16488b1 \ # via google-api-python-client, google-auth-httplib2 idna==2.9 \ --hash=sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb \ @@ -280,11 +274,7 @@ idna==2.9 \ importlib-metadata==1.6.0 \ --hash=sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f \ --hash=sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e \ - # via -r /opt/sql-requirements.txt, importlib-resources, pluggy, pytest, virtualenv -importlib-resources==1.4.0 \ - --hash=sha256:4019b6a9082d8ada9def02bece4a76b131518866790d58fdda0b5f8c603b36c2 \ - --hash=sha256:dd98ceeef3f5ad2ef4cc287b8586da4ebad15877f351e9688987ad663a0a29b8 \ - # via -r /opt/sql-requirements.txt, virtualenv + # via -r /opt/sql-requirements.txt, pluggy, pytest ipython-genutils==0.2.0 \ --hash=sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8 \ --hash=sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8 \ @@ -301,13 +291,13 @@ itsdangerous==1.1.0 \ --hash=sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19 \ --hash=sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749 \ # via -r /opt/sql-requirements.txt, flask -jedi==0.16.0 \ - --hash=sha256:b4f4052551025c6b0b0b193b29a6ff7bdb74c52450631206c262aef9f7159ad2 \ - --hash=sha256:d5c871cb9360b414f981e7072c52c33258d598305280fef91c6cae34739d65d5 \ +jedi==0.17.0 \ + --hash=sha256:cd60c93b71944d628ccac47df9a60fec53150de53d42dc10a7fc4b5ba6aae798 \ + --hash=sha256:df40c97641cb943661d2db4c33c2e1ff75d491189423249e989bcea4464f3030 \ # via -r /opt/sql-requirements.txt, ipython -jinja2==2.11.1 \ - --hash=sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250 \ - --hash=sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49 \ +jinja2==2.11.2 \ + --hash=sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0 \ + --hash=sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035 \ # via -r /opt/sql-requirements.txt, flask mako==1.1.2 \ --hash=sha256:3139c5d64aa5d175dbafb95027057128b5fbd05a40c53999f3905ceb53366d9d \ @@ -347,7 +337,7 @@ markupsafe==1.1.1 \ --hash=sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2 \ --hash=sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7 \ --hash=sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be \ - # via -r /opt/sql-requirements.txt, jinja2, mako + # via -r /opt/sql-requirements.txt, jinja2, mako, wtforms marshmallow==3.5.1 \ --hash=sha256:90854221bbb1498d003a0c3cc9d8390259137551917961c8b5258c64026b2f85 \ --hash=sha256:ac2e13b30165501b7d41fc0371b8df35944f5849769d136f20e2c5f6cdc6e665 \ @@ -384,9 +374,9 @@ packaging==20.3 \ --hash=sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3 \ --hash=sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752 \ # via -r /opt/sql-requirements.txt, dparse, pytest, safety -parso==0.6.2 \ - --hash=sha256:0c5659e0c6eba20636f99a04f469798dca8da279645ce5c387315b2c23912157 \ - --hash=sha256:8515fc12cfca6ee3aa59138741fc5624d62340c97e401c74875769948d4f2995 \ +parso==0.7.0 \ + --hash=sha256:158c140fc04112dc45bca311633ae5033c2c2a7b732fa33d0955bad8152a8dd0 \ + --hash=sha256:908e9fae2144a076d72ae4e25539143d40b8e3eafbaeae03c1bfe226f4cdf12c \ # via -r /opt/sql-requirements.txt, jedi pathspec==0.8.0 \ --hash=sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0 \ @@ -400,18 +390,13 @@ pickleshare==0.7.5 \ --hash=sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca \ --hash=sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56 \ # via -r /opt/sql-requirements.txt, ipython -pipenv==2018.11.26 \ - --hash=sha256:56ad5f5cb48f1e58878e14525a6e3129d4306049cb76d2f6a3e95df0d5fc6330 \ - --hash=sha256:7df8e33a2387de6f537836f48ac6fcd94eda6ed9ba3d5e3fd52e35b5bc7ff49e \ - --hash=sha256:a673e606e8452185e9817a987572b55360f4d28b50831ef3b42ac3cab3fee846 \ - # via -r /opt/sql-requirements.txt, dparse pluggy==0.13.1 \ --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d \ # via -r /opt/sql-requirements.txt, pytest prometheus-client==0.7.1 \ --hash=sha256:71cd24a2b3eb335cb800c7159f423df1bd4dcd5171b234be15e3f31ec9f622da \ - # via -r /build/requirements.in + # via -r /opt/requirements/requirements.in prompt-toolkit==3.0.5 \ --hash=sha256:563d1a4140b63ff9dd587bda9557cffb2fe73650205ab6f4383092fb882e7dc8 \ --hash=sha256:df7e9e63aea609b1da3a65641ceaf5bc7d05e0a04de5bd45d05dbeffbabf9e04 \ @@ -419,6 +404,7 @@ prompt-toolkit==3.0.5 \ protobuf==3.11.3 \ --hash=sha256:0bae429443cc4748be2aadfdaf9633297cfaeb24a9a02d0ab15849175ce90fab \ --hash=sha256:24e3b6ad259544d717902777b33966a1a069208c885576254c112663e6a5bb0f \ + --hash=sha256:2affcaba328c4662f3bc3c0e9576ea107906b2c2b6422344cdad961734ff6b93 \ --hash=sha256:310a7aca6e7f257510d0c750364774034272538d51796ca31d42c3925d12a52a \ --hash=sha256:52e586072612c1eec18e1174f8e3bb19d08f075fc2e3f91d3b16c919078469d0 \ --hash=sha256:73152776dc75f335c476d11d52ec6f0f6925774802cd48d6189f4d5d7fe753f4 \ @@ -460,12 +446,34 @@ py==1.8.1 \ --hash=sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0 \ # via -r /opt/sql-requirements.txt, pytest pyasn1-modules==0.2.8 \ + --hash=sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8 \ + --hash=sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199 \ + --hash=sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811 \ + --hash=sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed \ + --hash=sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4 \ --hash=sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e \ --hash=sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74 \ + --hash=sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb \ + --hash=sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45 \ + --hash=sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd \ + --hash=sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0 \ + --hash=sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d \ + --hash=sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405 \ # via google-auth pyasn1==0.4.8 \ + --hash=sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359 \ + --hash=sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576 \ + --hash=sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf \ + --hash=sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7 \ --hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \ + --hash=sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00 \ + --hash=sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8 \ + --hash=sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86 \ + --hash=sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12 \ + --hash=sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776 \ --hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba \ + --hash=sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2 \ + --hash=sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3 \ # via -r /opt/sql-requirements.txt, pyasn1-modules, python-jose, rsa pycodestyle==2.5.0 \ --hash=sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56 \ @@ -503,6 +511,8 @@ python-editor==1.0.4 \ --hash=sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d \ --hash=sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b \ --hash=sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8 \ + --hash=sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77 \ + --hash=sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522 \ # via -r /opt/sql-requirements.txt, alembic python-http-client==3.2.7 \ --hash=sha256:93d6a26b426e48b04e589c1f103e7c040193e4ccc379ea50cd6e12f94cca7c69 \ @@ -511,9 +521,9 @@ python-jose==3.1.0 \ --hash=sha256:1ac4caf4bfebd5a70cf5bd82702ed850db69b0b6e1d0ae7368e5f99ac01c9571 \ --hash=sha256:8484b7fdb6962e9d242cce7680469ecf92bda95d10bbcbbeb560cacdff3abfce \ # via -r /opt/sql-requirements.txt -pytz==2019.3 \ - --hash=sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d \ - --hash=sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be \ +pytz==2020.1 \ + --hash=sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed \ + --hash=sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048 \ # via google-api-core, google-cloud-firestore pyyaml==5.3.1 \ --hash=sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97 \ @@ -563,18 +573,19 @@ rsa==4.0 \ --hash=sha256:14ba45700ff1ec9eeb206a2ce76b32814958a98e372006c8fb76ba820211be66 \ --hash=sha256:1a836406405730121ae9823e19c6e806c62bbad73f890574fff50efa4122c487 \ # via -r /opt/sql-requirements.txt, google-auth, python-jose -safety==1.8.7 \ - --hash=sha256:05f77773bbab834502328b29ed013677aa53ed0c22b6e330aef7d2a7e1dfd838 \ - --hash=sha256:3016631e0dd17193d6cf12e8ed1af92df399585e8ee0e4b1300d9e7e32b54903 \ +safety==1.9.0 \ + --hash=sha256:23bf20690d4400edc795836b0c983c2b4cbbb922233108ff925b7dd7750f00c9 \ + --hash=sha256:86c1c4a031fe35bd624fce143fbe642a0234d29f7cbf7a9aa269f244a955b087 \ # via -r /opt/sql-requirements.txt -sendgrid==6.2.1 \ - --hash=sha256:2954caf82c94c3566147e78ff7772f0883a7cc668de5a1c5ff1c49dcb9c1af31 \ - --hash=sha256:a58c8d3f99e1b4fed6c041e6bb0e30893a6c40ffc0ecd33430a08d044fe78832 \ - # via -r /build/requirements.in +sendgrid==6.3.0 \ + --hash=sha256:37d8215974ec3c79085fc455332961bb788eb5d899adde1469640b605a940ec4 \ + --hash=sha256:692c0ac35a6afed093e66e883b86dc7ec60b6b774bf5ef931d42efd19443a1fc \ + --hash=sha256:a74a70faf8d84be4ca07924bf229ca0e28f5607e0b10cb0adffce23e03bb9ccb \ + # via -r /opt/requirements/requirements.in six==1.14.0 \ --hash=sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a \ --hash=sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c \ - # via -r /opt/sql-requirements.txt, ecdsa, flask-apispec, flask-cors, google-api-core, google-api-python-client, google-auth, google-resumable-media, grpcio, packaging, protobuf, python-dateutil, python-jose, traitlets, virtualenv + # via -r /opt/sql-requirements.txt, ecdsa, flask-apispec, flask-cors, google-api-core, google-api-python-client, google-auth, google-resumable-media, grpcio, packaging, protobuf, python-dateutil, python-jose, traitlets snowballstemmer==2.0.0 \ --hash=sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0 \ --hash=sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52 \ @@ -603,6 +614,7 @@ sqlalchemy==1.3.16 \ toml==0.10.0 \ --hash=sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c \ --hash=sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e \ + --hash=sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3 \ # via -r /opt/sql-requirements.txt, black, dparse traitlets==4.3.3 \ --hash=sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44 \ @@ -635,18 +647,10 @@ uritemplate==3.0.1 \ --hash=sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f \ --hash=sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae \ # via google-api-python-client -urllib3==1.25.8 \ - --hash=sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc \ - --hash=sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc \ +urllib3==1.25.9 \ + --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \ + --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 \ # via -r /opt/sql-requirements.txt, requests -virtualenv-clone==0.5.4 \ - --hash=sha256:07e74418b7cc64f4fda987bf5bc71ebd59af27a7bc9e8a8ee9fd54b1f2390a27 \ - --hash=sha256:665e48dd54c84b98b71a657acb49104c54e7652bce9c1c4f6c6976ed4c827a29 \ - # via -r /opt/sql-requirements.txt, pipenv -virtualenv==20.0.16 \ - --hash=sha256:6ea131d41c477f6c4b7863948a9a54f7fa196854dbef73efbdff32b509f4d8bf \ - --hash=sha256:94f647e12d1e6ced2541b93215e51752aecbd1bbb18eb1816e2867f7532b1fe1 \ - # via -r /opt/sql-requirements.txt, pipenv wcwidth==0.1.9 \ --hash=sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1 \ --hash=sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1 \ @@ -660,16 +664,17 @@ werkzeug==1.0.1 \ --hash=sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43 \ --hash=sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c \ # via -r /opt/sql-requirements.txt, flask -wtforms==2.2.1 \ - --hash=sha256:0cdbac3e7f6878086c334aa25dc5a33869a3954e9d1e015130d65a69309b3b61 \ - --hash=sha256:e3ee092c827582c50877cdbd49e9ce6d2c5c1f6561f849b3b068c1b8029626f1 \ +wtforms==2.3.1 \ + --hash=sha256:6ff8635f4caeed9f38641d48cfe019d0d3896f41910ab04494143fc027866e1b \ + --hash=sha256:861a13b3ae521d6700dac3b2771970bd354a63ba7043ecc3a82b5288596a1972 \ # via -r /opt/sql-requirements.txt, flask-admin zipp==3.1.0 \ --hash=sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b \ --hash=sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96 \ - # via -r /opt/sql-requirements.txt, importlib-metadata, importlib-resources + # via -r /opt/sql-requirements.txt, importlib-metadata -# WARNING: The following packages were not pinned, but pip requires them to be -# pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag. -# pip -# setuptools +# The following packages are considered to be unsafe in a requirements file: +setuptools==46.1.3 \ + --hash=sha256:4fe404eec2738c20ab5841fa2d791902d2a645f32318a7850ef26f8d7215a8ee \ + --hash=sha256:795e0475ba6cd7fa082b1ee6e90d552209995627a2a227a47c6ea93282f4bfb1 \ + # via -r /opt/sql-requirements.txt, google-api-core, google-auth, gunicorn, ipython, protobuf, safety From 2d845e43cf5dfa6d199f4d6920a28e82540f9cc5 Mon Sep 17 00:00:00 2001 From: Midnighter Date: Thu, 30 Apr 2020 11:26:12 +0200 Subject: [PATCH 03/11] fix: remove double copy statement --- Dockerfile | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 689d0d5..c1e2404 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,13 +34,10 @@ ENV PYTHONPATH="${CWD}/src" WORKDIR "${CWD}" -COPY requirements ./requirements/ - # Install openssh to be able to generate rsa keys RUN apk add --update --no-cache openssh -# Install python dependencies -COPY requirements ./requirements +COPY requirements ./requirements/ RUN set -eux \ # build-base is required to build grpcio->firebase-admin From 06846ec2a86c49b7a50917e83066918d9bb2e899 Mon Sep 17 00:00:00 2001 From: Midnighter Date: Thu, 30 Apr 2020 11:28:55 +0200 Subject: [PATCH 04/11] style: apply QA --- src/iam/app.py | 61 +++-- src/iam/domain.py | 28 +-- src/iam/errorhandlers.py | 6 +- src/iam/hasher.py | 11 +- src/iam/jwt.py | 57 +++-- src/iam/metrics.py | 15 +- src/iam/models.py | 246 +++++++++++--------- src/iam/resources.py | 197 +++++++++------- src/iam/schemas.py | 60 +++-- src/iam/settings.py | 100 ++++---- tests/conftest.py | 90 ++++---- tests/integration/test_database.py | 37 +-- tests/integration/test_endpoints.py | 342 ++++++++++++++-------------- tests/unit/test_domain.py | 19 +- tests/unit/test_hasher.py | 35 +-- tests/unit/test_models.py | 59 ++--- 16 files changed, 743 insertions(+), 620 deletions(-) diff --git a/src/iam/app.py b/src/iam/app.py index f464ffc..af962f2 100644 --- a/src/iam/app.py +++ b/src/iam/app.py @@ -43,14 +43,23 @@ def init_app(application, db): # that need to import the flaskk app. from . import errorhandlers, jwt, resources from .models import ( - Organization, OrganizationProject, OrganizationUser, Project, - RefreshToken, Team, TeamProject, TeamUser, User, UserProject) + Organization, + OrganizationProject, + OrganizationUser, + Project, + RefreshToken, + Team, + TeamProject, + TeamUser, + User, + UserProject, + ) from .settings import current_config application.config.from_object(current_config()) # Configure logging - logging.config.dictConfig(application.config['LOGGING']) + logging.config.dictConfig(application.config["LOGGING"]) logger.info("Logging configured") @@ -59,30 +68,35 @@ def init_app(application, db): db.init_app(application) logger.debug("Initializing sentry") - if application.config['SENTRY_DSN']: - sentry = Sentry(dsn=application.config['SENTRY_DSN'], logging=True, - level=logging.ERROR) + if application.config["SENTRY_DSN"]: + sentry = Sentry( + dsn=application.config["SENTRY_DSN"], + logging=True, + level=logging.ERROR, + ) sentry.init_app(application) logger.debug("Initializing CORS") CORS(application) - if application.config['FEAT_TOGGLE_FIREBASE']: + if application.config["FEAT_TOGGLE_FIREBASE"]: logger.info("Initializing Firebase") - cred = credentials.Certificate({ - 'type': 'service_account', - 'auth_uri': 'https://accounts.google.com/o/oauth2/auth', - 'token_uri': 'https://accounts.google.com/o/oauth2/token', - 'auth_provider_x509_cert_url': - 'https://www.googleapis.com/oauth2/v1/certs', - 'project_id': application.config['FIREBASE_PROJECT_ID'], - 'private_key_id': application.config['FIREBASE_PRIVATE_KEY_ID'], - 'private_key': application.config['FIREBASE_PRIVATE_KEY'], - 'client_email': application.config['FIREBASE_CLIENT_EMAIL'], - 'client_id': application.config['FIREBASE_CLIENT_ID'], - 'client_x509_cert_url': - application.config['FIREBASE_CLIENT_CERT_URL'], - }) + cred = credentials.Certificate( + { + "type": "service_account", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://accounts.google.com/o/oauth2/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "project_id": application.config["FIREBASE_PROJECT_ID"], + "private_key_id": application.config["FIREBASE_PRIVATE_KEY_ID"], + "private_key": application.config["FIREBASE_PRIVATE_KEY"], + "client_email": application.config["FIREBASE_CLIENT_EMAIL"], + "client_id": application.config["FIREBASE_CLIENT_ID"], + "client_x509_cert_url": application.config[ + "FIREBASE_CLIENT_CERT_URL" + ], + } + ) firebase_admin.initialize_app(cred) else: logger.info("Firebase feature toggle is off") @@ -95,8 +109,7 @@ def init_app(application, db): ############################################################################ logger.debug("Registering admin views") - admin = Admin(application, template_mode='bootstrap3', - url=f"/admin") + admin = Admin(application, template_mode="bootstrap3", url=f"/admin") admin.add_view(ModelView(Organization, db.session)) admin.add_view(ModelView(Team, db.session)) admin.add_view(ModelView(User, db.session)) @@ -134,7 +147,7 @@ def users(): print(user) @application.cli.command() - @click.argument('id') + @click.argument("id") def set_password(id): """Set a users password. (Run 'users' to see all user ids).""" try: diff --git a/src/iam/domain.py b/src/iam/domain.py index 8ab06c8..39baa6e 100644 --- a/src/iam/domain.py +++ b/src/iam/domain.py @@ -30,39 +30,39 @@ def sign_claims(user): user=user, user_id=user.id, token=secrets.token_hex(32), - expiry=(datetime.now() + app.config['REFRESH_TOKEN_VALIDITY']), + expiry=(datetime.now() + app.config["REFRESH_TOKEN_VALIDITY"]), ) db.session.add(refresh_token) db.session.commit() claims = { - 'exp': int((datetime.now() + app.config['JWT_VALIDITY']) - .strftime('%s')) + "exp": int((datetime.now() + app.config["JWT_VALIDITY"]).strftime("%s")) } claims.update(user.claims) - signed_token = jwt.encode(claims, app.config['RSA_PRIVATE_KEY'], - app.config['ALGORITHM']) + signed_token = jwt.encode( + claims, app.config["RSA_PRIVATE_KEY"], app.config["ALGORITHM"] + ) return { - 'jwt': signed_token, - 'refresh_token': { - 'val': refresh_token.token, - 'exp': int(refresh_token.expiry.strftime('%s')), - } + "jwt": signed_token, + "refresh_token": { + "val": refresh_token.token, + "exp": int(refresh_token.expiry.strftime("%s")), + }, } def create_firebase_user(uid, decoded_token): """Create a Firebase user from the provided uid and decoded token.""" - name = decoded_token.get('name', '') - if ' ' in name: + name = decoded_token.get("name", "") + if " " in name: first_name, last_name = name.split(None, 1) else: - first_name, last_name = name, '' + first_name, last_name = name, "" user = User( firebase_uid=uid, first_name=first_name, last_name=last_name, - email=decoded_token['email'], + email=decoded_token["email"], ) db.session.add(user) db.session.commit() diff --git a/src/iam/errorhandlers.py b/src/iam/errorhandlers.py index 5b7d3eb..418e34b 100644 --- a/src/iam/errorhandlers.py +++ b/src/iam/errorhandlers.py @@ -47,7 +47,7 @@ def handle_webargs_error(error): Note that this sets some constrains for manually throwing 422 exceptions as error messages must be made available in the same way. """ - response = jsonify(error.data['messages']) + response = jsonify(error.data["messages"]) response.status_code = error.code return response @@ -64,7 +64,7 @@ def handle_http_error(error): if isinstance(error, RoutingException): return error else: - response = jsonify({'message': error.description}) + response = jsonify({"message": error.description}) response.status_code = error.code return response @@ -81,6 +81,6 @@ def handle_uncaught_error(error): client. """ logger.error("Uncaught exception", exc_info=sys.exc_info()) - response = jsonify({'message': "Internal server error"}) + response = jsonify({"message": "Internal server error"}) response.status_code = 500 return response diff --git a/src/iam/hasher.py b/src/iam/hasher.py index 9eb8b88..4b50cbb 100644 --- a/src/iam/hasher.py +++ b/src/iam/hasher.py @@ -26,7 +26,7 @@ def new_salt(n=12): """Generate a new salt.""" salt_chars = string.ascii_letters + string.digits salt = [secrets.choice(salt_chars) for _ in range(n)] - return ''.join(salt) + return "".join(salt) def encode(password, salt=None, iterations=100000): @@ -35,9 +35,10 @@ def encode(password, salt=None, iterations=100000): salt = new_salt() if not isinstance(password, bytes): password = password.encode() - hash = hashlib.pbkdf2_hmac(hashlib.sha256().name, password, salt.encode(), - iterations, None) - hash = base64.b64encode(hash).decode('ascii').strip() + hash = hashlib.pbkdf2_hmac( + hashlib.sha256().name, password, salt.encode(), iterations, None + ) + hash = base64.b64encode(hash).decode("ascii").strip() return "{:d}${}${}".format(iterations, salt, hash) @@ -45,6 +46,6 @@ def verify(password, encoded): """Return True if the given password matches the given encoded password.""" if not isinstance(password, bytes): password = password.encode() - iterations, salt, hash = encoded.split('$', 2) + iterations, salt, hash = encoded.split("$", 2) encoded_new = encode(password, salt, int(iterations)) return hmac.compare_digest(encoded.encode(), encoded_new.encode()) diff --git a/src/iam/jwt.py b/src/iam/jwt.py index 9f89b5e..38d41e3 100644 --- a/src/iam/jwt.py +++ b/src/iam/jwt.py @@ -27,38 +27,43 @@ def init_app(app): """Add the jwt decoding middleware to the app.""" + @app.before_request def decode_jwt(): - if 'Authorization' not in request.headers: + if "Authorization" not in request.headers: logger.debug("No JWT provided") g.jwt_valid = False - g.jwt_claims = {'prj': {}} + g.jwt_claims = {"prj": {}} return - auth = request.headers['Authorization'] - if not auth.startswith('Bearer '): + auth = request.headers["Authorization"] + if not auth.startswith("Bearer "): logger.debug( - f"No JWT provided, unknown Authorization header: {auth}") + f"No JWT provided, unknown Authorization header: {auth}" + ) g.jwt_valid = False - g.jwt_claims = {'prj': {}} + g.jwt_claims = {"prj": {}} return try: # Note: `auth` is guaranteed to contain a space due to the above # check for `auth.startswith('Bearer ')`. - token = auth.split(' ', 1)[1] + token = auth.split(" ", 1)[1] g.jwt_claims = jwt.decode( - token, app.config['RSA_PUBLIC_KEY'], app.config['ALGORITHM']) + token, app.config["RSA_PUBLIC_KEY"], app.config["ALGORITHM"] + ) # JSON object names can only be strings. Map project ids to ints for # easier handling - g.jwt_claims['prj'] = { - int(key): value - for key, value in g.jwt_claims['prj'].items() + g.jwt_claims["prj"] = { + int(key): value for key, value in g.jwt_claims["prj"].items() } g.jwt_valid = True logger.debug(f"JWT claims accepted: {g.jwt_claims}") - except (jwt.JWTError, jwt.ExpiredSignatureError, - jwt.JWTClaimsError) as e: + except ( + jwt.JWTError, + jwt.ExpiredSignatureError, + jwt.JWTClaimsError, + ) as e: abort(401, f"JWT authentication failed: {e}") @@ -68,11 +73,13 @@ def jwt_required(function): Use this as a decorator for endpoints requiring JWT to be provided. """ + @wraps(function) def wrapper(*args, **kwargs): if not g.jwt_valid: abort(401, "JWT authentication required") return function(*args, **kwargs) + return wrapper @@ -89,23 +96,25 @@ def jwt_require_claim(project_id, required_level): :return: None """ ACCESS_LEVELS = { - 'admin': 3, - 'write': 2, - 'read': 1, + "admin": 3, + "write": 2, + "read": 1, } if required_level not in ACCESS_LEVELS: raise ValueError(f"Invalid claim level '{required_level}'") - logger.debug(f"Looking for '{required_level}' access to project " - f"'{project_id}' in claims '{g.jwt_claims}'") + logger.debug( + f"Looking for '{required_level}' access to project " + f"'{project_id}' in claims '{g.jwt_claims}'" + ) # Nobody can write to public projects - if project_id is None and required_level != 'read': + if project_id is None and required_level != "read": abort(403, "Public data can not be modified") try: - claim_level = g.jwt_claims['prj'][project_id] + claim_level = g.jwt_claims["prj"][project_id] except KeyError: # The given project id is not included in the users claims abort(403, "You do not have access to the requested resource") @@ -113,6 +122,8 @@ def jwt_require_claim(project_id, required_level): # The given project id is included in the claims; verify that the access # level is sufficient if ACCESS_LEVELS[claim_level] < ACCESS_LEVELS[required_level]: - abort(403, - f"This operation requires access level '{required_level}', your " - f"access level is '{claim_level}'") + abort( + 403, + f"This operation requires access level '{required_level}', your " + f"access level is '{claim_level}'", + ) diff --git a/src/iam/metrics.py b/src/iam/metrics.py index 80eb222..1183b66 100644 --- a/src/iam/metrics.py +++ b/src/iam/metrics.py @@ -22,9 +22,10 @@ # service: The current service (always 'iam') # environment: The current runtime environment ('production' or 'staging') USER_COUNT = prometheus_client.Gauge( - 'decaf_user_count', + "decaf_user_count", "The current number of users in the database", - ['service', 'environment']) + ["service", "environment"], +) # ORGANIZATION_COUNT: The current number of users in the database @@ -32,9 +33,10 @@ # service: The current service (always 'iam') # environment: The current runtime environment ('production' or 'staging') ORGANIZATION_COUNT = prometheus_client.Gauge( - 'decaf_organization_count', + "decaf_organization_count", "The current number of users in the database", - ['service', 'environment']) + ["service", "environment"], +) # PROJECT_COUNT: The current number of projects in the database @@ -42,6 +44,7 @@ # service: The current service (always 'iam') # environment: The current runtime environment ('production' or 'staging') PROJECT_COUNT = prometheus_client.Gauge( - 'decaf_project_count', + "decaf_project_count", "The current number of projects in the database", - ['service', 'environment']) + ["service", "environment"], +) diff --git a/src/iam/models.py b/src/iam/models.py index dbd1fd5..e058e24 100644 --- a/src/iam/models.py +++ b/src/iam/models.py @@ -38,10 +38,11 @@ class Organization(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(256), nullable=False) - teams = db.relationship('Team', back_populates='organization') - users = db.relationship('OrganizationUser', back_populates='organization') - projects = db.relationship('OrganizationProject', - back_populates='organization') + teams = db.relationship("Team", back_populates="organization") + users = db.relationship("OrganizationUser", back_populates="organization") + projects = db.relationship( + "OrganizationProject", back_populates="organization" + ) def __repr__(self): """Return a printable representation.""" @@ -54,13 +55,14 @@ class Team(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(256), nullable=False) - organization_id = db.Column(db.Integer, db.ForeignKey('organization.id'), - nullable=False) - organization = db.relationship('Organization', back_populates='teams') + organization_id = db.Column( + db.Integer, db.ForeignKey("organization.id"), nullable=False + ) + organization = db.relationship("Organization", back_populates="teams") - users = db.relationship('TeamUser', back_populates='team', lazy='joined') + users = db.relationship("TeamUser", back_populates="team", lazy="joined") - projects = db.relationship('TeamProject', back_populates='team') + projects = db.relationship("TeamProject", back_populates="team") def __repr__(self): """Return a printable representation.""" @@ -81,31 +83,37 @@ class User(db.Model): last_name = db.Column(db.String(256)) email = db.Column(db.String(256), unique=True, nullable=False) - organizations = db.relationship('OrganizationUser', - back_populates='user', - lazy='joined', - cascade='delete, delete-orphan') - teams = db.relationship('TeamUser', - back_populates='user', - lazy='joined', - cascade='delete, delete-orphan') - - projects = db.relationship('UserProject', - back_populates='user', - cascade='delete, delete-orphan') - - refresh_tokens = db.relationship('RefreshToken', - back_populates='user', - cascade='delete, delete-orphan') - - consents = db.relationship('Consent', - back_populates='user', - cascade='delete, delete-orphan') + organizations = db.relationship( + "OrganizationUser", + back_populates="user", + lazy="joined", + cascade="delete, delete-orphan", + ) + teams = db.relationship( + "TeamUser", + back_populates="user", + lazy="joined", + cascade="delete, delete-orphan", + ) + + projects = db.relationship( + "UserProject", back_populates="user", cascade="delete, delete-orphan" + ) + + refresh_tokens = db.relationship( + "RefreshToken", back_populates="user", cascade="delete, delete-orphan" + ) + + consents = db.relationship( + "Consent", back_populates="user", cascade="delete, delete-orphan" + ) def __repr__(self): """Return a printable representation.""" - return (f"<{self.__class__.__name__} {self.id}: {self.full_name} " - f"({self.email})>") + return ( + f"<{self.__class__.__name__} {self.id}: {self.full_name} " + f"({self.email})>" + ) @property def full_name(self): @@ -125,12 +133,13 @@ def check_password(self, password): @property def claims(self): """Return this users' claims for use in a JWT.""" + def add_claim(id, role): """Add claims, if there is no existing higher claim.""" if id in project_claims: - if role == 'read' and project_claims[id] in ('admin', 'write'): + if role == "read" and project_claims[id] in ("admin", "write"): return - if role == 'write' and project_claims[id] == 'admin': + if role == "write" and project_claims[id] == "admin": return project_claims[id] = role @@ -138,15 +147,15 @@ def add_claim(id, role): project_claims = {} for user_role in self.organizations: - if user_role.role == 'owner': + if user_role.role == "owner": # Add admin role for all projects in the organization for project_role in user_role.organization.projects: - add_claim(project_role.project.id, 'admin') + add_claim(project_role.project.id, "admin") # Add admin role for all projects in organization teams for team in user_role.organization.teams: for team_role in team.projects: - add_claim(team_role.project.id, 'admin') + add_claim(team_role.project.id, "admin") else: # Add the assigned role for all projects in the organization for org_role in user_role.organization.projects: @@ -161,12 +170,12 @@ def add_claim(id, role): for user_role in self.projects: add_claim(user_role.project.id, user_role.role) - return {'usr': self.id, 'prj': project_claims} + return {"usr": self.id, "prj": project_claims} def get_reset_token(self): claims = { "exp": int(datetime.timestamp(datetime.now() + timedelta(hours=1))), - "usr": self.id + "usr": self.id, } return jwt.encode( claims, app.config["RSA_PRIVATE_KEY"], app.config["ALGORITHM"] @@ -186,13 +195,15 @@ def send_reset_email(self): personalization = Personalization() personalization.add_to(Email(self.email)) personalization.dynamic_template_data = { - "link": - f"{app.config['ROOT_URL']}/password-reset/{token}" + "link": f"{app.config['ROOT_URL']}/password-reset/{token}" } mail.add_personalization(personalization) sendgrid.client.mail.send.post(request_body=mail.get()) - return '''An email has been sent with instructions - to reset your password.''', 200 + return ( + """An email has been sent with instructions + to reset your password.""", + 200, + ) except Exception as error: logger.warning( "Unable to send email with password reset link", exc_info=error @@ -211,8 +222,7 @@ class RefreshToken(db.Model): def __repr__(self): """Return a printable representation.""" - return f"<{self.__class__.__name__} {self.token}: " \ - f"{self.expiry}>" + return f"<{self.__class__.__name__} {self.token}: " f"{self.expiry}>" class Project(db.Model): @@ -221,13 +231,17 @@ class Project(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(256), nullable=False) - organizations = db.relationship('OrganizationProject', - back_populates='project', - cascade='all, delete-orphan') - teams = db.relationship('TeamProject', back_populates='project', - cascade='all, delete-orphan') - users = db.relationship('UserProject', back_populates='project', - cascade='all, delete-orphan') + organizations = db.relationship( + "OrganizationProject", + back_populates="project", + cascade="all, delete-orphan", + ) + teams = db.relationship( + "TeamProject", back_populates="project", cascade="all, delete-orphan" + ) + users = db.relationship( + "UserProject", back_populates="project", cascade="all, delete-orphan" + ) def __repr__(self): """Return a printable representation.""" @@ -242,106 +256,132 @@ class Consent(db.Model): type = db.Column(db.Enum(ConsentType), nullable=False) category = db.Column(db.Text, nullable=False) status = db.Column(db.Enum(ConsentStatus), nullable=False) - timestamp = db.Column(db.DateTime(timezone=True), - nullable=False, - default=datetime.now(timezone.utc)) + timestamp = db.Column( + db.DateTime(timezone=True), + nullable=False, + default=datetime.now(timezone.utc), + ) valid_until = db.Column(db.DateTime(timezone=True)) message = db.Column(db.Text) source = db.Column(db.Text) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) - user = db.relationship('User', back_populates='consents') + user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) + user = db.relationship("User", back_populates="consents") def __repr__(self): """Return a printable representation.""" - return (f"<{self.__class__.__name__} {self.id}: {self.type} " - f"({self.category})>") + return ( + f"<{self.__class__.__name__} {self.id}: {self.type} " + f"({self.category})>" + ) # # Association tables # + class OrganizationUser(db.Model): """Role for a user belonging to an organization.""" - organization_id = db.Column(db.Integer, db.ForeignKey('organization.id'), - primary_key=True) - organization = db.relationship('Organization', back_populates='users') - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) - user = db.relationship('User', back_populates='organizations') - role = db.Column(db.Enum('owner', 'member', name='organization_user_roles'), - nullable=False) + organization_id = db.Column( + db.Integer, db.ForeignKey("organization.id"), primary_key=True + ) + organization = db.relationship("Organization", back_populates="users") + user_id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True) + user = db.relationship("User", back_populates="organizations") + role = db.Column( + db.Enum("owner", "member", name="organization_user_roles"), + nullable=False, + ) def __repr__(self): """Return a printable representation.""" - return (f"<{self.__class__.__name__} {self.role}: {self.user} in " - f"{self.organization}>") + return ( + f"<{self.__class__.__name__} {self.role}: {self.user} in " + f"{self.organization}>" + ) class TeamUser(db.Model): """Role for a user belonging to a team.""" - team_id = db.Column(db.Integer, db.ForeignKey('team.id'), primary_key=True) - team = db.relationship('Team', back_populates='users') - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) - user = db.relationship('User', back_populates='teams') - role = db.Column(db.Enum('maintainer', 'member', name='team_user_roles'), - nullable=False) + team_id = db.Column(db.Integer, db.ForeignKey("team.id"), primary_key=True) + team = db.relationship("Team", back_populates="users") + user_id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True) + user = db.relationship("User", back_populates="teams") + role = db.Column( + db.Enum("maintainer", "member", name="team_user_roles"), nullable=False + ) def __repr__(self): """Return a printable representation.""" - return (f"<{self.__class__.__name__} {self.role}: {self.user} in " - f"{self.team}>") + return ( + f"<{self.__class__.__name__} {self.role}: {self.user} in " + f"{self.team}>" + ) class OrganizationProject(db.Model): """Access rule for an organization to a project.""" - organization_id = db.Column(db.Integer, db.ForeignKey('organization.id'), - primary_key=True) - organization = db.relationship('Organization', back_populates='projects') - project_id = db.Column(db.Integer, db.ForeignKey('project.id'), - primary_key=True) - project = db.relationship('Project', back_populates='organizations') - role = db.Column(db.Enum('admin', 'write', 'read', name='project_roles'), - nullable=False) + organization_id = db.Column( + db.Integer, db.ForeignKey("organization.id"), primary_key=True + ) + organization = db.relationship("Organization", back_populates="projects") + project_id = db.Column( + db.Integer, db.ForeignKey("project.id"), primary_key=True + ) + project = db.relationship("Project", back_populates="organizations") + role = db.Column( + db.Enum("admin", "write", "read", name="project_roles"), nullable=False + ) def __repr__(self): """Return a printable representation.""" - return (f"<{self.__class__.__name__} {self.organization} has role " - f"{self.role} in {self.project}>") + return ( + f"<{self.__class__.__name__} {self.organization} has role " + f"{self.role} in {self.project}>" + ) class TeamProject(db.Model): """Access rule for a team to a project.""" - team_id = db.Column(db.Integer, db.ForeignKey('team.id'), primary_key=True) - team = db.relationship('Team', back_populates='projects') - project_id = db.Column(db.Integer, db.ForeignKey('project.id'), - primary_key=True) - project = db.relationship('Project', back_populates='teams') - role = db.Column(db.Enum('admin', 'write', 'read', name='project_roles'), - nullable=False) + team_id = db.Column(db.Integer, db.ForeignKey("team.id"), primary_key=True) + team = db.relationship("Team", back_populates="projects") + project_id = db.Column( + db.Integer, db.ForeignKey("project.id"), primary_key=True + ) + project = db.relationship("Project", back_populates="teams") + role = db.Column( + db.Enum("admin", "write", "read", name="project_roles"), nullable=False + ) def __repr__(self): """Return a printable representation.""" - return (f"<{self.__class__.__name__} {self.team} has role " - f"{self.role} in {self.project}>") + return ( + f"<{self.__class__.__name__} {self.team} has role " + f"{self.role} in {self.project}>" + ) class UserProject(db.Model): """Access role for a user to a project.""" - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) - user = db.relationship('User', back_populates='projects') - project_id = db.Column(db.Integer, db.ForeignKey('project.id'), - primary_key=True) - project = db.relationship('Project', back_populates='users') - role = db.Column(db.Enum('admin', 'write', 'read', name='project_roles'), - nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True) + user = db.relationship("User", back_populates="projects") + project_id = db.Column( + db.Integer, db.ForeignKey("project.id"), primary_key=True + ) + project = db.relationship("Project", back_populates="users") + role = db.Column( + db.Enum("admin", "write", "read", name="project_roles"), nullable=False + ) def __repr__(self): """Return a printable representation.""" - return (f"<{self.__class__.__name__} {self.user} has role " - f"{self.role} in {self.project}>") + return ( + f"<{self.__class__.__name__} {self.user} has role " + f"{self.role} in {self.project}>" + ) diff --git a/src/iam/resources.py b/src/iam/resources.py index 93b35be..4030d41 100644 --- a/src/iam/resources.py +++ b/src/iam/resources.py @@ -33,16 +33,35 @@ from .jwt import jwt_require_claim, jwt_required from .metrics import ORGANIZATION_COUNT, PROJECT_COUNT, USER_COUNT from .models import ( - Consent, Organization, Project, RefreshToken, User, UserProject, db) + Consent, + Organization, + Project, + RefreshToken, + User, + UserProject, + db, +) from .schemas import ( - ConsentRegisterSchema, ConsentResponseSchema, FirebaseCredentialsSchema, - JWKKeysSchema, JWTSchema, LocalCredentialsSchema, PasswordResetSchema, - ProjectRequestSchema, ProjectResponseSchema, RefreshRequestSchema, - ResetRequestSchema, TokenSchema, UserRegisterSchema, UserResponseSchema) + ConsentRegisterSchema, + ConsentResponseSchema, + FirebaseCredentialsSchema, + JWKKeysSchema, + JWTSchema, + LocalCredentialsSchema, + PasswordResetSchema, + ProjectRequestSchema, + ProjectResponseSchema, + RefreshRequestSchema, + ResetRequestSchema, + TokenSchema, + UserRegisterSchema, + UserResponseSchema, +) def init_app(app): """Register API resources on the provided Flask application.""" + def register(path, resource): app.add_url_rule(path, view_func=resource.as_view(resource.__name__)) with warnings.catch_warnings(): @@ -50,8 +69,8 @@ def register(path, resource): docs.register(resource, endpoint=resource.__name__) docs = FlaskApiSpec(app) - app.add_url_rule('/healthz', healthz.__name__, healthz) - app.add_url_rule('/metrics', metrics.__name__, metrics) + app.add_url_rule("/healthz", healthz.__name__, healthz) + app.add_url_rule("/metrics", metrics.__name__, metrics) register("/authenticate/local", LocalAuthResource) register("/authenticate/firebase", FirebaseAuthResource) register("/refresh", RefreshResource) @@ -74,8 +93,8 @@ def healthz(): checks = [] # Database ping - db.session.execute('select version()').fetchall() - checks.append({'name': "DB Connectivity", 'status': 'pass'}) + db.session.execute("select version()").fetchall() + checks.append({"name": "DB Connectivity", "status": "pass"}) return jsonify(checks) @@ -83,13 +102,15 @@ def healthz(): def metrics(): """Expose metrics to prometheus.""" # Update persistent metrics like database counts - labels = ('iam', os.environ['ENVIRONMENT']) + labels = ("iam", os.environ["ENVIRONMENT"]) USER_COUNT.labels(*labels).set(User.query.count()) ORGANIZATION_COUNT.labels(*labels).set(Organization.query.count()) PROJECT_COUNT.labels(*labels).set(Project.query.count()) - return Response(prometheus_client.generate_latest(), - mimetype=prometheus_client.CONTENT_TYPE_LATEST) + return Response( + prometheus_client.generate_latest(), + mimetype=prometheus_client.CONTENT_TYPE_LATEST, + ) @doc(description="Authenticate with email credentials") @@ -100,13 +121,12 @@ class LocalAuthResource(MethodResource): @marshal_with(TokenSchema, code=200) def post(self, email, password): """Authenticate with credentials in the local database.""" - if not app.config['FEAT_TOGGLE_LOCAL_AUTH']: + if not app.config["FEAT_TOGGLE_LOCAL_AUTH"]: return "Local user authentication is disabled", 501 try: user = User.query.filter( - User.email == email, - User.password.isnot(None), + User.email == email, User.password.isnot(None), ).one() if user.check_password(password): return sign_claims(user) @@ -124,7 +144,7 @@ class FirebaseAuthResource(MethodResource): @marshal_with(TokenSchema, code=200) def post(self, uid, token): """Authenticate with Firebase uid and token.""" - if not app.config['FEAT_TOGGLE_FIREBASE']: + if not app.config["FEAT_TOGGLE_FIREBASE"]: return "Firebase authentication is disabled", 501 try: @@ -132,9 +152,8 @@ def post(self, uid, token): except ValueError: return "Invalid firebase credentials", 401 - if 'email' not in decoded_token: - decoded_token['email'] = ( - auth.get_user(uid).provider_data[0].email) + if "email" not in decoded_token: + decoded_token["email"] = auth.get_user(uid).provider_data[0].email try: user = User.query.filter_by(firebase_uid=uid).one() @@ -142,7 +161,7 @@ def post(self, uid, token): try: # no firebase user for this provider, but they may have # signed up with a different provider but the same email - user = User.query.filter_by(email=decoded_token['email']).one() + user = User.query.filter_by(email=decoded_token["email"]).one() except NoResultFound: # no such user - create a new one user = create_firebase_user(uid, decoded_token) @@ -162,51 +181,55 @@ def post(self, refresh_token): token = RefreshToken.query.filter_by(token=refresh_token).one() user = User.query.filter_by(id=token.user_id).one() if datetime.now() >= token.expiry: - return ("The refresh token has expired, please re-authenticate", - 401) + return ( + "The refresh token has expired, please re-authenticate", + 401, + ) claims = { - 'exp': int((datetime.now() + app.config['JWT_VALIDITY']) - .strftime('%s')) + "exp": int( + (datetime.now() + app.config["JWT_VALIDITY"]).strftime("%s") + ) } claims.update(user.claims) - return {'jwt': jwt.encode(claims, - app.config['RSA_PRIVATE_KEY'], - app.config['ALGORITHM'])} + return { + "jwt": jwt.encode( + claims, + app.config["RSA_PRIVATE_KEY"], + app.config["ALGORITHM"], + ) + } except NoResultFound: return "Invalid refresh token", 401 -@doc(description="""List of public keys used for JWT signing. +@doc( + description="""List of public keys used for JWT signing. See [RFC 7517](https://tools.ietf.org/html/rfc7517) or [the OpenID Connect implementation](https://connect2id.com/products/server/docs/api/jwk-set#keys)""" - ) +) class PublicKeysResource(MethodResource): @marshal_with(JWKKeysSchema, code=200) def get(self): - return {'keys': [app.config['RSA_PUBLIC_KEY']]} + return {"keys": [app.config["RSA_PUBLIC_KEY"]]} @doc(description="List projects") class ProjectsResource(MethodResource): @marshal_with(ProjectResponseSchema(many=True), code=200) def get(self): - return Project.query.filter(Project.id.in_(g.jwt_claims['prj'])), 200 + return Project.query.filter(Project.id.in_(g.jwt_claims["prj"])), 200 @use_kwargs(ProjectRequestSchema) @jwt_required def post(self, name): - user = User.query.filter(User.id == g.jwt_claims['usr']).one() + user = User.query.filter(User.id == g.jwt_claims["usr"]).one() project = Project(name=name) - user_project = UserProject( - user=user, - project=project, - role='admin', - ) + user_project = UserProject(user=user, project=project, role="admin",) db.session.add(project) db.session.add(user_project) db.session.commit() - return {'id': project.id}, 201 + return {"id": project.id}, 201 @doc(description="List projects") @@ -214,10 +237,13 @@ class ProjectResource(MethodResource): @marshal_with(ProjectResponseSchema(), code=200) def get(self, project_id): try: - return Project.query.filter( - Project.id == project_id, - Project.id.in_(g.jwt_claims['prj']) - ).one(), 200 + return ( + Project.query.filter( + Project.id == project_id, + Project.id.in_(g.jwt_claims["prj"]), + ).one(), + 200, + ) except NoResultFound: return f"No project with id {project_id}", 404 @@ -226,13 +252,12 @@ def get(self, project_id): def put(self, project_id, name): try: project = Project.query.filter( - Project.id == project_id, - Project.id.in_(g.jwt_claims['prj']) + Project.id == project_id, Project.id.in_(g.jwt_claims["prj"]) ).one() except NoResultFound: return f"No project with id {project_id}", 404 else: - jwt_require_claim(project.id, 'write') + jwt_require_claim(project.id, "write") project.name = name db.session.commit() return "", 204 @@ -241,13 +266,12 @@ def put(self, project_id, name): def delete(self, project_id): try: project = Project.query.filter( - Project.id == project_id, - Project.id.in_(g.jwt_claims['prj']) + Project.id == project_id, Project.id.in_(g.jwt_claims["prj"]) ).one() except NoResultFound: return f"No project with id {project_id}", 404 else: - jwt_require_claim(project.id, 'admin') + jwt_require_claim(project.id, "admin") db.session.delete(project) db.session.commit() return "", 204 @@ -259,7 +283,7 @@ class UserResource(MethodResource): @jwt_required def get(self): try: - return User.query.filter(User.id == g.jwt_claims['usr']).one(), 200 + return User.query.filter(User.id == g.jwt_claims["usr"]).one(), 200 except NoResultFound: return f"No user with id {g.jwt_claims['usr']}", 404 @@ -273,11 +297,7 @@ def post(self, first_name, last_name, email, password): if exists: return f"User with provided email already exists", 400 - user = User( - first_name=first_name, - last_name=last_name, - email=email - ) + user = User(first_name=first_name, last_name=last_name, email=email) user.set_password(password) db.session.add(user) db.session.commit() @@ -285,8 +305,10 @@ def post(self, first_name, last_name, email, password): return sign_claims(user) -@doc(description="Retrieve and submit user consents for the user claim in the " - "provided JWT") +@doc( + description="Retrieve and submit user consents for the user claim in the " + "provided JWT" +) class ConsentResource(MethodResource): @marshal_with(ConsentResponseSchema(many=True), code=200) @jwt_required @@ -297,29 +319,30 @@ def get(self): # Query adapted from https://stackoverflow.com/questions/40537934 # NOTE: If multiple consents with same type, category and timestamp # are found, returns the one with greatest ID - subquery = db.session.query( - Consent.user_id.label("user_id"), - Consent.type.label("type"), - Consent.category.label("category"), - func.max(Consent.timestamp).label("latest_timestamp"), - func.max(Consent.id).label("greatest_id"), - ).group_by( - Consent.user_id, - Consent.type, - Consent.category, - ).subquery() - query = db.session.query(Consent).join( - subquery, - and_( - Consent.user_id == g.jwt_claims['usr'], - Consent.type == subquery.c.type, - Consent.category == subquery.c.category, - Consent.timestamp == subquery.c.latest_timestamp, - Consent.id == subquery.c.greatest_id, + subquery = ( + db.session.query( + Consent.user_id.label("user_id"), + Consent.type.label("type"), + Consent.category.label("category"), + func.max(Consent.timestamp).label("latest_timestamp"), + func.max(Consent.id).label("greatest_id"), + ) + .group_by(Consent.user_id, Consent.type, Consent.category,) + .subquery() + ) + query = ( + db.session.query(Consent) + .join( + subquery, + and_( + Consent.user_id == g.jwt_claims["usr"], + Consent.type == subquery.c.type, + Consent.category == subquery.c.category, + Consent.timestamp == subquery.c.latest_timestamp, + Consent.id == subquery.c.greatest_id, + ), ) - ).order_by( - Consent.type, - Consent.category + .order_by(Consent.type, Consent.category) ) return query, 200 except NoResultFound: @@ -327,21 +350,29 @@ def get(self): @use_kwargs(ConsentRegisterSchema) @jwt_required - def post(self, type, category, status, timestamp=None, valid_until=None, - message=None, source=None): + def post( + self, + type, + category, + status, + timestamp=None, + valid_until=None, + message=None, + source=None, + ): consent = Consent( type=type, category=category, status=status, timestamp=timestamp if timestamp is not None else datetime.now(), - user_id=g.jwt_claims['usr'], + user_id=g.jwt_claims["usr"], valid_until=valid_until, message=message, - source=source + source=source, ) db.session.add(consent) db.session.commit() - return {'id': consent.id}, 201 + return {"id": consent.id}, 201 @doc(description="Request password reset link") diff --git a/src/iam/schemas.py b/src/iam/schemas.py index 746190b..9618975 100644 --- a/src/iam/schemas.py +++ b/src/iam/schemas.py @@ -16,7 +16,12 @@ """Marshmallow schemas for marshalling the API endpoints.""" from marshmallow import ( - Schema, ValidationError, fields, validates, validates_schema) + Schema, + ValidationError, + fields, + validates, + validates_schema, +) from .enums import ConsentStatus, ConsentType, CookieConsentCategory @@ -29,6 +34,7 @@ class Meta: # Request schemas ################################################################################ + class LocalCredentialsSchema(StrictSchema): email = fields.String(required=True, description="Email address") password = fields.String(required=True, description="Password") @@ -58,20 +64,25 @@ class ProjectRequestSchema(StrictSchema): class ConsentRegisterSchema(StrictSchema): type = fields.String( required=True, - description="Type of the consent, e.g. GDPR or cookie consent") + description="Type of the consent, e.g. GDPR or cookie consent", + ) category = fields.String( - required=True, - description="Category the consent relates to.") + required=True, description="Category the consent relates to." + ) status = fields.String( required=True, - description="Whether the consent was accepted or rejected.") - timestamp = fields.DateTime(description="Time of when user responded to " - "the consent") + description="Whether the consent was accepted or rejected.", + ) + timestamp = fields.DateTime( + description="Time of when user responded to " "the consent" + ) valid_until = fields.DateTime( description="Time of when the consent should" - "be revoked. Null implies unlimited validity") - message = fields.String(description="Exact wording of what the user " - "consented to.") + "be revoked. Null implies unlimited validity" + ) + message = fields.String( + description="Exact wording of what the user " "consented to." + ) source = fields.String(description="Source of the consent.") @validates_schema @@ -81,7 +92,7 @@ def validate_category(self, data, **kwargs): # can't find the field. # For more info, this occurs in marshmallow(2.x).schema.py:_do_load try: - consent_type = data['type'] + consent_type = data["type"] except KeyError: # Field 'type' has been removed from the response, so the data has # already been validated and failed and appropriate response is @@ -90,30 +101,33 @@ def validate_category(self, data, **kwargs): return if consent_type == ConsentType.cookie.name: try: - CookieConsentCategory[data['category']] + CookieConsentCategory[data["category"]] except KeyError: raise ValidationError( f'Invalid cookie consent category "{data["category"]}". ' - 'Category must be one of ' - f'{[c.name for c in CookieConsentCategory]}.') + "Category must be one of " + f"{[c.name for c in CookieConsentCategory]}." + ) - @validates('type') + @validates("type") def validate_type(self, value, **kwargs): try: ConsentType[value] except KeyError: raise ValidationError( f'Invalid consent type "{value}". Type must be one of ' - f'{[c.name for c in ConsentType]}.') + f"{[c.name for c in ConsentType]}." + ) - @validates('status') + @validates("status") def validate_status(self, value, **kwargs): try: ConsentStatus[value] except KeyError: raise ValidationError( f'Invalid consent status "{value}". Status must be one of ' - f'{[c.name for c in ConsentStatus]}.') + f"{[c.name for c in ConsentStatus]}." + ) return value @@ -125,9 +139,11 @@ class RefreshTokenSchema(StrictSchema): val = fields.String( required=True, description="Refresh token. Use this to request a new JWT when it " - "expires") - exp = fields.Integer(required=True, - description="Refresh token expiry (unix time)") + "expires", + ) + exp = fields.Integer( + required=True, description="Refresh token expiry (unix time)" + ) class TokenSchema(StrictSchema): @@ -144,7 +160,7 @@ class JWKSchema(StrictSchema): keys = fields.List( fields.Nested(JWKSchema), - description=("List of public keys used for signing.") + description=("List of public keys used for signing."), ) diff --git a/src/iam/settings.py b/src/iam/settings.py index b7c9f63..442d9a8 100644 --- a/src/iam/settings.py +++ b/src/iam/settings.py @@ -24,13 +24,13 @@ def current_config(): """Return the appropriate configuration object based on the environment.""" - if os.environ['ENVIRONMENT'] == 'production': + if os.environ["ENVIRONMENT"] == "production": return Production() - elif os.environ['ENVIRONMENT'] == 'staging': + elif os.environ["ENVIRONMENT"] == "staging": return Staging() - elif os.environ['ENVIRONMENT'] == 'testing': + elif os.environ["ENVIRONMENT"] == "testing": return Testing() - elif os.environ['ENVIRONMENT'] == 'development': + elif os.environ["ENVIRONMENT"] == "development": return Development() else: raise KeyError(f"Unknown environment '{os.environ['ENVIRONMENT']}'") @@ -41,72 +41,72 @@ class Default: def __init__(self): """Initialize the default configuration.""" - self.CORS_ORIGINS = os.environ['ALLOWED_ORIGINS'].split(',') - self.RSA_PRIVATE_KEY = pathlib.Path('keys/rsa').read_text() - self.ALGORITHM = 'RS512' - self.RSA_PUBLIC_KEY = jwk.get_key(self.ALGORITHM)( - self.RSA_PRIVATE_KEY, - self.ALGORITHM, - ).public_key().to_dict() + self.CORS_ORIGINS = os.environ["ALLOWED_ORIGINS"].split(",") + self.RSA_PRIVATE_KEY = pathlib.Path("keys/rsa").read_text() + self.ALGORITHM = "RS512" + self.RSA_PUBLIC_KEY = ( + jwk.get_key(self.ALGORITHM)(self.RSA_PRIVATE_KEY, self.ALGORITHM,) + .public_key() + .to_dict() + ) self.JWT_VALIDITY = timedelta(minutes=10) self.REFRESH_TOKEN_VALIDITY = timedelta(days=30) - self.SENTRY_DSN = os.environ.get('SENTRY_DSN') + self.SENTRY_DSN = os.environ.get("SENTRY_DSN") self.APISPEC_TITLE = "iam" self.APISPEC_SWAGGER_UI_URL = "/" - self.BASIC_AUTH_USERNAME = os.environ['BASIC_AUTH_USERNAME'] - self.BASIC_AUTH_PASSWORD = os.environ['BASIC_AUTH_PASSWORD'] + self.BASIC_AUTH_USERNAME = os.environ["BASIC_AUTH_USERNAME"] + self.BASIC_AUTH_PASSWORD = os.environ["BASIC_AUTH_PASSWORD"] - self.FEAT_TOGGLE_LOCAL_AUTH = bool(os.environ['FEAT_TOGGLE_LOCAL_AUTH']) + self.FEAT_TOGGLE_LOCAL_AUTH = bool(os.environ["FEAT_TOGGLE_LOCAL_AUTH"]) self.SQLALCHEMY_DATABASE_URI = ( f"postgresql://{os.environ['DB_USERNAME']}:" f"{os.environ['DB_PASSWORD']}@{os.environ['DB_HOST']}:" f"{os.environ['DB_PORT']}/{os.environ['DB_NAME']}" - f"?connect_timeout=10") + f"?connect_timeout=10" + ) self.SQLALCHEMY_TRACK_MODIFICATIONS = False - self.FEAT_TOGGLE_FIREBASE = bool(os.environ['FEAT_TOGGLE_FIREBASE']) + self.FEAT_TOGGLE_FIREBASE = bool(os.environ["FEAT_TOGGLE_FIREBASE"]) self.FIREBASE_CLIENT_CERT_URL = os.environ.get( - 'FIREBASE_CLIENT_CERT_URL') - self.FIREBASE_CLIENT_EMAIL = os.environ.get('FIREBASE_CLIENT_EMAIL') - self.FIREBASE_CLIENT_ID = os.environ.get('FIREBASE_CLIENT_ID') - self.FIREBASE_PRIVATE_KEY = os.environ.get('FIREBASE_PRIVATE_KEY') - self.FIREBASE_PRIVATE_KEY_ID = os.environ.get('FIREBASE_PRIVATE_KEY_ID') - self.FIREBASE_PROJECT_ID = os.environ.get('FIREBASE_PROJECT_ID') - self.SENDGRID_API_KEY = os.environ.get('SENDGRID_API_KEY') + "FIREBASE_CLIENT_CERT_URL" + ) + self.FIREBASE_CLIENT_EMAIL = os.environ.get("FIREBASE_CLIENT_EMAIL") + self.FIREBASE_CLIENT_ID = os.environ.get("FIREBASE_CLIENT_ID") + self.FIREBASE_PRIVATE_KEY = os.environ.get("FIREBASE_PRIVATE_KEY") + self.FIREBASE_PRIVATE_KEY_ID = os.environ.get("FIREBASE_PRIVATE_KEY_ID") + self.FIREBASE_PROJECT_ID = os.environ.get("FIREBASE_PROJECT_ID") + self.SENDGRID_API_KEY = os.environ.get("SENDGRID_API_KEY") self.LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'simple': { - 'format': "%(asctime)s [%(levelname)s] [%(name)s] " - "%(filename)s:%(funcName)s:%(lineno)d | " - "%(message)s" + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "simple": { + "format": "%(asctime)s [%(levelname)s] [%(name)s] " + "%(filename)s:%(funcName)s:%(lineno)d | " + "%(message)s" }, }, - 'handlers': { - 'console': { - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - 'formatter': 'simple', + "handlers": { + "console": { + "level": "DEBUG", + "class": "logging.StreamHandler", + "formatter": "simple", }, }, - 'loggers': { + "loggers": { # All loggers will by default use the root logger below (and # hence be very verbose). To silence spammy/uninteresting log # output, add the loggers here and increase the loglevel. - 'parso': { - 'level': 'INFO', - 'handlers': ['console'], - 'propagate': False, + "parso": { + "level": "INFO", + "handlers": ["console"], + "propagate": False, } }, - 'root': { - 'level': 'DEBUG', - 'handlers': ['console'], - }, + "root": {"level": "DEBUG", "handlers": ["console"],}, } @@ -131,7 +131,7 @@ def __init__(self): self.SECRET_KEY = os.urandom(24) self.TESTING = True self.SQLALCHEMY_DATABASE_URI = ( - 'postgresql://postgres:@postgres:5432/iam_test' + "postgresql://postgres:@postgres:5432/iam_test" ) self.ROOT_URL = "http://localhost:4200" @@ -143,8 +143,8 @@ def __init__(self): """Initialize the staging configuration.""" super().__init__() self.DEBUG = False - self.SECRET_KEY = os.environ['SECRET_KEY'] - self.LOGGING['root']['level'] = 'INFO' + self.SECRET_KEY = os.environ["SECRET_KEY"] + self.LOGGING["root"]["level"] = "INFO" self.ROOT_URL = "https://staging.dd-decaf.eu" @@ -155,6 +155,6 @@ def __init__(self): """Initialize the production configuration.""" super().__init__() self.DEBUG = False - self.SECRET_KEY = os.environ['SECRET_KEY'] - self.LOGGING['root']['level'] = 'INFO' + self.SECRET_KEY = os.environ["SECRET_KEY"] + self.LOGGING["root"]["level"] = "INFO" self.ROOT_URL = "https://caffeine.dd-decaf.eu" diff --git a/tests/conftest.py b/tests/conftest.py index 0948c1a..2a9bd31 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -58,32 +58,38 @@ def db_fixtures(db_tables): These are installed session-wide and shared for performance reasons, but tests may of course create their own encapsulated test data if needed. """ - organization = Organization(name='OrgName') - team = Team(name='TeamName', organization=organization) - user = User(first_name='User', last_name='Name', email='user@name.test') - user.set_password('hunter2') - user2 = User(first_name='User2', last_name='Name2', email='user2@name.test') - user2.set_password('hunter2') - project = Project(name='ProjectName') + organization = Organization(name="OrgName") + team = Team(name="TeamName", organization=organization) + user = User(first_name="User", last_name="Name", email="user@name.test") + user.set_password("hunter2") + user2 = User(first_name="User2", last_name="Name2", email="user2@name.test") + user2.set_password("hunter2") + project = Project(name="ProjectName") # user1 consent - consent = Consent(type=ConsentType.cookie.name, - category=CookieConsentCategory.statistics.name, - status=ConsentStatus.accepted.name, - user=user) + consent = Consent( + type=ConsentType.cookie.name, + category=CookieConsentCategory.statistics.name, + status=ConsentStatus.accepted.name, + user=user, + ) # This consent has the same category and type, and is created more # recently, so on request, this consent should be returned instead. - consent_override = Consent(type=ConsentType.cookie.name, - category=CookieConsentCategory.statistics.name, - status=ConsentStatus.rejected.name, - user=user) + consent_override = Consent( + type=ConsentType.cookie.name, + category=CookieConsentCategory.statistics.name, + status=ConsentStatus.rejected.name, + user=user, + ) # This consent has the same category and type, and is created more # recently, but is associated with another user, so on user1's request, # this consent should not be returned. - consent_conflict = Consent(type=ConsentType.cookie.name, - category=CookieConsentCategory.statistics.name, - status=ConsentStatus.accepted.name, - user=user2) + consent_conflict = Consent( + type=ConsentType.cookie.name, + category=CookieConsentCategory.statistics.name, + status=ConsentStatus.accepted.name, + user=user2, + ) db_.session.add(organization) db_.session.add(team) @@ -123,7 +129,8 @@ def session(db_tables, connection): # rolled back independently of the session state. transaction = connection.begin() db_.session = db_.create_scoped_session( - options={'bind': connection, 'binds': {}}) + options={"bind": connection, "binds": {}} + ) yield db_.session # Roll back anything that occurred in the test session and reset the db @@ -137,11 +144,11 @@ def session(db_tables, connection): def models(db_fixtures, session): """Provide preinstalled db fixtures queried from the current db session.""" return { - 'organization': Organization.query.one(), - 'team': Team.query.one(), - 'user': User.query.all(), - 'project': Project.query.one(), - 'consent': Consent.query.all() + "organization": Organization.query.one(), + "team": Team.query.one(), + "user": User.query.all(), + "project": Project.query.one(), + "consent": Consent.query.all(), } @@ -149,28 +156,19 @@ def models(db_fixtures, session): def tokens(app): """Provide user 1 with read, write and admin JWT claims to project 1.""" return { - 'read': jwt.encode( - { - 'usr': 1, - 'prj': {1: 'read'} - }, - app.config['RSA_PRIVATE_KEY'], - app.config['ALGORITHM'], + "read": jwt.encode( + {"usr": 1, "prj": {1: "read"}}, + app.config["RSA_PRIVATE_KEY"], + app.config["ALGORITHM"], ), - 'write': jwt.encode( - { - 'usr': 1, - 'prj': {1: 'write'} - }, - app.config['RSA_PRIVATE_KEY'], - app.config['ALGORITHM'], + "write": jwt.encode( + {"usr": 1, "prj": {1: "write"}}, + app.config["RSA_PRIVATE_KEY"], + app.config["ALGORITHM"], ), - 'admin': jwt.encode( - { - 'usr': 1, - 'prj': {1: 'admin'} - }, - app.config['RSA_PRIVATE_KEY'], - app.config['ALGORITHM'], + "admin": jwt.encode( + {"usr": 1, "prj": {1: "admin"}}, + app.config["RSA_PRIVATE_KEY"], + app.config["ALGORITHM"], ), } diff --git a/tests/integration/test_database.py b/tests/integration/test_database.py index 517ff14..81295cc 100644 --- a/tests/integration/test_database.py +++ b/tests/integration/test_database.py @@ -15,38 +15,47 @@ """Tests for the database integration.""" from iam.models import ( - OrganizationProject, OrganizationUser, TeamProject, TeamUser, UserProject) + OrganizationProject, + OrganizationUser, + TeamProject, + TeamUser, + UserProject, +) def test_owner_role(session, models): """Test a users admin access to a project through the organization.""" - user = models['user'][0] + user = models["user"][0] # Give user owner role, and assign the project to the organization - OrganizationUser(organization=models['organization'], user=user, - role='owner') - OrganizationProject(organization=models['organization'], - project=models['project'], role='read') + OrganizationUser( + organization=models["organization"], user=user, role="owner" + ) + OrganizationProject( + organization=models["organization"], + project=models["project"], + role="read", + ) # Verify that the user has admin role for the project - assert user.claims['prj'][models['project'].id] == 'admin' + assert user.claims["prj"][models["project"].id] == "admin" def test_team_role(session, models): """Test a users access to a project through a team.""" - user = models['user'][0] + user = models["user"][0] # Assign the user to the team, and give the team write access to the project - TeamUser(team=models['team'], user=user, role='member') - TeamProject(team=models['team'], project=models['project'], role='write') + TeamUser(team=models["team"], user=user, role="member") + TeamProject(team=models["team"], project=models["project"], role="write") # Verify that the user has write role for the project - assert user.claims['prj'][models['project'].id] == 'write' + assert user.claims["prj"][models["project"].id] == "write" def test_user_role(session, models): """Test a users direct access to a project.""" - user = models['user'][0] + user = models["user"][0] # Assign the user to the project with read access - UserProject(user=user, project=models['project'], role='read') + UserProject(user=user, project=models["project"], role="read") # Verify that the user has write role for the project - assert user.claims['prj'][models['project'].id] == 'read' + assert user.claims["prj"][models["project"].id] == "read" diff --git a/tests/integration/test_endpoints.py b/tests/integration/test_endpoints.py index 4e1bf52..b88331f 100644 --- a/tests/integration/test_endpoints.py +++ b/tests/integration/test_endpoints.py @@ -28,167 +28,172 @@ def test_openapi_schema(app, client): """Test OpenAPI schema resource.""" - response = client.get('/swagger/') + response = client.get("/swagger/") assert response.status_code == 200 - assert len(json.loads(response.data)['paths']) > 0 + assert len(json.loads(response.data)["paths"]) > 0 def test_healthz(client): """Test the readiness endpoint.""" - response = client.get('/healthz') + response = client.get("/healthz") assert response.status_code == 200 def test_metrics(client): """Test the metrics endpoint.""" - response = client.get('/metrics') + response = client.get("/metrics") assert response.status_code == 200 def test_get_admin_unauthorized(client): """Test unauthorized access to the admin view.""" - response = client.get('/admin/') + response = client.get("/admin/") assert response.status_code == 401 def test_get_admin_authorized(app, client): """Test authorized access to the admin view.""" - credentials = base64.b64encode(f'{app.config["BASIC_AUTH_USERNAME"]}:' - f'{app.config["BASIC_AUTH_PASSWORD"]}' - .encode()).decode() - response = client.get('/admin/', - headers={'Authorization': f'Basic {credentials}'}) + credentials = base64.b64encode( + f'{app.config["BASIC_AUTH_USERNAME"]}:' + f'{app.config["BASIC_AUTH_PASSWORD"]}'.encode() + ).decode() + response = client.get( + "/admin/", headers={"Authorization": f"Basic {credentials}"} + ) assert response.status_code == 200 def test_authenticate_failure(app, client, models): """Test invalid local authentication.""" - response = client.post('/authenticate/local') + response = client.post("/authenticate/local") assert response.status_code == 422 - user = models['user'][0] - response = client.post('/authenticate/local', data={ - 'email': user.email, - 'password': 'invalid', - }) + user = models["user"][0] + response = client.post( + "/authenticate/local", + data={"email": user.email, "password": "invalid",}, + ) assert response.status_code == 401 def test_authenticate_success(app, client, session, models): """Test valid local authentication.""" - user = models['user'][0] - response = client.post('/authenticate/local', data={ - 'email': user.email, - 'password': 'hunter2', - }) + user = models["user"][0] + response = client.post( + "/authenticate/local", + data={"email": user.email, "password": "hunter2",}, + ) assert response.status_code == 200 - raw_jwt_token = json.loads(response.data)['jwt'] + raw_jwt_token = json.loads(response.data)["jwt"] returned_claims = jwt.decode( - raw_jwt_token, - app.config['RSA_PUBLIC_KEY'], - app.config['ALGORITHM'], + raw_jwt_token, app.config["RSA_PUBLIC_KEY"], app.config["ALGORITHM"], ) - del returned_claims['exp'] + del returned_claims["exp"] assert user.claims == returned_claims def test_authenticate_refresh(app, client, session, models): """Test the token refresh endpoint.""" - user = models['user'][0] + user = models["user"][0] # Authenticate to receive a refresh token - response = client.post('/authenticate/local', data={ - 'email': user.email, - 'password': 'hunter2', - }) - refresh_token = json.loads(response.data)['refresh_token'] + response = client.post( + "/authenticate/local", + data={"email": user.email, "password": "hunter2",}, + ) + refresh_token = json.loads(response.data)["refresh_token"] # Check that token values are as expected - assert len(refresh_token['val']) == 64 - assert datetime.fromtimestamp(refresh_token['exp']) > datetime.now() - assert datetime.fromtimestamp(refresh_token['exp']) < ( - datetime.now() + app.config['REFRESH_TOKEN_VALIDITY']) + assert len(refresh_token["val"]) == 64 + assert datetime.fromtimestamp(refresh_token["exp"]) > datetime.now() + assert datetime.fromtimestamp(refresh_token["exp"]) < ( + datetime.now() + app.config["REFRESH_TOKEN_VALIDITY"] + ) # Check that the returned token is now stored in the database - assert refresh_token['val'] == \ - user.refresh_tokens[0].token + assert refresh_token["val"] == user.refresh_tokens[0].token # Expect refreshing token to succeed - response = client.post('/refresh', - data={'refresh_token': refresh_token['val']}) + response = client.post( + "/refresh", data={"refresh_token": refresh_token["val"]} + ) assert response.status_code == 200 - raw_jwt_token = json.loads(response.data)['jwt'] + raw_jwt_token = json.loads(response.data)["jwt"] # Expect that the new claims are equal to the user claims, except for the # expiry which will have refreshed refresh_claims = jwt.decode( - raw_jwt_token, - app.config['RSA_PUBLIC_KEY'], - app.config['ALGORITHM'], + raw_jwt_token, app.config["RSA_PUBLIC_KEY"], app.config["ALGORITHM"], ) - del refresh_claims['exp'] + del refresh_claims["exp"] assert user.claims == refresh_claims # Expect refreshing an expired token to fail token = user.refresh_tokens[0] token.expiry = datetime.now() - timedelta(seconds=1) - response = client.post('/refresh', data={'refresh_token': token.token}) + response = client.post("/refresh", data={"refresh_token": token.token}) assert response.status_code == 401 def test_create_project(client, session, tokens): """Create a new project.""" - response = client.post("/projects", json={ - 'name': "New Project", - 'organizations': [], - 'teams': [], - 'users': [], - }, headers={ - 'Authorization': f"Bearer {tokens['write']}", - }) + response = client.post( + "/projects", + json={ + "name": "New Project", + "organizations": [], + "teams": [], + "users": [], + }, + headers={"Authorization": f"Bearer {tokens['write']}",}, + ) assert response.status_code == 201 - project_id = response.json['id'] + project_id = response.json["id"] assert Project.query.filter(Project.id == project_id).count() == 1 def test_get_projects(client, session, models, tokens): """Retrieve list of models.""" - response = client.get("/projects", headers={ - 'Authorization': f"Bearer {tokens['read']}", - }) + response = client.get( + "/projects", headers={"Authorization": f"Bearer {tokens['read']}",} + ) assert response.status_code == 200 assert len(response.json) > 0 def test_get_project(client, session, models, tokens): """Retrieve single models.""" - response = client.get(f"/projects/{models['project'].id}", headers={ - 'Authorization': f"Bearer {tokens['read']}", - }) + response = client.get( + f"/projects/{models['project'].id}", + headers={"Authorization": f"Bearer {tokens['read']}",}, + ) assert response.status_code == 200 - assert response.json['name'] == "ProjectName" + assert response.json["name"] == "ProjectName" def test_put_project(client, session, models, tokens): """Retrieve single models.""" - response = client.put(f"/projects/{models['project'].id}", json={ - 'name': "Changed", - 'organizations': [], - 'teams': [], - 'users': [], - }, headers={ - 'Authorization': f"Bearer {tokens['write']}", - }) + response = client.put( + f"/projects/{models['project'].id}", + json={ + "name": "Changed", + "organizations": [], + "teams": [], + "users": [], + }, + headers={"Authorization": f"Bearer {tokens['write']}",}, + ) assert response.status_code == 204 - project = Project.query.filter(Project.id == models['project'].id).one() + project = Project.query.filter(Project.id == models["project"].id).one() assert project.name == "Changed" def test_delete_project(client, session, models, tokens): """Delete a project.""" - response = client.delete(f"/projects/{models['project'].id}", headers={ - 'Authorization': f"Bearer {tokens['admin']}", - }) + response = client.delete( + f"/projects/{models['project'].id}", + headers={"Authorization": f"Bearer {tokens['admin']}",}, + ) assert response.status_code == 204 assert Project.query.filter(Project.id == 1).count() == 0 @@ -197,26 +202,24 @@ def test_keys(app, client): """Retrieve public key from the /keys endpoint.""" response = client.get("/keys") assert response.status_code == 200 - assert len(response.json['keys']) > 0 + assert len(response.json["keys"]) > 0 def test_user(app, client, session, models, tokens): """Retrieve user data based on given token.""" - response = client.get("/user", headers={ - 'Authorization': f"Bearer {tokens['read']}", - }) + response = client.get( + "/user", headers={"Authorization": f"Bearer {tokens['read']}",} + ) assert response.status_code == 200 # Verify returned data against the database user_id = jwt.decode( - tokens['read'], - app.config['RSA_PUBLIC_KEY'], - app.config['ALGORITHM'], - )['usr'] + tokens["read"], app.config["RSA_PUBLIC_KEY"], app.config["ALGORITHM"], + )["usr"] user = User.query.filter(User.id == user_id).one() - assert user.first_name == response.json['first_name'] - assert user.last_name == response.json['last_name'] - assert user.email == response.json['email'] + assert user.first_name == response.json["first_name"] + assert user.last_name == response.json["last_name"] + assert user.email == response.json["email"] def test_user_no_jwt(client): @@ -225,93 +228,100 @@ def test_user_no_jwt(client): assert response.status_code == 401 -@pytest.mark.parametrize('input', [ - # full definition - { - 'type': 'gdpr', - 'category': 'newsletter', - 'status': 'accepted', - 'timestamp': datetime.now(timezone('UTC')).isoformat(), - 'valid_until': datetime.now(timezone('UTC')).isoformat(), - 'message': 'I consent to the ToS', - 'source': 'pytest' - }, - # minimal - { - 'type': 'cookie', - 'category': 'preferences', - 'status': 'rejected', - } -]) +@pytest.mark.parametrize( + "input", + [ + # full definition + { + "type": "gdpr", + "category": "newsletter", + "status": "accepted", + "timestamp": datetime.now(timezone("UTC")).isoformat(), + "valid_until": datetime.now(timezone("UTC")).isoformat(), + "message": "I consent to the ToS", + "source": "pytest", + }, + # minimal + {"type": "cookie", "category": "preferences", "status": "rejected",}, + ], +) def test_create_consent(client, session, tokens, input): """Create a new consent.""" - response = client.post("/consent", json=input, headers={ - 'Authorization': f"Bearer {tokens['write']}", - }) + response = client.post( + "/consent", + json=input, + headers={"Authorization": f"Bearer {tokens['write']}",}, + ) assert response.status_code == 201 - consent_id = response.json['id'] + consent_id = response.json["id"] assert Consent.query.filter(Consent.id == consent_id).count() == 1 -def test_create_consent_fail_on_incorrect_cookie_category(client, session, - tokens): +def test_create_consent_fail_on_incorrect_cookie_category( + client, session, tokens +): """Fail to create a new consent with incorrect cookie category.""" data = { - 'type': 'cookie', - 'category': 'fumctiomal', - 'status': 'accepted', + "type": "cookie", + "category": "fumctiomal", + "status": "accepted", } - response = client.post("/consent", json=data, headers={ - 'Authorization': f"Bearer {tokens['write']}", - }) + response = client.post( + "/consent", + json=data, + headers={"Authorization": f"Bearer {tokens['write']}",}, + ) assert response.status_code == 422 def test_create_consent_fail_on_incorrect_status(client, session, tokens): """Fail to create a new consent with incorrect status.""" data = { - 'type': 'cookie', - 'category': 'strictly_necessary', - 'status': 'akcepted', + "type": "cookie", + "category": "strictly_necessary", + "status": "akcepted", } - response = client.post("/consent", json=data, headers={ - 'Authorization': f"Bearer {tokens['write']}", - }) + response = client.post( + "/consent", + json=data, + headers={"Authorization": f"Bearer {tokens['write']}",}, + ) assert response.status_code == 422 def test_create_consent_fail_on_incorrect_type(client, session, tokens): """Fail to create a new consent with incorrect type.""" data = { - 'type': 'gp_dr', - 'category': 'newsletter', - 'status': 'accepted', + "type": "gp_dr", + "category": "newsletter", + "status": "accepted", } - response = client.post("/consent", json=data, headers={ - 'Authorization': f"Bearer {tokens['write']}", - }) + response = client.post( + "/consent", + json=data, + headers={"Authorization": f"Bearer {tokens['write']}",}, + ) assert response.status_code == 422 def test_get_consent(app, client, session, models, tokens): """Retrieve user consent data based on given token.""" - response = client.get("/consent", headers={ - 'Authorization': f"Bearer {tokens['read']}", - }) + response = client.get( + "/consent", headers={"Authorization": f"Bearer {tokens['read']}",} + ) assert response.status_code == 200 def test_get_consent_returns_unique_consents( - app, client, session, models, tokens): + app, client, session, models, tokens +): """Test consents include only one consent per category + type.""" - response = client.get("/consent", headers={ - 'Authorization': f"Bearer {tokens['read']}", - }) + response = client.get( + "/consent", headers={"Authorization": f"Bearer {tokens['read']}",} + ) user_id = jwt.decode( - tokens['read'], - app.config['RSA_PUBLIC_KEY'], - app.config['ALGORITHM'], - )['usr'] + tokens["read"], app.config["RSA_PUBLIC_KEY"], app.config["ALGORITHM"], + )["usr"] # Get expected consents - Consents with most recent timestamp value # for each group of consents that have identical combination of user_id, # type, and category @@ -336,17 +346,16 @@ def keyfunc(consent): unique_consents[consent_key] = consent -def test_get_consent_returns_latest_consents(app, client, session, models, - tokens): +def test_get_consent_returns_latest_consents( + app, client, session, models, tokens +): """Test that retrieved user consent data match the latest values.""" - response = client.get("/consent", headers={ - 'Authorization': f"Bearer {tokens['read']}", - }) + response = client.get( + "/consent", headers={"Authorization": f"Bearer {tokens['read']}",} + ) user_id = jwt.decode( - tokens['read'], - app.config['RSA_PUBLIC_KEY'], - app.config['ALGORITHM'], - )['usr'] + tokens["read"], app.config["RSA_PUBLIC_KEY"], app.config["ALGORITHM"], + )["usr"] # Get expected consents - Consents with most recent timestamp value # for each group of consents that have identical combination of user_id, # type, and category @@ -364,18 +373,17 @@ def keyfunc(consent): assert len(response.json) == len(latest_consents) # Test that timestamps in our collection match those retrieved via request latest_timestamps = [ - c.timestamp.isoformat() - for c in latest_consents.values() + c.timestamp.isoformat() for c in latest_consents.values() ] for consent in response.json: - assert consent['timestamp'] in latest_timestamps + assert consent["timestamp"] in latest_timestamps def test_reset_request_non_existing_email(client, models): """Reset request with non-existing email.""" - response = client.post("/password/reset-request", json={ - 'email': "not-a-real@email.com" - }) + response = client.post( + "/password/reset-request", json={"email": "not-a-real@email.com"} + ) assert response.status_code == 404 @@ -383,10 +391,10 @@ def test_password_reset(client, models): """Change password with valid reset token.""" user = models["user"][0] encoded_token = user.get_reset_token() - new_password = 'password' - response = client.post(f"/password/reset/{encoded_token}", json={ - 'password': new_password - }) + new_password = "password" + response = client.post( + f"/password/reset/{encoded_token}", json={"password": new_password} + ) assert response.status_code == 200 assert user.check_password(new_password) @@ -396,15 +404,15 @@ def test_password_reset_expired_token(app, client, models): user = models["user"][0] claims = { "exp": int(datetime.timestamp(datetime.now() - timedelta(minutes=1))), - "usr": user.id + "usr": user.id, } encoded_token = jwt.encode( claims, app.config["RSA_PRIVATE_KEY"], app.config["ALGORITHM"] ) - new_password = 'password' - response = client.post(f"/password/reset/{encoded_token}", json={ - 'password': new_password - }) + new_password = "password" + response = client.post( + f"/password/reset/{encoded_token}", json={"password": new_password} + ) assert response.status_code == 400 assert not user.check_password(new_password) @@ -414,12 +422,12 @@ def test_password_reset_wrong_signature(app, client, models): user = models["user"][0] claims = { "exp": int(datetime.timestamp(datetime.now() + timedelta(hours=1))), - "usr": user.id + "usr": user.id, } encoded_token = jwt.encode(claims, "secret", "HS256") - new_password = 'password' - response = client.post(f"/password/reset/{encoded_token}", json={ - 'password': new_password - }) + new_password = "password" + response = client.post( + f"/password/reset/{encoded_token}", json={"password": new_password} + ) assert response.status_code == 400 assert not user.check_password(new_password) diff --git a/tests/unit/test_domain.py b/tests/unit/test_domain.py index d579ec6..41cc57a 100644 --- a/tests/unit/test_domain.py +++ b/tests/unit/test_domain.py @@ -22,20 +22,19 @@ def test_sign_claims(app, models): """Test the sign_claims function.""" - user = models['user'][0] + user = models["user"][0] claims = sign_claims(user) - assert len(claims['jwt']) > 0 - assert len(claims['refresh_token']['val']) > 0 - expiry = datetime.fromtimestamp(claims['refresh_token']['exp']) + assert len(claims["jwt"]) > 0 + assert len(claims["refresh_token"]["val"]) > 0 + expiry = datetime.fromtimestamp(claims["refresh_token"]["exp"]) assert datetime.now() < expiry - assert datetime.now() + app.config['REFRESH_TOKEN_VALIDITY'] >= expiry + assert datetime.now() + app.config["REFRESH_TOKEN_VALIDITY"] >= expiry def test_create_firebase_user(session): """Test creating a Firebase user.""" - user = create_firebase_user('foo_token', { - 'name': 'Foo Bar', - 'email': 'foo@bar.dk', - }) + user = create_firebase_user( + "foo_token", {"name": "Foo Bar", "email": "foo@bar.dk",} + ) assert isinstance(user, User) - assert len(user.claims['prj'].keys()) == 0 + assert len(user.claims["prj"].keys()) == 0 diff --git a/tests/unit/test_hasher.py b/tests/unit/test_hasher.py index 90dcb54..b61b6b5 100644 --- a/tests/unit/test_hasher.py +++ b/tests/unit/test_hasher.py @@ -19,25 +19,30 @@ def test_encode(): """Test encoding password with various parameters.""" - assert hasher.verify('foo', hasher.encode('foo')) - assert hasher.verify('foo', hasher.encode('foo', iterations=50)) - assert hasher.verify('foo', hasher.encode('foo', salt='bar', iterations=50)) - assert not hasher.verify('foo', hasher.encode('bar', iterations=50)) + assert hasher.verify("foo", hasher.encode("foo")) + assert hasher.verify("foo", hasher.encode("foo", iterations=50)) + assert hasher.verify("foo", hasher.encode("foo", salt="bar", iterations=50)) + assert not hasher.verify("foo", hasher.encode("bar", iterations=50)) - assert hasher.encode('foo', iterations=99) != hasher.encode('foo', - iterations=100) + assert hasher.encode("foo", iterations=99) != hasher.encode( + "foo", iterations=100 + ) def test_str_bytes(): """Test that verification is str/bytes agnostic.""" - password_str = 'æøå' + password_str = "æøå" password_bytes = password_str.encode() iterations = 50 # for faster tests - assert hasher.verify(password_str, hasher.encode(password_str, - iterations=iterations)) - assert hasher.verify(password_bytes, hasher.encode(password_str, - iterations=iterations)) - assert hasher.verify(password_str, hasher.encode(password_bytes, - iterations=iterations)) - assert hasher.verify(password_bytes, hasher.encode(password_bytes, - iterations=iterations)) + assert hasher.verify( + password_str, hasher.encode(password_str, iterations=iterations) + ) + assert hasher.verify( + password_bytes, hasher.encode(password_str, iterations=iterations) + ) + assert hasher.verify( + password_str, hasher.encode(password_bytes, iterations=iterations) + ) + assert hasher.verify( + password_bytes, hasher.encode(password_bytes, iterations=iterations) + ) diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index f86083e..3dce771 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -31,53 +31,42 @@ def test_reset_token(app, models): assert decoded_token["usr"] == user.id -@pytest.mark.parametrize('input', [ - # cookie consent - { - "type": "cookie", - "category": "statistics", - "status": "accepted" - }, - # gdpr consent - { - 'type': 'gdpr', - 'category': 'newsletter', - 'status': 'rejected', - } -]) +@pytest.mark.parametrize( + "input", + [ + # cookie consent + {"type": "cookie", "category": "statistics", "status": "accepted"}, + # gdpr consent + {"type": "gdpr", "category": "newsletter", "status": "rejected",}, + ], +) def test_create_consent(models, input): - user = models['user'][0] + user = models["user"][0] consent = Consent(**input, user=user) assert consent def test_create_consent_fail_on_invalid_type(models): - user = models['user'][0] + user = models["user"][0] with pytest.raises(DataError): - consent = Consent(category="performance", - type="wookie", - status="accepted", - user=user) + consent = Consent( + category="performance", type="wookie", status="accepted", user=user + ) db.session.add(consent) db.session.commit() -@pytest.mark.parametrize('input', [ - # invalid reject status - { - 'type': 'gdpr', - 'category': 'newsletter', - 'status': 'rej', - }, - # invalid accept status - { - 'type': 'cookie', - 'category': 'statistics', - 'status': 'akceptted', - } -]) +@pytest.mark.parametrize( + "input", + [ + # invalid reject status + {"type": "gdpr", "category": "newsletter", "status": "rej",}, + # invalid accept status + {"type": "cookie", "category": "statistics", "status": "akceptted",}, + ], +) def test_create_consent_fail_on_invalid_status(models, input): - user = models['user'][0] + user = models["user"][0] with pytest.raises(DataError): consent = Consent(**input, user=user) db.session.add(consent) From 33eec5196bafa1b3cfb7ca41b283a98100257e25 Mon Sep 17 00:00:00 2001 From: Midnighter Date: Thu, 30 Apr 2020 11:32:55 +0200 Subject: [PATCH 05/11] style: appease flake8 --- src/iam/app.py | 2 +- src/iam/settings.py | 2 +- tests/integration/test_endpoints.py | 34 ++++++++++++++--------------- tests/unit/test_domain.py | 2 +- tests/unit/test_models.py | 6 ++--- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/iam/app.py b/src/iam/app.py index af962f2..321e9bf 100644 --- a/src/iam/app.py +++ b/src/iam/app.py @@ -86,7 +86,7 @@ def init_app(application, db): "type": "service_account", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://accounts.google.com/o/oauth2/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", # noqa: E501 "project_id": application.config["FIREBASE_PROJECT_ID"], "private_key_id": application.config["FIREBASE_PRIVATE_KEY_ID"], "private_key": application.config["FIREBASE_PRIVATE_KEY"], diff --git a/src/iam/settings.py b/src/iam/settings.py index 442d9a8..7b1ed0f 100644 --- a/src/iam/settings.py +++ b/src/iam/settings.py @@ -106,7 +106,7 @@ def __init__(self): "propagate": False, } }, - "root": {"level": "DEBUG", "handlers": ["console"],}, + "root": {"level": "DEBUG", "handlers": ["console"]}, } diff --git a/tests/integration/test_endpoints.py b/tests/integration/test_endpoints.py index b88331f..ef5c85c 100644 --- a/tests/integration/test_endpoints.py +++ b/tests/integration/test_endpoints.py @@ -71,7 +71,7 @@ def test_authenticate_failure(app, client, models): user = models["user"][0] response = client.post( "/authenticate/local", - data={"email": user.email, "password": "invalid",}, + data={"email": user.email, "password": "invalid"}, ) assert response.status_code == 401 @@ -81,7 +81,7 @@ def test_authenticate_success(app, client, session, models): user = models["user"][0] response = client.post( "/authenticate/local", - data={"email": user.email, "password": "hunter2",}, + data={"email": user.email, "password": "hunter2"}, ) assert response.status_code == 200 raw_jwt_token = json.loads(response.data)["jwt"] @@ -99,7 +99,7 @@ def test_authenticate_refresh(app, client, session, models): # Authenticate to receive a refresh token response = client.post( "/authenticate/local", - data={"email": user.email, "password": "hunter2",}, + data={"email": user.email, "password": "hunter2"}, ) refresh_token = json.loads(response.data)["refresh_token"] @@ -145,7 +145,7 @@ def test_create_project(client, session, tokens): "teams": [], "users": [], }, - headers={"Authorization": f"Bearer {tokens['write']}",}, + headers={"Authorization": f"Bearer {tokens['write']}"}, ) assert response.status_code == 201 project_id = response.json["id"] @@ -155,7 +155,7 @@ def test_create_project(client, session, tokens): def test_get_projects(client, session, models, tokens): """Retrieve list of models.""" response = client.get( - "/projects", headers={"Authorization": f"Bearer {tokens['read']}",} + "/projects", headers={"Authorization": f"Bearer {tokens['read']}"} ) assert response.status_code == 200 assert len(response.json) > 0 @@ -165,7 +165,7 @@ def test_get_project(client, session, models, tokens): """Retrieve single models.""" response = client.get( f"/projects/{models['project'].id}", - headers={"Authorization": f"Bearer {tokens['read']}",}, + headers={"Authorization": f"Bearer {tokens['read']}"}, ) assert response.status_code == 200 assert response.json["name"] == "ProjectName" @@ -181,7 +181,7 @@ def test_put_project(client, session, models, tokens): "teams": [], "users": [], }, - headers={"Authorization": f"Bearer {tokens['write']}",}, + headers={"Authorization": f"Bearer {tokens['write']}"}, ) assert response.status_code == 204 project = Project.query.filter(Project.id == models["project"].id).one() @@ -192,7 +192,7 @@ def test_delete_project(client, session, models, tokens): """Delete a project.""" response = client.delete( f"/projects/{models['project'].id}", - headers={"Authorization": f"Bearer {tokens['admin']}",}, + headers={"Authorization": f"Bearer {tokens['admin']}"}, ) assert response.status_code == 204 assert Project.query.filter(Project.id == 1).count() == 0 @@ -208,7 +208,7 @@ def test_keys(app, client): def test_user(app, client, session, models, tokens): """Retrieve user data based on given token.""" response = client.get( - "/user", headers={"Authorization": f"Bearer {tokens['read']}",} + "/user", headers={"Authorization": f"Bearer {tokens['read']}"} ) assert response.status_code == 200 @@ -242,7 +242,7 @@ def test_user_no_jwt(client): "source": "pytest", }, # minimal - {"type": "cookie", "category": "preferences", "status": "rejected",}, + {"type": "cookie", "category": "preferences", "status": "rejected"}, ], ) def test_create_consent(client, session, tokens, input): @@ -250,7 +250,7 @@ def test_create_consent(client, session, tokens, input): response = client.post( "/consent", json=input, - headers={"Authorization": f"Bearer {tokens['write']}",}, + headers={"Authorization": f"Bearer {tokens['write']}"}, ) assert response.status_code == 201 consent_id = response.json["id"] @@ -269,7 +269,7 @@ def test_create_consent_fail_on_incorrect_cookie_category( response = client.post( "/consent", json=data, - headers={"Authorization": f"Bearer {tokens['write']}",}, + headers={"Authorization": f"Bearer {tokens['write']}"}, ) assert response.status_code == 422 @@ -284,7 +284,7 @@ def test_create_consent_fail_on_incorrect_status(client, session, tokens): response = client.post( "/consent", json=data, - headers={"Authorization": f"Bearer {tokens['write']}",}, + headers={"Authorization": f"Bearer {tokens['write']}"}, ) assert response.status_code == 422 @@ -299,7 +299,7 @@ def test_create_consent_fail_on_incorrect_type(client, session, tokens): response = client.post( "/consent", json=data, - headers={"Authorization": f"Bearer {tokens['write']}",}, + headers={"Authorization": f"Bearer {tokens['write']}"}, ) assert response.status_code == 422 @@ -307,7 +307,7 @@ def test_create_consent_fail_on_incorrect_type(client, session, tokens): def test_get_consent(app, client, session, models, tokens): """Retrieve user consent data based on given token.""" response = client.get( - "/consent", headers={"Authorization": f"Bearer {tokens['read']}",} + "/consent", headers={"Authorization": f"Bearer {tokens['read']}"} ) assert response.status_code == 200 @@ -317,7 +317,7 @@ def test_get_consent_returns_unique_consents( ): """Test consents include only one consent per category + type.""" response = client.get( - "/consent", headers={"Authorization": f"Bearer {tokens['read']}",} + "/consent", headers={"Authorization": f"Bearer {tokens['read']}"} ) user_id = jwt.decode( tokens["read"], app.config["RSA_PUBLIC_KEY"], app.config["ALGORITHM"], @@ -351,7 +351,7 @@ def test_get_consent_returns_latest_consents( ): """Test that retrieved user consent data match the latest values.""" response = client.get( - "/consent", headers={"Authorization": f"Bearer {tokens['read']}",} + "/consent", headers={"Authorization": f"Bearer {tokens['read']}"} ) user_id = jwt.decode( tokens["read"], app.config["RSA_PUBLIC_KEY"], app.config["ALGORITHM"], diff --git a/tests/unit/test_domain.py b/tests/unit/test_domain.py index 41cc57a..7b4f248 100644 --- a/tests/unit/test_domain.py +++ b/tests/unit/test_domain.py @@ -34,7 +34,7 @@ def test_sign_claims(app, models): def test_create_firebase_user(session): """Test creating a Firebase user.""" user = create_firebase_user( - "foo_token", {"name": "Foo Bar", "email": "foo@bar.dk",} + "foo_token", {"name": "Foo Bar", "email": "foo@bar.dk"} ) assert isinstance(user, User) assert len(user.claims["prj"].keys()) == 0 diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 3dce771..6af501d 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -37,7 +37,7 @@ def test_reset_token(app, models): # cookie consent {"type": "cookie", "category": "statistics", "status": "accepted"}, # gdpr consent - {"type": "gdpr", "category": "newsletter", "status": "rejected",}, + {"type": "gdpr", "category": "newsletter", "status": "rejected"}, ], ) def test_create_consent(models, input): @@ -60,9 +60,9 @@ def test_create_consent_fail_on_invalid_type(models): "input", [ # invalid reject status - {"type": "gdpr", "category": "newsletter", "status": "rej",}, + {"type": "gdpr", "category": "newsletter", "status": "rej"}, # invalid accept status - {"type": "cookie", "category": "statistics", "status": "akceptted",}, + {"type": "cookie", "category": "statistics", "status": "akceptted"}, ], ) def test_create_consent_fail_on_invalid_status(models, input): From d9741f562b21591d9dd203412a86a15fd3e85a47 Mon Sep 17 00:00:00 2001 From: Midnighter Date: Wed, 6 May 2020 00:05:53 +0200 Subject: [PATCH 06/11] fix: use Unix compatible date format --- .travis.yml | 4 ++-- Makefile | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index aab478a..4f852ee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,8 +18,8 @@ env: - BRANCH=${TRAVIS_BRANCH} - BUILD_COMMIT=${TRAVIS_COMMIT} - SHORT_COMMIT=${TRAVIS_COMMIT:0:7} - - BUILD_TIMESTAMP=$(date --utc --iso-8601=seconds) - - BUILD_DATE=$(date --utc --iso-8601=date) + - BUILD_TIMESTAMP=$(date -u +%Y-%m-%dT%T+00:00) + - BUILD_DATE=$(date -u +%Y-%m-%d) - BUILD_TAG=${BRANCH}_${BUILD_DATE}_${SHORT_COMMIT} before_install: diff --git a/Makefile b/Makefile index acc0288..7df1c64 100644 --- a/Makefile +++ b/Makefile @@ -9,8 +9,9 @@ IMAGE ?= gcr.io/dd-decaf-cfbf6/iam BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) BUILD_COMMIT ?= $(shell git rev-parse HEAD) SHORT_COMMIT ?= $(shell git rev-parse --short HEAD) -BUILD_TIMESTAMP ?= $(shell date --utc --iso-8601=seconds) -BUILD_DATE ?= $(shell date --utc --iso-8601=date) +# Full timestamp in UTC. Format corresponds to ISO-8601 but Unix compatible. +BUILD_TIMESTAMP ?= $(shell date -u +%Y-%m-%dT%T+00:00) +BUILD_DATE ?= $(shell date -u +%Y-%m-%d) BUILD_TAG ?= ${BRANCH}_${BUILD_DATE}_${SHORT_COMMIT} ################################################################################# From 8469a4b40dc953b34b61c13ba396ab04f3858217 Mon Sep 17 00:00:00 2001 From: Midnighter Date: Fri, 8 May 2020 15:08:40 +0200 Subject: [PATCH 07/11] fix: use POSIX compliant date --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7df1c64..1bf4066 100644 --- a/Makefile +++ b/Makefile @@ -53,7 +53,7 @@ build-travis: $(info ************************************************************) $(info * Building the service on the basis of:) $(info * dddecaf/postgres-base:$(LATEST_BASE_TAG)) - $(info * Today is $(shell date --utc --iso-8601=date).) + $(info * Today is $(shell date -u +%Y-%m-%d).) $(info * Please re-run `make lock` if you want to check for and) $(info * depend on a later version.) $(info ************************************************************) From aa597b8c399950ec4ebac60997b7a1ce01a59fb1 Mon Sep 17 00:00:00 2001 From: Midnighter Date: Tue, 26 May 2020 22:19:47 +0200 Subject: [PATCH 08/11] refactor: remove build timestamp --- .travis.yml | 1 - Dockerfile | 10 +- LATEST_BASE_TAG | 2 +- Makefile | 8 +- docker-compose.yml | 5 +- requirements/requirements.txt | 417 ++++++++++++++++++---------------- 6 files changed, 230 insertions(+), 213 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4f852ee..2754871 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,6 @@ env: - BRANCH=${TRAVIS_BRANCH} - BUILD_COMMIT=${TRAVIS_COMMIT} - SHORT_COMMIT=${TRAVIS_COMMIT:0:7} - - BUILD_TIMESTAMP=$(date -u +%Y-%m-%dT%T+00:00) - BUILD_DATE=$(date -u +%Y-%m-%d) - BUILD_TAG=${BRANCH}_${BUILD_DATE}_${SHORT_COMMIT} diff --git a/Dockerfile b/Dockerfile index c1e2404..f753014 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,14 +19,12 @@ FROM dddecaf/postgres-base:${BASE_TAG} ARG BASE_TAG=alpine ARG BUILD_COMMIT -ARG BUILD_TIMESTAMP LABEL dk.dtu.biosustain.iam.alpine.vendor="Novo Nordisk Foundation \ -Center for Biosustainability, Technical University of Denmark" \ - maintainer="niso@biosustain.dtu.dk" \ - dk.dtu.biosustain.iam.alpine.build.base-tag="${BASE_TAG}" \ - dk.dtu.biosustain.iam.alpine.build.commit="${BUILD_COMMIT}" \ - dk.dtu.biosustain.iam.alpine.build.timestamp="${BUILD_TIMESTAMP}" +Center for Biosustainability, Technical University of Denmark" +LABEL maintainer="niso@biosustain.dtu.dk" +LABEL dk.dtu.biosustain.iam.alpine.build.base-tag="${BASE_TAG}" +LABEL dk.dtu.biosustain.iam.alpine.build.commit="${BUILD_COMMIT}" ARG CWD="/app" diff --git a/LATEST_BASE_TAG b/LATEST_BASE_TAG index 6ed1dfc..e30e146 100644 --- a/LATEST_BASE_TAG +++ b/LATEST_BASE_TAG @@ -1 +1 @@ - alpine_2020-04-29_ae14192 + alpine_2020-05-26_97a4608 diff --git a/Makefile b/Makefile index 1bf4066..3d9cd99 100644 --- a/Makefile +++ b/Makefile @@ -9,8 +9,6 @@ IMAGE ?= gcr.io/dd-decaf-cfbf6/iam BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) BUILD_COMMIT ?= $(shell git rev-parse HEAD) SHORT_COMMIT ?= $(shell git rev-parse --short HEAD) -# Full timestamp in UTC. Format corresponds to ISO-8601 but Unix compatible. -BUILD_TIMESTAMP ?= $(shell date -u +%Y-%m-%dT%T+00:00) BUILD_DATE ?= $(shell date -u +%Y-%m-%d) BUILD_TAG ?= ${BRANCH}_${BUILD_DATE}_${SHORT_COMMIT} @@ -29,7 +27,7 @@ setup: network ## Generate the compiled requirements files. lock: docker pull dddecaf/tag-spy:latest - $(eval LATEST_BASE_TAG := $(shell docker run --rm dddecaf/tag-spy:latest tag-spy dddecaf/postgres-base alpine dk.dtu.biosustain.postgres-base.alpine.build.timestamp)) + $(eval LATEST_BASE_TAG := $(shell docker run --rm dddecaf/tag-spy:latest tag-spy dddecaf/postgres-base alpine)) $(file >LATEST_BASE_TAG, $(LATEST_BASE_TAG)) $(eval COMPILER_TAG := $(subst alpine,alpine-compiler,$(LATEST_BASE_TAG))) $(info ************************************************************) @@ -58,9 +56,9 @@ build-travis: $(info * depend on a later version.) $(info ************************************************************) docker pull dddecaf/postgres-base:$(LATEST_BASE_TAG) - docker build --build-arg BASE_TAG=$(LATEST_BASE_TAG) \ + docker build \ + --build-arg BASE_TAG=$(LATEST_BASE_TAG) \ --build-arg BUILD_COMMIT=$(BUILD_COMMIT) \ - --build-arg BUILD_TIMESTAMP=$(BUILD_TIMESTAMP) \ --tag $(IMAGE):$(BRANCH) \ --tag $(IMAGE):$(BUILD_TAG) \ . diff --git a/docker-compose.yml b/docker-compose.yml index 9adfc76..cd25771 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,10 +5,9 @@ services: context: . dockerfile: Dockerfile args: - - BASE_TAG=${BASE_TAG} + - BASE_TAG=${BASE_TAG:-alpine} - BUILD_COMMIT=${BUILD_COMMIT:-unknown} - - BUILD_TIMESTAMP=${BUILD_TIMESTAMP:-unknown} - image: gcr.io/dd-decaf-cfbf6/iam:${IMAGE_TAG:-latest} + image: gcr.io/dd-decaf-cfbf6/iam:${BUILD_TAG:-latest} networks: default: DD-DeCaF: diff --git a/requirements/requirements.txt b/requirements/requirements.txt index e0b6593..8d325ac 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -11,9 +11,9 @@ apispec==3.3.0 \ --hash=sha256:419d0564b899e182c2af50483ea074db8cb05fee60838be58bb4542095d5c08d \ --hash=sha256:9bf4e51d56c9067c60668b78210ae213894f060f85593dc2ad8805eb7d875a2a \ # via -r /opt/sql-requirements.txt, flask-apispec -appdirs==1.4.3 \ - --hash=sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92 \ - --hash=sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e \ +appdirs==1.4.4 \ + --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 \ + --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128 \ # via -r /opt/sql-requirements.txt, black attrs==19.3.0 \ --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \ @@ -95,13 +95,9 @@ ecdsa==0.15 \ --hash=sha256:867ec9cf6df0b03addc8ef66b56359643cb5d0c1dc329df76ba7ecfe256c8061 \ --hash=sha256:8f12ac317f8a1318efa75757ef0a651abe12e51fc1af8838fb91079445227277 \ # via -r /opt/sql-requirements.txt, python-jose -entrypoints==0.3 \ - --hash=sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19 \ - --hash=sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451 \ - # via -r /opt/sql-requirements.txt, flake8 -firebase-admin==4.1.0 \ - --hash=sha256:0ce190793a4fc73a73dc6301cab98c9abdd8c045ada490310beea7eba92a6c53 \ - --hash=sha256:86f753b69e2534bdc925c49e64ba97704a9cdf201374a480c883f4a69c7f46a6 \ +firebase-admin==4.3.0 \ + --hash=sha256:23deaede741f6c4785939c1b7e1385561431887d8b065202f27372d8ea73cf01 \ + --hash=sha256:7cf4ce710fa06e69308b58e7d55d5f2aebff559f07272578186c5ef3c3461730 \ # via -r /opt/requirements/requirements.in flake8-bugbear==20.1.4 \ --hash=sha256:a3ddc03ec28ba2296fc6f89444d1c946a6b76460f859795b35b77d4920a51b63 \ @@ -111,9 +107,9 @@ flake8-docstrings==1.5.0 \ --hash=sha256:3d5a31c7ec6b7367ea6506a87ec293b94a0a46c0bce2bb4975b7f1d09b6f3717 \ --hash=sha256:a256ba91bc52307bef1de59e2a009c3cf61c3d0952dbe035d6ff7208940c2edc \ # via -r /opt/sql-requirements.txt -flake8==3.7.9 \ - --hash=sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb \ - --hash=sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca \ +flake8==3.8.2 \ + --hash=sha256:c69ac1668e434d37a2d2880b3ca9aafd54b3a10a3ac1ab101d22f29e29cf8634 \ + --hash=sha256:ccaa799ef9893cebe69fdfefed76865aeaefbb94cb8545617b2298786a4de9a5 \ # via -r /opt/sql-requirements.txt, flake8-bugbear, flake8-docstrings flask-admin==1.5.6 \ --hash=sha256:68c761d8582d59b1f7702013e944a7ad11d7659a72f3006b89b68b0bd8df61b8 \ @@ -133,67 +129,65 @@ flask-migrate==2.5.3 \ --hash=sha256:4dc4a5cce8cbbb06b8dc963fd86cf8136bd7d875aabe2d840302ea739b243732 \ --hash=sha256:a69d508c2e09d289f6e55a417b3b8c7bfe70e640f53d2d9deb0d056a384f37ee \ # via -r /opt/sql-requirements.txt -flask-sqlalchemy==2.4.1 \ - --hash=sha256:0078d8663330dc05a74bc72b3b6ddc441b9a744e2f56fe60af1a5bfc81334327 \ - --hash=sha256:6974785d913666587949f7c2946f7001e4fa2cb2d19f4e69ead02e4b8f50b33d \ +flask-sqlalchemy==2.4.2 \ + --hash=sha256:2298f6b874c2a2f1f048eaf21ce5d984e36a04ca849b0ac473050a67c8dae76f \ + --hash=sha256:6cd9f71a97ef18ca5ae7d8bd316a32b82814efe7b088096ba68fddfd8a17cbe7 \ # via -r /opt/sql-requirements.txt, flask-migrate flask==1.1.2 \ --hash=sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060 \ --hash=sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557 \ # via -r /opt/sql-requirements.txt, flask-admin, flask-apispec, flask-basicauth, flask-cors, flask-migrate, flask-sqlalchemy, raven -gevent==20.4.0 \ - --hash=sha256:0b84a8d6f088b29a74402728681c9f11864b95e49f5587a666e6fbf5c683e597 \ - --hash=sha256:1ef086264e846371beb5742ebaeb148dc96adf72da2ff350ae5603421cdc2ad9 \ - --hash=sha256:2070c65896f89a85b39f49427d6132f7abd047129fc4da88b3670f0ba13b0cf7 \ - --hash=sha256:2fbe0bc43d8c5540153f06eece6235dda14e5f99bdd9183838396313100815d7 \ - --hash=sha256:32813de352918fb652a3db805fd6e08e0a1666a1a9304eef95938c9c426f9573 \ - --hash=sha256:38c45d8a3b647f56f8a68769a8ac4953be84a84735c7c7a4d7ca62022bd54036 \ - --hash=sha256:3b4c4d99f87c0d04b825879c5a91fbfa2b66da7c25b8689e9bdd9f4741d5f80d \ - --hash=sha256:42cae3be36b7458f411bd589c66aaba27e4e611ec3d3621e37fd732fe383f9b6 \ - --hash=sha256:4572dc7907a0ac3c39b9f0898dbdf390ae3250baaae5f7395661fb844e2e23be \ - --hash=sha256:6088bedd8b6bcdb815be322304a5d1c028ffa837d84e93b349928dadac62f354 \ - --hash=sha256:8a9aba59a3268f20c7b584119215bdc589cb81500d93dad4dab428eb02f72944 \ - --hash=sha256:8cca7ffd58559f8d51e5605ad73afcc6f348f9747d2fa539b336e70851b69b79 \ - --hash=sha256:956e82a5d0e90f8d71efe4cecccde602cfb657cd866c58bb953c9c30ca1b3d77 \ - --hash=sha256:b0aea12de542f8fcd6882087bdd5b4d7dc8bb316d28181f6b012dd0b91583285 \ - --hash=sha256:b46399f6c9eccc2e6de1dc1057d362be840443e5439b06cce8b01d114ba1a7ec \ - --hash=sha256:c0b38a654c8fde5b9d9bd27ea3261aeefe36bc9244b170b6d3b11d72a2163bdb \ - --hash=sha256:c516cc5d70c3faf07f271d50930d144339c69fb80f3cac9b687aa964e518535e \ - --hash=sha256:c7a62d51c6dca84f91a91b940037523c926a516f0568f47dc1386bd1682cf4e9 \ - --hash=sha256:cea28f958bc4206ae092043e0775cd7a2bb2536bcbece292732c6484c1076c01 \ - --hash=sha256:d56f36eb98532d2bccc51cb0964c31e9fbd9b2282074c297dc9b006b047e2966 \ - --hash=sha256:de6c0cbcb890d0a79323961d3b593a0f2f54dcb9fe38ee5167f2d514e69e3c8c \ - --hash=sha256:e0990009e7c1624f9a0f3335df1ab8d45678241c852659ac645b70ed8229097c \ - --hash=sha256:e7d23d5f32c9db6ae49c4b58585618dcafd6ad0babae251c9c8297afebc4744b \ - --hash=sha256:ee39caf14d66e619709cdfe3962bc68a234518e43ea8c811c0d67a864bc7c196 \ +gevent==20.5.1 \ + --hash=sha256:13ed2fa4a074c26fd60744a0757bf65004950554dfd9efd7c9deee1c241279af \ + --hash=sha256:1734f56ea545668780a4a283542a48d11298ab525c780a6001071f9d9d3c6880 \ + --hash=sha256:1c2ad11663597d785e06daa8b65978a1536347a42bc840cf32823b54a0209d15 \ + --hash=sha256:1cf6ed4f66ecc432939e4be9434a20dffcf3207fb0ab6bc0343e7a9ea76d233b \ + --hash=sha256:2504563f44bb188c1e48684e2ac7d2793f9f5b1e1cf119a8fdf8c36d2bf2eaf7 \ + --hash=sha256:28a71ac05cf8a80897a8402f3193dab89bd225a3f0d27042d7352ec37156ba6a \ + --hash=sha256:29eefad2557138fb654ba5cedfb94055f959e6c9705f9983518195cbcf250cc6 \ + --hash=sha256:3a1ec10c73fb70bd474cd778e4ab487c1375b7d93053c24db15acbda367e3734 \ + --hash=sha256:42ff095288b1f335f7ea96a7812f378d843a034f4f0e604edc24a3dddb001106 \ + --hash=sha256:52567bdc3769bc6df4693c1ea5ed1d82f825a6066835b405676ece437caf3fb9 \ + --hash=sha256:5c07973cd9f5a73480a386d1805b6a6b94e69aa906ee42f84a0cba02619a19e3 \ + --hash=sha256:653ad83784b872e78204c7e049b650c41c2e7ccb956142d8edc23a72e57ff80c \ + --hash=sha256:71438390acb6aea432d5f853d5dcb16fa2a6d3c1d2299a0ebe32eed03ac81547 \ + --hash=sha256:765b39e502c76a1d77f743b821b7b1afe2a816848cf73a3606b1d5a91841cb9c \ + --hash=sha256:7cb2fedafb0a692a3f1a14ddb13cbb3283863a1dfc3b536452f5ac6dfb88317a \ + --hash=sha256:7f1e339b6d51c354fa904ec8233b994b53c7c339b81c0743e07f2921b299d787 \ + --hash=sha256:867c77a6da601b2f4600b71b7f8663cadb8f11c31f294b3a49025cdbaf406110 \ + --hash=sha256:8bea8dccb6ea671ecf00e1ba16d5275da8b78464082ac035e7391097513db777 \ + --hash=sha256:ad01ef76f1d71cc7f2ce131cde6575ecc35d0a682a187a3229df3e977847f378 \ + --hash=sha256:b53cf1a495c065df8b4b65d9f73a1cd7c5fa010955c0ed7bc5de196062099e41 \ + --hash=sha256:f4a73e288fab042335b19f4b40407f8b44a40612626429943e37db23b40dd055 \ + --hash=sha256:fe3ede0282c023b6ac1d0441402866488017b8f90f47691794441d0a18342a65 \ # via -r /opt/sql-requirements.txt google-api-core[grpc]==1.17.0 \ --hash=sha256:c0e430658ed6be902d7ba7095fb0a9cac810270d71bf7ac4484e76c300407aae \ --hash=sha256:e4082a0b479dc2dee2f8d7b80ea8b5d0184885b773caab15ab1836277a01d689 \ # via firebase-admin, google-api-python-client, google-cloud-core, google-cloud-firestore -google-api-python-client==1.8.2 \ - --hash=sha256:8dd35a3704650c2db44e6cf52abdaf9de71f409c93c56bbe48a321ab5e14ebad \ - --hash=sha256:bf482c13fb41a6d01770f9d62be6b33fdcd41d68c97f2beb9be02297bdd9e725 \ +google-api-python-client==1.8.3 \ + --hash=sha256:b764be88cf2a1f8b4c4d17c9187a279fea93f4d767e7d7c24f71bf25385a8b10 \ + --hash=sha256:be4e8dcf399d7d1dcaae004b7f18694907f740bf6e6cebda91da8ebd968c5481 \ # via firebase-admin google-auth-httplib2==0.0.3 \ --hash=sha256:098fade613c25b4527b2c08fa42d11f3c2037dda8995d86de0745228e965d445 \ --hash=sha256:f1c437842155680cf9918df9bc51c1182fda41feef88c34004bd1978c8157e08 \ # via google-api-python-client -google-auth==1.14.1 \ - --hash=sha256:0c41a453b9a8e77975bfa436b8daedac00aed1c545d84410daff8272fff40fbb \ - --hash=sha256:e63b2210e03c4ed829063b72c4af0c4b867c2788efb3210b6b9439b488bd3afd \ +google-auth==1.15.0 \ + --hash=sha256:73b141d122942afe12e8bfdcb6900d5df35c27d39700f078363ba0b1298ad33b \ + --hash=sha256:fbf25fee328c0828ef293459d9c649ef84ee44c0b932bb999d19df0ead1b40cf \ # via google-api-core, google-api-python-client, google-auth-httplib2, google-cloud-storage google-cloud-core==1.3.0 \ --hash=sha256:6ae5c62931e8345692241ac1939b85a10d6c38dc9e2854bdbacb7e5ac3033229 \ --hash=sha256:878f9ad080a40cdcec85b92242c4b5819eeb8f120ebc5c9f640935e24fc129d8 \ # via google-cloud-firestore, google-cloud-storage -google-cloud-firestore==1.6.2 \ - --hash=sha256:31bfbc47865ae5933ffd24dad27c672b91ca0fc0ae0f9c4bddf7c2aaf9aa2edb \ - --hash=sha256:5ad4835c3a0f6350bcbbc42fd70e90f7568fca289fdb5e851888df394c4ebf80 \ +google-cloud-firestore==1.7.0 \ + --hash=sha256:afd986bc4bb5a92d6ebe02977cc1d5dc56bf401590d1df43c07609dbec21155d \ + --hash=sha256:f7cd210fdb44d945925e69243fecf79665a3edfb69a9ca780a6bb8e0d1d07bda \ # via firebase-admin -google-cloud-storage==1.28.0 \ - --hash=sha256:07998ac15de406e7b7d72c98713d598e060399858a699eeb2ca45dc7f22a7af9 \ - --hash=sha256:35ecd0b00d4b4147c666d73fa2a5c0c7d9a7fe0fe430a4f544d428f5dc68b544 \ +google-cloud-storage==1.28.1 \ + --hash=sha256:0b28536acab1d7e856a7a89bbfcad41f26f40b46af59786ca874ff0f94bbc0f9 \ + --hash=sha256:a7b5c326e7307a83fa1f1f0ef71aba9ad1f3a2bc6a768401e13fc02369fd8612 \ # via firebase-admin google-resumable-media==0.5.0 \ --hash=sha256:2a8fd188afe1cbfd5998bf20602f76b0336aa892de88fe842a806b9a3ed78d2a \ @@ -226,46 +220,46 @@ greenlet==0.4.15 \ --hash=sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656 \ --hash=sha256:e538b8dae561080b542b0f5af64d47ef859f22517f7eca617bb314e0e03fd7ef \ # via -r /opt/sql-requirements.txt, gevent -grpcio==1.28.1 \ - --hash=sha256:085bbf7fd0070b8d65e84aa32979f17cfe624d27b5ce23955ef770c19d2d9623 \ - --hash=sha256:0ae207a47ec0ad66eb1f53a27d566674d13a236c62ced409891335318ea9b8c5 \ - --hash=sha256:0c130204ff5de0b9f041bf3126db0d29369d69883592e4b0d3c19868ba0ced7e \ - --hash=sha256:0ef6b380a588c2c6b29c6cfa0ba7f5d367beb33d5504bcc68658fa241ad498d2 \ - --hash=sha256:16e1edb367763ea08d0994d4635ec05f4f8db9db59c39304b061097e3b93df43 \ - --hash=sha256:16f5523dacae5aaeda4cf900da7e980747f663298c38c18eb4e5317704aa007a \ - --hash=sha256:181b5078cf568f37915b8a118afcef5fc9f3128c59c38998ed93e7dd793e3928 \ - --hash=sha256:245564713cb4ac7bccb0f11be63781beb62299a44d8ab69031c859dbd9461728 \ - --hash=sha256:271abbe28eb99fa5c70b3f272c0c66b67dab7bb11e1d29d8e616b4e0e099d29a \ - --hash=sha256:2e1b01cba26988c811c7fb91a0bca19c9afb776cc3d228993f08d324bdd0510a \ - --hash=sha256:3366bd6412c1e73acb1ee27d7f0c7d7dbee118ad8d98c957c8173691b2effeec \ - --hash=sha256:3893b39a0a17d857dc3a42fdb02a26aa53a59bfce49987187bcc0261647f1f55 \ - --hash=sha256:3c7864d5ae63b787001b01b376f6315aef1a015aa9c809535235ed0ead907919 \ - --hash=sha256:42c6716adf3ec1f608b2b56e885f26dd86e80d2fc1617f51fc92d1b0b649e28e \ - --hash=sha256:4bef0756b9e0df78e8d67a5b1e0e89b7daf41525d575f74e1f14a993c55b680d \ - --hash=sha256:4fe081862e58b8fbef0e479aefc9a64f8f17f53074df1085d8c1fe825a6e5df4 \ - --hash=sha256:505a8d1b4ac571a51f10c4c995d5d4714f03c886604dc3c097ef5fd57bcfcf0b \ - --hash=sha256:5c2e81b6ab9768c43f2ca1c9a4c925823aad79ae95efb351007df4b92ebce592 \ - --hash=sha256:70ff2df0c1795c5cf585a72d95bb458838b40bad5653c314b9067ba819e918f9 \ - --hash=sha256:97b5612fc5d4bbf0490a2d80bed5eab5b59112ef1640440c1a9ac824bafa6968 \ - --hash=sha256:a35f8f4a0334ed8b05db90383aecef8e49923ab430689a4360a74052f3a89cf4 \ - --hash=sha256:aafe85a8210dfa1da3c46831b7f00c3735240b7b028eeba339eaea6ffdb593fb \ - --hash=sha256:c2e53eb253840f05278a8410628419ba7060815f86d48c9d83b6047de21c9956 \ - --hash=sha256:c3645887db3309fc87c3db740b977d403fb265ebab292f1f6a926c4661231fd5 \ - --hash=sha256:c6565cc92853af13237b2233f331efdad07339d27fe1f5f74256bfde7dc2f587 \ - --hash=sha256:cbc322c5d5615e67c2a15be631f64e6c2bab8c12505bc7c150948abdaa0bdbac \ - --hash=sha256:df749ee982ec35ab76d37a1e637b10a92b4573e2b4e1f86a5fa8a1273c40a850 \ - --hash=sha256:e9439d7b801c86df13c6cbb4c5a7e181c058f3c119d5e119a94a5f3090a8f060 \ - --hash=sha256:f493ac4754717f25ace3614a51dd408a32b8bff3c9c0c85e9356e7e0a120a8c8 \ - --hash=sha256:f80d10bdf1a306f7063046321fd4efc7732a606acdd4e6259b8a37349079b704 \ - --hash=sha256:f83b0c91796eb42865451a20e82246011078ba067ea0744f7301e12a94ae2e1b \ +grpcio==1.29.0 \ + --hash=sha256:10cdc8946a7c2284bbc8e16d346eaa2beeaae86ea598f345df86d4ef7dfedb84 \ + --hash=sha256:23bc395a32c2465564cb242e48bdd2fdbe5a4aebf307649a800da1b971ee7f29 \ + --hash=sha256:2637ce96b7c954d2b71060f50eb4c72f81668f1b2faa6cbdc74677e405978901 \ + --hash=sha256:3d8c510b6eabce5192ce126003d74d7751c7218d3e2ad39fcf02400d7ec43abe \ + --hash=sha256:5024b26e17a1bfc9390fb3b8077bf886eee02970af780fd23072970ef08cefe8 \ + --hash=sha256:517538a54afdd67162ea2af1ac3326c0752c5d13e6ddadbc4885f6a28e91ab28 \ + --hash=sha256:524ae8d3da61b856cf08abb3d0947df05402919e4be1f88328e0c1004031f72e \ + --hash=sha256:54e4658c09084b09cd83a5ea3a8bce78e4031ff1010bb8908c399a22a76a6f08 \ + --hash=sha256:57c8cc2ae8cb94c3a89671af7e1380a4cdfcd6bab7ba303f4461ec32ded250ae \ + --hash=sha256:5fd9ffe938e9225c654c60eb21ff011108cc27302db85200413807e0eda99a4a \ + --hash=sha256:75b2247307a7ecaf6abc9eb2bd04af8f88816c111b87bf0044d7924396e9549c \ + --hash=sha256:7bf3cb1e0f4a9c89f7b748583b994bdce183103d89d5ff486da48a7668a052c7 \ + --hash=sha256:7e02a7c40304eecee203f809a982732bd37fad4e798acad98fe73c66e44ff2db \ + --hash=sha256:806c9759f5589b3761561187408e0313a35c5c53f075c7590effab8d27d67dfe \ + --hash=sha256:80e9f9f6265149ca7c84e1c8c31c2cf3e2869c45776fbe8880a3133a11d6d290 \ + --hash=sha256:81bbf78a399e0ee516c81ddad8601f12af3fc9b30f2e4b2fbd64efd327304a4d \ + --hash=sha256:886d48c32960b39e059494637eb0157a694956248d03b0de814447c188b74799 \ + --hash=sha256:97b72bf2242a351a89184134adbb0ae3b422e6893c6c712bc7669e2eab21501b \ + --hash=sha256:97fcbdf1f12e0079d26db73da11ee35a09adc870b1e72fbff0211f6a8003a4e8 \ + --hash=sha256:9cfb4b71cc3c8757f137d47000f9d90d4bd818733f9ab4f78bd447e052a4cb9a \ + --hash=sha256:9ef0370bcf629ece4e7e37796e4604e2514b920669be2911fc3f9c163a73a57b \ + --hash=sha256:a6dddb177b3cfa0cfe299fb9e07d6a3382cc79466bef48fe9c4326d5c5b1dcb8 \ + --hash=sha256:a97ea91e31863c9a3879684b5fb3c6ab4b17c5431787548fc9f52b9483ea9c25 \ + --hash=sha256:b49f243936b0f6ae8eb6adf88a1e54e736f1c6724a1bff6b591d105d708263ad \ + --hash=sha256:b85f355fc24b68a6c52f2750e7141110d1fcd07dfdc9b282de0000550fe0511b \ + --hash=sha256:c3a0ef12ee86f6e72db50e01c3dba7735a76d8c30104b9b0f7fd9d65ceb9d93f \ + --hash=sha256:da0ca9b1089d00e39a8b83deec799a4e5c37ec1b44d804495424acde50531868 \ + --hash=sha256:e90f3d11185c36593186e5ff1f581acc6ddfa4190f145b0366e579de1f52803b \ + --hash=sha256:ebf0ccb782027ef9e213e03b6d00bbd8dabd80959db7d468c0738e6d94b5204c \ + --hash=sha256:eede3039c3998e2cc0f6713f4ac70f235bd32967c9b958a17bf937aceebc12c3 \ + --hash=sha256:ff7931241351521b8df01d7448800ce0d59364321d8d82c49b826d455678ff08 \ # via google-api-core gunicorn==20.0.4 \ --hash=sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626 \ --hash=sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c \ # via -r /opt/sql-requirements.txt -httplib2==0.17.3 \ - --hash=sha256:39dd15a333f67bfb70798faa9de8a6e99c819da6ad82b77f9a259a5c7b1225a2 \ - --hash=sha256:6d9722decd2deacd486ef10c5dd5e2f120ca3ba8736842b90509afcdc16488b1 \ +httplib2==0.18.1 \ + --hash=sha256:8af66c1c52c7ffe1aa5dc4bcd7c769885254b0756e6e69f953c7f0ab49a70ba3 \ + --hash=sha256:ca2914b015b6247791c4866782fa6042f495b94401a0f0bd3e1d6e0ba2236782 \ # via google-api-python-client, google-auth-httplib2 idna==2.9 \ --hash=sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb \ @@ -274,14 +268,14 @@ idna==2.9 \ importlib-metadata==1.6.0 \ --hash=sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f \ --hash=sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e \ - # via -r /opt/sql-requirements.txt, pluggy, pytest + # via -r /opt/sql-requirements.txt, flake8, pluggy, pytest ipython-genutils==0.2.0 \ --hash=sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8 \ --hash=sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8 \ # via -r /opt/sql-requirements.txt, traitlets -ipython==7.13.0 \ - --hash=sha256:ca478e52ae1f88da0102360e57e528b92f3ae4316aabac80a2cd7f7ab2efb48a \ - --hash=sha256:eb8d075de37f678424527b5ef6ea23f7b80240ca031c2dd6de5879d687a65333 \ +ipython==7.14.0 \ + --hash=sha256:5b241b84bbf0eb085d43ae9d46adf38a13b45929ca7774a740990c2c242534bb \ + --hash=sha256:f0126781d0f959da852fb3089e170ed807388e986a8dd4e6ac44855845b0fb1c \ # via -r /opt/sql-requirements.txt isort==4.3.21 \ --hash=sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1 \ @@ -338,17 +332,17 @@ markupsafe==1.1.1 \ --hash=sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7 \ --hash=sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be \ # via -r /opt/sql-requirements.txt, jinja2, mako, wtforms -marshmallow==3.5.1 \ - --hash=sha256:90854221bbb1498d003a0c3cc9d8390259137551917961c8b5258c64026b2f85 \ - --hash=sha256:ac2e13b30165501b7d41fc0371b8df35944f5849769d136f20e2c5f6cdc6e665 \ +marshmallow==3.6.0 \ + --hash=sha256:c2673233aa21dde264b84349dc2fd1dce5f30ed724a0a00e75426734de5b84ab \ + --hash=sha256:f88fe96434b1f0f476d54224d59333eba8ca1a203a2695683c1855675c4049a7 \ # via -r /opt/sql-requirements.txt, flask-apispec, webargs mccabe==0.6.1 \ --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \ --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f \ # via -r /opt/sql-requirements.txt, flake8 -more-itertools==8.2.0 \ - --hash=sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c \ - --hash=sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507 \ +more-itertools==8.3.0 \ + --hash=sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be \ + --hash=sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982 \ # via -r /opt/sql-requirements.txt, pytest msgpack==1.0.0 \ --hash=sha256:002a0d813e1f7b60da599bdf969e632074f9eec1b96cbed8fb0973a63160a408 \ @@ -370,9 +364,9 @@ msgpack==1.0.0 \ --hash=sha256:e7bbdd8e2b277b77782f3ce34734b0dfde6cbe94ddb74de8d733d603c7f9e2b1 \ --hash=sha256:ea41c9219c597f1d2bf6b374d951d310d58684b5de9dc4bd2976db9e1e22c140 \ # via cachecontrol -packaging==20.3 \ - --hash=sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3 \ - --hash=sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752 \ +packaging==20.4 \ + --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \ + --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 \ # via -r /opt/sql-requirements.txt, dparse, pytest, safety parso==0.7.0 \ --hash=sha256:158c140fc04112dc45bca311633ae5033c2c2a7b732fa33d0955bad8152a8dd0 \ @@ -394,8 +388,9 @@ pluggy==0.13.1 \ --hash=sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0 \ --hash=sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d \ # via -r /opt/sql-requirements.txt, pytest -prometheus-client==0.7.1 \ - --hash=sha256:71cd24a2b3eb335cb800c7159f423df1bd4dcd5171b234be15e3f31ec9f622da \ +prometheus-client==0.8.0 \ + --hash=sha256:983c7ac4b47478720db338f1491ef67a100b474e3bc7dafcbaefb7d0b8f9b01c \ + --hash=sha256:c6e6b706833a6bd1fd51711299edee907857be10ece535126a158f911ee80915 \ # via -r /opt/requirements/requirements.in prompt-toolkit==3.0.5 \ --hash=sha256:563d1a4140b63ff9dd587bda9557cffb2fe73650205ab6f4383092fb882e7dc8 \ @@ -404,7 +399,6 @@ prompt-toolkit==3.0.5 \ protobuf==3.11.3 \ --hash=sha256:0bae429443cc4748be2aadfdaf9633297cfaeb24a9a02d0ab15849175ce90fab \ --hash=sha256:24e3b6ad259544d717902777b33966a1a069208c885576254c112663e6a5bb0f \ - --hash=sha256:2affcaba328c4662f3bc3c0e9576ea107906b2c2b6422344cdad961734ff6b93 \ --hash=sha256:310a7aca6e7f257510d0c750364774034272538d51796ca31d42c3925d12a52a \ --hash=sha256:52e586072612c1eec18e1174f8e3bb19d08f075fc2e3f91d3b16c919078469d0 \ --hash=sha256:73152776dc75f335c476d11d52ec6f0f6925774802cd48d6189f4d5d7fe753f4 \ @@ -446,46 +440,24 @@ py==1.8.1 \ --hash=sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0 \ # via -r /opt/sql-requirements.txt, pytest pyasn1-modules==0.2.8 \ - --hash=sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8 \ - --hash=sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199 \ - --hash=sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811 \ - --hash=sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed \ - --hash=sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4 \ --hash=sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e \ --hash=sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74 \ - --hash=sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb \ - --hash=sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45 \ - --hash=sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd \ - --hash=sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0 \ - --hash=sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d \ - --hash=sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405 \ # via google-auth pyasn1==0.4.8 \ - --hash=sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359 \ - --hash=sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576 \ - --hash=sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf \ - --hash=sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7 \ --hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \ - --hash=sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00 \ - --hash=sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8 \ - --hash=sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86 \ - --hash=sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12 \ - --hash=sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776 \ --hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba \ - --hash=sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2 \ - --hash=sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3 \ # via -r /opt/sql-requirements.txt, pyasn1-modules, python-jose, rsa -pycodestyle==2.5.0 \ - --hash=sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56 \ - --hash=sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c \ +pycodestyle==2.6.0 \ + --hash=sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367 \ + --hash=sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e \ # via -r /opt/sql-requirements.txt, flake8 pydocstyle==5.0.2 \ --hash=sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586 \ --hash=sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5 \ # via -r /opt/sql-requirements.txt, flake8-docstrings -pyflakes==2.1.1 \ - --hash=sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0 \ - --hash=sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2 \ +pyflakes==2.2.0 \ + --hash=sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92 \ + --hash=sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8 \ # via -r /opt/sql-requirements.txt, flake8 pygments==2.6.1 \ --hash=sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44 \ @@ -495,13 +467,13 @@ pyparsing==2.4.7 \ --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \ --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b \ # via -r /opt/sql-requirements.txt, packaging -pytest-cov==2.8.1 \ - --hash=sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b \ - --hash=sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626 \ +pytest-cov==2.9.0 \ + --hash=sha256:b6a814b8ed6247bd81ff47f038511b57fe1ce7f4cc25b9106f1a4b106f1d9322 \ + --hash=sha256:c87dfd8465d865655a8213859f1b4749b43448b5fae465cb981e16d52a811424 \ # via -r /opt/sql-requirements.txt -pytest==5.4.1 \ - --hash=sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172 \ - --hash=sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970 \ +pytest==5.4.2 \ + --hash=sha256:95c710d0a72d91c13fae35dce195633c929c3792f54125919847fdcdf7caa0d3 \ + --hash=sha256:eb2b5e935f6a019317e455b6da83dd8650ac9ffd2ee73a7b657a30873d67a698 \ # via -r /opt/sql-requirements.txt, pytest-cov python-dateutil==2.8.1 \ --hash=sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c \ @@ -511,8 +483,6 @@ python-editor==1.0.4 \ --hash=sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d \ --hash=sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b \ --hash=sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8 \ - --hash=sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77 \ - --hash=sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522 \ # via -r /opt/sql-requirements.txt, alembic python-http-client==3.2.7 \ --hash=sha256:93d6a26b426e48b04e589c1f103e7c040193e4ccc379ea50cd6e12f94cca7c69 \ @@ -542,28 +512,28 @@ raven[flask]==6.10.0 \ --hash=sha256:3fa6de6efa2493a7c827472e984ce9b020797d0da16f1db67197bcc23c8fae54 \ --hash=sha256:44a13f87670836e153951af9a3c80405d36b43097db869a36e92809673692ce4 \ # via -r /opt/sql-requirements.txt -regex==2020.4.4 \ - --hash=sha256:08119f707f0ebf2da60d2f24c2f39ca616277bb67ef6c92b72cbf90cbe3a556b \ - --hash=sha256:0ce9537396d8f556bcfc317c65b6a0705320701e5ce511f05fc04421ba05b8a8 \ - --hash=sha256:1cbe0fa0b7f673400eb29e9ef41d4f53638f65f9a2143854de6b1ce2899185c3 \ - --hash=sha256:2294f8b70e058a2553cd009df003a20802ef75b3c629506be20687df0908177e \ - --hash=sha256:23069d9c07e115537f37270d1d5faea3e0bdded8279081c4d4d607a2ad393683 \ - --hash=sha256:24f4f4062eb16c5bbfff6a22312e8eab92c2c99c51a02e39b4eae54ce8255cd1 \ - --hash=sha256:295badf61a51add2d428a46b8580309c520d8b26e769868b922750cf3ce67142 \ - --hash=sha256:2a3bf8b48f8e37c3a40bb3f854bf0121c194e69a650b209628d951190b862de3 \ - --hash=sha256:4385f12aa289d79419fede43f979e372f527892ac44a541b5446617e4406c468 \ - --hash=sha256:5635cd1ed0a12b4c42cce18a8d2fb53ff13ff537f09de5fd791e97de27b6400e \ - --hash=sha256:5bfed051dbff32fd8945eccca70f5e22b55e4148d2a8a45141a3b053d6455ae3 \ - --hash=sha256:7e1037073b1b7053ee74c3c6c0ada80f3501ec29d5f46e42669378eae6d4405a \ - --hash=sha256:90742c6ff121a9c5b261b9b215cb476eea97df98ea82037ec8ac95d1be7a034f \ - --hash=sha256:a58dd45cb865be0ce1d5ecc4cfc85cd8c6867bea66733623e54bd95131f473b6 \ - --hash=sha256:c087bff162158536387c53647411db09b6ee3f9603c334c90943e97b1052a156 \ - --hash=sha256:c162a21e0da33eb3d31a3ac17a51db5e634fc347f650d271f0305d96601dc15b \ - --hash=sha256:c9423a150d3a4fc0f3f2aae897a59919acd293f4cb397429b120a5fcd96ea3db \ - --hash=sha256:ccccdd84912875e34c5ad2d06e1989d890d43af6c2242c6fcfa51556997af6cd \ - --hash=sha256:e91ba11da11cf770f389e47c3f5c30473e6d85e06d7fd9dcba0017d2867aab4a \ - --hash=sha256:ea4adf02d23b437684cd388d557bf76e3afa72f7fed5bbc013482cc00c816948 \ - --hash=sha256:fb95debbd1a824b2c4376932f2216cc186912e389bdb0e27147778cf6acb3f89 \ +regex==2020.5.14 \ + --hash=sha256:1386e75c9d1574f6aa2e4eb5355374c8e55f9aac97e224a8a5a6abded0f9c927 \ + --hash=sha256:27ff7325b297fb6e5ebb70d10437592433601c423f5acf86e5bc1ee2919b9561 \ + --hash=sha256:329ba35d711e3428db6b45a53b1b13a0a8ba07cbbcf10bbed291a7da45f106c3 \ + --hash=sha256:3a9394197664e35566242686d84dfd264c07b20f93514e2e09d3c2b3ffdf78fe \ + --hash=sha256:51f17abbe973c7673a61863516bdc9c0ef467407a940f39501e786a07406699c \ + --hash=sha256:579ea215c81d18da550b62ff97ee187b99f1b135fd894a13451e00986a080cad \ + --hash=sha256:70c14743320a68c5dac7fc5a0f685be63bc2024b062fe2aaccc4acc3d01b14a1 \ + --hash=sha256:7e61be8a2900897803c293247ef87366d5df86bf701083b6c43119c7c6c99108 \ + --hash=sha256:8044d1c085d49673aadb3d7dc20ef5cb5b030c7a4fa253a593dda2eab3059929 \ + --hash=sha256:89d76ce33d3266173f5be80bd4efcbd5196cafc34100fdab814f9b228dee0fa4 \ + --hash=sha256:99568f00f7bf820c620f01721485cad230f3fb28f57d8fbf4a7967ec2e446994 \ + --hash=sha256:a7c37f048ec3920783abab99f8f4036561a174f1314302ccfa4e9ad31cb00eb4 \ + --hash=sha256:c2062c7d470751b648f1cacc3f54460aebfc261285f14bc6da49c6943bd48bdd \ + --hash=sha256:c9bce6e006fbe771a02bda468ec40ffccbf954803b470a0345ad39c603402577 \ + --hash=sha256:ce367d21f33e23a84fb83a641b3834dd7dd8e9318ad8ff677fbfae5915a239f7 \ + --hash=sha256:ce450ffbfec93821ab1fea94779a8440e10cf63819be6e176eb1973a6017aff5 \ + --hash=sha256:ce5cc53aa9fbbf6712e92c7cf268274eaff30f6bd12a0754e8133d85a8fb0f5f \ + --hash=sha256:d466967ac8e45244b9dfe302bbe5e3337f8dc4dec8d7d10f5e950d83b140d33a \ + --hash=sha256:d881c2e657c51d89f02ae4c21d9adbef76b8325fe4d5cf0e9ad62f850f3a98fd \ + --hash=sha256:e565569fc28e3ba3e475ec344d87ed3cd8ba2d575335359749298a0899fe122e \ + --hash=sha256:ea55b80eb0d1c3f1d8d784264a6764f931e172480a2f1868f2536444c5f01e01 \ # via -r /opt/sql-requirements.txt, black requests==2.23.0 \ --hash=sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee \ @@ -577,44 +547,51 @@ safety==1.9.0 \ --hash=sha256:23bf20690d4400edc795836b0c983c2b4cbbb922233108ff925b7dd7750f00c9 \ --hash=sha256:86c1c4a031fe35bd624fce143fbe642a0234d29f7cbf7a9aa269f244a955b087 \ # via -r /opt/sql-requirements.txt -sendgrid==6.3.0 \ - --hash=sha256:37d8215974ec3c79085fc455332961bb788eb5d899adde1469640b605a940ec4 \ - --hash=sha256:692c0ac35a6afed093e66e883b86dc7ec60b6b774bf5ef931d42efd19443a1fc \ - --hash=sha256:a74a70faf8d84be4ca07924bf229ca0e28f5607e0b10cb0adffce23e03bb9ccb \ +sendgrid==6.3.1 \ + --hash=sha256:38c0853494c7bfbef64f33934c25bf98bb7648cf0e66a0cfb22410927e4ef4c7 \ + --hash=sha256:838e1f7b0f84d56714be6a18ef66fbcf8fba0bda782eee35c47c04d7a16efde8 \ # via -r /opt/requirements/requirements.in -six==1.14.0 \ - --hash=sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a \ - --hash=sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c \ +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced \ # via -r /opt/sql-requirements.txt, ecdsa, flask-apispec, flask-cors, google-api-core, google-api-python-client, google-auth, google-resumable-media, grpcio, packaging, protobuf, python-dateutil, python-jose, traitlets snowballstemmer==2.0.0 \ --hash=sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0 \ --hash=sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52 \ # via -r /opt/sql-requirements.txt, pydocstyle -sqlalchemy==1.3.16 \ - --hash=sha256:083e383a1dca8384d0ea6378bd182d83c600ed4ff4ec8247d3b2442cf70db1ad \ - --hash=sha256:0a690a6486658d03cc6a73536d46e796b6570ac1f8a7ec133f9e28c448b69828 \ - --hash=sha256:114b6ace30001f056e944cebd46daef38fdb41ebb98f5e5940241a03ed6cad43 \ - --hash=sha256:128f6179325f7597a46403dde0bf148478f868df44841348dfc8d158e00db1f9 \ - --hash=sha256:13d48cd8b925b6893a4e59b2dfb3e59a5204fd8c98289aad353af78bd214db49 \ - --hash=sha256:211a1ce7e825f7142121144bac76f53ac28b12172716a710f4bf3eab477e730b \ - --hash=sha256:2dc57ee80b76813759cccd1a7affedf9c4dbe5b065a91fb6092c9d8151d66078 \ - --hash=sha256:3e625e283eecc15aee5b1ef77203bfb542563fa4a9aa622c7643c7b55438ff49 \ - --hash=sha256:43078c7ec0457387c79b8d52fff90a7ad352ca4c7aa841c366238c3e2cf52fdf \ - --hash=sha256:5b1bf3c2c2dca738235ce08079783ef04f1a7fc5b21cf24adaae77f2da4e73c3 \ - --hash=sha256:6056b671aeda3fc451382e52ab8a753c0d5f66ef2a5ccc8fa5ba7abd20988b4d \ - --hash=sha256:68d78cf4a9dfade2e6cf57c4be19f7b82ed66e67dacf93b32bb390c9bed12749 \ - --hash=sha256:7025c639ce7e170db845e94006cf5f404e243e6fc00d6c86fa19e8ad8d411880 \ - --hash=sha256:7224e126c00b8178dfd227bc337ba5e754b197a3867d33b9f30dc0208f773d70 \ - --hash=sha256:7d98e0785c4cd7ae30b4a451416db71f5724a1839025544b4edbd92e00b91f0f \ - --hash=sha256:8d8c21e9d4efef01351bf28513648ceb988031be4159745a7ad1b3e28c8ff68a \ - --hash=sha256:bbb545da054e6297242a1bb1ba88e7a8ffb679f518258d66798ec712b82e4e07 \ - --hash=sha256:d00b393f05dbd4ecd65c989b7f5a81110eae4baea7a6a4cdd94c20a908d1456e \ - --hash=sha256:e18752cecaef61031252ca72031d4d6247b3212ebb84748fc5d1a0d2029c23ea \ +sqlalchemy==1.3.17 \ + --hash=sha256:128bc917ed20d78143a45024455ff0aed7d3b96772eba13d5dbaf9cc57e5c41b \ + --hash=sha256:156a27548ba4e1fed944ff9fcdc150633e61d350d673ae7baaf6c25c04ac1f71 \ + --hash=sha256:27e2efc8f77661c9af2681755974205e7462f1ae126f498f4fe12a8b24761d15 \ + --hash=sha256:2a12f8be25b9ea3d1d5b165202181f2b7da4b3395289000284e5bb86154ce87c \ + --hash=sha256:31c043d5211aa0e0773821fcc318eb5cbe2ec916dfbc4c6eea0c5188971988eb \ + --hash=sha256:65eb3b03229f684af0cf0ad3bcc771970c1260a82a791a8d07bffb63d8c95bcc \ + --hash=sha256:6cd157ce74a911325e164441ff2d9b4e244659a25b3146310518d83202f15f7a \ + --hash=sha256:703c002277f0fbc3c04d0ae4989a174753a7554b2963c584ce2ec0cddcf2bc53 \ + --hash=sha256:869bbb637de58ab0a912b7f20e9192132f9fbc47fc6b5111cd1e0f6cdf5cf9b0 \ + --hash=sha256:8a0e0cd21da047ea10267c37caf12add400a92f0620c8bc09e4a6531a765d6d7 \ + --hash=sha256:8d01e949a5d22e5c4800d59b50617c56125fc187fbeb8fa423e99858546de616 \ + --hash=sha256:925b4fe5e7c03ed76912b75a9a41dfd682d59c0be43bce88d3b27f7f5ba028fb \ + --hash=sha256:9cb1819008f0225a7c066cac8bb0cf90847b2c4a6eb9ebb7431dbd00c56c06c5 \ + --hash=sha256:a87d496884f40c94c85a647c385f4fd5887941d2609f71043e2b73f2436d9c65 \ + --hash=sha256:a9030cd30caf848a13a192c5e45367e3c6f363726569a56e75dc1151ee26d859 \ + --hash=sha256:a9e75e49a0f1583eee0ce93270232b8e7bb4b1edc89cc70b07600d525aef4f43 \ + --hash=sha256:b50f45d0e82b4562f59f0e0ca511f65e412f2a97d790eea5f60e34e5f1aabc9a \ + --hash=sha256:b7878e59ec31f12d54b3797689402ee3b5cfcb5598f2ebf26491732758751908 \ + --hash=sha256:ce1ddaadee913543ff0154021d31b134551f63428065168e756d90bdc4c686f5 \ + --hash=sha256:ce2646e4c0807f3461be0653502bb48c6e91a5171d6e450367082c79e12868bf \ + --hash=sha256:ce6c3d18b2a8ce364013d47b9cad71db815df31d55918403f8db7d890c9d07ae \ + --hash=sha256:e4e2664232005bd306f878b0f167a31f944a07c4de0152c444f8c61bbe3cfb38 \ + --hash=sha256:e8aa395482728de8bdcca9cc0faf3765ab483e81e01923aaa736b42f0294f570 \ + --hash=sha256:eb4fcf7105bf071c71068c6eee47499ab8d4b8f5a11fc35147c934f0faa60f23 \ + --hash=sha256:ed375a79f06cad285166e5be74745df1ed6845c5624aafadec4b7a29c25866ef \ + --hash=sha256:f35248f7e0d63b234a109dd72fbfb4b5cb6cb6840b221d0df0ecbf54ab087654 \ + --hash=sha256:f502ef245c492b391e0e23e94cba030ab91722dcc56963c85bfd7f3441ea2bbe \ + --hash=sha256:fe01bac7226499aedf472c62fa3b85b2c619365f3f14dd222ffe4f3aa91e5f98 \ # via -r /opt/sql-requirements.txt, alembic, flask-sqlalchemy -toml==0.10.0 \ - --hash=sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c \ - --hash=sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e \ - --hash=sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3 \ +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 \ # via -r /opt/sql-requirements.txt, black, dparse traitlets==4.3.3 \ --hash=sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44 \ @@ -672,9 +649,55 @@ zipp==3.1.0 \ --hash=sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b \ --hash=sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96 \ # via -r /opt/sql-requirements.txt, importlib-metadata +zope.event==4.4 \ + --hash=sha256:69c27debad9bdacd9ce9b735dad382142281ac770c4a432b533d6d65c4614bcf \ + --hash=sha256:d8e97d165fd5a0997b45f5303ae11ea3338becfe68c401dd88ffd2113fe5cae7 \ + # via -r /opt/sql-requirements.txt, gevent +zope.interface==5.1.0 \ + --hash=sha256:0103cba5ed09f27d2e3de7e48bb320338592e2fabc5ce1432cf33808eb2dfd8b \ + --hash=sha256:14415d6979356629f1c386c8c4249b4d0082f2ea7f75871ebad2e29584bd16c5 \ + --hash=sha256:1ae4693ccee94c6e0c88a4568fb3b34af8871c60f5ba30cf9f94977ed0e53ddd \ + --hash=sha256:1b87ed2dc05cb835138f6a6e3595593fea3564d712cb2eb2de963a41fd35758c \ + --hash=sha256:269b27f60bcf45438e8683269f8ecd1235fa13e5411de93dae3b9ee4fe7f7bc7 \ + --hash=sha256:27d287e61639d692563d9dab76bafe071fbeb26818dd6a32a0022f3f7ca884b5 \ + --hash=sha256:39106649c3082972106f930766ae23d1464a73b7d30b3698c986f74bf1256a34 \ + --hash=sha256:40e4c42bd27ed3c11b2c983fecfb03356fae1209de10686d03c02c8696a1d90e \ + --hash=sha256:461d4339b3b8f3335d7e2c90ce335eb275488c587b61aca4b305196dde2ff086 \ + --hash=sha256:4f98f70328bc788c86a6a1a8a14b0ea979f81ae6015dd6c72978f1feff70ecda \ + --hash=sha256:558a20a0845d1a5dc6ff87cd0f63d7dac982d7c3be05d2ffb6322a87c17fa286 \ + --hash=sha256:562dccd37acec149458c1791da459f130c6cf8902c94c93b8d47c6337b9fb826 \ + --hash=sha256:5e86c66a6dea8ab6152e83b0facc856dc4d435fe0f872f01d66ce0a2131b7f1d \ + --hash=sha256:60a207efcd8c11d6bbeb7862e33418fba4e4ad79846d88d160d7231fcb42a5ee \ + --hash=sha256:645a7092b77fdbc3f68d3cc98f9d3e71510e419f54019d6e282328c0dd140dcd \ + --hash=sha256:6874367586c020705a44eecdad5d6b587c64b892e34305bb6ed87c9bbe22a5e9 \ + --hash=sha256:74bf0a4f9091131de09286f9a605db449840e313753949fe07c8d0fe7659ad1e \ + --hash=sha256:7b726194f938791a6691c7592c8b9e805fc6d1b9632a833b9c0640828cd49cbc \ + --hash=sha256:8149ded7f90154fdc1a40e0c8975df58041a6f693b8f7edcd9348484e9dc17fe \ + --hash=sha256:8cccf7057c7d19064a9e27660f5aec4e5c4001ffcf653a47531bde19b5aa2a8a \ + --hash=sha256:911714b08b63d155f9c948da2b5534b223a1a4fc50bb67139ab68b277c938578 \ + --hash=sha256:a5f8f85986197d1dd6444763c4a15c991bfed86d835a1f6f7d476f7198d5f56a \ + --hash=sha256:a744132d0abaa854d1aad50ba9bc64e79c6f835b3e92521db4235a1991176813 \ + --hash=sha256:af2c14efc0bb0e91af63d00080ccc067866fb8cbbaca2b0438ab4105f5e0f08d \ + --hash=sha256:b054eb0a8aa712c8e9030065a59b5e6a5cf0746ecdb5f087cca5ec7685690c19 \ + --hash=sha256:b0becb75418f8a130e9d465e718316cd17c7a8acce6fe8fe07adc72762bee425 \ + --hash=sha256:b1d2ed1cbda2ae107283befd9284e650d840f8f7568cb9060b5466d25dc48975 \ + --hash=sha256:ba4261c8ad00b49d48bbb3b5af388bb7576edfc0ca50a49c11dcb77caa1d897e \ + --hash=sha256:d1fe9d7d09bb07228650903d6a9dc48ea649e3b8c69b1d263419cc722b3938e8 \ + --hash=sha256:d7804f6a71fc2dda888ef2de266727ec2f3915373d5a785ed4ddc603bbc91e08 \ + --hash=sha256:da2844fba024dd58eaa712561da47dcd1e7ad544a257482392472eae1c86d5e5 \ + --hash=sha256:dcefc97d1daf8d55199420e9162ab584ed0893a109f45e438b9794ced44c9fd0 \ + --hash=sha256:dd98c436a1fc56f48c70882cc243df89ad036210d871c7427dc164b31500dc11 \ + --hash=sha256:e74671e43ed4569fbd7989e5eecc7d06dc134b571872ab1d5a88f4a123814e9f \ + --hash=sha256:eb9b92f456ff3ec746cd4935b73c1117538d6124b8617bc0fe6fda0b3816e345 \ + --hash=sha256:ebb4e637a1fb861c34e48a00d03cffa9234f42bef923aec44e5625ffb9a8e8f9 \ + --hash=sha256:ef739fe89e7f43fb6494a43b1878a36273e5924869ba1d866f752c5812ae8d58 \ + --hash=sha256:f40db0e02a8157d2b90857c24d89b6310f9b6c3642369852cdc3b5ac49b92afc \ + --hash=sha256:f68bf937f113b88c866d090fea0bc52a098695173fc613b055a17ff0cf9683b6 \ + --hash=sha256:fb55c182a3f7b84c1a2d6de5fa7b1a05d4660d866b91dbf8d74549c57a1499e8 \ + # via -r /opt/sql-requirements.txt, gevent # The following packages are considered to be unsafe in a requirements file: -setuptools==46.1.3 \ - --hash=sha256:4fe404eec2738c20ab5841fa2d791902d2a645f32318a7850ef26f8d7215a8ee \ - --hash=sha256:795e0475ba6cd7fa082b1ee6e90d552209995627a2a227a47c6ea93282f4bfb1 \ - # via -r /opt/sql-requirements.txt, google-api-core, google-auth, gunicorn, ipython, protobuf, safety +setuptools==46.4.0 \ + --hash=sha256:4334fc63121aafb1cc98fd5ae5dd47ea8ad4a38ad638b47af03a686deb14ef5b \ + --hash=sha256:d05c2c47bbef97fd58632b63dd2b83426db38af18f65c180b2423fea4b67e6b8 \ + # via -r /opt/sql-requirements.txt, gevent, google-api-core, google-auth, gunicorn, ipython, protobuf, safety, zope.event, zope.interface From 0e31bcc9540ff4592ba8151c8c32e453b21aee5f Mon Sep 17 00:00:00 2001 From: Midnighter Date: Tue, 26 May 2020 22:50:11 +0200 Subject: [PATCH 09/11] style: remove f-strings --- src/iam/app.py | 2 +- src/iam/resources.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/iam/app.py b/src/iam/app.py index 321e9bf..5880999 100644 --- a/src/iam/app.py +++ b/src/iam/app.py @@ -109,7 +109,7 @@ def init_app(application, db): ############################################################################ logger.debug("Registering admin views") - admin = Admin(application, template_mode="bootstrap3", url=f"/admin") + admin = Admin(application, template_mode="bootstrap3", url="/admin") admin.add_view(ModelView(Organization, db.session)) admin.add_view(ModelView(Team, db.session)) admin.add_view(ModelView(User, db.session)) diff --git a/src/iam/resources.py b/src/iam/resources.py index 4030d41..74dcc95 100644 --- a/src/iam/resources.py +++ b/src/iam/resources.py @@ -295,7 +295,7 @@ def post(self, first_name, last_name, email, password): # Check if specified email already exists exists = db.session.query(User.id).filter_by(email=email).scalar() if exists: - return f"User with provided email already exists", 400 + return "User with provided email already exists", 400 user = User(first_name=first_name, last_name=last_name, email=email) user.set_password(password) From d15764b556d59bd21d31235db540f78396dd4220 Mon Sep 17 00:00:00 2001 From: Midnighter Date: Mon, 17 Aug 2020 19:20:13 +0200 Subject: [PATCH 10/11] fix: do not allocate a TTY --- scripts/wait_for_postgres.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/wait_for_postgres.sh b/scripts/wait_for_postgres.sh index adfbf17..2e21878 100755 --- a/scripts/wait_for_postgres.sh +++ b/scripts/wait_for_postgres.sh @@ -27,6 +27,6 @@ while [[ ! "$(docker-compose logs --no-color postgres)" = *"PostgreSQL init proc done echo "Waiting for postgres to accept connections..." -until docker-compose exec postgres psql -U postgres -l > /dev/null; do +until docker-compose exec -T postgres psql -U postgres -l > /dev/null; do sleep 1 done From e70ed924b5fdd4bed2a684a1bada43c2755fb0bc Mon Sep 17 00:00:00 2001 From: Midnighter Date: Tue, 18 Aug 2020 11:08:57 +0200 Subject: [PATCH 11/11] refactor: use postgres tool for ready check --- scripts/wait_for_postgres.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/wait_for_postgres.sh b/scripts/wait_for_postgres.sh index 2e21878..5e979e3 100755 --- a/scripts/wait_for_postgres.sh +++ b/scripts/wait_for_postgres.sh @@ -27,6 +27,6 @@ while [[ ! "$(docker-compose logs --no-color postgres)" = *"PostgreSQL init proc done echo "Waiting for postgres to accept connections..." -until docker-compose exec -T postgres psql -U postgres -l > /dev/null; do +until docker-compose exec -T postgres pg_isready --username postgres --quiet; do sleep 1 done