Merge branch 'master' into version-2021.12
This commit is contained in:
commit
741822424a
20
README.md
20
README.md
|
@ -38,3 +38,23 @@ See [Development Documentation](https://goauthentik.io/developer-docs/?utm_sourc
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
See [SECURITY.md](SECURITY.md)
|
See [SECURITY.md](SECURITY.md)
|
||||||
|
|
||||||
|
## Sponsors
|
||||||
|
|
||||||
|
This project is proudly sponsored by:
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://www.digitalocean.com/?utm_medium=opensource&utm_source=goauthentik.io">
|
||||||
|
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
DigitalOcean provides development and testing resources for authentik.
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://www.netlify.com">
|
||||||
|
<img src="https://www.netlify.com/img/global/badges/netlify-color-accent.svg" alt="Deploys by Netlify" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Netlify hosts the [goauthentik.io](goauthentik.io) site.
|
||||||
|
|
|
@ -278,7 +278,13 @@ class Application(PolicyBindingModel):
|
||||||
"""Get casted provider instance"""
|
"""Get casted provider instance"""
|
||||||
if not self.provider:
|
if not self.provider:
|
||||||
return None
|
return None
|
||||||
|
# if the Application class has been cache, self.provider is set
|
||||||
|
# but doing a direct query lookup will fail.
|
||||||
|
# In that case, just return None
|
||||||
|
try:
|
||||||
return Provider.objects.get_subclass(pk=self.provider.pk)
|
return Provider.objects.get_subclass(pk=self.provider.pk)
|
||||||
|
except Provider.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import time
|
import time
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from inspect import getmodule, stack
|
from inspect import currentframe
|
||||||
from smtplib import SMTPException
|
from smtplib import SMTPException
|
||||||
from typing import TYPE_CHECKING, Optional, Type, Union
|
from typing import TYPE_CHECKING, Optional, Type, Union
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
@ -192,14 +192,15 @@ class Event(ExpiringModel):
|
||||||
def new(
|
def new(
|
||||||
action: Union[str, EventAction],
|
action: Union[str, EventAction],
|
||||||
app: Optional[str] = None,
|
app: Optional[str] = None,
|
||||||
_inspect_offset: int = 1,
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> "Event":
|
) -> "Event":
|
||||||
"""Create new Event instance from arguments. Instance is NOT saved."""
|
"""Create new Event instance from arguments. Instance is NOT saved."""
|
||||||
if not isinstance(action, EventAction):
|
if not isinstance(action, EventAction):
|
||||||
action = EventAction.CUSTOM_PREFIX + action
|
action = EventAction.CUSTOM_PREFIX + action
|
||||||
if not app:
|
if not app:
|
||||||
app = getmodule(stack()[_inspect_offset][0]).__name__
|
current = currentframe()
|
||||||
|
parent = current.f_back
|
||||||
|
app = parent.f_globals["__name__"]
|
||||||
cleaned_kwargs = cleanse_dict(sanitize_dict(kwargs))
|
cleaned_kwargs = cleanse_dict(sanitize_dict(kwargs))
|
||||||
event = Event(action=action, app=app, context=cleaned_kwargs)
|
event = Event(action=action, app=app, context=cleaned_kwargs)
|
||||||
return event
|
return event
|
||||||
|
|
|
@ -28,6 +28,7 @@ from sentry_sdk.integrations.boto3 import Boto3Integration
|
||||||
from sentry_sdk.integrations.celery import CeleryIntegration
|
from sentry_sdk.integrations.celery import CeleryIntegration
|
||||||
from sentry_sdk.integrations.django import DjangoIntegration
|
from sentry_sdk.integrations.django import DjangoIntegration
|
||||||
from sentry_sdk.integrations.redis import RedisIntegration
|
from sentry_sdk.integrations.redis import RedisIntegration
|
||||||
|
from sentry_sdk.integrations.threading import ThreadingIntegration
|
||||||
|
|
||||||
from authentik import ENV_GIT_HASH_KEY, __version__
|
from authentik import ENV_GIT_HASH_KEY, __version__
|
||||||
from authentik.core.middleware import structlog_add_request_id
|
from authentik.core.middleware import structlog_add_request_id
|
||||||
|
@ -424,6 +425,7 @@ if _ERROR_REPORTING:
|
||||||
CeleryIntegration(),
|
CeleryIntegration(),
|
||||||
RedisIntegration(),
|
RedisIntegration(),
|
||||||
Boto3Integration(),
|
Boto3Integration(),
|
||||||
|
ThreadingIntegration(propagate_hub=True),
|
||||||
],
|
],
|
||||||
before_send=before_send,
|
before_send=before_send,
|
||||||
release=f"authentik@{__version__}",
|
release=f"authentik@{__version__}",
|
||||||
|
|
|
@ -60,6 +60,7 @@ type FlowExecutor struct {
|
||||||
|
|
||||||
func NewFlowExecutor(ctx context.Context, flowSlug string, refConfig *api.Configuration, logFields log.Fields) *FlowExecutor {
|
func NewFlowExecutor(ctx context.Context, flowSlug string, refConfig *api.Configuration, logFields log.Fields) *FlowExecutor {
|
||||||
rsp := sentry.StartSpan(ctx, "authentik.outposts.flow_executor")
|
rsp := sentry.StartSpan(ctx, "authentik.outposts.flow_executor")
|
||||||
|
rsp.Description = flowSlug
|
||||||
|
|
||||||
l := log.WithField("flow", flowSlug).WithFields(logFields)
|
l := log.WithField("flow", flowSlug).WithFields(logFields)
|
||||||
jar, err := cookiejar.New(nil)
|
jar, err := cookiejar.New(nil)
|
||||||
|
@ -153,8 +154,8 @@ func (fe *FlowExecutor) solveFlowChallenge(depth int) (bool, error) {
|
||||||
}
|
}
|
||||||
ch := challenge.GetActualInstance().(ChallengeInt)
|
ch := challenge.GetActualInstance().(ChallengeInt)
|
||||||
fe.log.WithField("component", ch.GetComponent()).WithField("type", ch.GetType()).Debug("Got challenge")
|
fe.log.WithField("component", ch.GetComponent()).WithField("type", ch.GetType()).Debug("Got challenge")
|
||||||
gcsp.SetTag("ak_challenge", string(ch.GetType()))
|
gcsp.SetTag("authentik.flow.challenge", string(ch.GetType()))
|
||||||
gcsp.SetTag("ak_component", ch.GetComponent())
|
gcsp.SetTag("authentik.flow.component", ch.GetComponent())
|
||||||
gcsp.Finish()
|
gcsp.Finish()
|
||||||
FlowTimingGet.With(prometheus.Labels{
|
FlowTimingGet.With(prometheus.Labels{
|
||||||
"stage": ch.GetComponent(),
|
"stage": ch.GetComponent(),
|
||||||
|
@ -202,8 +203,8 @@ func (fe *FlowExecutor) solveFlowChallenge(depth int) (bool, error) {
|
||||||
response, _, err := responseReq.Execute()
|
response, _, err := responseReq.Execute()
|
||||||
ch = response.GetActualInstance().(ChallengeInt)
|
ch = response.GetActualInstance().(ChallengeInt)
|
||||||
fe.log.WithField("component", ch.GetComponent()).WithField("type", ch.GetType()).Debug("Got response")
|
fe.log.WithField("component", ch.GetComponent()).WithField("type", ch.GetType()).Debug("Got response")
|
||||||
scsp.SetTag("ak_challenge", string(ch.GetType()))
|
scsp.SetTag("authentik.flow.challenge", string(ch.GetType()))
|
||||||
scsp.SetTag("ak_component", ch.GetComponent())
|
scsp.SetTag("authentik.flow.component", ch.GetComponent())
|
||||||
scsp.Finish()
|
scsp.Finish()
|
||||||
|
|
||||||
switch ch.GetComponent() {
|
switch ch.GetComponent() {
|
||||||
|
|
|
@ -23,9 +23,18 @@ type Request struct {
|
||||||
func NewRequest(bindDN string, bindPW string, conn net.Conn) (*Request, *sentry.Span) {
|
func NewRequest(bindDN string, bindPW string, conn net.Conn) (*Request, *sentry.Span) {
|
||||||
span := sentry.StartSpan(context.TODO(), "authentik.providers.ldap.bind",
|
span := sentry.StartSpan(context.TODO(), "authentik.providers.ldap.bind",
|
||||||
sentry.TransactionName("authentik.providers.ldap.bind"))
|
sentry.TransactionName("authentik.providers.ldap.bind"))
|
||||||
|
span.Description = bindDN
|
||||||
rid := uuid.New().String()
|
rid := uuid.New().String()
|
||||||
span.SetTag("request_uid", rid)
|
span.SetTag("request_uid", rid)
|
||||||
span.SetTag("user.username", bindDN)
|
hub := sentry.GetHubFromContext(span.Context())
|
||||||
|
if hub == nil {
|
||||||
|
hub = sentry.CurrentHub()
|
||||||
|
}
|
||||||
|
hub.Scope().SetUser(sentry.User{
|
||||||
|
Username: bindDN,
|
||||||
|
ID: bindDN,
|
||||||
|
IPAddress: utils.GetIP(conn.RemoteAddr()),
|
||||||
|
})
|
||||||
|
|
||||||
bindDN = strings.ToLower(bindDN)
|
bindDN = strings.ToLower(bindDN)
|
||||||
return &Request{
|
return &Request{
|
||||||
|
|
|
@ -2,6 +2,7 @@ package search
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -27,10 +28,19 @@ func NewRequest(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (*Re
|
||||||
bindDN = strings.ToLower(bindDN)
|
bindDN = strings.ToLower(bindDN)
|
||||||
searchReq.BaseDN = strings.ToLower(searchReq.BaseDN)
|
searchReq.BaseDN = strings.ToLower(searchReq.BaseDN)
|
||||||
span := sentry.StartSpan(context.TODO(), "authentik.providers.ldap.search", sentry.TransactionName("authentik.providers.ldap.search"))
|
span := sentry.StartSpan(context.TODO(), "authentik.providers.ldap.search", sentry.TransactionName("authentik.providers.ldap.search"))
|
||||||
|
span.Description = fmt.Sprintf("%s (%s)", searchReq.BaseDN, ldap.ScopeMap[searchReq.Scope])
|
||||||
span.SetTag("request_uid", rid)
|
span.SetTag("request_uid", rid)
|
||||||
span.SetTag("user.username", bindDN)
|
hub := sentry.GetHubFromContext(span.Context())
|
||||||
span.SetTag("ak_filter", searchReq.Filter)
|
if hub == nil {
|
||||||
span.SetTag("ak_base_dn", searchReq.BaseDN)
|
hub = sentry.CurrentHub()
|
||||||
|
}
|
||||||
|
hub.Scope().SetUser(sentry.User{
|
||||||
|
Username: bindDN,
|
||||||
|
ID: bindDN,
|
||||||
|
IPAddress: utils.GetIP(conn.RemoteAddr()),
|
||||||
|
})
|
||||||
|
span.SetTag("ldap_filter", searchReq.Filter)
|
||||||
|
span.SetTag("ldap_base_dn", searchReq.BaseDN)
|
||||||
return &Request{
|
return &Request{
|
||||||
SearchRequest: searchReq,
|
SearchRequest: searchReq,
|
||||||
BindDN: bindDN,
|
BindDN: bindDN,
|
||||||
|
|
|
@ -12,6 +12,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc"
|
"github.com/coreos/go-oidc"
|
||||||
|
"github.com/getsentry/sentry-go"
|
||||||
|
sentryhttp "github.com/getsentry/sentry-go/http"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -109,6 +111,15 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore
|
||||||
user := ""
|
user := ""
|
||||||
if c != nil {
|
if c != nil {
|
||||||
user = c.PreferredUsername
|
user = c.PreferredUsername
|
||||||
|
hub := sentry.GetHubFromContext(r.Context())
|
||||||
|
if hub == nil {
|
||||||
|
hub = sentry.CurrentHub()
|
||||||
|
}
|
||||||
|
hub.Scope().SetUser(sentry.User{
|
||||||
|
Username: user,
|
||||||
|
ID: c.Sub,
|
||||||
|
IPAddress: r.RemoteAddr,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
before := time.Now()
|
before := time.Now()
|
||||||
inner.ServeHTTP(rw, r)
|
inner.ServeHTTP(rw, r)
|
||||||
|
@ -124,6 +135,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore
|
||||||
}).Observe(float64(after))
|
}).Observe(float64(after))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
mux.Use(sentryhttp.New(sentryhttp.Options{}).Handle)
|
||||||
|
|
||||||
// Support /start and /sign_in for backwards compatibility
|
// Support /start and /sign_in for backwards compatibility
|
||||||
mux.HandleFunc("/akprox/start", a.handleRedirect)
|
mux.HandleFunc("/akprox/start", a.handleRedirect)
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
sentryhttp "github.com/getsentry/sentry-go/http"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/pires/go-proxyproto"
|
"github.com/pires/go-proxyproto"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
@ -52,6 +53,7 @@ func NewProxyServer(ac *ak.APIController, portOffset int) *ProxyServer {
|
||||||
|
|
||||||
globalMux := rootMux.NewRoute().Subrouter()
|
globalMux := rootMux.NewRoute().Subrouter()
|
||||||
globalMux.Use(web.NewLoggingHandler(l.WithField("logger", "authentik.outpost.proxyv2.http"), nil))
|
globalMux.Use(web.NewLoggingHandler(l.WithField("logger", "authentik.outpost.proxyv2.http"), nil))
|
||||||
|
globalMux.Use(sentryhttp.New(sentryhttp.Options{}).Handle)
|
||||||
s := &ProxyServer{
|
s := &ProxyServer{
|
||||||
Listen: "0.0.0.0:%d",
|
Listen: "0.0.0.0:%d",
|
||||||
PortOffset: portOffset,
|
PortOffset: portOffset,
|
||||||
|
|
|
@ -99,8 +99,8 @@ func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
h.handler.ServeHTTP(responseLogger, req)
|
h.handler.ServeHTTP(responseLogger, req)
|
||||||
duration := float64(time.Since(t)) / float64(time.Millisecond)
|
duration := float64(time.Since(t)) / float64(time.Millisecond)
|
||||||
h.afterHandler(h.logger.WithFields(log.Fields{
|
h.afterHandler(h.logger.WithFields(log.Fields{
|
||||||
"host": req.RemoteAddr,
|
"remote": req.RemoteAddr,
|
||||||
"vhost": GetHost(req),
|
"host": 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,
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
package web
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/getsentry/sentry-go"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"goauthentik.io/internal/utils/web"
|
|
||||||
)
|
|
||||||
|
|
||||||
func loggingMiddleware(l *log.Entry) func(next http.Handler) http.Handler {
|
|
||||||
return func(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
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)
|
|
||||||
after := time.Now()
|
|
||||||
l.WithFields(log.Fields{
|
|
||||||
"remote": r.RemoteAddr,
|
|
||||||
"method": r.Method,
|
|
||||||
"took": after.Sub(before),
|
|
||||||
"host": web.GetHost(r),
|
|
||||||
}).Info(r.RequestURI)
|
|
||||||
span.Finish()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
package web
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
sentryhttp "github.com/getsentry/sentry-go/http"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func recoveryMiddleware() func(next http.Handler) http.Handler {
|
|
||||||
sentryHandler := sentryhttp.New(sentryhttp.Options{})
|
|
||||||
l := log.WithField("logger", "authentik.router.sentry")
|
|
||||||
return func(next http.Handler) http.Handler {
|
|
||||||
sentryHandler.Handle(next)
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
defer func() {
|
|
||||||
re := recover()
|
|
||||||
if re == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err := re.(error)
|
|
||||||
if err != nil {
|
|
||||||
l.WithError(err).Warning("global panic handler")
|
|
||||||
jsonBody, _ := json.Marshal(struct {
|
|
||||||
Successful bool
|
|
||||||
Error string
|
|
||||||
}{
|
|
||||||
Successful: false,
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
_, err := w.Write(jsonBody)
|
|
||||||
if err != nil {
|
|
||||||
l.WithError(err).Warning("Failed to write sentry error body")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
sentryhttp "github.com/getsentry/sentry-go/http"
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/pires/go-proxyproto"
|
"github.com/pires/go-proxyproto"
|
||||||
|
@ -13,6 +14,7 @@ import (
|
||||||
"goauthentik.io/internal/config"
|
"goauthentik.io/internal/config"
|
||||||
"goauthentik.io/internal/gounicorn"
|
"goauthentik.io/internal/gounicorn"
|
||||||
"goauthentik.io/internal/outpost/proxyv2"
|
"goauthentik.io/internal/outpost/proxyv2"
|
||||||
|
"goauthentik.io/internal/utils/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WebServer struct {
|
type WebServer struct {
|
||||||
|
@ -34,13 +36,11 @@ type WebServer struct {
|
||||||
func NewWebServer(g *gounicorn.GoUnicorn) *WebServer {
|
func NewWebServer(g *gounicorn.GoUnicorn) *WebServer {
|
||||||
l := log.WithField("logger", "authentik.router")
|
l := log.WithField("logger", "authentik.router")
|
||||||
mainHandler := mux.NewRouter()
|
mainHandler := mux.NewRouter()
|
||||||
if config.G.ErrorReporting.Enabled {
|
mainHandler.Use(sentryhttp.New(sentryhttp.Options{}).Handle)
|
||||||
mainHandler.Use(recoveryMiddleware())
|
|
||||||
}
|
|
||||||
mainHandler.Use(handlers.ProxyHeaders)
|
mainHandler.Use(handlers.ProxyHeaders)
|
||||||
mainHandler.Use(handlers.CompressHandler)
|
mainHandler.Use(handlers.CompressHandler)
|
||||||
logginRouter := mainHandler.NewRoute().Subrouter()
|
logginRouter := mainHandler.NewRoute().Subrouter()
|
||||||
logginRouter.Use(loggingMiddleware(l))
|
logginRouter.Use(web.NewLoggingHandler(l, nil))
|
||||||
|
|
||||||
ws := &WebServer{
|
ws := &WebServer{
|
||||||
LegacyProxy: true,
|
LegacyProxy: true,
|
||||||
|
@ -72,7 +72,7 @@ func (ws *WebServer) Shutdown() {
|
||||||
func (ws *WebServer) listenPlain() {
|
func (ws *WebServer) listenPlain() {
|
||||||
ln, err := net.Listen("tcp", config.G.Web.Listen)
|
ln, err := net.Listen("tcp", config.G.Web.Listen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ws.log.WithError(err).Fatalf("failed to listen")
|
ws.log.WithError(err).Fatal("failed to listen")
|
||||||
}
|
}
|
||||||
ws.log.WithField("listen", config.G.Web.Listen).Info("Listening")
|
ws.log.WithField("listen", config.G.Web.Listen).Info("Listening")
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ func (ws *WebServer) listenPlain() {
|
||||||
|
|
||||||
err = http.ListenAndServe(config.G.Web.Listen, ws.m)
|
err = http.ListenAndServe(config.G.Web.Listen, ws.m)
|
||||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
ws.log.Errorf("ERROR: http.Serve() - %s", err)
|
ws.log.WithError(err).Error("failed to listen")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,14 +100,14 @@ func (ws *WebServer) serve(listener net.Listener) {
|
||||||
// We received an interrupt signal, shut down.
|
// We received an interrupt signal, shut down.
|
||||||
if err := srv.Shutdown(context.Background()); err != nil {
|
if err := srv.Shutdown(context.Background()); err != nil {
|
||||||
// Error from closing listeners, or context timeout:
|
// Error from closing listeners, or context timeout:
|
||||||
ws.log.Printf("HTTP server Shutdown: %v", err)
|
ws.log.WithError(err).Warning("HTTP server Shutdown")
|
||||||
}
|
}
|
||||||
close(idleConnsClosed)
|
close(idleConnsClosed)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err := srv.Serve(listener)
|
err := srv.Serve(listener)
|
||||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
ws.log.Errorf("ERROR: http.Serve() - %s", err)
|
ws.log.WithError(err).Error("ERROR: http.Serve()")
|
||||||
}
|
}
|
||||||
<-idleConnsClosed
|
<-idleConnsClosed
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,7 +67,7 @@ if [[ "$1" == "server" ]]; then
|
||||||
/authentik-proxy
|
/authentik-proxy
|
||||||
elif [[ "$1" == "worker" ]]; then
|
elif [[ "$1" == "worker" ]]; then
|
||||||
echo "worker" > $MODE_FILE
|
echo "worker" > $MODE_FILE
|
||||||
check_if_root "celery -A authentik.root.celery worker --autoscale 3,1 -E -B -s /tmp/celerybeat-schedule -Q authentik,authentik_scheduled,authentik_events"
|
check_if_root "celery -A authentik.root.celery worker -Ofair --autoscale 3,1 -E -B -s /tmp/celerybeat-schedule -Q authentik,authentik_scheduled,authentik_events"
|
||||||
elif [[ "$1" == "flower" ]]; then
|
elif [[ "$1" == "flower" ]]; then
|
||||||
echo "flower" > $MODE_FILE
|
echo "flower" > $MODE_FILE
|
||||||
celery -A authentik.root.celery flower
|
celery -A authentik.root.celery flower
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { CSSResult, LitElement, TemplateResult, html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||||
|
|
||||||
|
import AKGlobal from "../authentik.css";
|
||||||
|
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||||
|
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||||
|
|
||||||
|
export interface MarkdownDocument {
|
||||||
|
html: string;
|
||||||
|
metadata: { [key: string]: string };
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ak-markdown")
|
||||||
|
export class Markdown extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
md?: MarkdownDocument;
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [PFList, PFContent, AKGlobal];
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): TemplateResult {
|
||||||
|
if (!this.md) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
const finalHTML = this.md?.html.replace("<ul>", "<ul class='pf-c-list'>");
|
||||||
|
return html`${this.md?.metadata.title ? html`<h2>${this.md.metadata.title}</h2>` : html``}
|
||||||
|
${unsafeHTML(finalHTML)}`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -81,7 +81,7 @@ export class PageHeader extends LitElement {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
.notification-trigger.has-notifications {
|
.notification-trigger.has-notifications {
|
||||||
color: #2b9af3;
|
color: var(--pf-global--active-color--100);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
|
@ -183,9 +183,15 @@ export class NotificationDrawer extends LitElement {
|
||||||
composed: true,
|
composed: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent(EVENT_NOTIFICATION_DRAWER_TOGGLE, {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
class="pf-c-button pf-m-secondary pf-m-block"
|
class="pf-c-button pf-m-primary pf-m-block"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label=${t`Clear all`}
|
aria-label=${t`Clear all`}
|
||||||
>
|
>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||||
|
import PFSidebar from "@patternfly/patternfly/components/Sidebar/sidebar.css";
|
||||||
|
|
||||||
import "../../elements/PageHeader";
|
import "../../elements/PageHeader";
|
||||||
import { Table } from "./Table";
|
import { Table } from "./Table";
|
||||||
|
@ -14,7 +15,15 @@ export abstract class TablePage<T> extends Table<T> {
|
||||||
abstract pageIcon(): string;
|
abstract pageIcon(): string;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return super.styles.concat(PFPage, PFContent);
|
return super.styles.concat(PFPage, PFContent, PFSidebar);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSidebarBefore(): TemplateResult {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSidebarAfter(): TemplateResult {
|
||||||
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
|
@ -25,7 +34,15 @@ export abstract class TablePage<T> extends Table<T> {
|
||||||
>
|
>
|
||||||
</ak-page-header>
|
</ak-page-header>
|
||||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||||
|
<div class="pf-c-sidebar pf-m-gutter">
|
||||||
|
<div class="pf-c-sidebar__main">
|
||||||
|
${this.renderSidebarBefore()}
|
||||||
|
<div class="pf-c-sidebar__content">
|
||||||
<div class="pf-c-card">${this.renderTable()}</div>
|
<div class="pf-c-card">${this.renderTable()}</div>
|
||||||
|
</div>
|
||||||
|
${this.renderSidebarAfter()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>`;
|
</section>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
declare module "*.css";
|
declare module "*.css";
|
||||||
declare module "*.md" {
|
declare module "*.md" {
|
||||||
const html: string;
|
const html: string;
|
||||||
const metadata: object;
|
const metadata: { [key: string]: string };
|
||||||
const filename: string;
|
const filename: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -122,6 +122,10 @@ msgstr "API Token (can be used to access the API programmatically)"
|
||||||
msgid "API request failed"
|
msgid "API request failed"
|
||||||
msgstr "API request failed"
|
msgstr "API request failed"
|
||||||
|
|
||||||
|
#: src/pages/applications/ApplicationListPage.ts
|
||||||
|
msgid "About applications"
|
||||||
|
msgstr "About applications"
|
||||||
|
|
||||||
#: src/pages/sources/oauth/OAuthSourceViewPage.ts
|
#: src/pages/sources/oauth/OAuthSourceViewPage.ts
|
||||||
msgid "Access Key"
|
msgid "Access Key"
|
||||||
msgstr "Access Key"
|
msgstr "Access Key"
|
||||||
|
|
|
@ -128,6 +128,10 @@ msgstr "Jeton d'API (peut être utilisé pour accéder à l'API via un programme
|
||||||
msgid "API request failed"
|
msgid "API request failed"
|
||||||
msgstr "Requête d'API échouée"
|
msgstr "Requête d'API échouée"
|
||||||
|
|
||||||
|
#: src/pages/applications/ApplicationListPage.ts
|
||||||
|
msgid "About applications"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/sources/oauth/OAuthSourceViewPage.ts
|
#: src/pages/sources/oauth/OAuthSourceViewPage.ts
|
||||||
msgid "Access Key"
|
msgid "Access Key"
|
||||||
msgstr "Clé d'accès"
|
msgstr "Clé d'accès"
|
||||||
|
|
|
@ -122,6 +122,10 @@ msgstr ""
|
||||||
msgid "API request failed"
|
msgid "API request failed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/applications/ApplicationListPage.ts
|
||||||
|
msgid "About applications"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/sources/oauth/OAuthSourceViewPage.ts
|
#: src/pages/sources/oauth/OAuthSourceViewPage.ts
|
||||||
msgid "Access Key"
|
msgid "Access Key"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
|
@ -5,12 +5,15 @@ import { customElement, property } from "lit/decorators.js";
|
||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
|
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
|
||||||
|
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||||
|
|
||||||
import { Application, CoreApi } from "@goauthentik/api";
|
import { Application, CoreApi } from "@goauthentik/api";
|
||||||
|
|
||||||
|
import MDApplication from "../../../../website/docs/core/applications.md";
|
||||||
import { AKResponse } from "../../api/Client";
|
import { AKResponse } from "../../api/Client";
|
||||||
import { DEFAULT_CONFIG } from "../../api/Config";
|
import { DEFAULT_CONFIG } from "../../api/Config";
|
||||||
import { uiConfig } from "../../common/config";
|
import { uiConfig } from "../../common/config";
|
||||||
|
import "../../elements/Markdown";
|
||||||
import "../../elements/buttons/SpinnerButton";
|
import "../../elements/buttons/SpinnerButton";
|
||||||
import "../../elements/forms/DeleteBulkForm";
|
import "../../elements/forms/DeleteBulkForm";
|
||||||
import "../../elements/forms/ModalForm";
|
import "../../elements/forms/ModalForm";
|
||||||
|
@ -52,6 +55,7 @@ export class ApplicationListPage extends TablePage<Application> {
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return super.styles.concat(
|
return super.styles.concat(
|
||||||
PFAvatar,
|
PFAvatar,
|
||||||
|
PFCard,
|
||||||
css`
|
css`
|
||||||
tr td:first-child {
|
tr td:first-child {
|
||||||
width: auto;
|
width: auto;
|
||||||
|
@ -74,6 +78,17 @@ export class ApplicationListPage extends TablePage<Application> {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderSidebarAfter(): TemplateResult {
|
||||||
|
return html`<div class="pf-c-sidebar__panel pf-m-width-25">
|
||||||
|
<div class="pf-c-card">
|
||||||
|
<div class="pf-c-card__title">${t`About applications`}</div>
|
||||||
|
<div class="pf-c-card__body">
|
||||||
|
<ak-markdown .md=${MDApplication}></ak-markdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
renderToolbarSelected(): TemplateResult {
|
renderToolbarSelected(): TemplateResult {
|
||||||
const disabled = this.selectedElements.length < 1;
|
const disabled = this.selectedElements.length < 1;
|
||||||
return html`<ak-forms-delete-bulk
|
return html`<ak-forms-delete-bulk
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { t } from "@lingui/macro";
|
import { t } from "@lingui/macro";
|
||||||
|
|
||||||
import { CSSResult, LitElement, TemplateResult, html } from "lit";
|
import { CSSResult, LitElement, TemplateResult, html } from "lit";
|
||||||
import { unsafeHTML } from "lit-html/directives/unsafe-html.js";
|
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
import AKGlobal from "../../../authentik.css";
|
import AKGlobal from "../../../authentik.css";
|
||||||
|
@ -28,6 +27,8 @@ import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||||
import { EVENT_REFRESH } from "../../../constants";
|
import { EVENT_REFRESH } from "../../../constants";
|
||||||
import "../../../elements/CodeMirror";
|
import "../../../elements/CodeMirror";
|
||||||
import { PFColor } from "../../../elements/Label";
|
import { PFColor } from "../../../elements/Label";
|
||||||
|
import "../../../elements/Markdown";
|
||||||
|
import { MarkdownDocument } from "../../../elements/Markdown";
|
||||||
import "../../../elements/Tabs";
|
import "../../../elements/Tabs";
|
||||||
import "../../../elements/buttons/ModalButton";
|
import "../../../elements/buttons/ModalButton";
|
||||||
import "../../../elements/buttons/SpinnerButton";
|
import "../../../elements/buttons/SpinnerButton";
|
||||||
|
@ -90,20 +91,19 @@ export class ProxyProviderViewPage extends LitElement {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderConfigTemplate(tmpl: string): TemplateResult {
|
renderConfigTemplate(markdown: MarkdownDocument): MarkdownDocument {
|
||||||
// See website/docs/providers/proxy/forward_auth.mdx
|
// See website/docs/providers/proxy/forward_auth.mdx
|
||||||
let final = "";
|
|
||||||
if (this.provider?.mode === ProxyMode.ForwardSingle) {
|
if (this.provider?.mode === ProxyMode.ForwardSingle) {
|
||||||
final = tmpl
|
markdown.html = markdown.html
|
||||||
.replaceAll("authentik.company", window.location.hostname)
|
.replaceAll("authentik.company", window.location.hostname)
|
||||||
.replaceAll("outpost.company", window.location.hostname)
|
.replaceAll("outpost.company", window.location.hostname)
|
||||||
.replaceAll("app.company", this.provider?.externalHost || "");
|
.replaceAll("app.company", this.provider?.externalHost || "");
|
||||||
} else if (this.provider?.mode == ProxyMode.ForwardDomain) {
|
} else if (this.provider?.mode == ProxyMode.ForwardDomain) {
|
||||||
final = tmpl
|
markdown.html = markdown.html
|
||||||
.replaceAll("authentik.company", window.location.hostname)
|
.replaceAll("authentik.company", window.location.hostname)
|
||||||
.replaceAll("outpost.company", this.provider?.externalHost || "");
|
.replaceAll("outpost.company", this.provider?.externalHost || "");
|
||||||
}
|
}
|
||||||
return html`${unsafeHTML(final)}`;
|
return markdown;
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
|
@ -250,42 +250,54 @@ export class ProxyProviderViewPage extends LitElement {
|
||||||
data-tab-title="${t`Nginx (Ingress)`}"
|
data-tab-title="${t`Nginx (Ingress)`}"
|
||||||
class="pf-c-page__main-section pf-m-light pf-m-no-padding-mobile"
|
class="pf-c-page__main-section pf-m-light pf-m-no-padding-mobile"
|
||||||
>
|
>
|
||||||
${this.renderConfigTemplate(MDNginxIngress.html)}
|
<ak-markdown
|
||||||
|
.md=${this.renderConfigTemplate(MDNginxIngress)}
|
||||||
|
></ak-markdown>
|
||||||
</section>
|
</section>
|
||||||
<section
|
<section
|
||||||
slot="page-nginx-proxy-manager"
|
slot="page-nginx-proxy-manager"
|
||||||
data-tab-title="${t`Nginx (Proxy Manager)`}"
|
data-tab-title="${t`Nginx (Proxy Manager)`}"
|
||||||
class="pf-c-page__main-section pf-m-light pf-m-no-padding-mobile"
|
class="pf-c-page__main-section pf-m-light pf-m-no-padding-mobile"
|
||||||
>
|
>
|
||||||
${this.renderConfigTemplate(MDNginxPM.html)}
|
<ak-markdown
|
||||||
|
.md=${this.renderConfigTemplate(MDNginxPM)}
|
||||||
|
></ak-markdown>
|
||||||
</section>
|
</section>
|
||||||
<section
|
<section
|
||||||
slot="page-nginx-standalone"
|
slot="page-nginx-standalone"
|
||||||
data-tab-title="${t`Nginx (standalone)`}"
|
data-tab-title="${t`Nginx (standalone)`}"
|
||||||
class="pf-c-page__main-section pf-m-light pf-m-no-padding-mobile"
|
class="pf-c-page__main-section pf-m-light pf-m-no-padding-mobile"
|
||||||
>
|
>
|
||||||
${this.renderConfigTemplate(MDNginxStandalone.html)}
|
<ak-markdown
|
||||||
|
.md=${this.renderConfigTemplate(MDNginxStandalone)}
|
||||||
|
></ak-markdown>
|
||||||
</section>
|
</section>
|
||||||
<section
|
<section
|
||||||
slot="page-traefik-ingress"
|
slot="page-traefik-ingress"
|
||||||
data-tab-title="${t`Traefik (Ingress)`}"
|
data-tab-title="${t`Traefik (Ingress)`}"
|
||||||
class="pf-c-page__main-section pf-m-light pf-m-no-padding-mobile"
|
class="pf-c-page__main-section pf-m-light pf-m-no-padding-mobile"
|
||||||
>
|
>
|
||||||
${this.renderConfigTemplate(MDTraefikIngres.html)}
|
<ak-markdown
|
||||||
|
.md=${this.renderConfigTemplate(MDTraefikIngres)}
|
||||||
|
></ak-markdown>
|
||||||
</section>
|
</section>
|
||||||
<section
|
<section
|
||||||
slot="page-traefik-compose"
|
slot="page-traefik-compose"
|
||||||
data-tab-title="${t`Traefik (Compose)`}"
|
data-tab-title="${t`Traefik (Compose)`}"
|
||||||
class="pf-c-page__main-section pf-m-light pf-m-no-padding-mobile"
|
class="pf-c-page__main-section pf-m-light pf-m-no-padding-mobile"
|
||||||
>
|
>
|
||||||
${this.renderConfigTemplate(MDTraefikCompose.html)}
|
<ak-markdown
|
||||||
|
.md=${this.renderConfigTemplate(MDTraefikCompose)}
|
||||||
|
></ak-markdown>
|
||||||
</section>
|
</section>
|
||||||
<section
|
<section
|
||||||
slot="page-traefik-standalone"
|
slot="page-traefik-standalone"
|
||||||
data-tab-title="${t`Traefik (Standalone)`}"
|
data-tab-title="${t`Traefik (Standalone)`}"
|
||||||
class="pf-c-page__main-section pf-m-light pf-m-no-padding-mobile"
|
class="pf-c-page__main-section pf-m-light pf-m-no-padding-mobile"
|
||||||
>
|
>
|
||||||
${this.renderConfigTemplate(MDTraefikStandalone.html)}
|
<ak-markdown
|
||||||
|
.md=${this.renderConfigTemplate(MDTraefikStandalone)}
|
||||||
|
></ak-markdown>
|
||||||
</section>
|
</section>
|
||||||
</ak-tabs>
|
</ak-tabs>
|
||||||
`
|
`
|
||||||
|
|
|
@ -107,11 +107,15 @@ export class StageListPage extends TablePage<Stage> {
|
||||||
<div>${item.name}</div>
|
<div>${item.name}</div>
|
||||||
<small>${item.verboseName}</small>
|
<small>${item.verboseName}</small>
|
||||||
</div>`,
|
</div>`,
|
||||||
html`${item.flowSet?.map((flow) => {
|
html`<ul class="pf-c-list">
|
||||||
return html`<a href="#/flow/flows/${flow.slug}">
|
${item.flowSet?.map((flow) => {
|
||||||
|
return html`<li>
|
||||||
|
<a href="#/flow/flows/${flow.slug}">
|
||||||
<code>${flow.slug}</code>
|
<code>${flow.slug}</code>
|
||||||
</a>`;
|
</a>
|
||||||
})}`,
|
</li>`;
|
||||||
|
})}
|
||||||
|
</ul>`,
|
||||||
html` <ak-forms-modal>
|
html` <ak-forms-modal>
|
||||||
<span slot="submit"> ${t`Update`} </span>
|
<span slot="submit"> ${t`Update`} </span>
|
||||||
<span slot="header"> ${t`Update ${item.verboseName}`} </span>
|
<span slot="header"> ${t`Update ${item.verboseName}`} </span>
|
||||||
|
|
|
@ -3,7 +3,7 @@ title: Applications
|
||||||
slug: /applications
|
slug: /applications
|
||||||
---
|
---
|
||||||
|
|
||||||
Applications in authentik are the counterpart of providers. They exist in a 1-to-1 relationship, each application needs a provider and every provider can be used with one application.
|
Applications in authentik are the other half of providers. They exist in a 1-to-1 relationship, each application needs a provider and every provider can be used with one application.
|
||||||
|
|
||||||
Applications are used to configure and separate the authorization / access control and the appearance in the Library page.
|
Applications are used to configure and separate the authorization / access control and the appearance in the Library page.
|
||||||
|
|
||||||
|
|
|
@ -151,6 +151,19 @@ This release does not have any headline features, and mostly fixes bugs.
|
||||||
- web/admin: update overview page
|
- web/admin: update overview page
|
||||||
- web/flows: fix error when attempting to enroll new webauthn device
|
- web/flows: fix error when attempting to enroll new webauthn device
|
||||||
|
|
||||||
|
## Fixed in 2021.12.1
|
||||||
|
|
||||||
|
- core: fix error when attempting to provider from cached application
|
||||||
|
- events: improve app lookup for event creation
|
||||||
|
- internal: cleanup duplicate and redundant code, properly set sentry SDK scope settings
|
||||||
|
- lifecycle: add -Ofair to celery
|
||||||
|
- web/admin: add sidebar to applications
|
||||||
|
- web/admin: fix notification unread colours not matching on user and admin interface
|
||||||
|
- web/admin: fix stage related flows not being shown in a list
|
||||||
|
- web/elements: add Markdown component to improve rendering
|
||||||
|
- web/elements: add support for sidebar on table page
|
||||||
|
- web/elements: close notification drawer when clearing all notifications
|
||||||
|
|
||||||
## Upgrading
|
## Upgrading
|
||||||
|
|
||||||
This release does not introduce any new requirements.
|
This release does not introduce any new requirements.
|
||||||
|
|
|
@ -101,3 +101,9 @@ BookStack will attempt to match the SAML user to an existing BookStack user base
|
||||||
:::note
|
:::note
|
||||||
SAML Group Sync is supported by Bookstack. Review the BookStack documentation on the required Environment variables. https://www.bookstackapp.com/docs/admin/saml2-auth/
|
SAML Group Sync is supported by Bookstack. Review the BookStack documentation on the required Environment variables. https://www.bookstackapp.com/docs/admin/saml2-auth/
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
:::note
|
||||||
|
In some cases you might need to define the full SAML property name.
|
||||||
|
i.e.: `SAML2_GROUP_ATTRIBUTE="http://schemas.xmlsoap.org/claims/Group"`
|
||||||
|
See https://github.com/BookStackApp/BookStack/issues/3109 for more details.
|
||||||
|
:::
|
||||||
|
|
Reference in New Issue