From c779ad2e3b5b7588fcbbdbb13c65da374f6e497b Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sat, 11 Sep 2021 20:54:58 +0200 Subject: [PATCH] *: use common user agent for all outgoing requests Signed-off-by: Jens Langhammer --- authentik/admin/tasks.py | 9 ++- authentik/admin/tests/test_tasks.py | 101 +++++++++--------------- authentik/api/v3/sentry.py | 10 ++- authentik/events/models.py | 8 +- authentik/lib/expression/evaluator.py | 4 +- authentik/lib/utils/http.py | 17 ++++ authentik/policies/hibp/models.py | 4 +- authentik/sources/oauth/clients/base.py | 5 +- authentik/sources/plex/plex.py | 4 +- authentik/stages/captcha/stage.py | 8 +- 10 files changed, 82 insertions(+), 88 deletions(-) diff --git a/authentik/admin/tasks.py b/authentik/admin/tasks.py index 7d1775226..018b05f26 100644 --- a/authentik/admin/tasks.py +++ b/authentik/admin/tasks.py @@ -6,13 +6,14 @@ from django.core.cache import cache from django.core.validators import URLValidator from packaging.version import parse from prometheus_client import Info -from requests import RequestException, get +from requests import RequestException from structlog.stdlib import get_logger from authentik import ENV_GIT_HASH_KEY, __version__ from authentik.events.models import Event, EventAction from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus from authentik.lib.config import CONFIG +from authentik.lib.utils.http import get_http_session from authentik.root.celery import CELERY_APP LOGGER = get_logger() @@ -42,7 +43,9 @@ def update_latest_version(self: MonitoredTask): self.set_status(TaskResult(TaskResultStatus.WARNING, messages=["Version check disabled."])) return try: - response = get("https://version.goauthentik.io/version.json") + response = get_http_session().get( + "https://version.goauthentik.io/version.json", + ) response.raise_for_status() data = response.json() upstream_version = data.get("stable", {}).get("version") @@ -62,7 +65,7 @@ def update_latest_version(self: MonitoredTask): ).exists(): return event_dict = {"new_version": upstream_version} - if match := re.search(URL_FINDER, data.get("body", "")): + if match := re.search(URL_FINDER, data.get("stable", {}).get("changelog", "")): event_dict["message"] = f"Changelog: {match.group()}" Event.new(EventAction.UPDATE_AVAILABLE, **event_dict).save() except (RequestException, IndexError) as exc: diff --git a/authentik/admin/tests/test_tasks.py b/authentik/admin/tests/test_tasks.py index 39d488d2b..2cd3a6d20 100644 --- a/authentik/admin/tests/test_tasks.py +++ b/authentik/admin/tests/test_tasks.py @@ -1,85 +1,58 @@ """test admin tasks""" -import json -from dataclasses import dataclass -from unittest.mock import Mock, patch - from django.core.cache import cache from django.test import TestCase -from requests.exceptions import RequestException +from requests_mock import Mocker from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version from authentik.events.models import Event, EventAction - -@dataclass -class MockResponse: - """Mock class to emulate the methods of requests's Response we need""" - - status_code: int - response: str - - def json(self) -> dict: - """Get json parsed response""" - return json.loads(self.response) - - def raise_for_status(self): - """raise RequestException if status code is 400 or more""" - if self.status_code >= 400: - raise RequestException - - -REQUEST_MOCK_VALID = Mock( - return_value=MockResponse( - 200, - """{ - "$schema": "https://version.goauthentik.io/schema.json", - "stable": { - "version": "99999999.9999999", - "changelog": "See https://goauthentik.io/test", - "reason": "bugfix" - } - }""", - ) -) - -REQUEST_MOCK_INVALID = Mock(return_value=MockResponse(400, "{}")) +RESPONSE_VALID = { + "$schema": "https://version.goauthentik.io/schema.json", + "stable": { + "version": "99999999.9999999", + "changelog": "See https://goauthentik.io/test", + "reason": "bugfix", + }, +} class TestAdminTasks(TestCase): """test admin tasks""" - @patch("authentik.admin.tasks.get", REQUEST_MOCK_VALID) def test_version_valid_response(self): """Test Update checker with valid response""" - update_latest_version.delay().get() - self.assertEqual(cache.get(VERSION_CACHE_KEY), "99999999.9999999") - self.assertTrue( - Event.objects.filter( - action=EventAction.UPDATE_AVAILABLE, - context__new_version="99999999.9999999", - context__message="Changelog: https://goauthentik.io/test", - ).exists() - ) - # test that a consecutive check doesn't create a duplicate event - update_latest_version.delay().get() - self.assertEqual( - len( + with Mocker() as mocker: + mocker.get("https://version.goauthentik.io/version.json", json=RESPONSE_VALID) + update_latest_version.delay().get() + self.assertEqual(cache.get(VERSION_CACHE_KEY), "99999999.9999999") + self.assertTrue( Event.objects.filter( action=EventAction.UPDATE_AVAILABLE, context__new_version="99999999.9999999", context__message="Changelog: https://goauthentik.io/test", - ) - ), - 1, - ) + ).exists() + ) + # test that a consecutive check doesn't create a duplicate event + update_latest_version.delay().get() + self.assertEqual( + len( + Event.objects.filter( + action=EventAction.UPDATE_AVAILABLE, + context__new_version="99999999.9999999", + context__message="Changelog: https://goauthentik.io/test", + ) + ), + 1, + ) - @patch("authentik.admin.tasks.get", REQUEST_MOCK_INVALID) def test_version_error(self): """Test Update checker with invalid response""" - update_latest_version.delay().get() - self.assertEqual(cache.get(VERSION_CACHE_KEY), "0.0.0") - self.assertFalse( - Event.objects.filter( - action=EventAction.UPDATE_AVAILABLE, context__new_version="0.0.0" - ).exists() - ) + with Mocker() as mocker: + mocker.get("https://version.goauthentik.io/version.json", status_code=400) + update_latest_version.delay().get() + self.assertEqual(cache.get(VERSION_CACHE_KEY), "0.0.0") + self.assertFalse( + Event.objects.filter( + action=EventAction.UPDATE_AVAILABLE, context__new_version="0.0.0" + ).exists() + ) diff --git a/authentik/api/v3/sentry.py b/authentik/api/v3/sentry.py index 60b0d6738..89b5d530b 100644 --- a/authentik/api/v3/sentry.py +++ b/authentik/api/v3/sentry.py @@ -4,7 +4,6 @@ from json import loads from django.conf import settings from django.http.request import HttpRequest from django.http.response import HttpResponse -from requests import post from requests.exceptions import RequestException from rest_framework.authentication import SessionAuthentication from rest_framework.parsers import BaseParser @@ -14,6 +13,9 @@ from rest_framework.throttling import AnonRateThrottle from rest_framework.views import APIView from authentik.lib.config import CONFIG +from authentik.lib.utils.http import get_http_session + +SENTRY_SESSION = get_http_session() class PlainTextParser(BaseParser): @@ -54,10 +56,12 @@ class SentryTunnelView(APIView): dsn = header.get("dsn", "") if dsn != settings.SENTRY_DSN: return HttpResponse(status=400) - response = post( + response = SENTRY_SESSION.post( "https://sentry.beryju.org/api/8/envelope/", data=full_body, - headers={"Content-Type": "application/octet-stream"}, + headers={ + "Content-Type": "application/octet-stream", + }, ) try: response.raise_for_status() diff --git a/authentik/events/models.py b/authentik/events/models.py index 243496c20..bab7d1bfd 100644 --- a/authentik/events/models.py +++ b/authentik/events/models.py @@ -10,7 +10,7 @@ from django.db import models from django.http import HttpRequest from django.utils.timezone import now from django.utils.translation import gettext as _ -from requests import RequestException, post +from requests import RequestException from structlog.stdlib import get_logger from authentik import __version__ @@ -19,7 +19,7 @@ from authentik.core.models import ExpiringModel, Group, User from authentik.events.geo import GEOIP_READER from authentik.events.utils import cleanse_dict, get_user, model_to_dict, sanitize_dict from authentik.lib.sentry import SentryIgnoredException -from authentik.lib.utils.http import get_client_ip +from authentik.lib.utils.http import get_client_ip, get_http_session from authentik.lib.utils.time import timedelta_from_string from authentik.policies.models import PolicyBindingModel from authentik.stages.email.utils import TemplateEmailMessage @@ -240,7 +240,7 @@ class NotificationTransport(models.Model): def send_webhook(self, notification: "Notification") -> list[str]: """Send notification to generic webhook""" try: - response = post( + response = get_http_session().post( self.webhook_url, json={ "body": notification.body, @@ -297,7 +297,7 @@ class NotificationTransport(models.Model): if notification.event: body["attachments"][0]["title"] = notification.event.action try: - response = post(self.webhook_url, json=body) + response = get_http_session().post(self.webhook_url, json=body) response.raise_for_status() except RequestException as exc: text = exc.response.text if exc.response else str(exc) diff --git a/authentik/lib/expression/evaluator.py b/authentik/lib/expression/evaluator.py index 452eb4a85..fe40556b5 100644 --- a/authentik/lib/expression/evaluator.py +++ b/authentik/lib/expression/evaluator.py @@ -4,13 +4,13 @@ from textwrap import indent from typing import Any, Iterable, Optional from django.core.exceptions import FieldError -from requests import Session from rest_framework.serializers import ValidationError from sentry_sdk.hub import Hub from sentry_sdk.tracing import Span from structlog.stdlib import get_logger from authentik.core.models import User +from authentik.lib.utils.http import get_http_session LOGGER = get_logger() @@ -35,7 +35,7 @@ class BaseEvaluator: "ak_is_group_member": BaseEvaluator.expr_is_group_member, "ak_user_by": BaseEvaluator.expr_user_by, "ak_logger": get_logger(), - "requests": Session(), + "requests": get_http_session(), } self._context = {} self._filename = "BaseEvalautor" diff --git a/authentik/lib/utils/http.py b/authentik/lib/utils/http.py index 53d823d2c..deb1a2ffb 100644 --- a/authentik/lib/utils/http.py +++ b/authentik/lib/utils/http.py @@ -1,9 +1,13 @@ """http helpers""" +from os import environ from typing import Any, Optional from django.http import HttpRequest +from requests.sessions import Session from structlog.stdlib import get_logger +from authentik import ENV_GIT_HASH_KEY, __version__ + OUTPOST_REMOTE_IP_HEADER = "HTTP_X_AUTHENTIK_REMOTE_IP" OUTPOST_TOKEN_HEADER = "HTTP_X_AUTHENTIK_OUTPOST_TOKEN" # nosec DEFAULT_IP = "255.255.255.255" @@ -60,3 +64,16 @@ def get_client_ip(request: Optional[HttpRequest]) -> str: if override: return override return _get_client_ip_from_meta(request.META) + + +def authentik_user_agent() -> str: + """Get a common user agent""" + build = environ.get(ENV_GIT_HASH_KEY, "tagged") + return f"authentik@{__version__} (build={build})" + + +def get_http_session() -> Session: + """Get a requests session with common headers""" + session = Session() + session.headers["User-Agent"] = authentik_user_agent() + return session diff --git a/authentik/policies/hibp/models.py b/authentik/policies/hibp/models.py index 13b90a049..7fbf16c28 100644 --- a/authentik/policies/hibp/models.py +++ b/authentik/policies/hibp/models.py @@ -3,10 +3,10 @@ from hashlib import sha1 from django.db import models from django.utils.translation import gettext as _ -from requests import get from rest_framework.serializers import BaseSerializer from structlog.stdlib import get_logger +from authentik.lib.utils.http import get_http_session from authentik.policies.models import Policy, PolicyResult from authentik.policies.types import PolicyRequest @@ -49,7 +49,7 @@ class HaveIBeenPwendPolicy(Policy): pw_hash = sha1(password.encode("utf-8")).hexdigest() # nosec url = f"https://api.pwnedpasswords.com/range/{pw_hash[:5]}" - result = get(url).text + result = get_http_session().get(url).text final_count = 0 for line in result.split("\r\n"): full_hash, count = line.split(":") diff --git a/authentik/sources/oauth/clients/base.py b/authentik/sources/oauth/clients/base.py index e57fb68b3..df1e07054 100644 --- a/authentik/sources/oauth/clients/base.py +++ b/authentik/sources/oauth/clients/base.py @@ -8,8 +8,8 @@ from requests.exceptions import RequestException from requests.models import Response from structlog.stdlib import get_logger -from authentik import __version__ from authentik.events.models import Event, EventAction +from authentik.lib.utils.http import get_http_session from authentik.sources.oauth.models import OAuthSource LOGGER = get_logger() @@ -27,10 +27,9 @@ class BaseOAuthClient: def __init__(self, source: OAuthSource, request: HttpRequest, callback: Optional[str] = None): self.source = source - self.session = Session() + self.session = get_http_session() self.request = request self.callback = callback - self.session.headers.update({"User-Agent": f"authentik {__version__}"}) def get_access_token(self, **request_kwargs) -> Optional[dict[str, Any]]: "Fetch access token from callback request." diff --git a/authentik/sources/plex/plex.py b/authentik/sources/plex/plex.py index 81cfe2f0e..f1f91e395 100644 --- a/authentik/sources/plex/plex.py +++ b/authentik/sources/plex/plex.py @@ -2,12 +2,12 @@ from urllib.parse import urlencode from django.http.response import Http404 -from requests import Session from requests.exceptions import RequestException from structlog.stdlib import get_logger from authentik import __version__ from authentik.core.sources.flow_manager import SourceFlowManager +from authentik.lib.utils.http import get_http_session from authentik.sources.plex.models import PlexSource, PlexSourceConnection LOGGER = get_logger() @@ -24,7 +24,7 @@ class PlexAuth: def __init__(self, source: PlexSource, token: str): self._source = source self._token = token - self._session = Session() + self._session = get_http_session() self._session.headers.update( {"Accept": "application/json", "Content-Type": "application/json"} ) diff --git a/authentik/stages/captcha/stage.py b/authentik/stages/captcha/stage.py index 28c89b888..5884d5f23 100644 --- a/authentik/stages/captcha/stage.py +++ b/authentik/stages/captcha/stage.py @@ -1,11 +1,10 @@ """authentik captcha stage""" from django.http.response import HttpResponse -from requests import RequestException, post +from requests import RequestException from rest_framework.fields import CharField from rest_framework.serializers import ValidationError -from authentik import __version__ from authentik.flows.challenge import ( Challenge, ChallengeResponse, @@ -13,7 +12,7 @@ from authentik.flows.challenge import ( WithUserInfoChallenge, ) from authentik.flows.stage import ChallengeStageView -from authentik.lib.utils.http import get_client_ip +from authentik.lib.utils.http import get_client_ip, get_http_session from authentik.stages.captcha.models import CaptchaStage @@ -34,11 +33,10 @@ class CaptchaChallengeResponse(ChallengeResponse): """Validate captcha token""" stage: CaptchaStage = self.stage.executor.current_stage try: - response = post( + response = get_http_session().post( "https://www.google.com/recaptcha/api/siteverify", headers={ "Content-type": "application/x-www-form-urlencoded", - "User-agent": f"authentik {__version__} ReCaptcha", }, data={ "secret": stage.private_key,