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