From e6abced390d8ec808545f86983b17252955db9ed Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 7 Dec 2021 15:03:19 -0500 Subject: [PATCH 1/3] update requirements.txt --- requirements.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/requirements.txt b/requirements.txt index fb2e175..3cd3c53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,14 @@ +cffi==1.15.0 click==8.0.3 +cryptography==35.0.0 Flask==2.0.2 Flask-Cors==3.0.10 flask-talisman==0.8.1 itsdangerous==2.0.1 Jinja2==3.0.3 MarkupSafe==2.0.1 +pycparser==2.21 +PyJWT==2.3.0 python-dotenv==0.19.1 six==1.16.0 Werkzeug==2.0.2 From 43c8fef89497547ae183c2553c812e0846733622 Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 10 Dec 2021 14:43:06 -0500 Subject: [PATCH 2/3] update unauthorized_error and invalid_request_error message --- api/security/guards.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/security/guards.py b/api/security/guards.py index 3098296..085846e 100644 --- a/api/security/guards.py +++ b/api/security/guards.py @@ -7,13 +7,13 @@ from api.utils import json_abort unauthorized_error = { - "message": "Unauthorized." + "message": "Requires authentication" } invalid_request_error = { "error": "invalid_request", "error_description": "Authorization header value must follow this format: Bearer access-token", - "message": "Unauthorized." + "message": "Requires authentication" } From da122d2876e5db2c4e50bd264a7007efa31e2a8c Mon Sep 17 00:00:00 2001 From: Byron Motoche <37427699+byrpatrick@users.noreply.github.com> Date: Tue, 25 Jan 2022 14:10:11 -0500 Subject: [PATCH 3/3] [Basic-authorization] Update messages and docker set up (#3) * add dockerfiles and update messages responses * update dockerfiles and gunicorn config * change python version to match the distroless python version --- .dockerignore | 506 +++++++++++++++++++++++++++++++ Dockerfile | 18 ++ api/__init__.py | 29 +- api/messages/message.py | 6 + api/messages/messages_service.py | 6 +- api/messages/messages_views.py | 12 +- api/security/auth0_service.py | 2 +- api/wsgi.py | 3 + common/utils/__init__.py | 7 + docker-compose.yml | 9 + gunicorn.conf.py | 18 ++ requirements.txt | 1 + 12 files changed, 590 insertions(+), 27 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 api/wsgi.py create mode 100644 common/utils/__init__.py create mode 100644 docker-compose.yml create mode 100644 gunicorn.conf.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5be5fe7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,506 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/macos,windows,visualstudiocode,jetbrains+all,node,python,flask +# Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,visualstudiocode,jetbrains+all,node,python,flask + +### Flask ### +instance/* +!instance/.gitignore +.webassets-cache +.env + +### Flask.Python Stack ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +### JetBrains+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### JetBrains+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +# Sonarlint plugin +.idea/sonarlint + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Node ### +# Logs +logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env.test +.env.production + +# parcel-bundler cache (https://parceljs.org/) +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +### Python ### +# Byte-compiled / optimized / DLL files + +# C extensions + +# Distribution / packaging + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. + +# Installer logs + +# Unit test / coverage reports + +# Translations + +# Django stuff: + +# Flask stuff: + +# Scrapy stuff: + +# Sphinx documentation + +# PyBuilder + +# Jupyter Notebook + +# IPython + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow + +# Celery stuff + +# SageMath parsed files + +# Environments + +# Spyder project settings + +# Rope project settings + +# mkdocs documentation + +# mypy + +# Pyre type checker + +# pytype static type analyzer + +# Cython debug symbols + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/macos,windows,visualstudiocode,jetbrains+all,node,python,flask + +# Dockerfiles +.dockerignore +Dockerfile* +docker-compose* + +.git* +README.md +.env.example diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..718549d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.9.2-slim-buster@sha256:721de1d0aea3331da282531b8b9e428d552b89bd6fd0d0c14e634deaddc241fc as build +RUN groupadd auth0 && useradd -m developer -g auth0 +USER developer +WORKDIR /home/developer +COPY ./requirements.txt ./app/requirements.txt +RUN pip install --disable-pip-version-check -r ./app/requirements.txt --target ./packages +COPY ./api ./app/api +COPY ./common ./app/common +COPY ./gunicorn.conf.py ./app + +FROM gcr.io/distroless/python3@sha256:eb773dd9d39f0becdab47e2ef5f1b10e2988c93a40ac8d32ca593096b409d351 +COPY --from=build /home/developer/app /app +COPY --from=build /home/developer/packages /packages +USER 1000 +EXPOSE 6060 +ENV PYTHONPATH="/packages" +WORKDIR /app +CMD ["/packages/gunicorn/app/wsgiapp.py","api.wsgi:app"] diff --git a/api/__init__.py b/api/__init__.py index 742bf95..ffb9a58 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -2,8 +2,6 @@ # External Modules ########################################## -import os - from flask import Flask from flask_cors import CORS from flask_talisman import Talisman @@ -12,17 +10,15 @@ from api.messages import messages_views from api.security.auth0_service import auth0_service +from common.utils import safe_get_env_var def create_app(): ########################################## # Environment Variables ########################################## - client_origin_url = os.environ.get("CLIENT_ORIGIN_URL") - auth0_audience = os.environ.get("AUTH0_AUDIENCE") - auth0_domain = os.environ.get("AUTH0_DOMAIN") - - if not (client_origin_url and auth0_audience and auth0_domain): - raise NameError("The required environment variables are missing. Check .env file.") + client_origin_url = safe_get_env_var("CLIENT_ORIGIN_URL") + auth0_audience = safe_get_env_var("AUTH0_AUDIENCE") + auth0_domain = safe_get_env_var("AUTH0_DOMAIN") ########################################## # Flask App Instance @@ -39,21 +35,26 @@ def create_app(): 'frame-ancestors': ['\'none\''] } - Talisman(app, - frame_options='DENY', - content_security_policy=csp, - referrer_policy='no-referrer' - ) + Talisman( + app, + force_https=False, + frame_options='DENY', + content_security_policy=csp, + referrer_policy='no-referrer', + x_xss_protection=False, + x_content_type_options=True + ) auth0_service.initialize(auth0_domain, auth0_audience) @app.after_request def add_headers(response): response.headers['X-XSS-Protection'] = '0' - response.headers['Cache-Control'] = 'no-store, max-age=0' + response.headers['Cache-Control'] = 'no-store, max-age=0, must-revalidate' response.headers['Pragma'] = 'no-cache' response.headers['Expires'] = '0' response.headers['Content-Type'] = 'application/json; charset=utf-8' + response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' return response ########################################## diff --git a/api/messages/message.py b/api/messages/message.py index ff54fc7..0d2530f 100644 --- a/api/messages/message.py +++ b/api/messages/message.py @@ -1,3 +1,9 @@ class Message: def __init__(self, text): self.text = text + self.metadata = vars(Metadata()) + +class Metadata: + def __init__(self): + self.api = "api_flask_python_hello-world" + self.branch = "basic-authorization" diff --git a/api/messages/messages_service.py b/api/messages/messages_service.py index 1cb15c8..671233a 100644 --- a/api/messages/messages_service.py +++ b/api/messages/messages_service.py @@ -3,17 +3,17 @@ def get_public_message(): return Message( - "The API doesn't require an access token to share this message." + "This is a public message." ) def get_protected_message(): return Message( - "The API successfully validated your access token." + "This is a protected message." ) def get_admin_message(): return Message( - "The API successfully recognized you as an admin." + "This is an admin message." ) diff --git a/api/messages/messages_views.py b/api/messages/messages_views.py index 33b5b74..831fbcd 100644 --- a/api/messages/messages_views.py +++ b/api/messages/messages_views.py @@ -16,22 +16,16 @@ @bp.route("/public") def public(): - return { - "text": get_public_message().text - } + return vars(get_public_message()) @bp.route("/protected") @authorization_guard def protected(): - return { - "text": get_protected_message().text - } + return vars(get_protected_message()) @bp.route("/admin") @authorization_guard def admin(): - return { - "text": get_admin_message().text - } + return vars(get_admin_message()) diff --git a/api/security/auth0_service.py b/api/security/auth0_service.py index 717c37f..5dbe77f 100644 --- a/api/security/auth0_service.py +++ b/api/security/auth0_service.py @@ -46,7 +46,7 @@ def validate_jwt(self, token): json_abort(HTTPStatus.UNAUTHORIZED, { "error": "invalid_token", "error_description": error.__str__(), - "message": "Bad credentials." + "message": "Bad credentials" }) return diff --git a/api/wsgi.py b/api/wsgi.py new file mode 100644 index 0000000..852a82b --- /dev/null +++ b/api/wsgi.py @@ -0,0 +1,3 @@ +from api import create_app + +app = create_app() diff --git a/common/utils/__init__.py b/common/utils/__init__.py new file mode 100644 index 0000000..e637449 --- /dev/null +++ b/common/utils/__init__.py @@ -0,0 +1,7 @@ +from os import environ + +def safe_get_env_var(key): + try: + return environ[key] + except KeyError: + raise NameError(f"Missing {key} environment variable.") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4eb4fc0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +version: "3.8" +services: + api: + build: . + image: api_flask_python_hello-world:basic-authorization + ports: + - 6060:6060 + env_file: + - .env diff --git a/gunicorn.conf.py b/gunicorn.conf.py new file mode 100644 index 0000000..1bc159c --- /dev/null +++ b/gunicorn.conf.py @@ -0,0 +1,18 @@ + +import gunicorn.http.wsgi +from functools import wraps +from dotenv import load_dotenv +from common.utils import safe_get_env_var + +load_dotenv() + +wsgi_app = "api.wsgi:app" +bind = f"0.0.0.0:{safe_get_env_var('PORT')}" + +def wrap_default_headers(func): + @wraps(func) + def default_headers(*args, **kwargs): + return [header for header in func(*args, **kwargs) if not header.startswith('Server: ')] + return default_headers + +gunicorn.http.wsgi.Response.default_headers = wrap_default_headers(gunicorn.http.wsgi.Response.default_headers) diff --git a/requirements.txt b/requirements.txt index 3cd3c53..7a34af4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ cryptography==35.0.0 Flask==2.0.2 Flask-Cors==3.0.10 flask-talisman==0.8.1 +gunicorn==20.1.0 itsdangerous==2.0.1 Jinja2==3.0.3 MarkupSafe==2.0.1