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>
master
Jens L 2021-07-29 11:30:30 +02:00 committed by GitHub
parent 1b03aae7aa
commit f01bc20d44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 285 additions and 118 deletions

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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,
),
]

View File

@ -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))

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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()
}

View File

@ -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":

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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,

View File

@ -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

View File

@ -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{})

View File

@ -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,

View File

@ -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) {

View 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
}

View File

@ -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()
})

View File

@ -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

View File

@ -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) {

View File

@ -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": {

View File

@ -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()
)

View File

@ -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()

View File

@ -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>`;

View File

@ -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>