Embedded outpost (#1193)
* api: allow API requests as managed outpost's account when using secret_key Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * root: load secret key from env Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outposts: make listener IP configurable Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outpost/proxy: run outpost in background and pass requests conditionally Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * outpost: unify branding to embedded Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: fix embedded outpost not being editable Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web: fix mismatched host detection Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * tests/e2e: fix LDAP test not including user for embedded outpost Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * tests/e2e: fix user matching Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * api: add tests for secret_key auth Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * root: load environment variables using github.com/Netflix/go-env Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		@ -3,18 +3,20 @@ from base64 import b64decode
 | 
			
		||||
from binascii import Error
 | 
			
		||||
from typing import Any, Optional, Union
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from rest_framework.authentication import BaseAuthentication, get_authorization_header
 | 
			
		||||
from rest_framework.exceptions import AuthenticationFailed
 | 
			
		||||
from rest_framework.request import Request
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.core.models import Token, TokenIntents, User
 | 
			
		||||
from authentik.outposts.models import Outpost
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# pylint: disable=too-many-return-statements
 | 
			
		||||
def token_from_header(raw_header: bytes) -> Optional[Token]:
 | 
			
		||||
def bearer_auth(raw_header: bytes) -> Optional[User]:
 | 
			
		||||
    """raw_header in the Format of `Bearer dGVzdDp0ZXN0`"""
 | 
			
		||||
    auth_credentials = raw_header.decode()
 | 
			
		||||
    if auth_credentials == "" or " " not in auth_credentials:
 | 
			
		||||
@ -38,8 +40,26 @@ def token_from_header(raw_header: bytes) -> Optional[Token]:
 | 
			
		||||
        raise AuthenticationFailed("Malformed header")
 | 
			
		||||
    tokens = Token.filter_not_expired(key=password, intent=TokenIntents.INTENT_API)
 | 
			
		||||
    if not tokens.exists():
 | 
			
		||||
        raise AuthenticationFailed("Token invalid/expired")
 | 
			
		||||
    return tokens.first()
 | 
			
		||||
        LOGGER.info("Authenticating via secret_key")
 | 
			
		||||
        user = token_secret_key(password)
 | 
			
		||||
        if not user:
 | 
			
		||||
            raise AuthenticationFailed("Token invalid/expired")
 | 
			
		||||
        return user
 | 
			
		||||
    return tokens.first().user
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def token_secret_key(value: str) -> Optional[User]:
 | 
			
		||||
    """Check if the token is the secret key
 | 
			
		||||
    and return the service account for the managed outpost"""
 | 
			
		||||
    from authentik.outposts.managed import MANAGED_OUTPOST
 | 
			
		||||
 | 
			
		||||
    if value != settings.SECRET_KEY:
 | 
			
		||||
        return None
 | 
			
		||||
    outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
 | 
			
		||||
    if not outposts:
 | 
			
		||||
        return None
 | 
			
		||||
    outpost = outposts.first()
 | 
			
		||||
    return outpost.user
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TokenAuthentication(BaseAuthentication):
 | 
			
		||||
@ -49,9 +69,9 @@ class TokenAuthentication(BaseAuthentication):
 | 
			
		||||
        """Token-based authentication using HTTP Bearer authentication"""
 | 
			
		||||
        auth = get_authorization_header(request)
 | 
			
		||||
 | 
			
		||||
        token = token_from_header(auth)
 | 
			
		||||
        user = bearer_auth(auth)
 | 
			
		||||
        # None is only returned when the header isn't set.
 | 
			
		||||
        if not token:
 | 
			
		||||
        if not user:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        return (token.user, None)  # pragma: no cover
 | 
			
		||||
        return (user, None)  # pragma: no cover
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,14 @@
 | 
			
		||||
"""Test API Authentication"""
 | 
			
		||||
from base64 import b64encode
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from guardian.shortcuts import get_anonymous_user
 | 
			
		||||
from rest_framework.exceptions import AuthenticationFailed
 | 
			
		||||
 | 
			
		||||
from authentik.api.authentication import token_from_header
 | 
			
		||||
from authentik.core.models import Token, TokenIntents
 | 
			
		||||
from authentik.api.authentication import bearer_auth
 | 
			
		||||
from authentik.core.models import USER_ATTRIBUTE_SA, Token, TokenIntents
 | 
			
		||||
from authentik.outposts.managed import OutpostManager
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAPIAuth(TestCase):
 | 
			
		||||
@ -18,32 +20,41 @@ class TestAPIAuth(TestCase):
 | 
			
		||||
            intent=TokenIntents.INTENT_API, user=get_anonymous_user()
 | 
			
		||||
        )
 | 
			
		||||
        auth = b64encode(f":{token.key}".encode()).decode()
 | 
			
		||||
        self.assertEqual(token_from_header(f"Basic {auth}".encode()), token)
 | 
			
		||||
        self.assertEqual(bearer_auth(f"Basic {auth}".encode()), token.user)
 | 
			
		||||
 | 
			
		||||
    def test_valid_bearer(self):
 | 
			
		||||
        """Test valid token"""
 | 
			
		||||
        token = Token.objects.create(
 | 
			
		||||
            intent=TokenIntents.INTENT_API, user=get_anonymous_user()
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(token_from_header(f"Bearer {token.key}".encode()), token)
 | 
			
		||||
        self.assertEqual(bearer_auth(f"Bearer {token.key}".encode()), token.user)
 | 
			
		||||
 | 
			
		||||
    def test_invalid_type(self):
 | 
			
		||||
        """Test invalid type"""
 | 
			
		||||
        with self.assertRaises(AuthenticationFailed):
 | 
			
		||||
            token_from_header("foo bar".encode())
 | 
			
		||||
            bearer_auth("foo bar".encode())
 | 
			
		||||
 | 
			
		||||
    def test_invalid_decode(self):
 | 
			
		||||
        """Test invalid bas64"""
 | 
			
		||||
        with self.assertRaises(AuthenticationFailed):
 | 
			
		||||
            token_from_header("Basic bar".encode())
 | 
			
		||||
            bearer_auth("Basic bar".encode())
 | 
			
		||||
 | 
			
		||||
    def test_invalid_empty_password(self):
 | 
			
		||||
        """Test invalid with empty password"""
 | 
			
		||||
        with self.assertRaises(AuthenticationFailed):
 | 
			
		||||
            token_from_header("Basic :".encode())
 | 
			
		||||
            bearer_auth("Basic :".encode())
 | 
			
		||||
 | 
			
		||||
    def test_invalid_no_token(self):
 | 
			
		||||
        """Test invalid with no token"""
 | 
			
		||||
        with self.assertRaises(AuthenticationFailed):
 | 
			
		||||
            auth = b64encode(":abc".encode()).decode()
 | 
			
		||||
            self.assertIsNone(token_from_header(f"Basic :{auth}".encode()))
 | 
			
		||||
            self.assertIsNone(bearer_auth(f"Basic :{auth}".encode()))
 | 
			
		||||
 | 
			
		||||
    def test_managed_outpost(self):
 | 
			
		||||
        """Test managed outpost"""
 | 
			
		||||
        with self.assertRaises(AuthenticationFailed):
 | 
			
		||||
            user = bearer_auth(f"Bearer {settings.SECRET_KEY}".encode())
 | 
			
		||||
 | 
			
		||||
        OutpostManager().run()
 | 
			
		||||
        user = bearer_auth(f"Bearer {settings.SECRET_KEY}".encode())
 | 
			
		||||
        self.assertEqual(user.attributes[USER_ATTRIBUTE_SA], True)
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ from channels.generic.websocket import JsonWebsocketConsumer
 | 
			
		||||
from rest_framework.exceptions import AuthenticationFailed
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.api.authentication import token_from_header
 | 
			
		||||
from authentik.api.authentication import bearer_auth
 | 
			
		||||
from authentik.core.models import User
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
@ -24,12 +24,12 @@ class AuthJsonConsumer(JsonWebsocketConsumer):
 | 
			
		||||
        raw_header = headers[b"authorization"]
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            token = token_from_header(raw_header)
 | 
			
		||||
            # token is only None when no header was given, in which case we deny too
 | 
			
		||||
            if not token:
 | 
			
		||||
            user = bearer_auth(raw_header)
 | 
			
		||||
            # user is only None when no header was given, in which case we deny too
 | 
			
		||||
            if not user:
 | 
			
		||||
                raise DenyConnection()
 | 
			
		||||
        except AuthenticationFailed as exc:
 | 
			
		||||
            LOGGER.warning("Failed to authenticate", exc=exc)
 | 
			
		||||
            raise DenyConnection()
 | 
			
		||||
 | 
			
		||||
        self.user = token.user
 | 
			
		||||
        self.user = user
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,7 @@ class AuthentikOutpostConfig(AppConfig):
 | 
			
		||||
 | 
			
		||||
    def ready(self):
 | 
			
		||||
        import_module("authentik.outposts.signals")
 | 
			
		||||
        import_module("authentik.outposts.managed")
 | 
			
		||||
        try:
 | 
			
		||||
            from authentik.outposts.tasks import outpost_local_connection
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,8 @@
 | 
			
		||||
from authentik.managed.manager import EnsureExists, ObjectManager
 | 
			
		||||
from authentik.outposts.models import Outpost, OutpostType
 | 
			
		||||
 | 
			
		||||
MANAGED_OUTPOST = "goauthentik.io/outposts/embedded"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OutpostManager(ObjectManager):
 | 
			
		||||
    """Outpost managed objects"""
 | 
			
		||||
@ -10,9 +12,8 @@ class OutpostManager(ObjectManager):
 | 
			
		||||
        return [
 | 
			
		||||
            EnsureExists(
 | 
			
		||||
                Outpost,
 | 
			
		||||
                "goauthentik.io/outposts/inbuilt",
 | 
			
		||||
                name="authentik Bundeled Outpost",
 | 
			
		||||
                object_field="name",
 | 
			
		||||
                MANAGED_OUTPOST,
 | 
			
		||||
                name="authentik Embedded Outpost",
 | 
			
		||||
                type=OutpostType.PROXY,
 | 
			
		||||
            ),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,6 @@
 | 
			
		||||
"""outpost tests"""
 | 
			
		||||
from django.apps import apps
 | 
			
		||||
from django.contrib.auth.management import create_permissions
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from guardian.models import UserObjectPermission
 | 
			
		||||
 | 
			
		||||
@ -11,6 +13,10 @@ from authentik.providers.proxy.models import ProxyProvider
 | 
			
		||||
class OutpostTests(TestCase):
 | 
			
		||||
    """Outpost Tests"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        create_permissions(apps.get_app_config("authentik_outposts"))
 | 
			
		||||
        return super().setUp()
 | 
			
		||||
 | 
			
		||||
    def test_service_account_permissions(self):
 | 
			
		||||
        """Test that the service account has correct permissions"""
 | 
			
		||||
        provider: ProxyProvider = ProxyProvider.objects.create(
 | 
			
		||||
@ -31,7 +37,6 @@ class OutpostTests(TestCase):
 | 
			
		||||
 | 
			
		||||
        # We add a provider, user should only have access to outpost and provider
 | 
			
		||||
        outpost.providers.add(provider)
 | 
			
		||||
        outpost.save()
 | 
			
		||||
        permissions = UserObjectPermission.objects.filter(user=outpost.user).order_by(
 | 
			
		||||
            "content_type__model"
 | 
			
		||||
        )
 | 
			
		||||
@ -53,7 +58,6 @@ class OutpostTests(TestCase):
 | 
			
		||||
 | 
			
		||||
        # Remove provider from outpost, user should only have access to outpost
 | 
			
		||||
        outpost.providers.remove(provider)
 | 
			
		||||
        outpost.save()
 | 
			
		||||
        permissions = UserObjectPermission.objects.filter(user=outpost.user)
 | 
			
		||||
        self.assertEqual(len(permissions), 1)
 | 
			
		||||
        self.assertEqual(permissions[0].object_pk, str(outpost.pk))
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,8 @@ package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/getsentry/sentry-go"
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
@ -9,6 +11,8 @@ import (
 | 
			
		||||
	"goauthentik.io/internal/config"
 | 
			
		||||
	"goauthentik.io/internal/constants"
 | 
			
		||||
	"goauthentik.io/internal/gounicorn"
 | 
			
		||||
	"goauthentik.io/internal/outpost/ak"
 | 
			
		||||
	"goauthentik.io/internal/outpost/proxy"
 | 
			
		||||
	"goauthentik.io/internal/web"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -25,6 +29,10 @@ func main() {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.WithError(err).Debug("failed to local config")
 | 
			
		||||
	}
 | 
			
		||||
	err = config.FromEnv()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.WithError(err).Debug("failed to environment variables")
 | 
			
		||||
	}
 | 
			
		||||
	config.ConfigureLogger()
 | 
			
		||||
 | 
			
		||||
	if config.G.ErrorReporting.Enabled {
 | 
			
		||||
@ -43,7 +51,7 @@ func main() {
 | 
			
		||||
	ex := common.Init()
 | 
			
		||||
	defer common.Defer()
 | 
			
		||||
 | 
			
		||||
	// u, _ := url.Parse("http://localhost:8000")
 | 
			
		||||
	u, _ := url.Parse("http://localhost:8000")
 | 
			
		||||
 | 
			
		||||
	g := gounicorn.NewGoUnicorn()
 | 
			
		||||
	ws := web.NewWebServer()
 | 
			
		||||
@ -52,7 +60,7 @@ func main() {
 | 
			
		||||
	for {
 | 
			
		||||
		go attemptStartBackend(g)
 | 
			
		||||
		ws.Start()
 | 
			
		||||
		// go attemptProxyStart(u)
 | 
			
		||||
		go attemptProxyStart(ws, u)
 | 
			
		||||
 | 
			
		||||
		<-ex
 | 
			
		||||
		running = false
 | 
			
		||||
@ -73,35 +81,36 @@ func attemptStartBackend(g *gounicorn.GoUnicorn) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// func attemptProxyStart(u *url.URL) error {
 | 
			
		||||
// 	maxTries := 100
 | 
			
		||||
// 	attempt := 0
 | 
			
		||||
// 	for {
 | 
			
		||||
// 		log.WithField("logger", "authentik").Debug("attempting to init outpost")
 | 
			
		||||
// 		ac := ak.NewAPIController(*u, config.G.SecretKey)
 | 
			
		||||
// 		if ac == nil {
 | 
			
		||||
// 			attempt += 1
 | 
			
		||||
// 			time.Sleep(1 * time.Second)
 | 
			
		||||
// 			if attempt > maxTries {
 | 
			
		||||
// 				break
 | 
			
		||||
// 			}
 | 
			
		||||
// 			continue
 | 
			
		||||
// 		}
 | 
			
		||||
// 		ac.Server = proxy.NewServer(ac)
 | 
			
		||||
// 		err := ac.Start()
 | 
			
		||||
// 		log.WithField("logger", "authentik").Debug("attempting to start outpost")
 | 
			
		||||
// 		if err != nil {
 | 
			
		||||
// 			attempt += 1
 | 
			
		||||
// 			time.Sleep(5 * time.Second)
 | 
			
		||||
// 			if attempt > maxTries {
 | 
			
		||||
// 				break
 | 
			
		||||
// 			}
 | 
			
		||||
// 			continue
 | 
			
		||||
// 		}
 | 
			
		||||
// 		if !running {
 | 
			
		||||
// 			ac.Shutdown()
 | 
			
		||||
// 			return nil
 | 
			
		||||
// 		}
 | 
			
		||||
// 	}
 | 
			
		||||
// 	return nil
 | 
			
		||||
// }
 | 
			
		||||
func attemptProxyStart(ws *web.WebServer, u *url.URL) {
 | 
			
		||||
	maxTries := 100
 | 
			
		||||
	attempt := 0
 | 
			
		||||
	// Sleep to wait for the app server to start
 | 
			
		||||
	time.Sleep(30 * time.Second)
 | 
			
		||||
	for {
 | 
			
		||||
		log.WithField("logger", "authentik").Debug("attempting to init outpost")
 | 
			
		||||
		ac := ak.NewAPIController(*u, config.G.SecretKey)
 | 
			
		||||
		if ac == nil {
 | 
			
		||||
			attempt += 1
 | 
			
		||||
			time.Sleep(1 * time.Second)
 | 
			
		||||
			if attempt > maxTries {
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		srv := proxy.NewServer(ac)
 | 
			
		||||
		ws.ProxyServer = srv
 | 
			
		||||
		ac.Server = srv
 | 
			
		||||
		log.WithField("logger", "authentik").Debug("attempting to start outpost")
 | 
			
		||||
		err := ac.StartBackgorundTasks()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			attempt += 1
 | 
			
		||||
			time.Sleep(15 * time.Second)
 | 
			
		||||
			if attempt > maxTries {
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
			continue
 | 
			
		||||
		} else {
 | 
			
		||||
			select {}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							@ -3,6 +3,7 @@ module goauthentik.io
 | 
			
		||||
go 1.16
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/Netflix/go-env v0.0.0-20210215222557-e437a7e7f9fb // indirect
 | 
			
		||||
	github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
 | 
			
		||||
	github.com/coreos/go-oidc v2.2.1+incompatible
 | 
			
		||||
	github.com/getsentry/sentry-go v0.11.0
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
									
									
									
									
								
							@ -45,6 +45,8 @@ github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3
 | 
			
		||||
github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo=
 | 
			
		||||
github.com/FZambia/sentinel v1.0.0/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2FT26vP5gI=
 | 
			
		||||
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
 | 
			
		||||
github.com/Netflix/go-env v0.0.0-20210215222557-e437a7e7f9fb h1:w9IDEB7P1VzNcBpOG7kMpFkZp2DkyJIUt0gDx5MBhRU=
 | 
			
		||||
github.com/Netflix/go-env v0.0.0-20210215222557-e437a7e7f9fb/go.mod h1:9XMFaCeRyW7fC9XJOWQ+NdAv8VLG7ys7l3x4ozEGLUQ=
 | 
			
		||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 | 
			
		||||
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
 | 
			
		||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,6 @@ func Init() chan os.Signal {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Defer() {
 | 
			
		||||
	defer sentry.Flush(time.Second * 5)
 | 
			
		||||
	defer sentry.Recover()
 | 
			
		||||
	sentry.Flush(time.Second * 5)
 | 
			
		||||
	sentry.Recover()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,8 +2,8 @@ package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	env "github.com/Netflix/go-env"
 | 
			
		||||
	"github.com/imdario/mergo"
 | 
			
		||||
	"github.com/pkg/errors"
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
@ -35,9 +35,8 @@ func LoadConfig(path string) error {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errors.Wrap(err, "Failed to load config file")
 | 
			
		||||
	}
 | 
			
		||||
	rawExpanded := os.ExpandEnv(string(raw))
 | 
			
		||||
	nc := Config{}
 | 
			
		||||
	err = yaml.Unmarshal([]byte(rawExpanded), &nc)
 | 
			
		||||
	err = yaml.Unmarshal(raw, &nc)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errors.Wrap(err, "Failed to parse YAML")
 | 
			
		||||
	}
 | 
			
		||||
@ -48,6 +47,19 @@ func LoadConfig(path string) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func FromEnv() error {
 | 
			
		||||
	nc := Config{}
 | 
			
		||||
	_, err := env.UnmarshalFromEnviron(&nc)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errors.Wrap(err, "failed to load environment variables")
 | 
			
		||||
	}
 | 
			
		||||
	if err := mergo.Merge(&G, nc, mergo.WithOverride); err != nil {
 | 
			
		||||
		return errors.Wrap(err, "failed to overlay config")
 | 
			
		||||
	}
 | 
			
		||||
	log.Debug("Loaded config from environment")
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ConfigureLogger() {
 | 
			
		||||
	switch G.LogLevel {
 | 
			
		||||
	case "trace":
 | 
			
		||||
 | 
			
		||||
@ -1,18 +1,18 @@
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
type Config struct {
 | 
			
		||||
	Debug          bool                 `yaml:"debug"`
 | 
			
		||||
	SecretKey      string               `yaml:"secret_key"`
 | 
			
		||||
	Debug          bool                 `yaml:"debug" env:"AUTHENTIK_DEBUG"`
 | 
			
		||||
	SecretKey      string               `yaml:"secret_key" env:"AUTHENTIK_SECRET_KEY"`
 | 
			
		||||
	Web            WebConfig            `yaml:"web"`
 | 
			
		||||
	Paths          PathsConfig          `yaml:"paths"`
 | 
			
		||||
	LogLevel       string               `yaml:"log_level"`
 | 
			
		||||
	LogLevel       string               `yaml:"log_level" env:"AUTHENTIK_LOG_LEVEL"`
 | 
			
		||||
	ErrorReporting ErrorReportingConfig `yaml:"error_reporting"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type WebConfig struct {
 | 
			
		||||
	Listen         string `yaml:"listen"`
 | 
			
		||||
	ListenTLS      string `yaml:"listen_tls"`
 | 
			
		||||
	LoadLocalFiles bool   `yaml:"load_local_files"`
 | 
			
		||||
	LoadLocalFiles bool   `yaml:"load_local_files" env:"AUTHENTIK_WEB_LOAD_LOCAL_FILES"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PathsConfig struct {
 | 
			
		||||
@ -20,7 +20,7 @@ type PathsConfig struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ErrorReportingConfig struct {
 | 
			
		||||
	Enabled     bool   `yaml:"enabled"`
 | 
			
		||||
	Environment string `yaml:"environment"`
 | 
			
		||||
	SendPII     bool   `yaml:"send_pii"`
 | 
			
		||||
	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"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -80,6 +80,20 @@ func NewAPIController(akURL url.URL, token string) *APIController {
 | 
			
		||||
 | 
			
		||||
// Start Starts all handlers, non-blocking
 | 
			
		||||
func (a *APIController) Start() error {
 | 
			
		||||
	err := a.StartBackgorundTasks()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	go func() {
 | 
			
		||||
		err := a.Server.Start()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			panic(err)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *APIController) StartBackgorundTasks() error {
 | 
			
		||||
	err := a.Server.Refresh()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errors.Wrap(err, "failed to run initial refresh")
 | 
			
		||||
@ -96,11 +110,5 @@ func (a *APIController) Start() error {
 | 
			
		||||
		a.logger.Debug("Starting Interval updater...")
 | 
			
		||||
		a.startIntervalUpdater()
 | 
			
		||||
	}()
 | 
			
		||||
	go func() {
 | 
			
		||||
		err := a.Server.Start()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			panic(err)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import (
 | 
			
		||||
 | 
			
		||||
	sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
 | 
			
		||||
	"github.com/oauth2-proxy/oauth2-proxy/pkg/cookies"
 | 
			
		||||
	"goauthentik.io/internal/utils/web"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// MakeCSRFCookie creates a cookie for CSRF
 | 
			
		||||
@ -19,7 +20,7 @@ func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, ex
 | 
			
		||||
	cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains)
 | 
			
		||||
 | 
			
		||||
	if cookieDomain != "" {
 | 
			
		||||
		domain := getHost(req)
 | 
			
		||||
		domain := web.GetHost(req)
 | 
			
		||||
		if h, _, err := net.SplitHostPort(domain); err == nil {
 | 
			
		||||
			domain = h
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
	"goauthentik.io/internal/utils/web"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// responseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP status
 | 
			
		||||
@ -107,7 +108,7 @@ func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	duration := float64(time.Since(t)) / float64(time.Millisecond)
 | 
			
		||||
	h.logger.WithFields(log.Fields{
 | 
			
		||||
		"host":              req.RemoteAddr,
 | 
			
		||||
		"vhost":             getHost(req),
 | 
			
		||||
		"vhost":             web.GetHost(req),
 | 
			
		||||
		"request_protocol":  req.Proto,
 | 
			
		||||
		"runtime":           fmt.Sprintf("%0.3f", duration),
 | 
			
		||||
		"method":            req.Method,
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ import (
 | 
			
		||||
	"github.com/oauth2-proxy/oauth2-proxy/pkg/upstream"
 | 
			
		||||
	"github.com/oauth2-proxy/oauth2-proxy/providers"
 | 
			
		||||
	"goauthentik.io/api"
 | 
			
		||||
	"goauthentik.io/internal/utils/web"
 | 
			
		||||
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
)
 | 
			
		||||
@ -308,7 +309,7 @@ func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request)
 | 
			
		||||
				// Optional suffix, which is appended to the URL
 | 
			
		||||
				suffix := ""
 | 
			
		||||
				if p.mode == api.PROXYMODE_FORWARD_SINGLE {
 | 
			
		||||
					host = getHost(req)
 | 
			
		||||
					host = web.GetHost(req)
 | 
			
		||||
				} else if p.mode == api.PROXYMODE_FORWARD_DOMAIN {
 | 
			
		||||
					host = p.ExternalHost
 | 
			
		||||
					// set the ?rd flag to the current URL we have, since we redirect
 | 
			
		||||
 | 
			
		||||
@ -4,19 +4,23 @@ import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/tls"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/pires/go-proxyproto"
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
	"goauthentik.io/internal/crypto"
 | 
			
		||||
	"goauthentik.io/internal/outpost/ak"
 | 
			
		||||
	"goauthentik.io/internal/utils/web"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Server represents an HTTP server
 | 
			
		||||
type Server struct {
 | 
			
		||||
	Handlers map[string]*providerBundle
 | 
			
		||||
	Listen   string
 | 
			
		||||
 | 
			
		||||
	stop        chan struct{} // channel for waiting shutdown
 | 
			
		||||
	logger      *log.Entry
 | 
			
		||||
@ -33,6 +37,7 @@ func NewServer(ac *ak.APIController) *Server {
 | 
			
		||||
	}
 | 
			
		||||
	return &Server{
 | 
			
		||||
		Handlers:    make(map[string]*providerBundle),
 | 
			
		||||
		Listen:      "0.0.0.0:%d",
 | 
			
		||||
		logger:      log.WithField("logger", "authentik.outpost.proxy-http-server"),
 | 
			
		||||
		defaultCert: defaultCert,
 | 
			
		||||
		ak:          ac,
 | 
			
		||||
@ -40,12 +45,27 @@ func NewServer(ac *ak.APIController) *Server {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
// ServeHTTP constructs a net.Listener and starts handling HTTP requests
 | 
			
		||||
func (s *Server) ServeHTTP() {
 | 
			
		||||
	listenAddress := fmt.Sprintf(s.Listen, 4180)
 | 
			
		||||
	listener, err := net.Listen("tcp", listenAddress)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		s.logger.Fatalf("FATAL: listen (%s) failed - %s", listenAddress, err)
 | 
			
		||||
	}
 | 
			
		||||
	proxyListener := &proxyproto.Listener{Listener: listener}
 | 
			
		||||
	defer proxyListener.Close()
 | 
			
		||||
 | 
			
		||||
	s.logger.Printf("listening on %s", listener.Addr())
 | 
			
		||||
	s.serve(proxyListener)
 | 
			
		||||
	s.logger.Printf("closing %s", listener.Addr())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Server) Handler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	if r.URL.Path == "/akprox/ping" {
 | 
			
		||||
		w.WriteHeader(204)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	host := getHost(r)
 | 
			
		||||
	host := web.GetHost(r)
 | 
			
		||||
	handler, ok := s.Handlers[host]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		// If we only have one handler, host name switching doesn't matter
 | 
			
		||||
@ -68,7 +88,7 @@ func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Server) serve(listener net.Listener) {
 | 
			
		||||
	srv := &http.Server{Handler: http.HandlerFunc(s.handler)}
 | 
			
		||||
	srv := &http.Server{Handler: http.HandlerFunc(s.Handler)}
 | 
			
		||||
 | 
			
		||||
	// See https://golang.org/pkg/net/http/#Server.Shutdown
 | 
			
		||||
	idleConnsClosed := make(chan struct{})
 | 
			
		||||
 | 
			
		||||
@ -2,27 +2,13 @@ package proxy
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/tls"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"github.com/pires/go-proxyproto"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ServeHTTP constructs a net.Listener and starts handling HTTP requests
 | 
			
		||||
func (s *Server) ServeHTTP() {
 | 
			
		||||
	listenAddress := "0.0.0.0:4180"
 | 
			
		||||
	listener, err := net.Listen("tcp", listenAddress)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		s.logger.Fatalf("FATAL: listen (%s) failed - %s", listenAddress, err)
 | 
			
		||||
	}
 | 
			
		||||
	proxyListener := &proxyproto.Listener{Listener: listener}
 | 
			
		||||
	defer proxyListener.Close()
 | 
			
		||||
 | 
			
		||||
	s.logger.Printf("listening on %s", listener.Addr())
 | 
			
		||||
	s.serve(proxyListener)
 | 
			
		||||
	s.logger.Printf("closing %s", listener.Addr())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Server) getCertificates(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
 | 
			
		||||
	handler, ok := s.Handlers[info.ServerName]
 | 
			
		||||
	if !ok {
 | 
			
		||||
@ -38,7 +24,7 @@ func (s *Server) getCertificates(info *tls.ClientHelloInfo) (*tls.Certificate, e
 | 
			
		||||
 | 
			
		||||
// ServeHTTPS constructs a net.Listener and starts handling HTTPS requests
 | 
			
		||||
func (s *Server) ServeHTTPS() {
 | 
			
		||||
	listenAddress := "0.0.0.0:4443"
 | 
			
		||||
	listenAddress := fmt.Sprintf(s.Listen, 4443)
 | 
			
		||||
	config := &tls.Config{
 | 
			
		||||
		MinVersion:     tls.VersionTLS12,
 | 
			
		||||
		MaxVersion:     tls.VersionTLS12,
 | 
			
		||||
 | 
			
		||||
@ -1,25 +1,9 @@
 | 
			
		||||
package proxy
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var xForwardedHost = http.CanonicalHeaderKey("X-Forwarded-Host")
 | 
			
		||||
 | 
			
		||||
func getHost(req *http.Request) string {
 | 
			
		||||
	host := req.Host
 | 
			
		||||
	if req.Header.Get(xForwardedHost) != "" {
 | 
			
		||||
		host = req.Header.Get(xForwardedHost)
 | 
			
		||||
	}
 | 
			
		||||
	hostOnly, _, err := net.SplitHostPort(host)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return host
 | 
			
		||||
	}
 | 
			
		||||
	return hostOnly
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// toString Generic to string function, currently supports actual strings and integers
 | 
			
		||||
func toString(in interface{}) string {
 | 
			
		||||
	switch v := in.(type) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										15
									
								
								internal/utils/web/host.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								internal/utils/web/host.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
package web
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var xForwardedHost = http.CanonicalHeaderKey("X-Forwarded-Host")
 | 
			
		||||
 | 
			
		||||
func GetHost(req *http.Request) string {
 | 
			
		||||
	host := req.Host
 | 
			
		||||
	if req.Header.Get(xForwardedHost) != "" {
 | 
			
		||||
		host = req.Header.Get(xForwardedHost)
 | 
			
		||||
	}
 | 
			
		||||
	return host
 | 
			
		||||
}
 | 
			
		||||
@ -6,11 +6,12 @@ import (
 | 
			
		||||
 | 
			
		||||
	"github.com/getsentry/sentry-go"
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
	"goauthentik.io/internal/utils/web"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func loggingMiddleware(next http.Handler) http.Handler {
 | 
			
		||||
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		span := sentry.StartSpan(r.Context(), "request.logging")
 | 
			
		||||
		span := sentry.StartSpan(r.Context(), "authentik.go.request")
 | 
			
		||||
		before := time.Now()
 | 
			
		||||
		// Call the next handler, which can be another middleware in the chain, or the final handler.
 | 
			
		||||
		next.ServeHTTP(w, r)
 | 
			
		||||
@ -19,6 +20,7 @@ func loggingMiddleware(next http.Handler) http.Handler {
 | 
			
		||||
			"remote": r.RemoteAddr,
 | 
			
		||||
			"method": r.Method,
 | 
			
		||||
			"took":   after.Sub(before),
 | 
			
		||||
			"host":   web.GetHost(r),
 | 
			
		||||
		}).Info(r.RequestURI)
 | 
			
		||||
		span.Finish()
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,7 @@ import (
 | 
			
		||||
	"github.com/pires/go-proxyproto"
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
	"goauthentik.io/internal/config"
 | 
			
		||||
	"goauthentik.io/internal/outpost/proxy"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type WebServer struct {
 | 
			
		||||
@ -21,6 +22,8 @@ type WebServer struct {
 | 
			
		||||
 | 
			
		||||
	stop chan struct{} // channel for waiting shutdown
 | 
			
		||||
 | 
			
		||||
	ProxyServer *proxy.Server
 | 
			
		||||
 | 
			
		||||
	m   *mux.Router
 | 
			
		||||
	lh  *mux.Router
 | 
			
		||||
	log *log.Entry
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,12 @@
 | 
			
		||||
package web
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/http/httputil"
 | 
			
		||||
	"net/url"
 | 
			
		||||
 | 
			
		||||
	"goauthentik.io/internal/utils/web"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (ws *WebServer) configureProxy() {
 | 
			
		||||
@ -23,7 +26,25 @@ func (ws *WebServer) configureProxy() {
 | 
			
		||||
	rp := &httputil.ReverseProxy{Director: director}
 | 
			
		||||
	rp.ErrorHandler = ws.proxyErrorHandler
 | 
			
		||||
	rp.ModifyResponse = ws.proxyModifyResponse
 | 
			
		||||
	ws.m.PathPrefix("/").Handler(rp)
 | 
			
		||||
	ws.m.PathPrefix("/akprox").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		if ws.ProxyServer != nil {
 | 
			
		||||
			ws.ProxyServer.Handler(rw, r)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		ws.proxyErrorHandler(rw, r, fmt.Errorf("proxy not running"))
 | 
			
		||||
	})
 | 
			
		||||
	ws.m.PathPrefix("/").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		host := web.GetHost(r)
 | 
			
		||||
		if ws.ProxyServer != nil {
 | 
			
		||||
			if _, ok := ws.ProxyServer.Handlers[host]; ok {
 | 
			
		||||
				ws.log.WithField("host", host).Trace("routing to proxy outpost")
 | 
			
		||||
				ws.ProxyServer.Handler(rw, r)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		ws.log.WithField("host", host).Trace("routing to application server")
 | 
			
		||||
		rp.ServeHTTP(rw, r)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ws *WebServer) proxyErrorHandler(rw http.ResponseWriter, req *http.Request, err error) {
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@ from ldap3.core.exceptions import LDAPInvalidCredentialsResult
 | 
			
		||||
from authentik.core.models import Application, Group, User
 | 
			
		||||
from authentik.events.models import Event, EventAction
 | 
			
		||||
from authentik.flows.models import Flow
 | 
			
		||||
from authentik.outposts.managed import MANAGED_OUTPOST
 | 
			
		||||
from authentik.outposts.models import Outpost, OutpostType
 | 
			
		||||
from authentik.providers.ldap.models import LDAPProvider
 | 
			
		||||
from tests.e2e.utils import (
 | 
			
		||||
@ -193,6 +194,9 @@ class TestProviderLDAP(SeleniumTestCase):
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        embedded_account = Outpost.objects.filter(managed=MANAGED_OUTPOST).first().user
 | 
			
		||||
 | 
			
		||||
        _connection.search(
 | 
			
		||||
            "ou=users,dc=ldap,dc=goauthentik,dc=io",
 | 
			
		||||
            "(objectClass=user)",
 | 
			
		||||
@ -232,6 +236,31 @@ class TestProviderLDAP(SeleniumTestCase):
 | 
			
		||||
                    },
 | 
			
		||||
                    "type": "searchResEntry",
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "dn": f"cn={embedded_account.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
 | 
			
		||||
                    "attributes": {
 | 
			
		||||
                        "cn": [embedded_account.username],
 | 
			
		||||
                        "uid": [embedded_account.uid],
 | 
			
		||||
                        "name": [""],
 | 
			
		||||
                        "displayName": [""],
 | 
			
		||||
                        "mail": [""],
 | 
			
		||||
                        "objectClass": [
 | 
			
		||||
                            "user",
 | 
			
		||||
                            "organizationalPerson",
 | 
			
		||||
                            "goauthentik.io/ldap/user",
 | 
			
		||||
                        ],
 | 
			
		||||
                        "uidNumber": [str(2000 + embedded_account.pk)],
 | 
			
		||||
                        "gidNumber": [str(2000 + embedded_account.pk)],
 | 
			
		||||
                        "memberOf": [],
 | 
			
		||||
                        "accountStatus": ["true"],
 | 
			
		||||
                        "superuser": ["false"],
 | 
			
		||||
                        "goauthentik.io/ldap/active": ["true"],
 | 
			
		||||
                        "goauthentik.io/ldap/superuser": ["false"],
 | 
			
		||||
                        "goauthentik.io/user/override-ips": ["true"],
 | 
			
		||||
                        "goauthentik.io/user/service-account": ["true"],
 | 
			
		||||
                    },
 | 
			
		||||
                    "type": "searchResEntry",
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "dn": f"cn={USER().username},ou=users,dc=ldap,dc=goauthentik,dc=io",
 | 
			
		||||
                    "attributes": {
 | 
			
		||||
 | 
			
		||||
@ -164,6 +164,7 @@ class TestSourceSAML(SeleniumTestCase):
 | 
			
		||||
 | 
			
		||||
        self.assert_user(
 | 
			
		||||
            User.objects.exclude(username="akadmin")
 | 
			
		||||
            .exclude(username__startswith="ak-outpost")
 | 
			
		||||
            .exclude(pk=get_anonymous_user().pk)
 | 
			
		||||
            .first()
 | 
			
		||||
        )
 | 
			
		||||
@ -249,6 +250,7 @@ class TestSourceSAML(SeleniumTestCase):
 | 
			
		||||
 | 
			
		||||
        self.assert_user(
 | 
			
		||||
            User.objects.exclude(username="akadmin")
 | 
			
		||||
            .exclude(username__startswith="ak-outpost")
 | 
			
		||||
            .exclude(pk=get_anonymous_user().pk)
 | 
			
		||||
            .first()
 | 
			
		||||
        )
 | 
			
		||||
@ -321,6 +323,7 @@ class TestSourceSAML(SeleniumTestCase):
 | 
			
		||||
 | 
			
		||||
        self.assert_user(
 | 
			
		||||
            User.objects.exclude(username="akadmin")
 | 
			
		||||
            .exclude(username__startswith="ak-outpost")
 | 
			
		||||
            .exclude(pk=get_anonymous_user().pk)
 | 
			
		||||
            .first()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -48,6 +48,7 @@ class SeleniumTestCase(StaticLiveServerTestCase):
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        super().setUp()
 | 
			
		||||
        self.maxDiff = None
 | 
			
		||||
        self.wait_timeout = 60
 | 
			
		||||
        self.driver = self._get_driver()
 | 
			
		||||
        self.driver.maximize_window()
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,9 @@ export class OutpostHealthElement extends LitElement {
 | 
			
		||||
    @property({attribute: false})
 | 
			
		||||
    outpostHealth?: OutpostHealth[];
 | 
			
		||||
 | 
			
		||||
    @property({attribute: false})
 | 
			
		||||
    showVersion = true;
 | 
			
		||||
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [PFBase, AKGlobal];
 | 
			
		||||
    }
 | 
			
		||||
@ -56,12 +59,13 @@ export class OutpostHealthElement extends LitElement {
 | 
			
		||||
                    <li role="cell">
 | 
			
		||||
                        <ak-label color=${PFColor.Green} text=${t`Last seen: ${h.lastSeen?.toLocaleTimeString()}`}></ak-label>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li role="cell">
 | 
			
		||||
                    ${this.showVersion ?
 | 
			
		||||
                    html`<li role="cell">
 | 
			
		||||
                        ${h.versionOutdated ?
 | 
			
		||||
                        html`<ak-label color=${PFColor.Red}
 | 
			
		||||
                            text=${t`${h.version}, should be ${h.versionShould}`}></ak-label>` :
 | 
			
		||||
                        html`<ak-label color=${PFColor.Green} text=${t`Version: ${h.version || ""}`}></ak-label>`}
 | 
			
		||||
                    </li>
 | 
			
		||||
                    </li>` : html``}
 | 
			
		||||
                </ul>
 | 
			
		||||
            </li>`;
 | 
			
		||||
        })}</ul>`;
 | 
			
		||||
 | 
			
		||||
@ -53,6 +53,9 @@ export class OutpostListPage extends TablePage<Outpost> {
 | 
			
		||||
    order = "name";
 | 
			
		||||
 | 
			
		||||
    row(item: Outpost): TemplateResult[] {
 | 
			
		||||
        if (item.managed === "goauthentik.io/outposts/embedded") {
 | 
			
		||||
            return this.rowInbuilt(item);
 | 
			
		||||
        }
 | 
			
		||||
        return [
 | 
			
		||||
            html`${item.name}`,
 | 
			
		||||
            html`<ul>${item.providersObj?.map((p) => {
 | 
			
		||||
@ -99,6 +102,30 @@ export class OutpostListPage extends TablePage<Outpost> {
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    rowInbuilt(item: Outpost): TemplateResult[] {
 | 
			
		||||
        return [
 | 
			
		||||
            html`${item.name}`,
 | 
			
		||||
            html`<ul>${item.providersObj?.map((p) => {
 | 
			
		||||
                return html`<li><a href="#/core/providers/${p.pk}">${p.name}</a></li>`;
 | 
			
		||||
            })}</ul>`,
 | 
			
		||||
            html`-`,
 | 
			
		||||
            html`<ak-outpost-health ?showVersion=${false} outpostId=${ifDefined(item.pk)}></ak-outpost-health>`,
 | 
			
		||||
            html`<ak-forms-modal>
 | 
			
		||||
                <span slot="submit">
 | 
			
		||||
                    ${t`Update`}
 | 
			
		||||
                </span>
 | 
			
		||||
                <span slot="header">
 | 
			
		||||
                    ${t`Update Outpost`}
 | 
			
		||||
                </span>
 | 
			
		||||
                <ak-outpost-form slot="form" .instancePk=${item.pk}>
 | 
			
		||||
                </ak-outpost-form>
 | 
			
		||||
                <button slot="trigger" class="pf-c-button pf-m-secondary">
 | 
			
		||||
                    ${t`Edit`}
 | 
			
		||||
                </button>
 | 
			
		||||
            </ak-forms-modal>`,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderToolbar(): TemplateResult {
 | 
			
		||||
        return html`
 | 
			
		||||
        <ak-forms-modal>
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user