diff --git a/noggin/__init__.py b/noggin/__init__.py index 6537fedff..9b9502287 100644 --- a/noggin/__init__.py +++ b/noggin/__init__.py @@ -1,7 +1,8 @@ from flask import Flask, Blueprint, request from flask_babel import Babel -from flask_wtf.csrf import CSRFProtect +from flask_healthz import healthz from flask_mail import Mail +from flask_wtf.csrf import CSRFProtect from whitenoise import WhiteNoise from noggin.security.ipa_admin import IPAAdmin @@ -32,6 +33,7 @@ template_folder="themes/" + themename + "/templates/", ) app.register_blueprint(blueprint) +app.register_blueprint(healthz, url_prefix="/healthz") # Flask-Mail mailer = Mail(app) diff --git a/noggin/controller/root.py b/noggin/controller/root.py index 00ddcf580..2fdde88b7 100644 --- a/noggin/controller/root.py +++ b/noggin/controller/root.py @@ -1,6 +1,7 @@ from flask import render_template, request, redirect, url_for, session, jsonify +from flask_healthz import HealthError -from noggin import app +from noggin import app, ipa_admin from noggin.form.register_user import RegisterUserForm from noggin.form.login_user import LoginUserForm from noggin.representation.group import Group @@ -75,3 +76,14 @@ def search_json(ipa): res.append({'cn': group_.name, 'description': group_.description}) return jsonify(res) + + +def liveness(): + pass + + +def readiness(): + try: + ipa_admin.ping() + except Exception: + raise HealthError("Can't connect to the FreeIPA Server") diff --git a/noggin/defaults.cfg b/noggin/defaults.cfg index d52da471d..cc38d89f8 100644 --- a/noggin/defaults.cfg +++ b/noggin/defaults.cfg @@ -16,4 +16,9 @@ HIDE_GROUPS_IN = "hidden_groups" AVATAR_SERVICE_URL = "https://seccdn.libravatar.org/" AVATAR_DEFAULT_TYPE = "robohash" -MAIL_DOMAIN_BLOCKLIST = ['fedoraproject.org'] \ No newline at end of file +MAIL_DOMAIN_BLOCKLIST = ['fedoraproject.org'] + +HEALTHZ = { + "live": "noggin.controller.root.liveness", + "ready": "noggin.controller.root.readiness", +} \ No newline at end of file diff --git a/noggin/security/ipa_admin.py b/noggin/security/ipa_admin.py index 7886fe3cf..e3fa3be69 100644 --- a/noggin/security/ipa_admin.py +++ b/noggin/security/ipa_admin.py @@ -14,6 +14,7 @@ class IPAAdmin(object): "stageuser_add", "stageuser_show", "stageuser_activate", + "ping", ) __WRAPPED_METHODS_TESTING = ( "user_del", diff --git a/noggin/tests/unit/controller/cassettes/test_root/test_healthz_readiness_ok.yaml b/noggin/tests/unit/controller/cassettes/test_root/test_healthz_readiness_ok.yaml new file mode 100644 index 000000000..5724d8bdc --- /dev/null +++ b/noggin/tests/unit/controller/cassettes/test_root/test_healthz_readiness_ok.yaml @@ -0,0 +1,220 @@ +interactions: +- request: + body: user=admin&password=adminPassw0rd%21 + headers: + Accept: + - text/plain + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '36' + Content-Type: + - application/x-www-form-urlencoded + Referer: + - https://ipa.example.com/ipa/session/login_password + User-Agent: + - python-requests/2.23.0 + method: POST + uri: https://ipa.example.com/ipa/session/login_password + response: + body: + string: !!binary | + H4sIAAAAAAAAAwMAAAAAAAAAAAA= + headers: + Cache-Control: + - no-cache, private + Connection: + - Keep-Alive + Content-Encoding: + - gzip + Content-Length: + - '20' + Content-Security-Policy: + - frame-ancestors 'none' + Content-Type: + - text/plain; charset=UTF-8 + Date: + - Wed, 03 Jun 2020 11:41:01 GMT + Keep-Alive: + - timeout=30, max=100 + Server: + - Apache/2.4.43 (Fedora) OpenSSL/1.1.1d mod_wsgi/4.6.6 Python/3.7 mod_auth_gssapi/1.6.1 + Set-Cookie: + - ipa_session=MagBearerToken=NN8eWQkKxtS8Jv2YQzPJUTr5CCvzmFMiGoBXkyPuY%2fFpzPUj5aBFW%2fFidEABif1EHAfRlT6sylDuPfEQ5ld%2b6dgO%2fOk9pSqYOEysRjOlI1a1hz%2fMlprsOn6YUUmMDDdaJzHNJ9V2eX6RtBtjczy%2fpja9GtyclCSkZM2M9jh%2btDuy26yaZ3wwZiEEK9t59bm%2b;path=/ipa;httponly;secure; + Vary: + - Accept-Encoding + X-Frame-Options: + - DENY + status: + code: 200 + message: Success +- request: + body: '{"method": "ping", "params": [[], {}]}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '38' + Content-Type: + - application/json + Cookie: + - ipa_session=MagBearerToken=NN8eWQkKxtS8Jv2YQzPJUTr5CCvzmFMiGoBXkyPuY%2fFpzPUj5aBFW%2fFidEABif1EHAfRlT6sylDuPfEQ5ld%2b6dgO%2fOk9pSqYOEysRjOlI1a1hz%2fMlprsOn6YUUmMDDdaJzHNJ9V2eX6RtBtjczy%2fpja9GtyclCSkZM2M9jh%2btDuy26yaZ3wwZiEEK9t59bm%2b + Referer: + - https://ipa.example.com/ipa + User-Agent: + - python-requests/2.23.0 + method: POST + uri: https://ipa.example.com/ipa/session/json + response: + body: + string: !!binary | + H4sIAAAAAAAAA0yQTWvDMAyG/4rxZZcQ+jHK2Glh9FBYWE5jMMZQYzcYYjtI9koI+e+TE7P2YJAl + Pa9eaZKoKfZBPotJUrQWcORYnppKkMZfjYIfGe/EY/lUHkpRNaf/1K7c7Q+yENJqIug0Mfo1yTAO + OolcAZ1xXWpwYJfUx0rWhihXMpqKSTo3CBftmYdfgYTzgb24UIiLR9ZUovV2gGDOpjdhXOpdBAQX + tFbskHgRVs8LPNC95+JmuvUqjd3uN5stfxUEWM+wYD8ZSMZWZJ6/Z+7TiB4562Lf89eoWzygca0Z + oE8QKDbxcvys6ubtWL6+12nmnehyTzn/AQAA//8DAOyF6AGCAQAA + headers: + Cache-Control: + - no-cache, private + Connection: + - Keep-Alive + Content-Encoding: + - gzip + Content-Security-Policy: + - frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 03 Jun 2020 11:41:01 GMT + Keep-Alive: + - timeout=30, max=99 + Server: + - Apache/2.4.43 (Fedora) OpenSSL/1.1.1d mod_wsgi/4.6.6 Python/3.7 mod_auth_gssapi/1.6.1 + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + X-Frame-Options: + - DENY + status: + code: 200 + message: Success +- request: + body: '{"method": "ping", "params": [[], {}]}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '38' + Content-Type: + - application/json + Cookie: + - ipa_session=MagBearerToken=NN8eWQkKxtS8Jv2YQzPJUTr5CCvzmFMiGoBXkyPuY%2fFpzPUj5aBFW%2fFidEABif1EHAfRlT6sylDuPfEQ5ld%2b6dgO%2fOk9pSqYOEysRjOlI1a1hz%2fMlprsOn6YUUmMDDdaJzHNJ9V2eX6RtBtjczy%2fpja9GtyclCSkZM2M9jh%2btDuy26yaZ3wwZiEEK9t59bm%2b + Referer: + - https://ipa.example.com/ipa + User-Agent: + - python-requests/2.23.0 + method: POST + uri: https://ipa.example.com/ipa/session/json + response: + body: + string: !!binary | + H4sIAAAAAAAAA0yQTWvDMAyG/4rxZZcQ+jHK2Glh9FBYWE5jMMZQYzcYYjtI9koI+e+TE7P2YJAl + Pa9eaZKoKfZBPotJUrQWcORYnppKkMZfjYIfGe/EY/lUHkpRNaf/1K7c7Q+yENJqIug0Mfo1yTAO + OolcAZ1xXWpwYJfUx0rWhihXMpqKSTo3CBftmYdfgYTzgb24UIiLR9ZUovV2gGDOpjdhXOpdBAQX + tFbskHgRVs8LPNC95+JmuvUqjd3uN5stfxUEWM+wYD8ZSMZWZJ6/Z+7TiB4562Lf89eoWzygca0Z + oE8QKDbxcvys6ubtWL6+12nmnehyTzn/AQAA//8DAOyF6AGCAQAA + headers: + Cache-Control: + - no-cache, private + Connection: + - Keep-Alive + Content-Encoding: + - gzip + Content-Security-Policy: + - frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 03 Jun 2020 11:41:01 GMT + Keep-Alive: + - timeout=30, max=98 + Server: + - Apache/2.4.43 (Fedora) OpenSSL/1.1.1d mod_wsgi/4.6.6 Python/3.7 mod_auth_gssapi/1.6.1 + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + X-Frame-Options: + - DENY + status: + code: 200 + message: Success +- request: + body: '{"method": "session_logout", "params": [[], {}]}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '48' + Content-Type: + - application/json + Cookie: + - ipa_session=MagBearerToken=NN8eWQkKxtS8Jv2YQzPJUTr5CCvzmFMiGoBXkyPuY%2fFpzPUj5aBFW%2fFidEABif1EHAfRlT6sylDuPfEQ5ld%2b6dgO%2fOk9pSqYOEysRjOlI1a1hz%2fMlprsOn6YUUmMDDdaJzHNJ9V2eX6RtBtjczy%2fpja9GtyclCSkZM2M9jh%2btDuy26yaZ3wwZiEEK9t59bm%2b + Referer: + - https://ipa.example.com/ipa + User-Agent: + - python-requests/2.23.0 + method: POST + uri: https://ipa.example.com/ipa/session/json + response: + body: + string: !!binary | + H4sIAAAAAAAAA0yPy2rDQAxFf0XMphtj8qKUrmpKFoWaZFUKpRTFo5oBe8ZI44Rg/O/V2AZ3p8e9 + 50qDYZK+ieYZhrX0fdNkYFoSwZpEJ1+DifeOtDI3ZO98bVTgsZ1GH8Tigi+dyLJZrGlZnN9gESi4 + vRDDDQV8iCDkYwa/gZVpoQpth9FdXOPifdrXPTL6SGRzKET6Vulq4ivxg0ACX2dwBrt8t39MyVWw + KXa732y22lqMOD03234WQzpstozj96g6Yg68vu7sWnfsfOU6bJIJrR7xcvwsyvP7MX89lSnzH/SQ + P+UK/QMAAP//AwBCBmNsWAEAAA== + headers: + Cache-Control: + - no-cache, private + Connection: + - Keep-Alive + Content-Encoding: + - gzip + Content-Security-Policy: + - frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 03 Jun 2020 11:41:01 GMT + Keep-Alive: + - timeout=30, max=97 + Server: + - Apache/2.4.43 (Fedora) OpenSSL/1.1.1d mod_wsgi/4.6.6 Python/3.7 mod_auth_gssapi/1.6.1 + Set-Cookie: + - ipa_session=;Max-Age=0;path=/ipa;httponly;secure; + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + X-Frame-Options: + - DENY + status: + code: 200 + message: Success +version: 1 diff --git a/noggin/tests/unit/controller/test_root.py b/noggin/tests/unit/controller/test_root.py index 529f65f3d..2dcae3414 100644 --- a/noggin/tests/unit/controller/test_root.py +++ b/noggin/tests/unit/controller/test_root.py @@ -1,4 +1,5 @@ import pytest +from unittest import mock from bs4 import BeautifulSoup @@ -42,3 +43,29 @@ def test_search_json_empty(client, logged_in_dummy_user): result = client.get('/search/json') assert result.status_code == 200 assert result.json == [] + + +@pytest.mark.vcr() +def test_healthz_liveness(client): + """Test the /healthz/live check endpoint""" + result = client.get('/healthz/live') + assert result.status_code == 200 + assert result.data == b'OK\n' + + +@pytest.mark.vcr() +def test_healthz_readiness_ok(client): + """Test the /healthz/ready check endpoint""" + result = client.get('/healthz/ready') + assert result.status_code == 200 + assert result.data == b'OK\n' + + +@pytest.mark.vcr() +def test_healthz_readiness_not_ok(client): + """Test the /healthz/ready check endpoint when not ready (IPA disabled)""" + with mock.patch("noggin.ipa_admin.ping") as ipaping: + ipaping.side_effect = Exception() + result = client.get('/healthz/ready') + assert result.status_code == 503 + assert result.data == b"Can't connect to the FreeIPA Server\n" diff --git a/poetry.lock b/poetry.lock index 07d415cf8..1d41d9493 100644 --- a/poetry.lock +++ b/poetry.lock @@ -327,6 +327,17 @@ Flask = "*" Jinja2 = ">=2.5" pytz = "*" +[[package]] +category = "main" +description = "A simple module to allow you to easily add health endpoints to your Flask application" +name = "flask-healthz" +optional = false +python-versions = ">=3.6,<4.0" +version = "0.0.1" + +[package.dependencies] +Flask = ">=1.1.1,<2.0.0" + [[package]] category = "main" description = "Flask extension for sending email" @@ -1229,7 +1240,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] deploy = ["gunicorn"] [metadata] -content-hash = "8da63328c6a8af871361316956975074f431e03e9e913119383ef973caa9337d" +content-hash = "5e808d260cad2306b0f7d2024b9a5dd19b0b23175849e64af3eca18d7bfa5bcd" python-versions = "^3.6" [metadata.files] @@ -1413,6 +1424,10 @@ flask-babel = [ {file = "Flask-Babel-1.0.0.tar.gz", hash = "sha256:d6a70468f9a8919d59fba2a291a003da3a05ff884275dddbd965f3b98b09ab3e"}, {file = "Flask_Babel-1.0.0-py3-none-any.whl", hash = "sha256:247f4ec34cf605d03781f480bccb1a5acb719df1d1a2a743c091ab3db5d5fde2"}, ] +flask-healthz = [ + {file = "flask-healthz-0.0.1.tar.gz", hash = "sha256:5c532a6eb773307df62cfca7d85ae62fcaf7db3f348f809163534f188dcac540"}, + {file = "flask_healthz-0.0.1-py3-none-any.whl", hash = "sha256:2115ed79be11c65f39b9cde173dd6fbec40f857f08e8390caf1d43fffe2b5306"}, +] flask-mail = [ {file = "Flask-Mail-0.9.1.tar.gz", hash = "sha256:22e5eb9a940bf407bcf30410ecc3708f3c56cc44b29c34e1726fe85006935f41"}, ] diff --git a/pyproject.toml b/pyproject.toml index 4f481ab48..1a0eb02f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ backoff = "^1.10.0" noggin-messages = "^0.0.1" whitenoise = "^5.0.1" flask-babel = "^1.0.0" +flask-healthz = "^0.0.1" [tool.poetry.dev-dependencies] pytest = "^5.3"