Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
506 changes: 506 additions & 0 deletions .dockerignore

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
29 changes: 15 additions & 14 deletions api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
# External Modules
##########################################

import os

from flask import Flask
from flask_cors import CORS
from flask_talisman import Talisman
Expand All @@ -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
Expand All @@ -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

##########################################
Expand Down
6 changes: 6 additions & 0 deletions api/messages/message.py
Original file line number Diff line number Diff line change
@@ -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"
6 changes: 3 additions & 3 deletions api/messages/messages_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
)
12 changes: 3 additions & 9 deletions api/messages/messages_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
2 changes: 1 addition & 1 deletion api/security/auth0_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions api/security/guards.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}


Expand Down
3 changes: 3 additions & 0 deletions api/wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from api import create_app

app = create_app()
7 changes: 7 additions & 0 deletions common/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -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.")
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: "3.8"
services:
api:
build: .
image: api_flask_python_hello-world:basic-authorization
ports:
- 6060:6060
env_file:
- .env
18 changes: 18 additions & 0 deletions gunicorn.conf.py
Original file line number Diff line number Diff line change
@@ -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)
5 changes: 5 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
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
gunicorn==20.1.0
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