diff --git a/.flake8 b/.flake8 deleted file mode 100644 index dc53bee7e..000000000 --- a/.flake8 +++ /dev/null @@ -1,15 +0,0 @@ -[flake8] -# Rule definitions: http://flake8.pycqa.org/en/latest/user/error-codes.html -exclude = venv*,__pycache__,node_modules,cache -max-complexity = 14 -max-line-length = 120 - -# B003 assigning to os.environ -# B306 BaseException.message has been deprecated -# W503: line break before binary operator -# W504 line break after binary operator -extend_ignore= - B003, - B306, - W503, - W504 diff --git a/.gitignore b/.gitignore index 449df7b66..962dff7e1 100644 --- a/.gitignore +++ b/.gitignore @@ -109,6 +109,8 @@ environment.sh .env .env* varsfile +*.log.json +logs/** # CloudFoundry .cf diff --git a/app/__init__.py b/app/__init__.py index 39ea3f285..adcb21406 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -37,6 +37,7 @@ from werkzeug.local import LocalProxy from app import proxy_fix, webauthn_server from app.asset_fingerprinter import asset_fingerprinter from app.config import configs +from app.custom_auth import CustomBasicAuth from app.extensions import antivirus_client, redis_client, zendesk_client from app.formatters import ( convert_to_boolean, @@ -134,7 +135,7 @@ from app.url_converters import ( login_manager = LoginManager() csrf = CSRFProtect() metrics = GDSMetrics() - +basic_auth = CustomBasicAuth() # The current service attached to the request stack. def _get_current_service(): @@ -221,6 +222,8 @@ def create_app(application): login_manager.session_protection = None login_manager.anonymous_user = AnonymousUser + setup_basic_auth(application) + # make sure we handle unicode correctly redis_client.redis_store.decode_responses = True @@ -586,3 +589,6 @@ def init_jinja(application): ] jinja_loader = jinja2.FileSystemLoader(template_folders) application.jinja_loader = jinja_loader + +def setup_basic_auth(application): + application.basic_auth = CustomBasicAuth(application) diff --git a/app/config.py b/app/config.py index 33baca579..9b9bae42b 100644 --- a/app/config.py +++ b/app/config.py @@ -4,13 +4,16 @@ import os if os.environ.get('VCAP_APPLICATION'): # on cloudfoundry, config is a json blob in VCAP_APPLICATION - unpack it, and populate # standard environment variables from it - from app.cloudfoundry_config import extract_cloudfoundry_config - extract_cloudfoundry_config() + # from app.cloudfoundry_config import extract_cloudfoundry_config + # extract_cloudfoundry_config() + vcap_services = json.loads(os.environ['VCAP_SERVICES']) + os.environ['REDIS_URL'] = vcap_services['aws-elasticache-redis'][0]['credentials']['uri'] class Config(object): ADMIN_CLIENT_SECRET = os.environ.get('ADMIN_CLIENT_SECRET') - API_HOST_NAME = os.environ.get('API_HOST_NAME') + ADMIN_CLIENT_USER_NAME = os.environ.get('ADMIN_CLIENT_USERNAME') + API_HOST_NAME = os.environ.get('API_HOST_NAME', 'localhost') SECRET_KEY = os.environ.get('SECRET_KEY') DANGEROUS_SALT = os.environ.get('DANGEROUS_SALT') ZENDESK_API_KEY = os.environ.get('ZENDESK_API_KEY') @@ -23,9 +26,7 @@ class Config(object): # Logging DEBUG = False - NOTIFY_LOG_PATH = os.getenv('NOTIFY_LOG_PATH') - - ADMIN_CLIENT_USER_NAME = 'notify-admin' + NOTIFY_LOG_PATH = os.environ.get('NOTIFY_LOG_PATH', 'application.log') ANTIVIRUS_API_HOST = os.environ.get('ANTIVIRUS_API_HOST') ANTIVIRUS_API_KEY = os.environ.get('ANTIVIRUS_API_KEY') @@ -40,7 +41,7 @@ class Config(object): HEADER_COLOUR = '#81878b' # mix(govuk-colour("dark-grey"), govuk-colour("mid-grey")) HTTP_PROTOCOL = 'http' NOTIFY_APP_NAME = 'admin' - NOTIFY_LOG_LEVEL = 'DEBUG' + NOTIFY_LOG_LEVEL = os.environ.get('NOTIFY_LOG_LEVEL', 'DEBUG') PERMANENT_SESSION_LIFETIME = 20 * 60 * 60 # 20 hours SEND_FILE_MAX_AGE_DEFAULT = 365 * 24 * 60 * 60 # 1 year SESSION_COOKIE_HTTPONLY = True @@ -62,13 +63,17 @@ class Config(object): LOGO_UPLOAD_BUCKET_NAME = 'public-logos-local' MOU_BUCKET_NAME = 'local-mou' TRANSIENT_UPLOADED_LETTERS = 'local-transient-uploaded-letters' - ROUTE_SECRET_KEY_1 = os.environ.get('ROUTE_SECRET_KEY_1', '') - ROUTE_SECRET_KEY_2 = os.environ.get('ROUTE_SECRET_KEY_2', '') + ROUTE_SECRET_KEY_1 = os.environ.get('ROUTE_SECRET_KEY_1', 'dev-route-secret-key-1') + ROUTE_SECRET_KEY_2 = os.environ.get('ROUTE_SECRET_KEY_2', 'dev-route-secret-key-2') CHECK_PROXY_HEADER = False ANTIVIRUS_ENABLED = True REDIS_URL = os.environ.get('REDIS_URL') REDIS_ENABLED = True + + BASIC_AUTH_USERNAME = os.environ.get('BASIC_AUTH_USERNAME') + BASIC_AUTH_PASSWORD = os.environ.get('BASIC_AUTH_PASSWORD') + BASIC_AUTH_FORCE = True ASSET_DOMAIN = '' ASSET_PATH = '/static/' @@ -93,6 +98,7 @@ class Config(object): class Development(Config): + BASIC_AUTH_FORCE = True NOTIFY_LOG_PATH = 'application.log' DEBUG = True SESSION_COOKIE_SECURE = False @@ -104,7 +110,7 @@ class Development(Config): MOU_BUCKET_NAME = 'notify.tools-mou' TRANSIENT_UPLOADED_LETTERS = 'development-transient-uploaded-letters' PRECOMPILED_ORIGINALS_BACKUP_LETTERS = 'development-letters-precompiled-originals-backup' - ADMIN_CLIENT_SECRET = 'dev-notify-secret-key' + ADMIN_CLIENT_SECRET = os.environ.get('ADMIN_CLIENT_SECRET') # check for local compose orchestration variable API_HOST_NAME = os.environ.get('DEV_API_HOST_NAME', 'http://dev:6011') DANGEROUS_SALT = 'dev-notify-salt' @@ -116,11 +122,11 @@ class Development(Config): ASSET_PATH = '/static/' REDIS_URL = os.environ.get('DEV_REDIS_URL', 'http://redis:6379') - - REDIS_ENABLED = os.environ.get('REDIS_ENABLED') == '1' + REDIS_ENABLED = True class Test(Development): + BASIC_AUTH_FORCE = False DEBUG = True TESTING = True WTF_CSRF_ENABLED = False @@ -143,6 +149,7 @@ class Test(Development): class Preview(Config): + BASIC_AUTH_FORCE = True HTTP_PROTOCOL = 'https' HEADER_COLOUR = '#F499BE' # $baby-pink CSV_UPLOAD_BUCKET_NAME = 'preview-notifications-csv-upload' @@ -162,6 +169,7 @@ class Preview(Config): class Staging(Config): + BASIC_AUTH_FORCE = True HTTP_PROTOCOL = 'https' HEADER_COLOUR = '#6F72AF' # $mauve CSV_UPLOAD_BUCKET_NAME = 'staging-notifications-csv-upload' @@ -178,6 +186,7 @@ class Staging(Config): class Live(Config): + BASIC_AUTH_FORCE = True HEADER_COLOUR = '#005EA5' # $govuk-blue HTTP_PROTOCOL = 'https' CSV_UPLOAD_BUCKET_NAME = 'notifications.prototype.csv_upload' @@ -191,6 +200,18 @@ class Live(Config): CHECK_PROXY_HEADER = False ASSET_DOMAIN = 'static.notifications.service.gov.uk' ASSET_PATH = 'https://static.notifications.service.gov.uk/' + + REDIS_URL = os.environ.get('REDIS_URL') + REDIS_ENABLED = True + + ADMIN_CLIENT_SECRET = os.environ.get('ADMIN_CLIENT_SECRET') + ADMIN_CLIENT_USER_NAME = os.environ.get('ADMIN_CLIENT_USERNAME') + API_HOST_NAME = os.environ.get('API_HOST_NAME') + DANGEROUS_SALT = os.environ.get('DANGEROUS_SALT') + SECRET_KEY = os.environ.get('SECRET_KEY') + ANTIVIRUS_API_HOST = 'http://localhost:6016' + ANTIVIRUS_API_KEY = 'test-key' + ANTIVIRUS_ENABLED = False class CloudFoundryConfig(Config): diff --git a/app/custom_auth.py b/app/custom_auth.py new file mode 100644 index 000000000..7959a107a --- /dev/null +++ b/app/custom_auth.py @@ -0,0 +1,15 @@ +from flask_basicauth import BasicAuth +from flask import jsonify, request + +class CustomBasicAuth(BasicAuth): + """ + Description: + Override BasicAuth to permit anonymous healthcheck at /_status?simple=true + """ + def challenge(self): + if "/_status" in request.url: + if request.args.get('elb', None) or request.args.get('simple', None): + return jsonify(status="ok"), 200 + return super(CustomBasicAuth, self).challenge() + +custom_basic_auth = CustomBasicAuth() diff --git a/app/notify_client/status_api_client.py b/app/notify_client/status_api_client.py index 3ae128ce0..92de16bef 100644 --- a/app/notify_client/status_api_client.py +++ b/app/notify_client/status_api_client.py @@ -1,6 +1,6 @@ +import os from app.notify_client import NotifyAdminAPIClient, cache - class StatusApiClient(NotifyAdminAPIClient): def get_status(self, *params): diff --git a/application.log.json b/application.log.json deleted file mode 100644 index 4ed2ff25e..000000000 --- a/application.log.json +++ /dev/null @@ -1,12 +0,0 @@ -Logging configured -Logging configured -Logging configured -Logging configured again -Logging configured -Logging configured again -Logging configured -Logging configured again -Logging configured -Logging configured again -Logging configured -Logging configured again diff --git a/devcontainer-admin/.devcontainer.json b/devcontainer-admin/.devcontainer.json index 9fafccdc3..284cfc055 100644 --- a/devcontainer-admin/.devcontainer.json +++ b/devcontainer-admin/.devcontainer.json @@ -23,16 +23,13 @@ } }, "extensions": [ - "ms-python.python", // "ms-python.black-formatter", "donjayamanne.python-extension-pack", - "ms-azuretools.vscode-docker", "ms-python.vscode-pylance", "eamodio.gitlens", "wholroyd.jinja", "pmbenjamin.vscode-snyk", "visualstudioexptteam.vscodeintellicode", "yzhang.markdown-all-in-one", - "ms-ossdata.vscode-postgresql", "GitHub.copilot" ], "forwardPorts": [ diff --git a/devcontainer-admin/scripts/notify-admin-entrypoint.sh b/devcontainer-admin/scripts/notify-admin-entrypoint.sh index eef517d64..11947087b 100755 --- a/devcontainer-admin/scripts/notify-admin-entrypoint.sh +++ b/devcontainer-admin/scripts/notify-admin-entrypoint.sh @@ -7,6 +7,8 @@ set -ex # tools and the filesystem mount enabled should be located here. ################################################################### +echo "RUNNING ENTRYPOINT SCRIPT" + # Define aliases echo -e "\n\n# User's Aliases" >> ~/.zshrc echo -e "alias fd=fdfind" >> ~/.zshrc @@ -29,6 +31,7 @@ pip3 install -r requirements_for_test.txt make generate-version-file # make babel +# npm ci install if [ ! -d "/node_modules" ]; then npm ci install fi @@ -36,4 +39,6 @@ fi npm run build # run flask -# make run \ No newline at end of file +# make run + +echo "FINISHED ENTRYPOINT SCRIPT" diff --git a/manifest.yml b/manifest.yml index 7b0b9704b..68080ef65 100644 --- a/manifest.yml +++ b/manifest.yml @@ -18,22 +18,25 @@ applications: env: NOTIFY_APP_NAME: admin NOTIFY_LOG_PATH: /home/vcap/logs/app.log + NOTIFY_LOG_LEVEL: DEBUG FLASK_APP: application.py FLASK_ENV: production + REDIS_ENABLED: ((REDIS_ENABLED)) NOTIFY_ENVIRONMENT: live # Credentials variables ADMIN_CLIENT_SECRET: ((ADMIN_CLIENT_SECRET)) - ADMIN_BASE_URL: notifications-admin.app.cloud.gov - API_HOST_NAME: notifications-api.app.cloud.gov + ADMIN_CLIENT_USERNAME: ((ADMIN_CLIENT_USERNAME)) + ADMIN_BASE_URL: https://notifications-admin.app.cloud.gov + API_HOST_NAME: https://notifications-api.app.cloud.gov DANGEROUS_SALT: ((DANGEROUS_SALT)) SECRET_KEY: ((SECRET_KEY)) - ROUTE_SECRET_KEY_1: ((ROUTE_SECRET_KEY_1)) - ROUTE_SECRET_KEY_2: ((ROUTE_SECRET_KEY_2)) AWS_REGION: us-west-2 AWS_ACCESS_KEY_ID: ((AWS_ACCESS_KEY_ID)) AWS_SECRET_ACCESS_KEY: ((AWS_SECRET_ACCESS_KEY)) + BASIC_AUTH_USERNAME: ((BASIC_AUTH_USERNAME)) + BASIC_AUTH_PASSWORD: ((BASIC_AUTH_PASSWORD)) NOTIFY_BILLING_DETAILS: [] diff --git a/requirements.in b/requirements.in index dd5ab2761..b1dc387d0 100644 --- a/requirements.in +++ b/requirements.in @@ -10,6 +10,7 @@ wtforms==3.0.1 Flask-Login==0.6.1 Werkzeug==2.1.2 jinja2==3.1.2 +Flask-BasicAuth==0.2.0 blinker==1.4 pyexcel==0.7.0 diff --git a/requirements.txt b/requirements.txt index f3cb81f49..1eba90250 100644 --- a/requirements.txt +++ b/requirements.txt @@ -71,6 +71,8 @@ flask-redis==0.4.0 # via notifications-utils flask-wtf==1.0.1 # via -r requirements.in +Flask-BasicAuth==0.2.0 + # via -r requirements.in gds-metrics @ git+https://github.com/alphagov/gds_metrics_python.git@6f1840a57b6fb1ee40b7e84f2f18ec229de8aa72 # via -r requirements.in geojson==2.5.0 diff --git a/varsfile.sample b/varsfile.sample index 4045654e7..cc0fef4ee 100644 --- a/varsfile.sample +++ b/varsfile.sample @@ -5,3 +5,5 @@ ROUTE_SECRET_KEY_1: asdf ROUTE_SECRET_KEY_2: asdf AWS_ACCESS_KEY_ID: asdf AWS_SECRET_ACCESS_KEY: asdf +BASIC_AUTH_USERNAME: asdf +BASIC_AUTH_PASSWORD: asdf