Merge branch 'main' into 5165-password-strength-indicator

* main:
  providers/ldap: rework Schema and DSE (#5838)
  web/flows: update default flow background (#5905)
  web: bump @formatjs/intl-listformat from 7.2.2 to 7.3.0 in /web (#5866)
  website/integrations: add account linking note for WriteFreely (#5804)
  web: bump @storybook/addon-essentials from 7.0.18 to 7.0.20 in /web (#5894)
  web: bump @storybook/web-components-vite from 7.0.18 to 7.0.20 in /web (#5895)
  web: bump @storybook/blocks from 7.0.18 to 7.0.20 in /web (#5893)
  web: bump storybook from 7.0.18 to 7.0.20 in /web (#5896)
  website/docs: correct LDAP StartTLS documentation (#5886)
  core: bump python from 3.11.3-slim-bullseye to 3.11.4-slim-bullseye (#5891)
  ci: bump docker/setup-qemu-action from 2.1.0 to 2.2.0 (#5892)
  core: bump selenium from 4.9.1 to 4.10.0 (#5897)
  web: bump pyright from 1.1.312 to 1.1.313 in /web (#5898)
  web: bump @storybook/addon-links from 7.0.18 to 7.0.20 in /web (#5899)
  web: bump @storybook/web-components from 7.0.18 to 7.0.20 in /web (#5900)
  core: bump urllib3 from 2.0.2 to 2.0.3 (#5901)
  core: bump ruff from 0.0.271 to 0.0.272 (#5902)
  core: bump sentry-sdk from 1.25.0 to 1.25.1 (#5903)
This commit is contained in:
Ken Sternberg 2023-06-08 08:42:11 -07:00
commit 1c85dc512f
29 changed files with 5421 additions and 1062 deletions

View File

@ -190,7 +190,7 @@ jobs:
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0 uses: docker/setup-qemu-action@v2.2.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: prepare variables - name: prepare variables
@ -234,7 +234,7 @@ jobs:
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0 uses: docker/setup-qemu-action@v2.2.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: prepare variables - name: prepare variables

View File

@ -68,7 +68,7 @@ jobs:
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0 uses: docker/setup-qemu-action@v2.2.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: prepare variables - name: prepare variables

View File

@ -10,7 +10,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0 uses: docker/setup-qemu-action@v2.2.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: prepare variables - name: prepare variables
@ -59,7 +59,7 @@ jobs:
with: with:
go-version-file: "go.mod" go-version-file: "go.mod"
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0 uses: docker/setup-qemu-action@v2.2.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: prepare variables - name: prepare variables

View File

@ -20,7 +20,7 @@ WORKDIR /work/web
RUN npm ci --include=dev && npm run build RUN npm ci --include=dev && npm run build
# Stage 3: Poetry to requirements.txt export # Stage 3: Poetry to requirements.txt export
FROM docker.io/python:3.11.3-slim-bullseye AS poetry-locker FROM docker.io/python:3.11.4-slim-bullseye AS poetry-locker
WORKDIR /work WORKDIR /work
COPY ./pyproject.toml /work COPY ./pyproject.toml /work
@ -63,7 +63,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
" "
# Stage 6: Run # Stage 6: Run
FROM docker.io/python:3.11.3-slim-bullseye AS final-image FROM docker.io/python:3.11.4-slim-bullseye AS final-image
LABEL org.opencontainers.image.url https://goauthentik.io LABEL org.opencontainers.image.url https://goauthentik.io
LABEL org.opencontainers.image.description goauthentik.io Main server image, see https://goauthentik.io for more info. LABEL org.opencontainers.image.description goauthentik.io Main server image, see https://goauthentik.io for more info.

View File

@ -7,7 +7,7 @@ import (
) )
type Binder interface { type Binder interface {
GetUsername(string) (string, error) GetUsername(dn string) (string, error)
Bind(username string, req *Request) (ldap.LDAPResultCode, error) Bind(username string, req *Request) (ldap.LDAPResultCode, error)
Unbind(username string, req *Request) (ldap.LDAPResultCode, error) Unbind(username string, req *Request) (ldap.LDAPResultCode, error)
TimerFlowCacheExpiry(context.Context) TimerFlowCacheExpiry(context.Context)

View File

@ -1,9 +1,18 @@
package constants package constants
const OC = "objectClass"
const ( const (
OCTop = "top" OCTop = "top"
OCDomain = "domain" OCDomain = "domain"
OCNSContainer = "nsContainer" OCNSContainer = "nsContainer"
OCSubSchema = "subschema"
)
const (
SearchAttributeNone = "1.1"
SearchAttributeAllUser = "*"
SearchAttributeAllOperational = "+"
) )
const ( const (

View File

@ -2,16 +2,13 @@ package ldap
import ( import (
"crypto/tls" "crypto/tls"
"fmt"
"strings" "strings"
"sync" "sync"
"beryju.io/ldap"
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"goauthentik.io/api/v3" "goauthentik.io/api/v3"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/outpost/ldap/bind" "goauthentik.io/internal/outpost/ldap/bind"
ldapConstants "goauthentik.io/internal/outpost/ldap/constants" ldapConstants "goauthentik.io/internal/outpost/ldap/constants"
"goauthentik.io/internal/outpost/ldap/flags" "goauthentik.io/internal/outpost/ldap/flags"
@ -107,43 +104,6 @@ func (pi *ProviderInstance) GetSearchAllowedGroups() []*strfmt.UUID {
return pi.searchAllowedGroups return pi.searchAllowedGroups
} }
func (pi *ProviderInstance) GetBaseEntry() *ldap.Entry {
return &ldap.Entry{
DN: pi.GetBaseDN(),
Attributes: []*ldap.EntryAttribute{
{
Name: "distinguishedName",
Values: []string{pi.GetBaseDN()},
},
{
Name: "objectClass",
Values: []string{ldapConstants.OCTop, ldapConstants.OCDomain},
},
{
Name: "supportedLDAPVersion",
Values: []string{"3"},
},
{
Name: "namingContexts",
Values: []string{
pi.GetBaseDN(),
pi.GetBaseUserDN(),
pi.GetBaseGroupDN(),
pi.GetBaseVirtualGroupDN(),
},
},
{
Name: "vendorName",
Values: []string{"goauthentik.io"},
},
{
Name: "vendorVersion",
Values: []string{fmt.Sprintf("authentik LDAP Outpost Version %s", constants.FullVersion())},
},
},
}
}
func (pi *ProviderInstance) GetNeededObjects(scope int, baseDN string, filterOC string) (bool, bool) { func (pi *ProviderInstance) GetNeededObjects(scope int, baseDN string, filterOC string) (bool, bool) {
needUsers := false needUsers := false
needGroups := false needGroups := false

View File

@ -1,15 +1,13 @@
package ldap package ldap
import ( import (
"errors"
"net" "net"
"strings"
"beryju.io/ldap" "beryju.io/ldap"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
goldap "github.com/go-ldap/ldap/v3"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"goauthentik.io/internal/outpost/ldap/constants"
"goauthentik.io/internal/outpost/ldap/metrics" "goauthentik.io/internal/outpost/ldap/metrics"
"goauthentik.io/internal/outpost/ldap/search" "goauthentik.io/internal/outpost/ldap/search"
) )
@ -36,38 +34,20 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n
sentry.CaptureException(err.(error)) sentry.CaptureException(err.(error))
}() }()
if searchReq.BaseDN == "" { selectedProvider := ls.providerForRequest(req)
return ldap.ServerSearchResult{ if selectedProvider == nil {
Entries: []*ldap.Entry{ return ls.fallbackRootDSE(req)
{
DN: "",
Attributes: []*ldap.EntryAttribute{
{
Name: "objectClass",
Values: []string{"top", "OpenLDAProotDSE"},
},
{
Name: "subschemaSubentry",
Values: []string{"cn=subschema"},
},
},
},
},
Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess,
}, nil
} }
bd, err := goldap.ParseDN(strings.ToLower(searchReq.BaseDN)) selectedApp = selectedProvider.GetAppSlug()
result, err := ls.searchRoute(req, selectedProvider)
if err != nil { if err != nil {
req.Log().WithError(err).Info("failed to parse basedn") return result, nil
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("invalid DN")
}
for _, provider := range ls.providers {
providerBase, _ := goldap.ParseDN(strings.ToLower(provider.BaseDN))
if providerBase.AncestorOf(bd) || providerBase.Equal(bd) {
selectedApp = provider.GetAppSlug()
return provider.searcher.Search(req)
}
} }
return ls.filterResultAttributes(req, result), nil
}
func (ls *LDAPServer) fallbackRootDSE(req *search.Request) (ldap.ServerSearchResult, error) {
req.Log().Trace("returning fallback Root DSE")
return ldap.ServerSearchResult{ return ldap.ServerSearchResult{
Entries: []*ldap.Entry{ Entries: []*ldap.Entry{
{ {
@ -75,15 +55,30 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n
Attributes: []*ldap.EntryAttribute{ Attributes: []*ldap.EntryAttribute{
{ {
Name: "objectClass", Name: "objectClass",
Values: []string{"top", "OpenLDAProotDSE"}, Values: []string{constants.OCTop},
},
{
Name: "entryDN",
Values: []string{""},
}, },
{ {
Name: "subschemaSubentry", Name: "subschemaSubentry",
Values: []string{"cn=subschema"}, Values: []string{"cn=subschema"},
}, },
{
Name: "namingContexts",
Values: []string{},
},
{
Name: "description",
Values: []string{
"This LDAP server requires an authenticated session.",
},
},
}, },
}, },
}, },
Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess,
}, nil }, nil
} }

View File

@ -2,40 +2,51 @@ package direct
import ( import (
"fmt" "fmt"
"strings"
"beryju.io/ldap" "beryju.io/ldap"
"goauthentik.io/internal/constants" "goauthentik.io/internal/constants"
ldapConstants "goauthentik.io/internal/outpost/ldap/constants"
"goauthentik.io/internal/outpost/ldap/search" "goauthentik.io/internal/outpost/ldap/search"
) )
func (ds *DirectSearcher) SearchBase(req *search.Request, authz bool) (ldap.ServerSearchResult, error) { func (ds *DirectSearcher) SearchBase(req *search.Request) (ldap.ServerSearchResult, error) {
dn := "" if req.Scope == ldap.ScopeSingleLevel {
if authz { return ldap.ServerSearchResult{
dn = req.SearchRequest.BaseDN ResultCode: ldap.LDAPResultNoSuchObject,
}, nil
} }
return ldap.ServerSearchResult{ return ldap.ServerSearchResult{
Entries: []*ldap.Entry{ Entries: []*ldap.Entry{
{ {
DN: dn, DN: "",
Attributes: []*ldap.EntryAttribute{ Attributes: []*ldap.EntryAttribute{
{ {
Name: "distinguishedName", Name: "objectClass",
Values: []string{ds.si.GetBaseDN()}, Values: []string{ldapConstants.OCTop},
}, },
{ {
Name: "objectClass", Name: "entryDN",
Values: []string{"top", "domain"}, Values: []string{""},
}, },
{ {
Name: "supportedLDAPVersion", Name: "supportedLDAPVersion",
Values: []string{"3"}, Values: []string{"3"},
}, },
{
Name: "subschemaSubentry",
Values: []string{"cn=subschema"},
},
{ {
Name: "namingContexts", Name: "namingContexts",
Values: []string{ Values: []string{
ds.si.GetBaseDN(), strings.ToLower(ds.si.GetBaseDN()),
ds.si.GetBaseUserDN(), },
ds.si.GetBaseGroupDN(), },
{
Name: "rootDomainNamingContext",
Values: []string{
strings.ToLower(ds.si.GetBaseDN()),
}, },
}, },
{ {

View File

@ -31,7 +31,6 @@ func NewDirectSearcher(si server.LDAPServerInstance) *DirectSearcher {
si: si, si: si,
log: log.WithField("logger", "authentik.outpost.ldap.searcher.direct"), log: log.WithField("logger", "authentik.outpost.ldap.searcher.direct"),
} }
ds.log.Info("initialised direct searcher")
return ds return ds
} }
@ -39,16 +38,6 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access") accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access")
baseDN := ds.si.GetBaseDN() baseDN := ds.si.GetBaseDN()
filterOC, err := ldap.GetFilterObjectClass(req.Filter)
if err != nil {
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": ds.si.GetOutpostName(),
"type": "search",
"reason": "filter_parse_fail",
"app": ds.si.GetAppSlug(),
}).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", req.Filter)
}
if len(req.BindDN) < 1 { if len(req.BindDN) < 1 {
metrics.RequestsRejected.With(prometheus.Labels{ metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": ds.si.GetOutpostName(), "outpost_name": ds.si.GetOutpostName(),
@ -99,11 +88,17 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
c.GetConfig().AddDefaultHeader("X-authentik-outpost-ldap-query", req.Filter) c.GetConfig().AddDefaultHeader("X-authentik-outpost-ldap-query", req.Filter)
scope := req.SearchRequest.Scope scope := req.SearchRequest.Scope
needUsers, needGroups := ds.si.GetNeededObjects(scope, req.BaseDN, filterOC) needUsers, needGroups := ds.si.GetNeededObjects(scope, req.BaseDN, req.FilterObjectClass)
if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) { if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) {
if utils.IncludeObjectClass(filterOC, constants.GetDomainOCs()) { if utils.IncludeObjectClass(req.FilterObjectClass, constants.GetDomainOCs()) {
entries = append(entries, ds.si.GetBaseEntry()) rootEntries, _ := ds.SearchBase(req)
// Since `SearchBase` returns entries for the root DN, we need to go through the
// entries and update the base DN
for _, e := range rootEntries.Entries {
e.DN = ds.si.GetBaseDN()
entries = append(entries, e)
}
} }
scope -= 1 // Bring it from WholeSubtree to SingleLevel and so on scope -= 1 // Bring it from WholeSubtree to SingleLevel and so on
@ -197,12 +192,12 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
if scope >= 0 && (strings.EqualFold(req.BaseDN, ds.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ds.si.GetBaseUserDN())) { if scope >= 0 && (strings.EqualFold(req.BaseDN, ds.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ds.si.GetBaseUserDN())) {
singleu := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseUserDN()) singleu := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseUserDN())
if !singleu && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) { if !singleu && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseUserDN(), constants.OUUsers)) entries = append(entries, utils.GetContainerEntry(req.FilterObjectClass, ds.si.GetBaseUserDN(), constants.OUUsers))
scope -= 1 scope -= 1
} }
if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetUserOCs()) { if scope >= 0 && users != nil && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetUserOCs()) {
for _, u := range *users { for _, u := range *users {
entry := ds.si.UserEntry(u) entry := ds.si.UserEntry(u)
if strings.EqualFold(req.BaseDN, entry.DN) || !singleu { if strings.EqualFold(req.BaseDN, entry.DN) || !singleu {
@ -217,12 +212,12 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
if scope >= 0 && (strings.EqualFold(req.BaseDN, ds.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ds.si.GetBaseGroupDN())) { if scope >= 0 && (strings.EqualFold(req.BaseDN, ds.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ds.si.GetBaseGroupDN())) {
singleg := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseGroupDN()) singleg := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseGroupDN())
if !singleg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) { if !singleg && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseGroupDN(), constants.OUGroups)) entries = append(entries, utils.GetContainerEntry(req.FilterObjectClass, ds.si.GetBaseGroupDN(), constants.OUGroups))
scope -= 1 scope -= 1
} }
if scope >= 0 && groups != nil && utils.IncludeObjectClass(filterOC, constants.GetGroupOCs()) { if scope >= 0 && groups != nil && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetGroupOCs()) {
for _, g := range *groups { for _, g := range *groups {
entry := group.FromAPIGroup(g, ds.si).Entry() entry := group.FromAPIGroup(g, ds.si).Entry()
if strings.EqualFold(req.BaseDN, entry.DN) || !singleg { if strings.EqualFold(req.BaseDN, entry.DN) || !singleg {
@ -237,12 +232,12 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
if scope >= 0 && (strings.EqualFold(req.BaseDN, ds.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ds.si.GetBaseVirtualGroupDN())) { if scope >= 0 && (strings.EqualFold(req.BaseDN, ds.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ds.si.GetBaseVirtualGroupDN())) {
singlevg := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseVirtualGroupDN()) singlevg := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseVirtualGroupDN())
if !singlevg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) { if !singlevg && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups)) entries = append(entries, utils.GetContainerEntry(req.FilterObjectClass, ds.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups))
scope -= 1 scope -= 1
} }
if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetVirtualGroupOCs()) { if scope >= 0 && users != nil && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetVirtualGroupOCs()) {
for _, u := range *users { for _, u := range *users {
entry := group.FromAPIUser(u, ds.si).Entry() entry := group.FromAPIUser(u, ds.si).Entry()
if strings.EqualFold(req.BaseDN, entry.DN) || !singlevg { if strings.EqualFold(req.BaseDN, entry.DN) || !singlevg {

View File

@ -0,0 +1,96 @@
package direct
import (
"beryju.io/ldap"
"goauthentik.io/internal/outpost/ldap/constants"
"goauthentik.io/internal/outpost/ldap/search"
)
func (ds *DirectSearcher) SearchSubschema(req *search.Request) (ldap.ServerSearchResult, error) {
return ldap.ServerSearchResult{
Entries: []*ldap.Entry{
{
DN: "cn=subschema",
Attributes: []*ldap.EntryAttribute{
{
Name: "cn",
Values: []string{"subschema"},
},
{
Name: constants.OC,
Values: []string{constants.OCTop, "subSchema"},
},
{
Name: "ldapSyntaxes",
Values: []string{
"( 1.3.6.1.4.1.1466.115.121.1.40 DESC 'Octet String' )",
"( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Directory String' )",
"( 1.3.6.1.4.1.1466.115.121.1.7 DESC 'Boolean' )",
},
},
{
Name: "objectClasses",
Values: []string{
"( 2.5.6.0 NAME 'top' ABSTRACT MUST ( objectClass ) MAY (cn $ description $ displayName $ memberOf $ name ) )",
"( 2.5.6.6 NAME 'person' SUP top STRUCTURAL MUST ( cn ) MAY (sn $ telephoneNumber ) )",
"( 2.5.6.7 NAME 'organizationalPerson' SUP person STRUCTURAL MAY (c $ l $ o $ ou $ title $ givenName $ co $ department $ company $ division $ mail $ mobile $ telephoneNumber ) )",
"( 2.5.6.9 NAME 'groupOfNames' SUP top STRUCTURAL MUST (cn $ member ) MAY (o $ ou ) )",
"( 1.2.840.113556.1.5.9 NAME 'user' SUP organizationalPerson STRUCTURAL MAY ( name $ displayName $ uid $ mail ) )",
"( 1.3.6.1.1.1.2.0 NAME 'posixAccount' SUP top AUXILIARY MAY (cn $ description $ homeDirectory $ uid $ uidNumber $ gidNumber ) )",
"( 2.16.840.1.113730.3.2.2 NAME 'inetOrgPerson' AUX ( posixAccount ) MUST ( sAMAccountName ) MAY ( uidNumber $ gidNumber ))",
// Custom attributes
// Temporarily use 1.3.6.1.4.1.26027.1.1 as a base
// https://docs.oracle.com/cd/E19450-01/820-6169/working-with-object-identifiers.html#obtaining-a-base-oid
"( 1.3.6.1.4.1.26027.1.1.1 NAME 'goauthentik.io/ldap/user' SUP organizationalPerson STRUCTURAL MAY ( ak-active $ sAMAccountName $ goauthentikio-user-sources $ goauthentik.io/user/sources $ goauthentik.io/ldap/active $ goauthentik.io/ldap/superuser $ goauthentikio-user-override-ips $ goauthentikio-user-service-account ) )",
},
},
{
Name: "attributeTypes",
Values: []string{
"( 2.5.4.0 NAME 'objectClass' DESC 'RFC4512: object classes of the entity' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )",
"( 1.3.6.1.4.1.1466.101.120.5 NAME 'namingContexts' DESC 'RFC4512: naming contexts' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 USAGE dSAOperation )",
"( 2.5.18.10 NAME 'subschemaSubentry' DESC 'RFC4512: name of controlling subschema entry' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )",
"( 1.3.6.1.4.1.1466.101.120.15 NAME 'supportedLDAPVersion' DESC 'RFC4512: supported LDAP versions' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 USAGE dSAOperation )",
"( 1.3.6.1.1.20 NAME 'entryDN' DESC 'DN of the entry' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )",
"( 1.3.6.1.1.4 NAME 'vendorName' DESC 'RFC3045: name of implementation vendor' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation )",
"( 1.3.6.1.1.5 NAME 'vendorVersion' DESC 'RFC3045: version of implementation' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation )",
"( 0.9.2342.19200300.100.1.1 NAME 'uid' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 0.9.2342.19200300.100.1.3 NAME 'mail' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 0.9.2342.19200300.100.1.41 NAME 'mobile' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.2.102 NAME 'memberOf' SYNTAX '1.3.6.1.4.1.1466.115.121.1.12' NO-USER-MODIFICATION )",
"( 1.2.840.113556.1.2.13 NAME 'displayName' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.4.1 NAME 'name' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE NO-USER-MODIFICATION )",
"( 1.2.840.113556.1.2.131 NAME 'co' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.2.141 NAME 'department' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.2.146 NAME 'company' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.4.44 NAME 'homeDirectory' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.4.221 NAME 'sAMAccountName' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.4.261 NAME 'division' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.3.6.1.1.1.1.0 NAME 'uidNumber' SYNTAX '1.3.6.1.4.1.1466.115.121.1.27' SINGLE-VALUE )",
"( 1.3.6.1.1.1.1.1 NAME 'gidNumber' SYNTAX '1.3.6.1.4.1.1466.115.121.1.27' SINGLE-VALUE )",
"( 2.5.4.6 NAME 'c' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.7 NAME 'l' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.10 NAME 'o' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' )",
"( 2.5.4.11 NAME 'ou' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' )",
"( 2.5.4.20 NAME 'telephoneNumber' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.42 NAME 'givenName' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.0 NAME 'objectClass' SYNTAX '1.3.6.1.4.1.1466.115.121.1.38' NO-USER-MODIFICATION )",
"( 2.5.4.3 NAME 'cn' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.4 NAME 'sn' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.12 NAME 'title' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.13 NAME 'description' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' )",
"( 2.5.4.31 NAME 'member' SYNTAX '1.3.6.1.4.1.1466.115.121.1.12' )",
// Custom attributes
// Temporarily use 1.3.6.1.4.1.26027.1.1 as a base
// https://docs.oracle.com/cd/E19450-01/820-6169/working-with-object-identifiers.html#obtaining-a-base-oid
"( 1.3.6.1.4.1.26027.1.1.2 NAME ( 'goauthentik.io/ldap/superuser' 'ak-superuser' ) SYNTAX '1.3.6.1.4.1.1466.115.121.1.7' SINGLE-VALUE )",
"( 1.3.6.1.4.1.26027.1.1.3 NAME ( 'goauthentik.io/ldap/active' 'ak-active' ) SYNTAX '1.3.6.1.4.1.1466.115.121.1.7' SINGLE-VALUE )",
"( 1.3.6.1.4.1.26027.1.1.4 NAME 'goauthentikio-user-override-ips' SYNTAX '1.3.6.1.4.1.1466.115.121.1.7' SINGLE-VALUE )",
"( 1.3.6.1.4.1.26027.1.1.5 NAME 'goauthentikio-user-service-account' SYNTAX '1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE' )",
},
},
},
},
},
}, nil
}

View File

@ -15,6 +15,7 @@ import (
"goauthentik.io/internal/outpost/ldap/group" "goauthentik.io/internal/outpost/ldap/group"
"goauthentik.io/internal/outpost/ldap/metrics" "goauthentik.io/internal/outpost/ldap/metrics"
"goauthentik.io/internal/outpost/ldap/search" "goauthentik.io/internal/outpost/ldap/search"
"goauthentik.io/internal/outpost/ldap/search/direct"
"goauthentik.io/internal/outpost/ldap/server" "goauthentik.io/internal/outpost/ldap/server"
"goauthentik.io/internal/outpost/ldap/utils" "goauthentik.io/internal/outpost/ldap/utils"
"goauthentik.io/internal/outpost/ldap/utils/paginator" "goauthentik.io/internal/outpost/ldap/utils/paginator"
@ -23,6 +24,7 @@ import (
type MemorySearcher struct { type MemorySearcher struct {
si server.LDAPServerInstance si server.LDAPServerInstance
log *log.Entry log *log.Entry
ds *direct.DirectSearcher
users []api.User users []api.User
groups []api.Group groups []api.Group
@ -32,6 +34,7 @@ func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher {
ms := &MemorySearcher{ ms := &MemorySearcher{
si: si, si: si,
log: log.WithField("logger", "authentik.outpost.ldap.searcher.memory"), log: log.WithField("logger", "authentik.outpost.ldap.searcher.memory"),
ds: direct.NewDirectSearcher(si),
} }
ms.log.Debug("initialised memory searcher") ms.log.Debug("initialised memory searcher")
ms.users = paginator.FetchUsers(ms.si.GetAPIClient().CoreApi.CoreUsersList(context.TODO())) ms.users = paginator.FetchUsers(ms.si.GetAPIClient().CoreApi.CoreUsersList(context.TODO()))
@ -39,20 +42,18 @@ func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher {
return ms return ms
} }
func (ms *MemorySearcher) SearchBase(req *search.Request) (ldap.ServerSearchResult, error) {
return ms.ds.SearchBase(req)
}
func (ms *MemorySearcher) SearchSubschema(req *search.Request) (ldap.ServerSearchResult, error) {
return ms.ds.SearchSubschema(req)
}
func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, error) { func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, error) {
accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access") accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access")
baseDN := ms.si.GetBaseDN() baseDN := ms.si.GetBaseDN()
filterOC, err := ldap.GetFilterObjectClass(req.Filter)
if err != nil {
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": ms.si.GetOutpostName(),
"type": "search",
"reason": "filter_parse_fail",
"app": ms.si.GetAppSlug(),
}).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", req.Filter)
}
if len(req.BindDN) < 1 { if len(req.BindDN) < 1 {
metrics.RequestsRejected.With(prometheus.Labels{ metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": ms.si.GetOutpostName(), "outpost_name": ms.si.GetOutpostName(),
@ -88,11 +89,15 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
entries := make([]*ldap.Entry, 0) entries := make([]*ldap.Entry, 0)
scope := req.SearchRequest.Scope scope := req.SearchRequest.Scope
needUsers, needGroups := ms.si.GetNeededObjects(scope, req.BaseDN, filterOC) needUsers, needGroups := ms.si.GetNeededObjects(scope, req.BaseDN, req.FilterObjectClass)
if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) { if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) {
if utils.IncludeObjectClass(filterOC, constants.GetDomainOCs()) { if utils.IncludeObjectClass(req.FilterObjectClass, constants.GetDomainOCs()) {
entries = append(entries, ms.si.GetBaseEntry()) rootEntries, _ := ms.SearchBase(req)
for _, e := range rootEntries.Entries {
e.DN = ms.si.GetBaseDN()
entries = append(entries, e)
}
} }
scope -= 1 // Bring it from WholeSubtree to SingleLevel and so on scope -= 1 // Bring it from WholeSubtree to SingleLevel and so on
@ -100,6 +105,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
var users *[]api.User var users *[]api.User
var groups []*group.LDAPGroup var groups []*group.LDAPGroup
var err error
if needUsers { if needUsers {
if flags.CanSearch { if flags.CanSearch {
@ -159,12 +165,12 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
if scope >= 0 && (strings.EqualFold(req.BaseDN, ms.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ms.si.GetBaseUserDN())) { if scope >= 0 && (strings.EqualFold(req.BaseDN, ms.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ms.si.GetBaseUserDN())) {
singleu := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseUserDN()) singleu := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseUserDN())
if !singleu && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) { if !singleu && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseUserDN(), constants.OUUsers)) entries = append(entries, utils.GetContainerEntry(req.FilterObjectClass, ms.si.GetBaseUserDN(), constants.OUUsers))
scope -= 1 scope -= 1
} }
if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetUserOCs()) { if scope >= 0 && users != nil && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetUserOCs()) {
for _, u := range *users { for _, u := range *users {
entry := ms.si.UserEntry(u) entry := ms.si.UserEntry(u)
if strings.EqualFold(req.BaseDN, entry.DN) || !singleu { if strings.EqualFold(req.BaseDN, entry.DN) || !singleu {
@ -179,12 +185,12 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
if scope >= 0 && (strings.EqualFold(req.BaseDN, ms.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ms.si.GetBaseGroupDN())) { if scope >= 0 && (strings.EqualFold(req.BaseDN, ms.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ms.si.GetBaseGroupDN())) {
singleg := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseGroupDN()) singleg := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseGroupDN())
if !singleg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) { if !singleg && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseGroupDN(), constants.OUGroups)) entries = append(entries, utils.GetContainerEntry(req.FilterObjectClass, ms.si.GetBaseGroupDN(), constants.OUGroups))
scope -= 1 scope -= 1
} }
if scope >= 0 && groups != nil && utils.IncludeObjectClass(filterOC, constants.GetGroupOCs()) { if scope >= 0 && groups != nil && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetGroupOCs()) {
for _, g := range groups { for _, g := range groups {
if strings.EqualFold(req.BaseDN, g.DN) || !singleg { if strings.EqualFold(req.BaseDN, g.DN) || !singleg {
entries = append(entries, g.Entry()) entries = append(entries, g.Entry())
@ -198,12 +204,12 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
if scope >= 0 && (strings.EqualFold(req.BaseDN, ms.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ms.si.GetBaseVirtualGroupDN())) { if scope >= 0 && (strings.EqualFold(req.BaseDN, ms.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ms.si.GetBaseVirtualGroupDN())) {
singlevg := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseVirtualGroupDN()) singlevg := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseVirtualGroupDN())
if !singlevg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) { if !singlevg && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups)) entries = append(entries, utils.GetContainerEntry(req.FilterObjectClass, ms.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups))
scope -= 1 scope -= 1
} }
if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetVirtualGroupOCs()) { if scope >= 0 && users != nil && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetVirtualGroupOCs()) {
for _, u := range *users { for _, u := range *users {
entry := group.FromAPIUser(u, ms.si).Entry() entry := group.FromAPIUser(u, ms.si).Entry()
if strings.EqualFold(req.BaseDN, entry.DN) || !singlevg { if strings.EqualFold(req.BaseDN, entry.DN) || !singlevg {

View File

@ -16,6 +16,7 @@ import (
type Request struct { type Request struct {
ldap.SearchRequest ldap.SearchRequest
BindDN string BindDN string
FilterObjectClass string
log *log.Entry log *log.Entry
id string id string
@ -40,11 +41,24 @@ func NewRequest(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (*Re
}) })
span.SetTag("ldap_filter", searchReq.Filter) span.SetTag("ldap_filter", searchReq.Filter)
span.SetTag("ldap_base_dn", searchReq.BaseDN) span.SetTag("ldap_base_dn", searchReq.BaseDN)
l := log.WithFields(log.Fields{
"bindDN": bindDN,
"baseDN": searchReq.BaseDN,
"requestId": rid,
"scope": ldap.ScopeMap[searchReq.Scope],
"client": utils.GetIP(conn.RemoteAddr()),
"filter": searchReq.Filter,
})
filterOC, err := ldap.GetFilterObjectClass(searchReq.Filter)
if err != nil && len(searchReq.Filter) > 0 {
l.WithError(err).WithField("objectClass", filterOC).Warning("invalid filter object class")
}
return &Request{ return &Request{
SearchRequest: searchReq, SearchRequest: searchReq,
BindDN: bindDN, BindDN: bindDN,
FilterObjectClass: filterOC,
conn: conn, conn: conn,
log: log.WithField("bindDN", bindDN).WithField("requestId", rid).WithField("scope", ldap.ScopeMap[searchReq.Scope]).WithField("client", utils.GetIP(conn.RemoteAddr())).WithField("filter", searchReq.Filter).WithField("baseDN", searchReq.BaseDN), log: l,
id: rid, id: rid,
ctx: span.Context(), ctx: span.Context(),
}, span }, span
@ -61,3 +75,19 @@ func (r *Request) Log() *log.Entry {
func (r *Request) RemoteAddr() string { func (r *Request) RemoteAddr() string {
return utils.GetIP(r.conn.RemoteAddr()) return utils.GetIP(r.conn.RemoteAddr())
} }
func (r *Request) FilterLDAPAttributes(res ldap.ServerSearchResult, cb func(attr *ldap.EntryAttribute) bool) ldap.ServerSearchResult {
for _, e := range res.Entries {
newAttrs := []*ldap.EntryAttribute{}
for _, attr := range e.Attributes {
include := cb(attr)
if include {
newAttrs = append(newAttrs, attr)
} else {
r.Log().WithField("key", attr.Name).Trace("filtering out field based on LDAP request")
}
}
e.Attributes = newAttrs
}
return res
}

View File

@ -6,4 +6,6 @@ import (
type Searcher interface { type Searcher interface {
Search(req *Request) (ldap.ServerSearchResult, error) Search(req *Request) (ldap.ServerSearchResult, error)
SearchBase(req *Request) (ldap.ServerSearchResult, error)
SearchSubschema(req *Request) (ldap.ServerSearchResult, error)
} }

View File

@ -0,0 +1,84 @@
package ldap
import (
"strings"
"beryju.io/ldap"
goldap "github.com/go-ldap/ldap/v3"
"goauthentik.io/internal/outpost/ldap/constants"
"goauthentik.io/internal/outpost/ldap/search"
)
func (ls *LDAPServer) providerForRequest(req *search.Request) *ProviderInstance {
parsedBaseDN, err := goldap.ParseDN(strings.ToLower(req.BaseDN))
if err != nil {
req.Log().WithError(err).Info("failed to parse base DN")
return nil
}
parsedBindDN, err := goldap.ParseDN(strings.ToLower(req.BindDN))
if err != nil {
req.Log().WithError(err).Info("failed to parse bind DN")
return nil
}
var selectedProvider *ProviderInstance
longestMatch := 0
for _, provider := range ls.providers {
providerBase, _ := goldap.ParseDN(strings.ToLower(provider.BaseDN))
// Try to match the provider primarily based on the search request's base DN
baseDNMatches := providerBase.AncestorOf(parsedBaseDN) || providerBase.Equal(parsedBaseDN)
// But also try to match the provider based on the bind DN
bindDNMatches := providerBase.AncestorOf(parsedBindDN) || providerBase.Equal(parsedBindDN)
if baseDNMatches || bindDNMatches {
// Only select the provider if it's a more precise match than previously
if len(provider.BaseDN) > longestMatch {
req.Log().WithField("provider", provider.BaseDN).Trace("selecting provider for search request")
selectedProvider = provider
longestMatch = len(provider.BaseDN)
}
}
}
return selectedProvider
}
func (ls *LDAPServer) searchRoute(req *search.Request, pi *ProviderInstance) (ldap.ServerSearchResult, error) {
// Route based on the base DN
if len(req.BaseDN) == 0 {
req.Log().Trace("routing to base")
return pi.searcher.SearchBase(req)
}
if strings.EqualFold(req.BaseDN, "cn=subschema") || req.FilterObjectClass == constants.OCSubSchema {
req.Log().Trace("routing to subschema")
return pi.searcher.SearchSubschema(req)
}
req.Log().Trace("routing to default")
return pi.searcher.Search(req)
}
func (ls *LDAPServer) filterResultAttributes(req *search.Request, result ldap.ServerSearchResult) ldap.ServerSearchResult {
allowedAttributes := []string{}
if len(req.Attributes) == 1 && req.Attributes[0] == constants.SearchAttributeNone {
allowedAttributes = []string{"objectClass"}
}
if len(req.Attributes) > 0 {
// Only strictly filter allowed attributes if we haven't already narrowed the attributes
// down
if len(allowedAttributes) < 1 {
allowedAttributes = req.Attributes
}
// Filter LDAP returned attributes by search requested attributes, taking "1.1"
// into consideration
return req.FilterLDAPAttributes(result, func(attr *ldap.EntryAttribute) bool {
for _, allowed := range allowedAttributes {
if allowed == constants.SearchAttributeAllUser ||
allowed == constants.SearchAttributeAllOperational {
return true
}
if strings.EqualFold(allowed, attr.Name) {
return true
}
}
return false
})
}
return result
}

View File

@ -35,6 +35,5 @@ type LDAPServerInstance interface {
GetFlags(dn string) *flags.UserFlags GetFlags(dn string) *flags.UserFlags
SetFlags(dn string, flags *flags.UserFlags) SetFlags(dn string, flags *flags.UserFlags)
GetBaseEntry() *ldap.Entry GetNeededObjects(scope int, baseDN string, filterOC string) (bool, bool)
GetNeededObjects(int, string, string) (bool, bool)
} }

54
poetry.lock generated
View File

@ -3114,41 +3114,41 @@ pyasn1 = ">=0.1.3"
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.0.271" version = "0.0.272"
description = "An extremely fast Python linter, written in Rust." description = "An extremely fast Python linter, written in Rust."
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "ruff-0.0.271-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:1a627978df924635f7d1a169a98abb2ea488c2d409da56a3f9e44a82d30606ac"}, {file = "ruff-0.0.272-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:ae9b57546e118660175d45d264b87e9b4c19405c75b587b6e4d21e6a17bf4fdf"},
{file = "ruff-0.0.271-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:f47d8a192f6869e95896dc5bb7e825a4f9c554136b9c3bddd38389e43d4db08b"}, {file = "ruff-0.0.272-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:1609b864a8d7ee75a8c07578bdea0a7db75a144404e75ef3162e0042bfdc100d"},
{file = "ruff-0.0.271-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e5de841e09ea75a26956a2cda930d1260c9d8d94cbe57c13b3e881d96526860"}, {file = "ruff-0.0.272-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee76b4f05fcfff37bd6ac209d1370520d509ea70b5a637bdf0a04d0c99e13dff"},
{file = "ruff-0.0.271-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:191cdddfc82165afd63ab29ad671419a90a5e699b026ac2d9c61232543965de6"}, {file = "ruff-0.0.272-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:48eccf225615e106341a641f826b15224b8a4240b84269ead62f0afd6d7e2d95"},
{file = "ruff-0.0.271-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e34ca86329a542ab5d31f4fc2702f556d62748f4217e2f6951aef93176190f0"}, {file = "ruff-0.0.272-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:677284430ac539bb23421a2b431b4ebc588097ef3ef918d0e0a8d8ed31fea216"},
{file = "ruff-0.0.271-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7543b8a32e000ed30727ca6e570a90ab26f8899ee23dffb28806dfc2618782fb"}, {file = "ruff-0.0.272-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9c4bfb75456a8e1efe14c52fcefb89cfb8f2a0d31ed8d804b82c6cf2dc29c42c"},
{file = "ruff-0.0.271-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fca503741f4b23a7179fd7a9bc50fc2cca637e9a4da027776f38690c50ae559f"}, {file = "ruff-0.0.272-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86bc788245361a8148ff98667da938a01e1606b28a45e50ac977b09d3ad2c538"},
{file = "ruff-0.0.271-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f445c56cdc8c12fc28a0b16588ba33abebb6340cb5b1b5a7d5668d4c0b31ad33"}, {file = "ruff-0.0.272-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27b2ea68d2aa69fff1b20b67636b1e3e22a6a39e476c880da1282c3e4bf6ee5a"},
{file = "ruff-0.0.271-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a73ffda5548ea8e28e0afcfa698a8675bb17f7048299327f4c1a1287b6e36a2"}, {file = "ruff-0.0.272-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd2bbe337a3f84958f796c77820d55ac2db1e6753f39d1d1baed44e07f13f96d"},
{file = "ruff-0.0.271-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:67525aa821ff0f8371eaa28c73dc467b8eea18931a8bd749775ad538fe1f35e6"}, {file = "ruff-0.0.272-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d5a208f8ef0e51d4746930589f54f9f92f84bb69a7d15b1de34ce80a7681bc00"},
{file = "ruff-0.0.271-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f3fd9e7c7afb7740d2734af3348e6c88226b42acba2e10a3d1e449caa67e4652"}, {file = "ruff-0.0.272-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:905ff8f3d6206ad56fcd70674453527b9011c8b0dc73ead27618426feff6908e"},
{file = "ruff-0.0.271-py3-none-musllinux_1_2_i686.whl", hash = "sha256:efdfe7fea656eb2ed54f123135c04f71744ad6e4c0c6be156d46e7a2f4730d48"}, {file = "ruff-0.0.272-py3-none-musllinux_1_2_i686.whl", hash = "sha256:19643d448f76b1eb8a764719072e9c885968971bfba872e14e7257e08bc2f2b7"},
{file = "ruff-0.0.271-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cd43c1aff3eefb2193a125a12124438f65a8d1a6da0e86f8545141d48f6a39fa"}, {file = "ruff-0.0.272-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:691d72a00a99707a4e0b2846690961157aef7b17b6b884f6b4420a9f25cd39b5"},
{file = "ruff-0.0.271-py3-none-win32.whl", hash = "sha256:403e8f9de18b2279d65015a45e0e0d98d60ad878d52f46904f502a4d09465815"}, {file = "ruff-0.0.272-py3-none-win32.whl", hash = "sha256:dc406e5d756d932da95f3af082814d2467943631a587339ee65e5a4f4fbe83eb"},
{file = "ruff-0.0.271-py3-none-win_amd64.whl", hash = "sha256:140e912a18a662062b04b489861e5aebdbe1a1668bf416e5a951f2347aa65907"}, {file = "ruff-0.0.272-py3-none-win_amd64.whl", hash = "sha256:a37ec80e238ead2969b746d7d1b6b0d31aa799498e9ba4281ab505b93e1f4b28"},
{file = "ruff-0.0.271-py3-none-win_arm64.whl", hash = "sha256:45b3c3551a798d9786779c6dd7ad2224af6e06162e17f4a0e7678d3e9115ae56"}, {file = "ruff-0.0.272-py3-none-win_arm64.whl", hash = "sha256:06b8ee4eb8711ab119db51028dd9f5384b44728c23586424fd6e241a5b9c4a3b"},
{file = "ruff-0.0.271.tar.gz", hash = "sha256:be4590137a31c47e7f6ef4488d60102c68102f842453355d8073193a30199aa7"}, {file = "ruff-0.0.272.tar.gz", hash = "sha256:273a01dc8c3c4fd4c2af7ea7a67c8d39bb09bce466e640dd170034da75d14cab"},
] ]
[[package]] [[package]]
name = "selenium" name = "selenium"
version = "4.9.1" version = "4.10.0"
description = "" description = ""
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "selenium-4.9.1-py3-none-any.whl", hash = "sha256:82aedaa85d55bc861f4c89ff9609e82f6c958e2e1e3da3ffcc36703f21d3ee16"}, {file = "selenium-4.10.0-py3-none-any.whl", hash = "sha256:40241b9d872f58959e9b34e258488bf11844cd86142fd68182bd41db9991fc5c"},
{file = "selenium-4.9.1.tar.gz", hash = "sha256:3444f4376321530c36ce8355b6b357d8cf4a7d588ce5cf772183465930bbed0e"}, {file = "selenium-4.10.0.tar.gz", hash = "sha256:871bf800c4934f745b909c8dfc7d15c65cf45bd2e943abd54451c810ada395e3"},
] ]
[package.dependencies] [package.dependencies]
@ -3159,14 +3159,14 @@ urllib3 = {version = ">=1.26,<3", extras = ["socks"]}
[[package]] [[package]]
name = "sentry-sdk" name = "sentry-sdk"
version = "1.25.0" version = "1.25.1"
description = "Python client for Sentry (https://sentry.io)" description = "Python client for Sentry (https://sentry.io)"
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "sentry-sdk-1.25.0.tar.gz", hash = "sha256:5be3296fc574fa8a4d9b213b4dcf8c8d0246c08f8bd78315c6286f386c37555a"}, {file = "sentry-sdk-1.25.1.tar.gz", hash = "sha256:aa796423eb6a2f4a8cd7a5b02ba6558cb10aab4ccdc0537f63a47b038c520c38"},
{file = "sentry_sdk-1.25.0-py2.py3-none-any.whl", hash = "sha256:fe85cf5d0b3d0aa3480df689f9f6dc487de783defb0a95043368375dc893645e"}, {file = "sentry_sdk-1.25.1-py2.py3-none-any.whl", hash = "sha256:79afb7c896014038e358401ad1d36889f97a129dfa8031c49b3f238cd1aa3935"},
] ]
[package.dependencies] [package.dependencies]
@ -3590,14 +3590,14 @@ files = [
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.0.2" version = "2.0.3"
description = "HTTP library with thread-safe connection pooling, file post, and more." description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "urllib3-2.0.2-py3-none-any.whl", hash = "sha256:d055c2f9d38dc53c808f6fdc8eab7360b6fdbbde02340ed25cfbcd817c62469e"}, {file = "urllib3-2.0.3-py3-none-any.whl", hash = "sha256:48e7fafa40319d358848e1bc6809b208340fafe2096f1725d05d67443d0483d1"},
{file = "urllib3-2.0.2.tar.gz", hash = "sha256:61717a1095d7e155cdb737ac7bb2f4324a858a1e2e6466f6d03ff630ca68d3cc"}, {file = "urllib3-2.0.3.tar.gz", hash = "sha256:bee28b5e56addb8226c96f7f13ac28cb4c301dd5ea8a6ca179c0b9835e032825"},
] ]
[package.dependencies] [package.dependencies]

View File

@ -238,88 +238,82 @@ class TestProviderLDAP(SeleniumTestCase):
{ {
"dn": f"cn={o_user.username},ou=users,dc=ldap,dc=goauthentik,dc=io", "dn": f"cn={o_user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
"attributes": { "attributes": {
"cn": [o_user.username], "cn": o_user.username,
"sAMAccountName": [o_user.username], "sAMAccountName": o_user.username,
"uid": [o_user.uid], "uid": o_user.uid,
"name": [o_user.name], "name": o_user.name,
"displayName": [o_user.name], "displayName": o_user.name,
"sn": [o_user.name], "sn": o_user.name,
"mail": [""], "mail": "",
"objectClass": [ "objectClass": [
"user", "user",
"organizationalPerson", "organizationalPerson",
"inetOrgPerson", "inetOrgPerson",
"goauthentik.io/ldap/user", "goauthentik.io/ldap/user",
], ],
"uidNumber": [str(2000 + o_user.pk)], "uidNumber": 2000 + o_user.pk,
"gidNumber": [str(2000 + o_user.pk)], "gidNumber": 2000 + o_user.pk,
"memberOf": [], "memberOf": [],
"homeDirectory": [ "homeDirectory": f"/home/{o_user.username}",
f"/home/{o_user.username}", "ak-active": True,
], "ak-superuser": False,
"ak-active": ["true"], "goauthentikio-user-override-ips": True,
"ak-superuser": ["false"], "goauthentikio-user-service-account": True,
"goauthentikio-user-override-ips": ["true"],
"goauthentikio-user-service-account": ["true"],
}, },
"type": "searchResEntry", "type": "searchResEntry",
}, },
{ {
"dn": f"cn={embedded_account.username},ou=users,dc=ldap,dc=goauthentik,dc=io", "dn": f"cn={embedded_account.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
"attributes": { "attributes": {
"cn": [embedded_account.username], "cn": embedded_account.username,
"sAMAccountName": [embedded_account.username], "sAMAccountName": embedded_account.username,
"uid": [embedded_account.uid], "uid": embedded_account.uid,
"name": [embedded_account.name], "name": embedded_account.name,
"displayName": [embedded_account.name], "displayName": embedded_account.name,
"sn": [embedded_account.name], "sn": embedded_account.name,
"mail": [""], "mail": "",
"objectClass": [ "objectClass": [
"user", "user",
"organizationalPerson", "organizationalPerson",
"inetOrgPerson", "inetOrgPerson",
"goauthentik.io/ldap/user", "goauthentik.io/ldap/user",
], ],
"uidNumber": [str(2000 + embedded_account.pk)], "uidNumber": 2000 + embedded_account.pk,
"gidNumber": [str(2000 + embedded_account.pk)], "gidNumber": 2000 + embedded_account.pk,
"memberOf": [], "memberOf": [],
"homeDirectory": [ "homeDirectory": f"/home/{embedded_account.username}",
f"/home/{embedded_account.username}", "ak-active": True,
], "ak-superuser": False,
"ak-active": ["true"], "goauthentikio-user-override-ips": True,
"ak-superuser": ["false"], "goauthentikio-user-service-account": True,
"goauthentikio-user-override-ips": ["true"],
"goauthentikio-user-service-account": ["true"],
}, },
"type": "searchResEntry", "type": "searchResEntry",
}, },
{ {
"dn": f"cn={self.user.username},ou=users,dc=ldap,dc=goauthentik,dc=io", "dn": f"cn={self.user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
"attributes": { "attributes": {
"cn": [self.user.username], "cn": self.user.username,
"sAMAccountName": [self.user.username], "sAMAccountName": self.user.username,
"uid": [self.user.uid], "uid": self.user.uid,
"name": [self.user.name], "name": self.user.name,
"displayName": [self.user.name], "displayName": self.user.name,
"sn": [self.user.name], "sn": self.user.name,
"mail": [self.user.email], "mail": self.user.email,
"objectClass": [ "objectClass": [
"user", "user",
"organizationalPerson", "organizationalPerson",
"inetOrgPerson", "inetOrgPerson",
"goauthentik.io/ldap/user", "goauthentik.io/ldap/user",
], ],
"uidNumber": [str(2000 + self.user.pk)], "uidNumber": 2000 + self.user.pk,
"gidNumber": [str(2000 + self.user.pk)], "gidNumber": 2000 + self.user.pk,
"memberOf": [ "memberOf": [
f"cn={group.name},ou=groups,dc=ldap,dc=goauthentik,dc=io" f"cn={group.name},ou=groups,dc=ldap,dc=goauthentik,dc=io"
for group in self.user.ak_groups.all() for group in self.user.ak_groups.all()
], ],
"homeDirectory": [ "homeDirectory": f"/home/{self.user.username}",
f"/home/{self.user.username}", "ak-active": True,
], "ak-superuser": True,
"ak-active": ["true"],
"ak-superuser": ["true"],
"extraAttribute": ["bar"], "extraAttribute": ["bar"],
}, },
"type": "searchResEntry", "type": "searchResEntry",

View File

@ -7,3 +7,4 @@ coverage
# Import order matters # Import order matters
poly.ts poly.ts
src/locale-codes.ts src/locale-codes.ts
src/locales/

View File

@ -0,0 +1,9 @@
import { create } from "@storybook/theming/create";
export default create({
base: "light",
brandTitle: "authentik Storybook",
brandUrl: "https://goauthentik.io",
brandImage: "https://goauthentik.io/img/icon_left_brand_colour.svg",
brandTarget: "_self",
});

View File

@ -0,0 +1,8 @@
// .storybook/manager.js
import { addons } from "@storybook/manager-api";
import authentikTheme from "./authentikTheme";
addons.setConfig({
theme: authentikTheme,
});

5803
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,7 @@
"@codemirror/lang-xml": "^6.0.2", "@codemirror/lang-xml": "^6.0.2",
"@codemirror/legacy-modes": "^6.3.2", "@codemirror/legacy-modes": "^6.3.2",
"@codemirror/theme-one-dark": "^6.1.2", "@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.2.2", "@formatjs/intl-listformat": "^7.3.0",
"@fortawesome/fontawesome-free": "^6.4.0", "@fortawesome/fontawesome-free": "^6.4.0",
"@goauthentik/api": "^2023.5.3-1685646044", "@goauthentik/api": "^2023.5.3-1685646044",
"@lit/localize": "^0.11.4", "@lit/localize": "^0.11.4",
@ -73,11 +73,11 @@
"@rollup/plugin-replace": "^5.0.2", "@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-typescript": "^11.1.1", "@rollup/plugin-typescript": "^11.1.1",
"@squoosh/cli": "^0.7.3", "@squoosh/cli": "^0.7.3",
"@storybook/addon-essentials": "^7.0.18", "@storybook/addon-essentials": "^7.0.20",
"@storybook/addon-links": "^7.0.18", "@storybook/addon-links": "^7.0.20",
"@storybook/blocks": "^7.0.18", "@storybook/blocks": "^7.0.20",
"@storybook/web-components": "^7.0.18", "@storybook/web-components": "^7.0.20",
"@storybook/web-components-vite": "^7.0.18", "@storybook/web-components-vite": "^7.0.20",
"@trivago/prettier-plugin-sort-imports": "^4.1.1", "@trivago/prettier-plugin-sort-imports": "^4.1.1",
"@types/chart.js": "^2.9.37", "@types/chart.js": "^2.9.37",
"@types/codemirror": "5.60.8", "@types/codemirror": "5.60.8",
@ -95,7 +95,7 @@
"lit-analyzer": "^1.2.1", "lit-analyzer": "^1.2.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"pyright": "^1.1.312", "pyright": "^1.1.313",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"rollup": "^2.79.1", "rollup": "^2.79.1",
@ -104,7 +104,7 @@
"rollup-plugin-minify-html-literals": "^1.2.6", "rollup-plugin-minify-html-literals": "^1.2.6",
"rollup-plugin-postcss-lit": "^2.0.0", "rollup-plugin-postcss-lit": "^2.0.0",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"storybook": "^7.0.18", "storybook": "^7.0.20",
"ts-lit-plugin": "^1.2.1", "ts-lit-plugin": "^1.2.1",
"tslib": "^2.5.3", "tslib": "^2.5.3",
"turnstile-types": "^1.1.2", "turnstile-types": "^1.1.2",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 399 KiB

After

Width:  |  Height:  |  Size: 595 KiB

View File

@ -525,7 +525,8 @@ export class FlowExecutor extends Interface implements StageHost {
${this.flowInfo?.background?.startsWith("/static") ${this.flowInfo?.background?.startsWith("/static")
? html` ? html`
<li> <li>
<a href="https://unsplash.com/@joshnh" <a
href="https://unsplash.com/@diegojimenez"
>${msg("Background image")}</a >${msg("Background image")}</a
> >
</li> </li>

View File

@ -3,5 +3,5 @@ import "@webcomponents/webcomponentsjs";
import "lit/polyfill-support.js"; import "lit/polyfill-support.js";
import "core-js/actual"; import "core-js/actual";
import "@formatjs/intl-listformat/polyfill.js"; import "@formatjs/intl-listformat/polyfill";
import "@formatjs/intl-listformat/locale-data/en.js"; import "@formatjs/intl-listformat/locale-data/en";

View File

@ -60,14 +60,13 @@ Starting with 2023.3, periods and slashes in custom attributes will be sanitized
You can also configure SSL for your LDAP Providers by selecting a certificate and a server name in the provider settings. You can also configure SSL for your LDAP Providers by selecting a certificate and a server name in the provider settings.
Starting with authentik 2023.6, StartTLS is supported, and the provider will pick the correct certificate based on the DN a bind attempt is made with. Starting with authentik 2023.6, StartTLS is supported, and the provider will pick the correct certificate based on the configured _TLS Server name_ field. The certificate is not picked based on the Bind DN, as the StartTLS operation should happen be the bind request to ensure bind credentials are transmitted over TLS.
This enables you to bind on port 636 using LDAPS. This enables you to bind on port 636 using LDAPS.
## Integrations ## Integrations
See the integration guide for [sssd](../../../integrations/services/sssd/) for See the integration guide for [sssd](../../../integrations/services/sssd/) for an example guide.
an example guide.
## Bind Modes ## Bind Modes

View File

@ -28,7 +28,7 @@ The following placeholders will be used:
Create a OAuth2/OpenID Provider (under _Applications/Providers_) with these settings: Create a OAuth2/OpenID Provider (under _Applications/Providers_) with these settings:
- Name : writefreely - Name: writefreely
- Redirect URI: `https://writefreely.company/oauth/callback/generic` - Redirect URI: `https://writefreely.company/oauth/callback/generic`
### Step 3 - Application ### Step 3 - Application
@ -88,6 +88,12 @@ map_email = email
Restart writefreely.service Restart writefreely.service
## Account linking
If your usernames in authentik and WriteFreely are different, you might need to link your accounts before being able to use SSO.
To link the accounts, first log into Writefreely with local credentials, and then navigate to **Customize -->Account Settings**. In the option "Linked Accounts", click on "authentik".
## Additional Resources ## Additional Resources
- https://writefreely.org/docs/latest/admin/config - https://writefreely.org/docs/latest/admin/config

View File

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 994.71 151.65"><defs><style>.cls-1{fill:#fd4b2d;}</style></defs><path class="cls-1" d="M284.72,50.4H305.5v82.84H284.72v-8.76a40.79,40.79,0,0,1-12.21,8.34,34.14,34.14,0,0,1-13.27,2.55q-16.05,0-27.76-12.45T219.77,92q0-19.18,11.33-31.45t27.53-12.26a34.94,34.94,0,0,1,14,2.82,38.32,38.32,0,0,1,12.1,8.45ZM262.87,67.45a21,21,0,0,0-16,6.82q-6.37,6.81-6.38,17.47T247,109.4a21,21,0,0,0,16,6.93,21.42,21.42,0,0,0,16.24-6.81q6.45-6.81,6.45-17.86,0-10.8-6.45-17.51A21.71,21.71,0,0,0,262.87,67.45Z"/><path class="cls-1" d="M335.8,50.4h21V90.29q0,11.65,1.6,16.18a14.16,14.16,0,0,0,5.16,7,14.76,14.76,0,0,0,8.74,2.51,15.25,15.25,0,0,0,8.81-2.48,14.49,14.49,0,0,0,5.38-7.27q1.31-3.57,1.3-15.3V50.4h20.79V85.5q0,21.69-3.43,29.69a32.32,32.32,0,0,1-12.33,15q-8.16,5.22-20.71,5.22-13.64,0-22.05-6.09a32.2,32.2,0,0,1-11.84-17q-2.43-7.55-2.43-27.41Z"/><path class="cls-1" d="M441.32,19.86H462.1V50.4h12.34V68.29H462.1v65H441.32V68.29H430.66V50.4h10.66Z"/><path class="cls-1" d="M495,18.42h20.63V58.77a47.41,47.41,0,0,1,12.26-7.88,31.62,31.62,0,0,1,12.49-2.63,28.13,28.13,0,0,1,20.78,8.53q7.23,7.4,7.24,21.7v54.75H547.9V96.92q0-14.4-1.37-19.49a13.6,13.6,0,0,0-4.68-7.62,13.19,13.19,0,0,0-8.18-2.51,15.43,15.43,0,0,0-10.85,4.19,22.14,22.14,0,0,0-6.28,11.42q-.91,3.72-.92,17v33.28H495Z"/><path class="cls-1" d="M680.84,97.83H614.06a22.25,22.25,0,0,0,7.73,14q6.29,5.22,16,5.21a27.7,27.7,0,0,0,20-8.14l17.51,8.22a41.31,41.31,0,0,1-15.68,13.74q-9.13,4.46-21.7,4.46-19.5,0-31.75-12.3T594,92.27q0-19,12.22-31.48t30.65-12.53q19.56,0,31.82,12.53t12.26,33.08ZM660.05,81.46a20.87,20.87,0,0,0-8.12-11.27,23.61,23.61,0,0,0-14.08-4.34,24.88,24.88,0,0,0-15.25,4.88q-4.11,3-7.62,10.73Z"/><path class="cls-1" d="M707,50.4H727.8v8.49a50.15,50.15,0,0,1,12.81-8.3,31.08,31.08,0,0,1,11.75-2.33,28.44,28.44,0,0,1,20.91,8.61q7.22,7.31,7.22,21.62v54.75H759.93V97q0-14.83-1.33-19.7A13.48,13.48,0,0,0,754,69.85a13,13,0,0,0-8.16-2.55A15.32,15.32,0,0,0,735,71.52a22.6,22.6,0,0,0-6.27,11.67q-.9,3.89-.91,16.81v33.24H707Z"/><path class="cls-1" d="M812.46,19.86h20.79V50.4h12.33V68.29H833.25v65H812.46V68.29H801.8V50.4h10.66Z"/><path class="cls-1" d="M874.16,16.29a12.74,12.74,0,0,1,9.38,3.95,13.18,13.18,0,0,1,3.91,9.6,13,13,0,0,1-3.87,9.48,12.6,12.6,0,0,1-9.27,3.92,12.73,12.73,0,0,1-9.45-4A13.39,13.39,0,0,1,861,29.53a12.78,12.78,0,0,1,3.87-9.36A12.71,12.71,0,0,1,874.16,16.29Z"/><rect class="cls-1" x="863.77" y="50.4" width="20.79" height="82.84"/><path class="cls-1" d="M913,18.42h20.78V84.55L964.34,50.4h26.11L954.76,90.1l40,43.14h-25.8L933.73,95.06v38.18H913Z"/><rect class="cls-1" x="107.1" y="34.93" width="6.37" height="18.2"/><rect class="cls-1" x="123.67" y="34.16" width="6.37" height="14.23"/><path class="cls-1" d="M30.83,55A23.23,23.23,0,0,0,10.41,67.13h10.8C26,63,32.94,61.8,38,67.13H49.39C44.93,61.09,38.24,55,30.83,55Z"/><path class="cls-1" d="M46.25,78.11c-14.89,31.15-41,4.6-25-11H10.41c-8.47,14.76,3.24,34.68,20.42,34.23,13.28,0,24.24-19.72,24.24-23.21,0-1.54-2.14-6.25-5.68-11H38A40.52,40.52,0,0,1,46.25,78.11Zm.4-.91Z"/><path class="cls-1" d="M189.62,34.71V117A28.62,28.62,0,0,1,161,145.54H148.89v-28H90.94v28H78.81A28.62,28.62,0,0,1,50.22,117V91.08h91.87V41.62H97.74V69.41H50.22V34.71a27.43,27.43,0,0,1,.19-3.29,27.09,27.09,0,0,1,.71-3.84c.1-.41.22-.82.34-1.21a2.13,2.13,0,0,1,.09-.3c.07-.21.13-.4.2-.59s.14-.4.21-.59.16-.44.25-.65.18-.43.26-.64a29.35,29.35,0,0,1,2.6-4.82l0-.05c.26-.37.53-.75.81-1.12s.47-.61.7-.91.57-.67.86-1,.56-.63.86-.93l0,0a4.53,4.53,0,0,1,.49-.49,29.23,29.23,0,0,1,3.4-2.84c.32-.24.66-.46,1-.68s.77-.49,1.17-.72a23.78,23.78,0,0,1,2.29-1.21l.75-.34a27.84,27.84,0,0,1,3.35-1.21c.44-.13.88-.24,1.33-.35a6.19,6.19,0,0,1,.65-.15,28.86,28.86,0,0,1,3.87-.57l.56,0h.28c.43,0,.87,0,1.31,0H161c.43,0,.87,0,1.3,0h.28l.56,0a29.25,29.25,0,0,1,3.88.57c.22,0,.43.09.65.15.45.11.88.22,1.32.35a27.23,27.23,0,0,1,3.35,1.21l.75.34a25.19,25.19,0,0,1,2.3,1.21c.39.23.78.47,1.16.72s.69.44,1,.68a29.23,29.23,0,0,1,3.91,3.36q.45.45.87.93c.29.32.57.66.85,1l.71.91c.28.37.54.75.8,1.12l0,.05a28.61,28.61,0,0,1,2.6,4.82l.27.64.24.65c.08.19.15.39.22.59l.19.59c0,.09.06.19.1.3.11.39.23.8.34,1.21a28.56,28.56,0,0,1,.7,3.84A27.42,27.42,0,0,1,189.62,34.71Z"/><path class="cls-1" d="M184.76,18.78H55.07A28.59,28.59,0,0,1,78.8,6.12H161A28.59,28.59,0,0,1,184.76,18.78Z"/><path class="cls-1" d="M189.43,31.43H50.4a28.29,28.29,0,0,1,4.67-12.65H184.76A28.17,28.17,0,0,1,189.43,31.43Z"/><path class="cls-1" d="M189.63,34.71v9.37H142.09V41.62H97.74v2.46H50.21V34.71a27.43,27.43,0,0,1,.19-3.29h139A27.42,27.42,0,0,1,189.63,34.71Z"/><rect class="cls-1" x="50.21" y="44.08" width="47.54" height="12.66"/><rect class="cls-1" x="142.09" y="44.08" width="47.54" height="12.66"/><rect class="cls-1" x="50.21" y="56.74" width="47.54" height="12.65"/><rect class="cls-1" x="142.09" y="56.74" width="47.54" height="12.65"/></svg>

After

Width:  |  Height:  |  Size: 4.7 KiB