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

root: support running authentik in subpath #8675

Merged
merged 25 commits into from
Nov 26, 2024
Merged
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
4 changes: 2 additions & 2 deletions authentik/brands/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ class CurrentBrandSerializer(PassiveSerializer):

matched_domain = CharField(source="domain")
branding_title = CharField()
branding_logo = CharField()
branding_favicon = CharField()
branding_logo = CharField(source="branding_logo_url")
branding_favicon = CharField(source="branding_favicon_url")
ui_footer_links = ListField(
child=FooterLinkSerializer(),
read_only=True,
Expand Down
13 changes: 13 additions & 0 deletions authentik/brands/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow
from authentik.lib.config import CONFIG
from authentik.lib.models import SerializerModel

LOGGER = get_logger()
Expand Down Expand Up @@ -71,6 +72,18 @@
)
attributes = models.JSONField(default=dict, blank=True)

def branding_logo_url(self) -> str:
"""Get branding_logo with the correct prefix"""
if self.branding_logo.startswith("/static"):
return CONFIG.get("web.path", "/")[:-1] + self.branding_logo
return self.branding_logo

Check warning on line 79 in authentik/brands/models.py

View check run for this annotation

Codecov / codecov/patch

authentik/brands/models.py#L79

Added line #L79 was not covered by tests

def branding_favicon_url(self) -> str:
"""Get branding_favicon with the correct prefix"""
if self.branding_favicon.startswith("/static"):
return CONFIG.get("web.path", "/")[:-1] + self.branding_favicon
return self.branding_favicon

Check warning on line 85 in authentik/brands/models.py

View check run for this annotation

Codecov / codecov/patch

authentik/brands/models.py#L85

Added line #L85 was not covered by tests

@property
def serializer(self) -> Serializer:
from authentik.brands.api import BrandSerializer
Expand Down
3 changes: 3 additions & 0 deletions authentik/core/templates/base/header_js.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
versionFamily: "{{ version_family }}",
versionSubdomain: "{{ version_subdomain }}",
build: "{{ build }}",
api: {
base: "{{ base_url }}",
},
};
window.addEventListener("DOMContentLoaded", function () {
{% for message in messages %}
Expand Down
4 changes: 2 additions & 2 deletions authentik/core/templates/base/skeleton.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
<link rel="icon" href="{{ brand.branding_favicon }}">
<link rel="shortcut icon" href="{{ brand.branding_favicon }}">
<link rel="icon" href="{{ brand.branding_favicon_url }}">
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}">
{% block head_before %}
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
Expand Down
6 changes: 3 additions & 3 deletions authentik/core/templates/login/base_full.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{% load i18n %}

{% block head_before %}
<link rel="prefetch" href="/static/dist/assets/images/flow_background.jpg" />
<link rel="prefetch" href="{% static 'dist/assets/images/flow_background.jpg' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
{% include "base/header_js.html" %}
Expand All @@ -13,7 +13,7 @@
{% block head %}
<style>
:root {
--ak-flow-background: url("/static/dist/assets/images/flow_background.jpg");
--ak-flow-background: url("{% static 'dist/assets/images/flow_background.jpg' %}");
--pf-c-background-image--BackgroundImage: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background);
Expand Down Expand Up @@ -50,7 +50,7 @@
<div class="ak-login-container">
<main class="pf-c-login__main">
<div class="pf-c-login__main-header pf-c-brand ak-brand">
<img src="{{ brand.branding_logo }}" alt="authentik Logo" />
<img src="{{ brand.branding_logo_url }}" alt="authentik Logo" />
</div>
<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
Expand Down
2 changes: 2 additions & 0 deletions authentik/core/views/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from authentik.brands.api import CurrentBrandSerializer
from authentik.brands.models import Brand
from authentik.core.models import UserTypes
from authentik.lib.config import CONFIG
from authentik.policies.denied import AccessDeniedResponse


Expand Down Expand Up @@ -51,6 +52,7 @@ def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
kwargs["version_subdomain"] = f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}"
kwargs["build"] = get_build_hash()
kwargs["url_kwargs"] = self.kwargs
kwargs["base_url"] = self.request.build_absolute_uri(CONFIG.get("web.path", "/"))
return super().get_context_data(**kwargs)


