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:
parent
1b03aae7aa
commit
f01bc20d44
|
@ -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) {
|
||||
|
|
|
@ -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