diff --git a/authentik/api/tasks.py b/authentik/api/tasks.py deleted file mode 100644 index 727a948da..000000000 --- a/authentik/api/tasks.py +++ /dev/null @@ -1,19 +0,0 @@ -"""API tasks""" - -from authentik.lib.utils.http import get_http_session -from authentik.root.celery import CELERY_APP - -SENTRY_SESSION = get_http_session() - - -@CELERY_APP.task() -def sentry_proxy(payload: str): - """Relay data to sentry""" - SENTRY_SESSION.post( - "https://sentry.beryju.org/api/8/envelope/", - data=payload, - headers={ - "Content-Type": "application/octet-stream", - }, - timeout=10, - ) diff --git a/authentik/api/v3/sentry.py b/authentik/api/v3/sentry.py deleted file mode 100644 index 273c3e80c..000000000 --- a/authentik/api/v3/sentry.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Sentry tunnel""" -from json import loads - -from django.conf import settings -from django.http.request import HttpRequest -from django.http.response import HttpResponse -from rest_framework.authentication import SessionAuthentication -from rest_framework.parsers import BaseParser -from rest_framework.permissions import AllowAny -from rest_framework.request import Request -from rest_framework.throttling import AnonRateThrottle -from rest_framework.views import APIView -from structlog.stdlib import get_logger - -from authentik.api.tasks import sentry_proxy -from authentik.lib.config import CONFIG - -LOGGER = get_logger() - - -class PlainTextParser(BaseParser): - """Plain text parser.""" - - media_type = "text/plain" - - def parse(self, stream, media_type=None, parser_context=None) -> str: - """Simply return a string representing the body of the request.""" - return stream.read() - - -class CsrfExemptSessionAuthentication(SessionAuthentication): - """CSRF-exempt Session authentication""" - - def enforce_csrf(self, request: Request): - return # To not perform the csrf check previously happening - - -class SentryTunnelView(APIView): - """Sentry tunnel, to prevent ad blockers from blocking sentry""" - - serializer_class = None - parser_classes = [PlainTextParser] - throttle_classes = [AnonRateThrottle] - permission_classes = [AllowAny] - authentication_classes = [CsrfExemptSessionAuthentication] - - def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: - """Sentry tunnel, to prevent ad blockers from blocking sentry""" - # Only allow usage of this endpoint when error reporting is enabled - if not CONFIG.y_bool("error_reporting.enabled", False): - LOGGER.debug("error reporting disabled") - return HttpResponse(status=400) - # Body is 2 json objects separated by \n - full_body = request.body - lines = full_body.splitlines() - if len(lines) < 1: - return HttpResponse(status=400) - header = loads(lines[0]) - # Check that the DSN is what we expect - dsn = header.get("dsn", "") - if dsn != settings.SENTRY_DSN: - LOGGER.debug("Invalid dsn", have=dsn, expected=settings.SENTRY_DSN) - return HttpResponse(status=400) - sentry_proxy.delay(full_body.decode()) - return HttpResponse(status=204) diff --git a/authentik/api/v3/urls.py b/authentik/api/v3/urls.py index 00af22cc9..d3e645dd9 100644 --- a/authentik/api/v3/urls.py +++ b/authentik/api/v3/urls.py @@ -11,7 +11,6 @@ from authentik.admin.api.tasks import TaskViewSet from authentik.admin.api.version import VersionView from authentik.admin.api.workers import WorkerView from authentik.api.v3.config import ConfigView -from authentik.api.v3.sentry import SentryTunnelView from authentik.api.views import APIBrowserView from authentik.core.api.applications import ApplicationViewSet from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet @@ -249,7 +248,6 @@ urlpatterns = ( FlowInspectorView.as_view(), name="flow-inspector", ), - path("sentry/", SentryTunnelView.as_view(), name="sentry"), path("schema/", cache_page(86400)(SpectacularAPIView.as_view()), name="schema"), ] ) diff --git a/cmd/server/main.go b/cmd/server/main.go index 453b4b11c..1a83f391a 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -38,7 +38,7 @@ func main() { if config.G.ErrorReporting.Enabled { err := sentry.Init(sentry.ClientOptions{ - Dsn: "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8", + Dsn: config.G.ErrorReporting.DSN, AttachStacktrace: true, TracesSampleRate: 0.6, Release: fmt.Sprintf("authentik@%s", constants.VERSION), diff --git a/internal/config/config.go b/internal/config/config.go index 8aba4f74f..ec75cab0a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -26,6 +26,7 @@ func DefaultConfig() { LogLevel: "info", ErrorReporting: ErrorReportingConfig{ Enabled: false, + DSN: "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8", }, } } diff --git a/internal/config/struct.go b/internal/config/struct.go index 242483c72..0c2ccee4d 100644 --- a/internal/config/struct.go +++ b/internal/config/struct.go @@ -42,4 +42,5 @@ type ErrorReportingConfig struct { Enabled bool `yaml:"enabled" env:"AUTHENTIK_ERROR_REPORTING__ENABLED"` Environment string `yaml:"environment" env:"AUTHENTIK_ERROR_REPORTING__ENVIRONMENT"` SendPII bool `yaml:"send_pii" env:"AUTHENTIK_ERROR_REPORTING__SEND_PII"` + DSN string } diff --git a/internal/web/sentry_proxy.go b/internal/web/sentry_proxy.go new file mode 100644 index 000000000..14b706e73 --- /dev/null +++ b/internal/web/sentry_proxy.go @@ -0,0 +1,53 @@ +package web + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "strings" + + "goauthentik.io/internal/config" +) + +type SentryRequest struct { + DSN string `json:"dsn"` +} + +func (ws *WebServer) APISentryProxy(rw http.ResponseWriter, r *http.Request) { + if !config.G.ErrorReporting.Enabled { + ws.log.Debug("error reporting disabled") + rw.WriteHeader(http.StatusBadRequest) + return + } + fullBody, err := ioutil.ReadAll(r.Body) + if err != nil { + ws.log.Debug("failed to read body") + rw.WriteHeader(http.StatusBadRequest) + return + } + lines := strings.Split(string(fullBody), "\n") + if len(lines) < 1 { + rw.WriteHeader(http.StatusBadRequest) + return + } + sd := SentryRequest{} + ws.log.Debug(lines[0]) + err = json.Unmarshal([]byte(lines[0]), &sd) + if err != nil { + ws.log.WithError(err).Warning("failed to parse sentry request") + rw.WriteHeader(http.StatusBadRequest) + return + } + if sd.DSN != config.G.ErrorReporting.DSN { + ws.log.WithField("have", sd.DSN).WithField("expected", config.G.ErrorReporting.DSN).Debug("invalid DSN") + rw.WriteHeader(http.StatusBadRequest) + return + } + res, err := http.DefaultClient.Post("https://sentry.beryju.org/api/8/envelope/", "application/octet-stream", strings.NewReader(string(fullBody))) + if err != nil { + ws.log.WithError(err).Warning("failed to proxy sentry") + rw.WriteHeader(http.StatusBadRequest) + return + } + rw.WriteHeader(res.StatusCode) +} diff --git a/internal/web/web.go b/internal/web/web.go index c94580676..f265a2b6a 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -51,10 +51,15 @@ func NewWebServer(g *gounicorn.GoUnicorn) *WebServer { p: g, } ws.configureStatic() + ws.configureRoutes() ws.configureProxy() return ws } +func (ws *WebServer) configureRoutes() { + ws.m.Path("/api/v3/sentry/").HandlerFunc(ws.APISentryProxy) +} + func (ws *WebServer) Start() { go ws.listenPlain() go ws.listenTLS()