Expand Down
4 changes: 2 additions & 2 deletions authentik/enterprise/providers/rac/templates/if/rac.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
<script src="{% versioned_script 'dist/enterprise/rac/index-%v.js' %}" type="module"></script>
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<link rel="icon" href="{{ tenant.branding_favicon }}">
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
<link rel="icon" href="{{ tenant.branding_favicon_url }}">
<link rel="shortcut icon" href="{{ tenant.branding_favicon_url }}">
{% include "base/header_js.html" %}
{% endblock %}

Expand Down
9 changes: 7 additions & 2 deletions authentik/flows/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from authentik.core.models import Token
from authentik.core.types import UserSettingSerializer
from authentik.flows.challenge import FlowLayout
from authentik.lib.config import CONFIG
from authentik.lib.models import InheritanceForeignKey, SerializerModel
from authentik.lib.utils.reflection import class_to_path
from authentik.policies.models import PolicyBindingModel
Expand Down Expand Up @@ -177,9 +178,13 @@
"""Get the URL to the background image. If the name is /static or starts with http
it is returned as-is"""
if not self.background:
return "/static/dist/assets/images/flow_background.jpg"
if self.background.name.startswith("http") or self.background.name.startswith("/static"):
return (
CONFIG.get("web.path", "/")[:-1] + "/static/dist/assets/images/flow_background.jpg"
)
if self.background.name.startswith("http"):

Check warning on line 184 in authentik/flows/models.py

View check run for this annotation

Codecov / codecov/patch

authentik/flows/models.py#L184

Added line #L184 was not covered by tests
return self.background.name
if self.background.name.startswith("/static"):
return CONFIG.get("web.path", "/")[:-1] + self.background.name

Check warning on line 187 in authentik/flows/models.py

View check run for this annotation

Codecov / codecov/patch

authentik/flows/models.py#L186-L187

Added lines #L186 - L187 were not covered by tests
return self.background.url

stages = models.ManyToManyField(Stage, through="FlowStageBinding", blank=True)
Expand Down
4 changes: 2 additions & 2 deletions authentik/flows/templates/if/flow-sfe.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
<link rel="icon" href="{{ brand.branding_favicon }}">
<link rel="shortcut icon" href="{{ brand.branding_favicon }}">
<link rel="icon" href="{{ brand.branding_favicon_url }}">
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}">
{% block head_before %}
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/sfe/bootstrap.min.css' %}">
Expand Down
1 change: 1 addition & 0 deletions authentik/lib/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ web:
# No default here as it's set dynamically
# workers: 2
threads: 4
path: /

worker:
concurrency: 2
Expand Down
3 changes: 2 additions & 1 deletion authentik/lib/sentry.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from authentik.lib.utils.reflection import get_env

LOGGER = get_logger()
_root_path = CONFIG.get("web.path", "/")


class SentryIgnoredException(Exception):
Expand Down Expand Up @@ -90,7 +91,7 @@ def traces_sampler(sampling_context: dict) -> float:
path = sampling_context.get("asgi_scope", {}).get("path", "")
_type = sampling_context.get("asgi_scope", {}).get("type", "")
# Ignore all healthcheck routes
if path.startswith("/-/health") or path.startswith("/-/metrics"):
if path.startswith(f"{_root_path}-/health") or path.startswith(f"{_root_path}-/metrics"):
return 0
if _type == "websocket":
return 0
Expand Down
4 changes: 3 additions & 1 deletion authentik/root/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
# Custom user model
AUTH_USER_MODEL = "authentik_core.User"

CSRF_COOKIE_PATH = LANGUAGE_COOKIE_PATH = SESSION_COOKIE_PATH = CONFIG.get("web.path", "/")

CSRF_COOKIE_NAME = "authentik_csrf"
CSRF_HEADER_NAME = "HTTP_X_AUTHENTIK_CSRF"
LANGUAGE_COOKIE_NAME = "authentik_language"
Expand Down Expand Up @@ -427,7 +429,7 @@
# https://docs.djangoproject.com/en/2.1/howto/static-files/

STATICFILES_DIRS = [BASE_DIR / Path("web")]
STATIC_URL = "/static/"
STATIC_URL = CONFIG.get("web.path", "/") + "static/"

