Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP]: Harden images and reorg #647

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
9 changes: 3 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ metagrid_configs/backups
# .nfs files are created when an open file is removed but is still being accessed
.nfs*


### VisualStudioCode template
**/.vscode/*

Expand Down Expand Up @@ -80,8 +79,6 @@ crashlytics.properties
crashlytics-build.properties
fabric.properties



### Windows template
# Windows thumbnail cache files
Thumbs.db
Expand All @@ -106,7 +103,6 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk


### macOS template
# General
*.DS_Store
Expand Down Expand Up @@ -135,7 +131,6 @@ Network Trash Folder
Temporary Items
.apdisk


### SublimeText template
# Cache files for Sublime Text
*.tmlanguage.cache
Expand Down Expand Up @@ -168,7 +163,6 @@ bh_unicode_properties.cache
# https://packagecontrol.io/packages/sublime-github
GitHub.sublime-settings


### Vim template
# Swap
[._]*.s[a-v][a-z]
Expand All @@ -189,3 +183,6 @@ tags
.env
.envs/*
!.envs/.local/

# Documentation artifacts
docs/site
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"editor.rulers": [72, 79, 120],
"editor.wordWrap": "wordWrapColumn",
"editor.wordWrapColumn": 120,
"editor.defaultFormatter": "ms-python.python"
"editor.defaultFormatter": "charliermarsh.ruff"
},
// Update as needed
"python.pythonPath": "backend/venv/bin/python",
Expand Down
42 changes: 0 additions & 42 deletions backend/.envs/.django
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The *.env files were removed in favor of ensuring that all settings had a sane default out of the box though each service still respects settings loaded from .env files as before in order to preserve backwards compatibility

This file was deleted.

14 changes: 0 additions & 14 deletions backend/.envs/.keycloak

This file was deleted.

5 changes: 0 additions & 5 deletions backend/.envs/.postgres

This file was deleted.

27 changes: 10 additions & 17 deletions backend/docker/production/django/Dockerfile → backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.9-slim-buster
FROM python:3.9-slim

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing the -buster suffix allows automatic building on newer versions to avoid outdated packages

ENV PYTHONUNBUFFERED 1

Expand All @@ -17,23 +17,16 @@ RUN addgroup --system django \
&& adduser --system --ingroup django django

# Requirements are installed here to ensure they will be cached.
COPY ./requirements /requirements
RUN pip3 install --no-cache-dir -r /requirements/production.txt \
&& rm -rf /requirements
COPY requirements /requirements
RUN pip3 install --no-cache-dir -r /requirements/local.txt

COPY ./docker/production/django/entrypoint /entrypoint
RUN sed -i 's/\r$//g' /entrypoint
RUN chmod +x /entrypoint
RUN chown django /entrypoint
COPY . /app
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files and directories owned by the running user are a security risk. Removing the entrypoint script in favor of running the gunicorn server directly allows avoiding the user chown


COPY ./docker/production/django/start /start
RUN sed -i 's/\r$//g' /start
RUN chmod +x /start
RUN chown django /start
COPY --chown=django:django . /app

USER django
RUN python /app/manage.py collectstatic --noinput \
&& mkdir -p /app/staticfiles/.well-known \
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running collectstatic during build allows faster startup and an immutable image

&& cyclonedx-py requirements requirements/base.txt --output-format json --outfile /app/staticfiles/.well-known/bom

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WORKDIR /app

ENTRYPOINT ["/entrypoint"]
USER django
EXPOSE 5000
CMD ["/usr/local/bin/gunicorn", "-c", "gunicorn_conf.py", "config.wsgi", "--chdir=/app"]
129 changes: 108 additions & 21 deletions backend/config/settings/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Base settings to build other settings files upon.
"""

from datetime import timedelta
from typing import List # noqa

Expand All @@ -21,7 +22,7 @@
# ------------------------------------------------------------------------------
# Set DEBUG to False as a default for safety
# https://docs.djangoproject.com/en/dev/ref/settings/#debug
DEBUG = env.bool("DJANGO_DEBUG", default=False)
DEBUG = env.bool("DJANGO_DEBUG", default=True)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Production config sets this back to False

APPEND_SLASH = False
TIME_ZONE = "UTC"
LANGUAGE_CODE = "en-us"
Expand Down Expand Up @@ -51,6 +52,8 @@
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"whitenoise.runserver_nostatic",
"django_extensions",
# Third party apps
"rest_framework", # utilities for rest apis
"rest_framework.authtoken",
Expand All @@ -63,6 +66,7 @@
"dj_rest_auth",
"drf_yasg",
"social_django",
"gunicorn",
# Your apps
"metagrid.api_proxy",
"metagrid.users",
Expand Down Expand Up @@ -98,12 +102,21 @@
ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1"]
ROOT_URLCONF = "config.urls"

# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
SECRET_KEY = env(
"DJANGO_SECRET_KEY",
default="7LynCTKfcjH6p2Nz77YM9XzSnTSvpPVUNz4bHEScGJ6flWcOslxGNMdAhsDioJFJ",
)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Production config sets this empty so that it raises ImproperlyConfigured if unset via env var or .env file


WSGI_APPLICATION = "config.wsgi.application"

# EMAIL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_BACKEND = env(
"DJANGO_EMAIL_BACKEND",
default="django.core.mail.backends.console.EmailBackend",
)
# https://docs.djangoproject.com/en/2.2/ref/settings/#email-timeout
EMAIL_TIMEOUT = 5

Expand All @@ -116,11 +129,20 @@
# https://docs.djangoproject.com/en/dev/ref/settings/#managers
MANAGERS = ADMINS

CORS_ORIGIN_ALLOW_ALL = True

# DATABASES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#databases
DATABASES = {"default": env.db("DATABASE_URL")}
DATABASES["default"]["ATOMIC_REQUESTS"] = True
# Default to allowing the standard Postgres variables to configure the default database
# https://www.postgresql.org/docs/current/libpq-envars.html

DATABASES = {
"default": {
"ATOMIC_REQUESTS": True,
**env.db("DATABASE_URL", default="pgsql:///postgres"),
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting this as the database url allows configuration via the standard postgres defaults and env vars

}
}

# STATIC
# ------------------------------------------------------------------------------
Expand Down Expand Up @@ -184,7 +206,7 @@
# https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{
Expand Down Expand Up @@ -250,7 +272,10 @@

# django-rest-framework - https://www.django-rest-framework.org/api-guide/settings/
# -------------------------------------------------------------------------------
DEFAULT_RENDERER_CLASSES = ["rest_framework.renderers.JSONRenderer"]
DEFAULT_RENDERER_CLASSES = [
"rest_framework.renderers.JSONRenderer",
"rest_framework.renderers.BrowsableAPIRenderer",
]
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": int(env("DJANGO_PAGINATION_LIMIT", default=10)),
Expand Down Expand Up @@ -281,15 +306,13 @@
SOCIALACCOUNT_PROVIDERS = {
"keycloak": {
"KEYCLOAK_URL": env(
"KEYCLOAK_URL",
),
"KEYCLOAK_REALM": env(
"KEYCLOAK_REALM",
"KEYCLOAK_URL", default="https://esgf-login.ceda.ac.uk/"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't sure if this should be e.g. https://keycloak for the local docker compose instance and the CEDA instance set as default in the Production config? I'm not sure of the status of keycloak support

),
"KEYCLOAK_REALM": env("KEYCLOAK_REALM", default="esgf"),
},
}
# Used in data migration to register Keycloak social app
KEYCLOAK_CLIENT_ID = env("KEYCLOAK_CLIENT_ID")
KEYCLOAK_CLIENT_ID = env("KEYCLOAK_CLIENT_ID", default="metagrid-localhost")

ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
Expand All @@ -305,8 +328,12 @@
# https://docs.djangoproject.com/en/3.2/ref/settings/#login-url
LOGIN_URL = "/login/globus/"

LOGIN_REDIRECT_URL = env("DJANGO_LOGIN_REDIRECT_URL")
LOGOUT_REDIRECT_URL = env("DJANGO_LOGOUT_REDIRECT_URL")
LOGIN_REDIRECT_URL = env(
"DJANGO_LOGIN_REDIRECT_URL", default="http://localhost:8080/search"
)
LOGOUT_REDIRECT_URL = env(
"DJANGO_LOGOUT_REDIRECT_URL", default="http://localhost:8080/search"
)

# This dictates which scopes will be requested on each user login
SOCIAL_AUTH_GLOBUS_SCOPE = [
Expand All @@ -317,13 +344,14 @@
"urn:globus:auth:scope:transfer.api.globus.org:all",
]

SOCIAL_AUTH_GLOBUS_KEY = env("GLOBUS_CLIENT_KEY")
SOCIAL_AUTH_GLOBUS_SECRET = env("GLOBUS_CLIENT_SECRET")
SOCIAL_AUTH_GLOBUS_KEY = env("GLOBUS_CLIENT_KEY", default=12345)
SOCIAL_AUTH_GLOBUS_SECRET = env("GLOBUS_CLIENT_SECRET", default=12345)
SOCIAL_AUTH_GLOBUS_AUTH_EXTRA_ARGUMENTS = {
"requested_scopes": SOCIAL_AUTH_GLOBUS_SCOPE,
"prompt": None,
}
SOCIAL_AUTH_JSONFIELD_ENABLED = True
SOCIAL_AUTH_REDIRECT_IS_HTTPS = False

# dj-rest-auth
# -------------------------------------------------------------------------------
Expand All @@ -335,12 +363,71 @@
# -------------------------------------------------------------------------------
# https://github.com/adamchainz/django-cors-headers#setup
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"http://localhost:8080",
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frontend now based on nginx unpriviliged which defaults to port 8080

]
CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_WHITELIST = env.list("CORS_ORIGIN_WHITELIST")
CORS_ORIGIN_WHITELIST = env.list(
"CORS_ORIGIN_WHITELIST", default="http://localhost:8080"
)
CSRF_TRUSTED_ORIGINS = ["http://localhost:8080"]

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are no longer needed by the frontend but still used by the backend for proxying. Renaming them would make migration less straightforward, but seems like it should be worth it

SEARCH_URL = env(
"REACT_APP_SEARCH_URL",
default="https://esgf-node.ornl.gov/esg-search/search",
)
WGET_URL = env(
"REACT_APP_WGET_API_URL",
default="https://esgf-node.ornl.gov/esg-search/wget",
)
STATUS_URL = env(
"REACT_APP_ESGF_NODE_STATUS_URL",
default="https://esgf-node.ornl.gov/proxy/status",
)
SOLR_URL = env(
"REACT_APP_ESGF_SOLR_URL", default="https://esgf-node.ornl.gov/esg-search"
)

FRONTEND_SETTINGS = {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All settings in the FRONTEND_SETTINGS dict will be sent to the frontend for future flexibility

"REACT_APP_CLIENT_ID": env("REACT_APP_CLIENT_ID", default="frontend"),
"REACT_APP_GOOGLE_ANALYTICS_TRACKING_ID": env(
"REACT_APP_GOOGLE_ANALYTICS_TRACKING_ID", default=""
),
"REACT_APP_GLOBUS_NODES": env.list(
"REACT_APP_GLOBUS_NODES",
default=[
"aims3.llnl.gov",
"esgf-data1.llnl.gov",
"esgf-data2.llnl.gov",
],
),
"REACT_APP_KEYCLOAK_REALM": env(
"REACT_APP_KEYCLOAK_REALM", default="esgf"
),
"REACT_APP_KEYCLOAK_URL": env(
"REACT_APP_KEYCLOAK_URL", default="https://esgf-login.ceda.ac.uk/"
),
"REACT_APP_KEYCLOAK_CLIENT_ID": env(
"REACT_APP_KEYCLOAK_CLIENT_ID", default="frontend"
),
"REACT_APP_HOTJAR_ID": env("REACT_APP_HOTJAR_ID", default=1234),
"REACT_APP_HOTJAR_SV": env("REACT_APP_HOTJAR_SV", default=1234),
"REACT_APP_AUTHENTICATION_METHOD": env(
"REACT_APP_AUTHENTICATION_METHOD", default="keycloak"
),
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basic sanity checks around the shape of settings. Perhaps move to Pydantic?

SEARCH_URL = env("REACT_APP_SEARCH_URL")
WGET_URL = env("REACT_APP_WGET_API_URL")
STATUS_URL = env("REACT_APP_ESGF_NODE_STATUS_URL")
SOLR_URL = env("REACT_APP_ESGF_SOLR_URL")
if not isinstance(FRONTEND_SETTINGS["REACT_APP_GLOBUS_NODES"], list):
raise environ.ImproperlyConfigured(
f"REACT_APP_GLOBUS_NODES must be of type list, not "
f"{FRONTEND_SETTINGS['REACT_APP_GLOBUS_NODES']} of type "
f"{type(FRONTEND_SETTINGS['REACT_APP_GLOBUS_NODES'])}"
)

if FRONTEND_SETTINGS["REACT_APP_AUTHENTICATION_METHOD"] not in (
"keycloak",
"globus",
):
raise environ.ImproperlyConfigured(
f"REACT_APP_AUTHENTICATION_METHOD must be one of keycloak or globus, "
f"not {FRONTEND_SETTINGS['REACT_APP_AUTHENTICATION_METHOD']}"
)
Loading
Loading