outposts: update permissions more eagerly (#17783)

* wip

* wip

* a

* a

Signed-off-by: Dominic R <dominic@sdko.org>

* rm

* this

* rm test files

* cover one more case

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Dominic R
2025-10-30 13:33:51 -04:00
committed by GitHub
parent 743e425894
commit ec00a918b3
7 changed files with 86 additions and 33 deletions
+15 -4
View File
@@ -49,6 +49,9 @@ def outpost_m2m_changed(sender, instance: Outpost | Provider, action: str, **_):
if action not in ["post_add", "post_remove", "post_clear"]:
return
if isinstance(instance, Outpost):
# Rebuild permissions when providers change
LOGGER.debug("Rebuilding outpost service account permissions", outpost=instance)
instance.build_user_permissions(instance.user)
outpost_controller.send_with_options(
args=(instance.pk,),
rel_obj=instance.service_connection,
@@ -74,10 +77,9 @@ def outpost_m2m_changed(sender, instance: Outpost | Provider, action: str, **_):
@receiver(post_save, sender=Outpost)
def outpost_post_save(sender, instance: Outpost, created: bool, **_):
if created:
LOGGER.info("New outpost saved, ensuring initial token and user are created")
_ = instance.token
def outpost_post_save(sender, instance: Outpost, **_):
LOGGER.info("Outpost saved, ensuring token and user are created and permissions are set")
_ = instance.token
outpost_controller.send_with_options(
args=(instance.pk,),
rel_obj=instance.service_connection,
@@ -92,6 +94,15 @@ def outpost_post_save(sender, instance: Outpost, created: bool, **_):
def outpost_related_post_save(sender, instance: OutpostServiceConnection | OutpostModel, **_):
for outpost in instance.outpost_set.all():
# Rebuild permissions in case provider's required objects changed
if isinstance(instance, OutpostModel):
LOGGER.info(
"Provider changed, rebuilding permissions and sending update",
outpost=outpost.name,
provider=instance.name if hasattr(instance, "name") else str(instance),
)
outpost.build_user_permissions(outpost.user)
LOGGER.debug("Sending update to outpost", outpost=outpost.name, trigger="provider_change")
outpost_send_update.send_with_options(
args=(outpost.pk,),
rel_obj=outpost,
+9 -1
View File
@@ -93,7 +93,7 @@ func NewAPIController(akURL url.URL, token string) *APIController {
}),
)
if len(outposts.Results) < 1 {
log.Panic("No outposts found with given token, ensure the given token corresponds to an authenitk Outpost")
log.Panic("No outposts found with given token, ensure the given token corresponds to an authentik Outpost")
}
outpost := outposts.Results[0]
@@ -122,6 +122,7 @@ func NewAPIController(akURL url.URL, token string) *APIController {
eventHandlers: []EventHandler{},
refreshHandlers: make([]func(), 0),
}
ac.logger.WithField("embedded", ac.IsEmbedded()).Info("Outpost mode")
ac.logger.WithField("offset", ac.reloadOffset.String()).Debug("HA Reload offset")
err = ac.initEvent(akURL, outpost.Pk)
if err != nil {
@@ -135,6 +136,13 @@ func (a *APIController) Log() *log.Entry {
return a.logger
}
func (a *APIController) IsEmbedded() bool {
if m := a.Outpost.Managed.Get(); m != nil {
return *m == "goauthentik.io/outposts/embedded"
}
return false
}
// Start Starts all handlers, non-blocking
func (a *APIController) Start() error {
err := a.Server.Refresh()
@@ -66,6 +66,7 @@ type Server interface {
API() *ak.APIController
Apps() []*Application
CryptoStore() *ak.CryptoStore
SessionBackend() string
}
func init() {
@@ -94,10 +95,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server, old
CallbackSignature: []string{"true"},
}.Encode()
isEmbedded := false
if m := server.API().Outpost.Managed.Get(); m != nil {
isEmbedded = *m == "goauthentik.io/outposts/embedded"
}
isEmbedded := server.API().IsEmbedded()
// Configure an OpenID Connect aware OAuth2 client.
endpoint := GetOIDCEndpoint(
p,
@@ -153,6 +151,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server, old
go a.authHeaderCache.Start()
if oldApp != nil && oldApp.sessions != nil {
a.sessions = oldApp.sessions
muxLogger.Debug("reusing existing session store")
} else {
sess, err := a.getStore(p, externalHost)
if err != nil {
+28 -23
View File
@@ -29,7 +29,10 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL)
// Add one to the validity to ensure we don't have a session with indefinite length
maxAge = int(*t) + 1
}
if a.isEmbedded {
sessionBackend := a.srv.SessionBackend()
switch sessionBackend {
case "postgres":
// New PostgreSQL store
ps, err := postgresstore.NewPostgresStore(a.log)
if err != nil {
@@ -46,30 +49,32 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL)
Path: "/",
})
a.log.Trace("using postgresql session backend")
return ps, nil
}
dir := os.TempDir()
cs, err := filesystemstore.GetPersistentStore(dir)
if err != nil {
return nil, err
}
cs.Codecs = codecs.CodecsFromPairs(maxAge, []byte(*p.CookieSecret))
// https://github.com/markbates/goth/commit/7276be0fdf719ddff753f3574ef0f967e4a5a5f7
// set the maxLength of the cookies stored on the disk to a larger number to prevent issues with:
// securecookie: the value is too long
// when using OpenID Connect, since this can contain a large amount of extra information in the id_token
case "filesystem":
dir := os.TempDir()
cs, err := filesystemstore.GetPersistentStore(dir)
if err != nil {
return nil, err
}
cs.Codecs = codecs.CodecsFromPairs(maxAge, []byte(*p.CookieSecret))
// https://github.com/markbates/goth/commit/7276be0fdf719ddff753f3574ef0f967e4a5a5f7
// set the maxLength of the cookies stored on the disk to a larger number to prevent issues with:
// securecookie: the value is too long
// when using OpenID Connect, since this can contain a large amount of extra information in the id_token
// Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk
cs.MaxLength(math.MaxInt)
cs.Options.HttpOnly = true
cs.Options.Secure = strings.ToLower(externalHost.Scheme) == "https"
cs.Options.Domain = *p.CookieDomain
cs.Options.SameSite = http.SameSiteLaxMode
cs.Options.MaxAge = maxAge
cs.Options.Path = "/"
a.log.WithField("dir", dir).Trace("using filesystem session backend")
return cs, nil
// Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk
cs.MaxLength(math.MaxInt)
cs.Options.HttpOnly = true
cs.Options.Secure = strings.ToLower(externalHost.Scheme) == "https"
cs.Options.Domain = *p.CookieDomain
cs.Options.SameSite = http.SameSiteLaxMode
cs.Options.MaxAge = maxAge
cs.Options.Path = "/"
return cs, nil
default:
a.log.WithField("backend", sessionBackend).Panic("unknown session backend type")
return nil, nil
}
}
func (a *Application) SessionName() string {
@@ -41,6 +41,10 @@ func (ts *testServer) Apps() []*Application {
return ts.apps
}
func (ts *testServer) SessionBackend() string {
return "filesystem"
}
func newTestApplication() *Application {
ts := newTestServer()
a, _ := NewApplication(
+5
View File
@@ -55,6 +55,11 @@ func NewProxyServer(ac *ak.APIController) ak.Outpost {
if ac.GlobalConfig.ErrorReporting.Enabled {
globalMux.Use(sentryhttp.New(sentryhttp.Options{}).Handle)
}
if ac.IsEmbedded() {
l.Info("using PostgreSQL session backend")
} else {
l.Info("using filesystem session backend")
}
s := &ProxyServer{
cryptoStore: ak.NewCryptoStore(ac.Client.CryptoApi),
apps: make(map[string]*application.Application),
+22 -1
View File
@@ -15,7 +15,9 @@ import (
)
func (ps *ProxyServer) Refresh() error {
providers, err := ak.Paginator(ps.akAPI.Client.OutpostsApi.OutpostsProxyList(context.Background()), ak.PaginatorOptions{
req := ps.akAPI.Client.OutpostsApi.OutpostsProxyList(context.Background())
ps.log.WithField("outpost_pk", ps.akAPI.Outpost.Pk).Debug("Requesting providers for outpost")
providers, err := ak.Paginator(req, ak.PaginatorOptions{
PageSize: 100,
Logger: ps.log,
})
@@ -25,6 +27,13 @@ func (ps *ProxyServer) Refresh() error {
if err != nil {
return err
}
ps.log.WithField("count", len(providers)).Debug("Fetched providers")
if len(providers) == 0 {
ps.log.Warning("No providers assigned to this outpost, check outpost configuration in authentik")
}
for i, p := range providers {
ps.log.WithField("index", i).WithField("name", p.Name).WithField("external_host", p.ExternalHost).WithField("assigned_to_app", p.AssignedApplicationName).Debug("Provider details")
}
apps := make(map[string]*application.Application)
for _, provider := range providers {
rsp := sentry.StartSpan(context.Background(), "authentik.outposts.proxy.application_ss")
@@ -52,6 +61,7 @@ func (ps *ProxyServer) Refresh() error {
ps.log.WithError(err).Warning("failed to setup application")
continue
}
ps.log.WithField("name", provider.Name).WithField("host", externalHost.Host).Info("Loaded application")
apps[externalHost.Host] = a
}
ps.apps = apps
@@ -70,3 +80,14 @@ func (ps *ProxyServer) CryptoStore() *ak.CryptoStore {
func (ps *ProxyServer) Apps() []*application.Application {
return maps.Values(ps.apps)
}
func (ps *ProxyServer) SessionBackend() string {
if ps.akAPI.IsEmbedded() {
return "postgres"
}
if !ps.akAPI.IsEmbedded() {
return "filesystem"
}
ps.log.Panic("failed to determine session backend type")
return ""
}