STORAGES = {
"staticfiles": {
Expand Down
9 changes: 6 additions & 3 deletions authentik/root/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from structlog.stdlib import get_logger

from authentik.core.views import error
from authentik.lib.config import CONFIG
from authentik.lib.utils.reflection import get_apps
from authentik.root.monitoring import LiveView, MetricsView, ReadyView

Expand All @@ -14,7 +15,7 @@
handler404 = error.NotFoundView.as_view()
handler500 = error.ServerErrorView.as_view()

urlpatterns = []
_urlpatterns = []

for _authentik_app in get_apps():
mountpoints = None
Expand All @@ -35,16 +36,18 @@
namespace=namespace,
),
)
urlpatterns.append(_path)
_urlpatterns.append(_path)
LOGGER.debug(
"Mounted URLs",
app_name=_authentik_app.name,
app_mountpoint=mountpoint,
namespace=namespace,
)

urlpatterns += [
_urlpatterns += [
path("-/metrics/", MetricsView.as_view(), name="metrics"),
path("-/health/live/", LiveView.as_view(), name="health-live"),
path("-/health/ready/", ReadyView.as_view(), name="health-ready"),
]

urlpatterns = [path(CONFIG.get("web.path", "/")[1:], include(_urlpatterns))]
14 changes: 12 additions & 2 deletions authentik/root/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

from importlib import import_module

from channels.routing import URLRouter
from django.urls import path
from structlog.stdlib import get_logger

from authentik.lib.config import CONFIG
from authentik.lib.utils.reflection import get_apps

LOGGER = get_logger()

websocket_urlpatterns = []
_websocket_urlpatterns = []
for _authentik_app in get_apps():
try:
api_urls = import_module(f"{_authentik_app.name}.urls")
Expand All @@ -17,8 +20,15 @@
if not hasattr(api_urls, "websocket_urlpatterns"):
continue
urls: list = api_urls.websocket_urlpatterns
websocket_urlpatterns.extend(urls)
_websocket_urlpatterns.extend(urls)
LOGGER.debug(
"Mounted Websocket URLs",
app_name=_authentik_app.name,
)

websocket_urlpatterns = [
path(
CONFIG.get("web.path", "/")[1:],
URLRouter(_websocket_urlpatterns),
),
]
2 changes: 1 addition & 1 deletion cmd/server/healthcheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func checkServer() int {
h := &http.Client{
Transport: web.NewUserAgentTransport("goauthentik.io/healthcheck", http.DefaultTransport),
}
url := fmt.Sprintf("http://%s/-/health/live/", config.Get().Listen.HTTP)
url := fmt.Sprintf("http://%s%s-/health/live/", config.Get().Listen.HTTP, config.Get().Web.Path)
res, err := h.Head(url)
if err != nil {
log.WithError(err).Warning("failed to send healthcheck request")
Expand Down
2 changes: 1 addition & 1 deletion cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ var rootCmd = &cobra.Command{
ex := common.Init()
defer common.Defer()

u, err := url.Parse(fmt.Sprintf("http://%s", config.Get().Listen.HTTP))
u, err := url.Parse(fmt.Sprintf("http://%s%s", config.Get().Listen.HTTP, config.Get().Web.Path))
if err != nil {
panic(err)
}
Expand Down
5 changes: 5 additions & 0 deletions internal/config/struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Config struct {
// Config for both core and outposts
Debug bool `yaml:"debug" env:"AUTHENTIK_DEBUG, overwrite"`
Listen ListenConfig `yaml:"listen" env:", prefix=AUTHENTIK_LISTEN__"`
Web WebConfig `yaml:"web" env:", prefix=AUTHENTIK_WEB__"`

// Outpost specific config
// These are only relevant for proxy/ldap outposts, and cannot be set via YAML
Expand Down Expand Up @@ -72,3 +73,7 @@ type OutpostConfig struct {
Discover bool `yaml:"discover" env:"DISCOVER, overwrite"`
DisableEmbeddedOutpost bool `yaml:"disable_embedded_outpost" env:"DISABLE_EMBEDDED_OUTPOST, overwrite"`
}

type WebConfig struct {
Path string `yaml:"path" env:"PATH, overwrite"`
}
17 changes: 11 additions & 6 deletions internal/outpost/ak/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ type APIController struct {
func NewAPIController(akURL url.URL, token string) *APIController {
rsp := sentry.StartSpan(context.Background(), "authentik.outposts.init")

config := api.NewConfiguration()
config.Host = akURL.Host
config.Scheme = akURL.Scheme
config.HTTPClient = &http.Client{
apiConfig := api.NewConfiguration()
apiConfig.Host = akURL.Host
apiConfig.Scheme = akURL.Scheme
apiConfig.HTTPClient = &http.Client{
Transport: web.NewUserAgentTransport(
constants.OutpostUserAgent(),
web.NewTracingTransport(
Expand All @@ -68,10 +68,15 @@ func NewAPIController(akURL url.URL, token string) *APIController {
),
),
}
config.AddDefaultHeader("Authorization", fmt.Sprintf("Bearer %s", token))
apiConfig.Servers = api.ServerConfigurations{
{
URL: fmt.Sprintf("%sapi/v3", akURL.Path),
},
}
apiConfig.AddDefaultHeader("Authorization", fmt.Sprintf("Bearer %s", token))

// create the API client, with the transport
apiClient := api.NewAPIClient(config)
apiClient := api.NewAPIClient(apiConfig)

log := log.WithField("logger", "authentik.outpost.ak-api-controller")

Expand Down
5 changes: 3 additions & 2 deletions internal/outpost/ak/api_ws.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
)

func (ac *APIController) initWS(akURL url.URL, outpostUUID string) error {
pathTemplate := "%s://%s/ws/outpost/%s/?%s"
pathTemplate := "%s://%s%sws/outpost/%s/?%s"
query := akURL.Query()
query.Set("instance_uuid", ac.instanceUUID.String())
scheme := strings.ReplaceAll(akURL.Scheme, "http", "ws")
Expand All @@ -37,7 +37,7 @@ func (ac *APIController) initWS(akURL url.URL, outpostUUID string) error {
},
}

ws, _, err := dialer.Dial(fmt.Sprintf(pathTemplate, scheme, akURL.Host, outpostUUID, akURL.Query().Encode()), header)
ws, _, err := dialer.Dial(fmt.Sprintf(pathTemplate, scheme, akURL.Host, akURL.Path, outpostUUID, akURL.Query().Encode()), header)
if err != nil {
ac.logger.WithError(err).Warning("failed to connect websocket")
return err
Expand Down Expand Up @@ -83,6 +83,7 @@ func (ac *APIController) reconnectWS() {
u := url.URL{
Host: ac.Client.GetConfig().Host,
Scheme: ac.Client.GetConfig().Scheme,
Path: strings.ReplaceAll(ac.Client.GetConfig().Servers[0].URL, "api/v3", ""),
}
attempt := 1
for {
Expand Down
2 changes: 1 addition & 1 deletion internal/web/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func (ws *WebServer) runMetricsServer() {
).ServeHTTP(rw, r)

// Get upstream metrics
re, err := http.NewRequest("GET", fmt.Sprintf("%s/-/metrics/", ws.ul.String()), nil)
re, err := http.NewRequest("GET", fmt.Sprintf("%s%s-/metrics/", ws.upstreamURL.String(), config.Get().Web.Path), nil)
if err != nil {
l.WithError(err).Warning("failed to get upstream metrics")
return
Expand Down
10 changes: 7 additions & 3 deletions internal/web/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import (
"time"

"github.com/prometheus/client_golang/prometheus"
"goauthentik.io/internal/config"
"goauthentik.io/internal/utils/sentry"
)

func (ws *WebServer) configureProxy() {
// Reverse proxy to the application server
director := func(req *http.Request) {
req.URL.Scheme = ws.ul.Scheme
req.URL.Host = ws.ul.Host
req.URL.Scheme = ws.upstreamURL.Scheme
req.URL.Host = ws.upstreamURL.Host
if _, ok := req.Header["User-Agent"]; !ok {
// explicitly disable User-Agent so it's not set to default value
req.Header.Set("User-Agent", "")
Expand All @@ -32,7 +33,10 @@ func (ws *WebServer) configureProxy() {
}
rp.ErrorHandler = ws.proxyErrorHandler
rp.ModifyResponse = ws.proxyModifyResponse
ws.m.PathPrefix("/").HandlerFunc(sentry.SentryNoSample(func(rw http.ResponseWriter, r *http.Request) {
ws.mainRouter.PathPrefix(config.Get().Web.Path).Path("/-/health/live/").HandlerFunc(sentry.SentryNoSample(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(204)
}))
ws.mainRouter.PathPrefix(config.Get().Web.Path).HandlerFunc(sentry.SentryNoSample(func(rw http.ResponseWriter, r *http.Request) {
if !ws.g.IsRunning() {
ws.proxyErrorHandler(rw, r, errors.New("authentik starting"))
return
Expand Down
Loading
Loading