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
	 Jens L
					Jens L