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:
commit
1c85dc512f
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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/
|
||||||
|
|
|
@ -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",
|
||||||
|
});
|
|
@ -0,0 +1,8 @@
|
||||||
|
// .storybook/manager.js
|
||||||
|
import { addons } from "@storybook/manager-api";
|
||||||
|
|
||||||
|
import authentikTheme from "./authentikTheme";
|
||||||
|
|
||||||
|
addons.setConfig({
|
||||||
|
theme: authentikTheme,
|
||||||
|
});
|
File diff suppressed because it is too large
Load Diff
|
@ -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 |
|
@ -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>
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 |
Reference in New Issue