Merge branch 'master' into version-2021.12

This commit is contained in:
Jens Langhammer 2021-12-16 15:48:53 +01:00
commit 741822424a
28 changed files with 235 additions and 128 deletions

View File

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

View File

@ -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
return Provider.objects.get_subclass(pk=self.provider.pk) # 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)
except Provider.DoesNotExist:
return None
def __str__(self): def __str__(self):
return self.name return self.name

View File

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

View File

@ -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__}",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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-card">${this.renderTable()}</div> <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>
${this.renderSidebarAfter()}
</div>
</div>
</section>`; </section>`;
} }
} }

2
web/src/global.d.ts vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) => {
<code>${flow.slug}</code> return html`<li>
</a>`; <a href="#/flow/flows/${flow.slug}">
})}`, <code>${flow.slug}</code>
</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>

View File

@ -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.
@ -15,23 +15,23 @@ By default, all users can access applications when no policies are bound.
When multiple policies/groups/users are attached, you can configure the *Policy engine mode* to either When multiple policies/groups/users are attached, you can configure the *Policy engine mode* to either
- Require users to pass all bindings/be member of all groups (ALL), or - Require users to pass all bindings/be member of all groups (ALL), or
- Require users to pass either binding/be member of either group (ANY) - Require users to pass either binding/be member of either group (ANY)
## Appearance ## Appearance
The following aspects can be configured: The following aspects can be configured:
- *Name*: This is the name shown for the application card - *Name*: This is the name shown for the application card
- *Launch URL*: The URL that is opened when a user clicks on the application. When left empty, authentik tries to guess it based on the provider - *Launch URL*: The URL that is opened when a user clicks on the application. When left empty, authentik tries to guess it based on the provider
- *Icon (URL)*: Optionally configure an Icon for the application - *Icon (URL)*: Optionally configure an Icon for the application
- *Publisher*: Text shown below the application - *Publisher*: Text shown below the application
- *Description*: Subtext shown on the application card below the publisher - *Description*: Subtext shown on the application card below the publisher
Applications are shown to users when Applications are shown to users when
- The user has access defined via policies (or the application has no policies bound) - The user has access defined via policies (or the application has no policies bound)
- A Valid Launch URL is configured/could be guessed, this consists of URLs starting with http:// and https:// - A Valid Launch URL is configured/could be guessed, this consists of URLs starting with http:// and https://
#### Hiding applications #### Hiding applications

View File

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

View File

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