Merge branch 'master' into version-2021.10
This commit is contained in:
commit
68fa8105e1
13
Makefile
13
Makefile
|
@ -48,7 +48,7 @@ gen-web:
|
|||
docker run \
|
||||
--rm -v ${PWD}:/local \
|
||||
--user ${UID}:${GID} \
|
||||
openapitools/openapi-generator-cli generate \
|
||||
ghcr.io/beryju/openapi-generator generate \
|
||||
-i /local/schema.yml \
|
||||
-g typescript-fetch \
|
||||
-o /local/web-api \
|
||||
|
@ -60,18 +60,19 @@ gen-web:
|
|||
\cp -rfv web-api/* web/node_modules/@goauthentik/api
|
||||
|
||||
gen-outpost:
|
||||
wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml -O config.yaml
|
||||
mkdir -p templates
|
||||
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache -O templates/README.mustache
|
||||
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/go.mod.mustache -O templates/go.mod.mustache
|
||||
docker run \
|
||||
--rm -v ${PWD}:/local \
|
||||
--user ${UID}:${GID} \
|
||||
openapitools/openapi-generator-cli generate \
|
||||
--git-host goauthentik.io \
|
||||
--git-repo-id outpost \
|
||||
--git-user-id api \
|
||||
-i /local/schema.yml \
|
||||
-g go \
|
||||
-o /local/api \
|
||||
--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true,disallowAdditionalPropertiesIfNotPresent=false
|
||||
rm -f api/go.mod api/go.sum
|
||||
-c /local/config.yaml
|
||||
go mod edit -replace goauthentik.io/api=./api
|
||||
|
||||
gen: gen-build gen-clean gen-web
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ from django.urls import include, path
|
|||
from authentik.api.v3.urls import urlpatterns as v3_urls
|
||||
|
||||
urlpatterns = [
|
||||
# Remove in 2022.1
|
||||
# TODO: Remove in 2022.1
|
||||
path("v2beta/", include(v3_urls)),
|
||||
path("v3/", include(v3_urls)),
|
||||
]
|
||||
|
|
0
authentik/core/management/__init__.py
Normal file
0
authentik/core/management/__init__.py
Normal file
0
authentik/core/management/commands/__init__.py
Normal file
0
authentik/core/management/commands/__init__.py
Normal file
15
authentik/core/management/commands/dump_config.py
Normal file
15
authentik/core/management/commands/dump_config.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
"""Output full config"""
|
||||
from json import dumps
|
||||
|
||||
from django.core.management.base import BaseCommand, no_translations
|
||||
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
|
||||
class Command(BaseCommand): # pragma: no cover
|
||||
"""Output full config"""
|
||||
|
||||
@no_translations
|
||||
def handle(self, *args, **options):
|
||||
"""Check permissions for all apps"""
|
||||
print(dumps(CONFIG.raw, indent=4))
|
|
@ -24,6 +24,7 @@ class LDAPProviderSerializer(ProviderSerializer):
|
|||
"uid_start_number",
|
||||
"gid_start_number",
|
||||
"outpost_set",
|
||||
"search_mode",
|
||||
]
|
||||
|
||||
|
||||
|
@ -68,6 +69,7 @@ class LDAPOutpostConfigSerializer(ModelSerializer):
|
|||
"tls_server_name",
|
||||
"uid_start_number",
|
||||
"gid_start_number",
|
||||
"search_mode",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
# Generated by Django 3.2.8 on 2021-11-05 09:41
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [
|
||||
("authentik_providers_ldap", "0001_initial"),
|
||||
("authentik_providers_ldap", "0002_ldapprovider_search_group"),
|
||||
("authentik_providers_ldap", "0003_auto_20210713_1138"),
|
||||
("authentik_providers_ldap", "0004_auto_20210713_2115"),
|
||||
("authentik_providers_ldap", "0005_ldapprovider_search_mode"),
|
||||
]
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0019_source_managed"),
|
||||
("authentik_crypto", "0002_create_self_signed_kp"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="LDAPProvider",
|
||||
fields=[
|
||||
(
|
||||
"provider_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_core.provider",
|
||||
),
|
||||
),
|
||||
(
|
||||
"base_dn",
|
||||
models.TextField(
|
||||
default="DC=ldap,DC=goauthentik,DC=io",
|
||||
help_text="DN under which objects are accessible.",
|
||||
),
|
||||
),
|
||||
(
|
||||
"search_group",
|
||||
models.ForeignKey(
|
||||
default=None,
|
||||
help_text="Users in this group can do search queries. If not set, every user can execute search queries.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||
to="authentik_core.group",
|
||||
),
|
||||
),
|
||||
(
|
||||
"certificate",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="authentik_crypto.certificatekeypair",
|
||||
),
|
||||
),
|
||||
("tls_server_name", models.TextField(blank=True, default="")),
|
||||
(
|
||||
"gid_start_number",
|
||||
models.IntegerField(
|
||||
default=4000,
|
||||
help_text="The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber",
|
||||
),
|
||||
),
|
||||
(
|
||||
"uid_start_number",
|
||||
models.IntegerField(
|
||||
default=2000,
|
||||
help_text="The start for uidNumbers, this number is added to the user.Pk to make sure that the numbers aren't too low for POSIX users. Default is 2000 to ensure that we don't collide with local users uidNumber",
|
||||
),
|
||||
),
|
||||
(
|
||||
"search_mode",
|
||||
models.TextField(
|
||||
choices=[("direct", "Direct"), ("cached", "Cached")], default="direct"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "LDAP Provider",
|
||||
"verbose_name_plural": "LDAP Providers",
|
||||
},
|
||||
bases=("authentik_core.provider", models.Model),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 3.2.8 on 2021-11-05 09:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_ldap", "0004_auto_20210713_2115"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="ldapprovider",
|
||||
name="search_mode",
|
||||
field=models.TextField(
|
||||
choices=[("direct", "Direct"), ("cached", "Cached")], default="direct"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -10,6 +10,13 @@ from authentik.crypto.models import CertificateKeyPair
|
|||
from authentik.outposts.models import OutpostModel
|
||||
|
||||
|
||||
class SearchModes(models.TextChoices):
|
||||
"""Search modes"""
|
||||
|
||||
DIRECT = "direct"
|
||||
CACHED = "cached"
|
||||
|
||||
|
||||
class LDAPProvider(OutpostModel, Provider):
|
||||
"""Allow applications to authenticate against authentik's users using LDAP."""
|
||||
|
||||
|
@ -59,6 +66,8 @@ class LDAPProvider(OutpostModel, Provider):
|
|||
),
|
||||
)
|
||||
|
||||
search_mode = models.TextField(default=SearchModes.DIRECT, choices=SearchModes.choices)
|
||||
|
||||
@property
|
||||
def launch_url(self) -> Optional[str]:
|
||||
"""LDAP never has a launch URL"""
|
||||
|
|
30
authentik/recovery/management/commands/create_admin_group.py
Normal file
30
authentik/recovery/management/commands/create_admin_group.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
"""authentik recovery create_admin_group"""
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from authentik.core.models import Group, User
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Create admin group if the default group gets deleted"""
|
||||
|
||||
help = _("Create admin group if the default group gets deleted.")
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("user", action="store", help="User to add to the admin group.")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""Create admin group if the default group gets deleted"""
|
||||
username = options.get("user")
|
||||
user = User.objects.filter(username=username).first()
|
||||
if not user:
|
||||
self.stderr.write(f"User '{username}' not found.")
|
||||
return
|
||||
group, _ = Group.objects.update_or_create(
|
||||
name="authentik Admins",
|
||||
defaults={
|
||||
"is_superuser": True,
|
||||
},
|
||||
)
|
||||
group.users.add(user)
|
||||
self.stdout.write(f"User '{username}' successfully added to the group 'authentik Admins'.")
|
|
@ -7,12 +7,9 @@ from django.urls import reverse
|
|||
from django.utils.text import slugify
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import Token, TokenIntents, User
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Create Token used to recover access"""
|
||||
|
|
|
@ -310,7 +310,7 @@ EMAIL_HOST = CONFIG.y("email.host")
|
|||
EMAIL_PORT = int(CONFIG.y("email.port"))
|
||||
EMAIL_HOST_USER = CONFIG.y("email.username")
|
||||
EMAIL_HOST_PASSWORD = CONFIG.y("email.password")
|
||||
EMAIL_USE_TLS = CONFIG.y_bool("email.use_tls", True)
|
||||
EMAIL_USE_TLS = CONFIG.y_bool("email.use_tls", False)
|
||||
EMAIL_USE_SSL = CONFIG.y_bool("email.use_ssl", False)
|
||||
EMAIL_TIMEOUT = int(CONFIG.y("email.timeout"))
|
||||
DEFAULT_FROM_EMAIL = CONFIG.y("email.from")
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""prompt models"""
|
||||
from typing import Type
|
||||
from typing import Any, Optional, Type
|
||||
from uuid import uuid4
|
||||
|
||||
from django.db import models
|
||||
|
@ -74,13 +74,15 @@ class Prompt(SerializerModel):
|
|||
|
||||
return PromptSerializer
|
||||
|
||||
@property
|
||||
def field(self) -> CharField:
|
||||
def field(self, default: Optional[Any]) -> CharField:
|
||||
"""Get field type for Challenge and response"""
|
||||
field_class = CharField
|
||||
kwargs = {
|
||||
"required": self.required,
|
||||
}
|
||||
if default:
|
||||
kwargs["initial"] = default
|
||||
|
||||
if self.type == FieldTypes.EMAIL:
|
||||
field_class = EmailField
|
||||
if self.type == FieldTypes.NUMBER:
|
||||
|
|
|
@ -65,7 +65,8 @@ class PromptChallengeResponse(ChallengeResponse):
|
|||
fields = list(self.stage.fields.all())
|
||||
for field in fields:
|
||||
field: Prompt
|
||||
self.fields[field.field_key] = field.field
|
||||
current = plan.context.get(PLAN_CONTEXT_PROMPT, {}).get(field.field_key)
|
||||
self.fields[field.field_key] = field.field(current)
|
||||
# Special handling for fields with username type
|
||||
# these check for existing users with the same username
|
||||
if field.type == FieldTypes.USERNAME:
|
||||
|
|
2
go.mod
2
go.mod
|
@ -29,7 +29,7 @@ require (
|
|||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/recws-org/recws v1.3.1
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
goauthentik.io/api v0.2021102.4
|
||||
goauthentik.io/api v0.2021102.5
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558
|
||||
|
|
4
go.sum
4
go.sum
|
@ -559,8 +559,8 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
|||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
goauthentik.io/api v0.2021102.4 h1:+2l4qHKjNYkRG2OIzSPLMPvb+KmrJixPunXVW66s/sE=
|
||||
goauthentik.io/api v0.2021102.4/go.mod h1:02nnD4FRd8lu8A1+ZuzqownBgvAhdCKzqkKX8v7JMTE=
|
||||
goauthentik.io/api v0.2021102.5 h1:TGUUPzQgnungUNrvDL2ShZy003MHMn3sgGOyfmjTMDo=
|
||||
goauthentik.io/api v0.2021102.5/go.mod h1:02nnD4FRd8lu8A1+ZuzqownBgvAhdCKzqkKX8v7JMTE=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
|
@ -18,7 +17,6 @@ import (
|
|||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/constants"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
"goauthentik.io/internal/utils"
|
||||
)
|
||||
|
||||
type StageComponent string
|
||||
|
@ -103,8 +101,8 @@ type ChallengeInt interface {
|
|||
GetResponseErrors() map[string][]api.ErrorDetail
|
||||
}
|
||||
|
||||
func (fe *FlowExecutor) DelegateClientIP(a net.Addr) {
|
||||
fe.cip = utils.GetIP(a)
|
||||
func (fe *FlowExecutor) DelegateClientIP(a string) {
|
||||
fe.cip = a
|
||||
fe.api.GetConfig().AddDefaultHeader(HeaderAuthentikRemoteIP, fe.cip)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
package ldap
|
||||
|
||||
import "crypto/tls"
|
||||
|
||||
func (ls *LDAPServer) getCertificates(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if len(ls.providers) == 1 {
|
||||
if ls.providers[0].cert != nil {
|
||||
ls.log.WithField("server-name", info.ServerName).Debug("We only have a single provider, using their cert")
|
||||
return ls.providers[0].cert, nil
|
||||
}
|
||||
}
|
||||
for _, provider := range ls.providers {
|
||||
if provider.tlsServerName == &info.ServerName {
|
||||
if provider.cert == nil {
|
||||
ls.log.WithField("server-name", info.ServerName).Debug("Handler does not have a certificate")
|
||||
return ls.defaultCert, nil
|
||||
}
|
||||
return provider.cert, nil
|
||||
}
|
||||
}
|
||||
ls.log.WithField("server-name", info.ServerName).Debug("Fallback to default cert")
|
||||
return ls.defaultCert, nil
|
||||
}
|
|
@ -1,44 +1,18 @@
|
|||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/google/uuid"
|
||||
"github.com/nmcclain/ldap"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/ldap/bind"
|
||||
"goauthentik.io/internal/outpost/ldap/metrics"
|
||||
"goauthentik.io/internal/utils"
|
||||
)
|
||||
|
||||
type BindRequest struct {
|
||||
BindDN string
|
||||
BindPW string
|
||||
id string
|
||||
conn net.Conn
|
||||
log *log.Entry
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) {
|
||||
span := sentry.StartSpan(context.TODO(), "authentik.providers.ldap.bind",
|
||||
sentry.TransactionName("authentik.providers.ldap.bind"))
|
||||
rid := uuid.New().String()
|
||||
span.SetTag("request_uid", rid)
|
||||
span.SetTag("user.username", bindDN)
|
||||
req, span := bind.NewRequest(bindDN, bindPW, conn)
|
||||
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
req := BindRequest{
|
||||
BindDN: bindDN,
|
||||
BindPW: bindPW,
|
||||
conn: conn,
|
||||
log: ls.log.WithField("bindDN", bindDN).WithField("requestId", rid).WithField("client", utils.GetIP(conn.RemoteAddr())),
|
||||
id: rid,
|
||||
ctx: span.Context(),
|
||||
}
|
||||
defer func() {
|
||||
span.Finish()
|
||||
metrics.Requests.With(prometheus.Labels{
|
||||
|
@ -46,19 +20,19 @@ func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LD
|
|||
"type": "bind",
|
||||
"filter": "",
|
||||
"dn": req.BindDN,
|
||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
||||
"client": req.RemoteAddr(),
|
||||
}).Observe(float64(span.EndTime.Sub(span.StartTime)))
|
||||
req.log.WithField("took-ms", span.EndTime.Sub(span.StartTime).Milliseconds()).Info("Bind request")
|
||||
req.Log().WithField("took-ms", span.EndTime.Sub(span.StartTime).Milliseconds()).Info("Bind request")
|
||||
}()
|
||||
for _, instance := range ls.providers {
|
||||
username, err := instance.getUsername(bindDN)
|
||||
username, err := instance.binder.GetUsername(bindDN)
|
||||
if err == nil {
|
||||
return instance.Bind(username, req)
|
||||
return instance.binder.Bind(username, req)
|
||||
} else {
|
||||
req.log.WithError(err).Debug("Username not for instance")
|
||||
req.Log().WithError(err).Debug("Username not for instance")
|
||||
}
|
||||
}
|
||||
req.log.WithField("request", "bind").Warning("No provider found for request")
|
||||
req.Log().WithField("request", "bind").Warning("No provider found for request")
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": ls.ac.Outpost.Name,
|
||||
"type": "bind",
|
||||
|
@ -68,10 +42,3 @@ func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LD
|
|||
}).Inc()
|
||||
return ldap.LDAPResultOperationsError, nil
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) TimerFlowCacheExpiry() {
|
||||
for _, p := range ls.providers {
|
||||
ls.log.WithField("flow", p.flowSlug).Debug("Pre-heating flow cache")
|
||||
p.TimerFlowCacheExpiry()
|
||||
}
|
||||
}
|
||||
|
|
9
internal/outpost/ldap/bind/binder.go
Normal file
9
internal/outpost/ldap/bind/binder.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package bind
|
||||
|
||||
import "github.com/nmcclain/ldap"
|
||||
|
||||
type Binder interface {
|
||||
GetUsername(string) (string, error)
|
||||
Bind(username string, req *Request) (ldap.LDAPResultCode, error)
|
||||
TimerFlowCacheExpiry()
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package ldap
|
||||
package direct
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
@ -12,14 +12,30 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost"
|
||||
"goauthentik.io/internal/outpost/ldap/bind"
|
||||
"goauthentik.io/internal/outpost/ldap/flags"
|
||||
"goauthentik.io/internal/outpost/ldap/metrics"
|
||||
"goauthentik.io/internal/utils"
|
||||
"goauthentik.io/internal/outpost/ldap/server"
|
||||
)
|
||||
|
||||
const ContextUserKey = "ak_user"
|
||||
|
||||
func (pi *ProviderInstance) getUsername(dn string) (string, error) {
|
||||
if !strings.HasSuffix(strings.ToLower(dn), strings.ToLower(pi.BaseDN)) {
|
||||
type DirectBinder struct {
|
||||
si server.LDAPServerInstance
|
||||
log *log.Entry
|
||||
}
|
||||
|
||||
func NewDirectBinder(si server.LDAPServerInstance) *DirectBinder {
|
||||
db := &DirectBinder{
|
||||
si: si,
|
||||
log: log.WithField("logger", "authentik.outpost.ldap.binder.direct"),
|
||||
}
|
||||
db.log.Info("initialised direct binder")
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *DirectBinder) GetUsername(dn string) (string, error) {
|
||||
if !strings.HasSuffix(strings.ToLower(dn), strings.ToLower(db.si.GetBaseDN())) {
|
||||
return "", errors.New("invalid base DN")
|
||||
}
|
||||
dns, err := goldap.ParseDN(dn)
|
||||
|
@ -36,13 +52,13 @@ func (pi *ProviderInstance) getUsername(dn string) (string, error) {
|
|||
return "", errors.New("failed to find cn")
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) Bind(username string, req BindRequest) (ldap.LDAPResultCode, error) {
|
||||
fe := outpost.NewFlowExecutor(req.ctx, pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{
|
||||
func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResultCode, error) {
|
||||
fe := outpost.NewFlowExecutor(req.Context(), db.si.GetFlowSlug(), db.si.GetAPIClient().GetConfig(), log.Fields{
|
||||
"bindDN": req.BindDN,
|
||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
||||
"requestId": req.id,
|
||||
"client": req.RemoteAddr(),
|
||||
"requestId": req.ID(),
|
||||
})
|
||||
fe.DelegateClientIP(req.conn.RemoteAddr())
|
||||
fe.DelegateClientIP(req.RemoteAddr())
|
||||
fe.Params.Add("goauthentik.io/outpost/ldap", "true")
|
||||
|
||||
fe.Answers[outpost.StageIdentification] = username
|
||||
|
@ -51,83 +67,82 @@ func (pi *ProviderInstance) Bind(username string, req BindRequest) (ldap.LDAPRes
|
|||
passed, err := fe.Execute()
|
||||
if !passed {
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": pi.outpostName,
|
||||
"outpost_name": db.si.GetOutpostName(),
|
||||
"type": "bind",
|
||||
"reason": "invalid_credentials",
|
||||
"dn": req.BindDN,
|
||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
||||
"client": req.RemoteAddr(),
|
||||
}).Inc()
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
if err != nil {
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": pi.outpostName,
|
||||
"outpost_name": db.si.GetOutpostName(),
|
||||
"type": "bind",
|
||||
"reason": "flow_error",
|
||||
"dn": req.BindDN,
|
||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
||||
"client": req.RemoteAddr(),
|
||||
}).Inc()
|
||||
req.log.WithError(err).Warning("failed to execute flow")
|
||||
req.Log().WithError(err).Warning("failed to execute flow")
|
||||
return ldap.LDAPResultOperationsError, nil
|
||||
}
|
||||
|
||||
access, err := fe.CheckApplicationAccess(pi.appSlug)
|
||||
access, err := fe.CheckApplicationAccess(db.si.GetAppSlug())
|
||||
if !access {
|
||||
req.log.Info("Access denied for user")
|
||||
req.Log().Info("Access denied for user")
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": pi.outpostName,
|
||||
"outpost_name": db.si.GetOutpostName(),
|
||||
"type": "bind",
|
||||
"reason": "access_denied",
|
||||
"dn": req.BindDN,
|
||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
||||
"client": req.RemoteAddr(),
|
||||
}).Inc()
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
if err != nil {
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": pi.outpostName,
|
||||
"outpost_name": db.si.GetOutpostName(),
|
||||
"type": "bind",
|
||||
"reason": "access_check_fail",
|
||||
"dn": req.BindDN,
|
||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
||||
"client": req.RemoteAddr(),
|
||||
}).Inc()
|
||||
req.log.WithError(err).Warning("failed to check access")
|
||||
req.Log().WithError(err).Warning("failed to check access")
|
||||
return ldap.LDAPResultOperationsError, nil
|
||||
}
|
||||
req.log.Info("User has access")
|
||||
uisp := sentry.StartSpan(req.ctx, "authentik.providers.ldap.bind.user_info")
|
||||
req.Log().Info("User has access")
|
||||
uisp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.bind.user_info")
|
||||
// Get user info to store in context
|
||||
userInfo, _, err := fe.ApiClient().CoreApi.CoreUsersMeRetrieve(context.Background()).Execute()
|
||||
if err != nil {
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": pi.outpostName,
|
||||
"outpost_name": db.si.GetOutpostName(),
|
||||
"type": "bind",
|
||||
"reason": "user_info_fail",
|
||||
"dn": req.BindDN,
|
||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
||||
"client": req.RemoteAddr(),
|
||||
}).Inc()
|
||||
req.log.WithError(err).Warning("failed to get user info")
|
||||
req.Log().WithError(err).Warning("failed to get user info")
|
||||
return ldap.LDAPResultOperationsError, nil
|
||||
}
|
||||
pi.boundUsersMutex.Lock()
|
||||
cs := pi.SearchAccessCheck(userInfo.User)
|
||||
pi.boundUsers[req.BindDN] = UserFlags{
|
||||
cs := db.SearchAccessCheck(userInfo.User)
|
||||
flags := flags.UserFlags{
|
||||
UserPk: userInfo.User.Pk,
|
||||
CanSearch: cs != nil,
|
||||
}
|
||||
if pi.boundUsers[req.BindDN].CanSearch {
|
||||
req.log.WithField("group", cs).Info("Allowed access to search")
|
||||
db.si.SetFlags(req.BindDN, flags)
|
||||
if flags.CanSearch {
|
||||
req.Log().WithField("group", cs).Info("Allowed access to search")
|
||||
}
|
||||
uisp.Finish()
|
||||
defer pi.boundUsersMutex.Unlock()
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
|
||||
// SearchAccessCheck Check if the current user is allowed to search
|
||||
func (pi *ProviderInstance) SearchAccessCheck(user api.UserSelf) *string {
|
||||
func (db *DirectBinder) SearchAccessCheck(user api.UserSelf) *string {
|
||||
for _, group := range user.Groups {
|
||||
for _, allowedGroup := range pi.searchAllowedGroups {
|
||||
pi.log.WithField("userGroup", group.Pk).WithField("allowedGroup", allowedGroup).Trace("Checking search access")
|
||||
for _, allowedGroup := range db.si.GetSearchAllowedGroups() {
|
||||
db.log.WithField("userGroup", group.Pk).WithField("allowedGroup", allowedGroup).Trace("Checking search access")
|
||||
if group.Pk == allowedGroup.String() {
|
||||
return &group.Name
|
||||
}
|
||||
|
@ -136,13 +151,13 @@ func (pi *ProviderInstance) SearchAccessCheck(user api.UserSelf) *string {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) TimerFlowCacheExpiry() {
|
||||
fe := outpost.NewFlowExecutor(context.Background(), pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{})
|
||||
func (db *DirectBinder) TimerFlowCacheExpiry() {
|
||||
fe := outpost.NewFlowExecutor(context.Background(), db.si.GetFlowSlug(), db.si.GetAPIClient().GetConfig(), log.Fields{})
|
||||
fe.Params.Add("goauthentik.io/outpost/ldap", "true")
|
||||
fe.Params.Add("goauthentik.io/outpost/ldap-warmup", "true")
|
||||
|
||||
err := fe.WarmUp()
|
||||
if err != nil {
|
||||
pi.log.WithError(err).Warning("failed to warm up flow cache")
|
||||
db.log.WithError(err).Warning("failed to warm up flow cache")
|
||||
}
|
||||
}
|
55
internal/outpost/ldap/bind/request.go
Normal file
55
internal/outpost/ldap/bind/request.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package bind
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/utils"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
BindDN string
|
||||
BindPW string
|
||||
id string
|
||||
conn net.Conn
|
||||
log *log.Entry
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func NewRequest(bindDN string, bindPW string, conn net.Conn) (*Request, *sentry.Span) {
|
||||
span := sentry.StartSpan(context.TODO(), "authentik.providers.ldap.bind",
|
||||
sentry.TransactionName("authentik.providers.ldap.bind"))
|
||||
rid := uuid.New().String()
|
||||
span.SetTag("request_uid", rid)
|
||||
span.SetTag("user.username", bindDN)
|
||||
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
return &Request{
|
||||
BindDN: bindDN,
|
||||
BindPW: bindPW,
|
||||
conn: conn,
|
||||
log: log.WithField("bindDN", bindDN).WithField("requestId", rid).WithField("client", utils.GetIP(conn.RemoteAddr())),
|
||||
id: rid,
|
||||
ctx: span.Context(),
|
||||
}, span
|
||||
}
|
||||
|
||||
func (r *Request) Context() context.Context {
|
||||
return r.ctx
|
||||
}
|
||||
|
||||
func (r *Request) Log() *log.Entry {
|
||||
return r.log
|
||||
}
|
||||
|
||||
func (r *Request) RemoteAddr() string {
|
||||
return utils.GetIP(r.conn.RemoteAddr())
|
||||
}
|
||||
|
||||
func (r *Request) ID() string {
|
||||
return r.id
|
||||
}
|
21
internal/outpost/ldap/constants/constants.go
Normal file
21
internal/outpost/ldap/constants/constants.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package constants
|
||||
|
||||
const (
|
||||
OCGroup = "group"
|
||||
OCGroupOfUniqueNames = "groupOfUniqueNames"
|
||||
OCAKGroup = "goauthentik.io/ldap/group"
|
||||
OCAKVirtualGroup = "goauthentik.io/ldap/virtual-group"
|
||||
)
|
||||
|
||||
const (
|
||||
OCUser = "user"
|
||||
OCOrgPerson = "organizationalPerson"
|
||||
OCInetOrgPerson = "inetOrgPerson"
|
||||
OCAKUser = "goauthentik.io/ldap/user"
|
||||
)
|
||||
|
||||
const (
|
||||
OUUsers = "users"
|
||||
OUGroups = "groups"
|
||||
OUVirtualGroups = "virtual-groups"
|
||||
)
|
33
internal/outpost/ldap/entries.go
Normal file
33
internal/outpost/ldap/entries.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package ldap
|
||||
|
||||
import (
|
||||
"github.com/nmcclain/ldap"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost/ldap/constants"
|
||||
"goauthentik.io/internal/outpost/ldap/utils"
|
||||
)
|
||||
|
||||
func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
|
||||
dn := pi.GetUserDN(u.Username)
|
||||
attrs := utils.AKAttrsToLDAP(u.Attributes)
|
||||
|
||||
attrs = utils.EnsureAttributes(attrs, map[string][]string{
|
||||
"memberOf": pi.GroupsForUser(u),
|
||||
// Old fields for backwards compatibility
|
||||
"accountStatus": {utils.BoolToString(*u.IsActive)},
|
||||
"superuser": {utils.BoolToString(u.IsSuperuser)},
|
||||
// End old fields
|
||||
"goauthentik.io/ldap/active": {utils.BoolToString(*u.IsActive)},
|
||||
"goauthentik.io/ldap/superuser": {utils.BoolToString(u.IsSuperuser)},
|
||||
"cn": {u.Username},
|
||||
"sAMAccountName": {u.Username},
|
||||
"uid": {u.Uid},
|
||||
"name": {u.Name},
|
||||
"displayName": {u.Name},
|
||||
"mail": {*u.Email},
|
||||
"objectClass": {constants.OCUser, constants.OCOrgPerson, constants.OCInetOrgPerson, constants.OCAKUser},
|
||||
"uidNumber": {pi.GetUidNumber(u)},
|
||||
"gidNumber": {pi.GetUidNumber(u)},
|
||||
})
|
||||
return &ldap.Entry{DN: dn, Attributes: attrs}
|
||||
}
|
9
internal/outpost/ldap/flags/flags.go
Normal file
9
internal/outpost/ldap/flags/flags.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package flags
|
||||
|
||||
import "goauthentik.io/api"
|
||||
|
||||
type UserFlags struct {
|
||||
UserInfo *api.User
|
||||
UserPk int32
|
||||
CanSearch bool
|
||||
}
|
66
internal/outpost/ldap/group/group.go
Normal file
66
internal/outpost/ldap/group/group.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package group
|
||||
|
||||
import (
|
||||
"github.com/nmcclain/ldap"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost/ldap/constants"
|
||||
"goauthentik.io/internal/outpost/ldap/server"
|
||||
"goauthentik.io/internal/outpost/ldap/utils"
|
||||
)
|
||||
|
||||
type LDAPGroup struct {
|
||||
DN string
|
||||
CN string
|
||||
Uid string
|
||||
GidNumber string
|
||||
Member []string
|
||||
IsSuperuser bool
|
||||
IsVirtualGroup bool
|
||||
AKAttributes interface{}
|
||||
}
|
||||
|
||||
func (lg *LDAPGroup) Entry() *ldap.Entry {
|
||||
attrs := utils.AKAttrsToLDAP(lg.AKAttributes)
|
||||
|
||||
objectClass := []string{constants.OCGroup, constants.OCGroupOfUniqueNames, constants.OCAKGroup}
|
||||
if lg.IsVirtualGroup {
|
||||
objectClass = append(objectClass, constants.OCAKVirtualGroup)
|
||||
}
|
||||
|
||||
attrs = utils.EnsureAttributes(attrs, map[string][]string{
|
||||
"objectClass": objectClass,
|
||||
"member": lg.Member,
|
||||
"goauthentik.io/ldap/superuser": {utils.BoolToString(lg.IsSuperuser)},
|
||||
"cn": {lg.CN},
|
||||
"uid": {lg.Uid},
|
||||
"sAMAccountName": {lg.CN},
|
||||
"gidNumber": {lg.GidNumber},
|
||||
})
|
||||
return &ldap.Entry{DN: lg.DN, Attributes: attrs}
|
||||
}
|
||||
|
||||
func FromAPIGroup(g api.Group, si server.LDAPServerInstance) *LDAPGroup {
|
||||
return &LDAPGroup{
|
||||
DN: si.GetGroupDN(g.Name),
|
||||
CN: g.Name,
|
||||
Uid: string(g.Pk),
|
||||
GidNumber: si.GetGidNumber(g),
|
||||
Member: si.UsersForGroup(g),
|
||||
IsVirtualGroup: false,
|
||||
IsSuperuser: *g.IsSuperuser,
|
||||
AKAttributes: g.Attributes,
|
||||
}
|
||||
}
|
||||
|
||||
func FromAPIUser(u api.User, si server.LDAPServerInstance) *LDAPGroup {
|
||||
return &LDAPGroup{
|
||||
DN: si.GetVirtualGroupDN(u.Username),
|
||||
CN: u.Username,
|
||||
Uid: u.Uid,
|
||||
GidNumber: si.GetUidNumber(u),
|
||||
Member: []string{si.GetUserDN(u.Username)},
|
||||
IsVirtualGroup: true,
|
||||
IsSuperuser: false,
|
||||
AKAttributes: nil,
|
||||
}
|
||||
}
|
4
internal/outpost/ldap/handler/handler.go
Normal file
4
internal/outpost/ldap/handler/handler.go
Normal file
|
@ -0,0 +1,4 @@
|
|||
package handler
|
||||
|
||||
type Handler interface {
|
||||
}
|
83
internal/outpost/ldap/instance.go
Normal file
83
internal/outpost/ldap/instance.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
package ldap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"sync"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost/ldap/bind"
|
||||
"goauthentik.io/internal/outpost/ldap/flags"
|
||||
"goauthentik.io/internal/outpost/ldap/search"
|
||||
)
|
||||
|
||||
type ProviderInstance struct {
|
||||
BaseDN string
|
||||
UserDN string
|
||||
VirtualGroupDN string
|
||||
GroupDN string
|
||||
|
||||
searcher search.Searcher
|
||||
binder bind.Binder
|
||||
|
||||
appSlug string
|
||||
flowSlug string
|
||||
s *LDAPServer
|
||||
log *log.Entry
|
||||
|
||||
tlsServerName *string
|
||||
cert *tls.Certificate
|
||||
outpostName string
|
||||
searchAllowedGroups []*strfmt.UUID
|
||||
boundUsersMutex sync.RWMutex
|
||||
boundUsers map[string]flags.UserFlags
|
||||
|
||||
uidStartNumber int32
|
||||
gidStartNumber int32
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetAPIClient() *api.APIClient {
|
||||
return pi.s.ac.Client
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetBaseDN() string {
|
||||
return pi.BaseDN
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetBaseGroupDN() string {
|
||||
return pi.GroupDN
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetBaseUserDN() string {
|
||||
return pi.UserDN
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetOutpostName() string {
|
||||
return pi.outpostName
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetFlags(dn string) (flags.UserFlags, bool) {
|
||||
pi.boundUsersMutex.RLock()
|
||||
flags, ok := pi.boundUsers[dn]
|
||||
pi.boundUsersMutex.RUnlock()
|
||||
return flags, ok
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) SetFlags(dn string, flag flags.UserFlags) {
|
||||
pi.boundUsersMutex.Lock()
|
||||
pi.boundUsers[dn] = flag
|
||||
pi.boundUsersMutex.Unlock()
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetAppSlug() string {
|
||||
return pi.appSlug
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetFlowSlug() string {
|
||||
return pi.flowSlug
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetSearchAllowedGroups() []*strfmt.UUID {
|
||||
return pi.searchAllowedGroups
|
||||
}
|
|
@ -1,244 +0,0 @@
|
|||
package ldap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/nmcclain/ldap"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost/ldap/metrics"
|
||||
"goauthentik.io/internal/utils"
|
||||
)
|
||||
|
||||
func (pi *ProviderInstance) SearchMe(req SearchRequest, f UserFlags) (ldap.ServerSearchResult, error) {
|
||||
if f.UserInfo == nil {
|
||||
u, _, err := pi.s.ac.Client.CoreApi.CoreUsersRetrieve(req.ctx, f.UserPk).Execute()
|
||||
if err != nil {
|
||||
req.log.WithError(err).Warning("Failed to get user info")
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("failed to get userinfo")
|
||||
}
|
||||
f.UserInfo = &u
|
||||
}
|
||||
entries := make([]*ldap.Entry, 1)
|
||||
entries[0] = pi.UserEntry(*f.UserInfo)
|
||||
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) Search(req SearchRequest) (ldap.ServerSearchResult, error) {
|
||||
accsp := sentry.StartSpan(req.ctx, "authentik.providers.ldap.search.check_access")
|
||||
baseDN := strings.ToLower("," + pi.BaseDN)
|
||||
|
||||
entries := []*ldap.Entry{}
|
||||
filterEntity, err := ldap.GetFilterObjectClass(req.Filter)
|
||||
if err != nil {
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": pi.outpostName,
|
||||
"type": "search",
|
||||
"reason": "filter_parse_fail",
|
||||
"dn": req.BindDN,
|
||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
||||
}).Inc()
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", req.Filter)
|
||||
}
|
||||
if len(req.BindDN) < 1 {
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": pi.outpostName,
|
||||
"type": "search",
|
||||
"reason": "empty_bind_dn",
|
||||
"dn": req.BindDN,
|
||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
||||
}).Inc()
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", req.BindDN)
|
||||
}
|
||||
if !strings.HasSuffix(req.BindDN, baseDN) {
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": pi.outpostName,
|
||||
"type": "search",
|
||||
"reason": "invalid_bind_dn",
|
||||
"dn": req.BindDN,
|
||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
||||
}).Inc()
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: BindDN %s not in our BaseDN %s", req.BindDN, pi.BaseDN)
|
||||
}
|
||||
|
||||
pi.boundUsersMutex.RLock()
|
||||
flags, ok := pi.boundUsers[req.BindDN]
|
||||
pi.boundUsersMutex.RUnlock()
|
||||
if !ok {
|
||||
pi.log.Debug("User info not cached")
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": pi.outpostName,
|
||||
"type": "search",
|
||||
"reason": "user_info_not_cached",
|
||||
"dn": req.BindDN,
|
||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
||||
}).Inc()
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied")
|
||||
}
|
||||
|
||||
if req.SearchRequest.Scope == ldap.ScopeBaseObject {
|
||||
pi.log.Debug("base scope, showing domain info")
|
||||
return pi.SearchBase(req, flags.CanSearch)
|
||||
}
|
||||
if !flags.CanSearch {
|
||||
pi.log.Debug("User can't search, showing info about user")
|
||||
return pi.SearchMe(req, flags)
|
||||
}
|
||||
accsp.Finish()
|
||||
|
||||
parsedFilter, err := ldap.CompileFilter(req.Filter)
|
||||
if err != nil {
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": pi.outpostName,
|
||||
"type": "search",
|
||||
"reason": "filter_parse_fail",
|
||||
"dn": req.BindDN,
|
||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
||||
}).Inc()
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", req.Filter)
|
||||
}
|
||||
|
||||
// Create a custom client to set additional headers
|
||||
c := api.NewAPIClient(pi.s.ac.Client.GetConfig())
|
||||
c.GetConfig().AddDefaultHeader("X-authentik-outpost-ldap-query", req.Filter)
|
||||
|
||||
switch filterEntity {
|
||||
default:
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": pi.outpostName,
|
||||
"type": "search",
|
||||
"reason": "unhandled_filter_type",
|
||||
"dn": req.BindDN,
|
||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
||||
}).Inc()
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: unhandled filter type: %s [%s]", filterEntity, req.Filter)
|
||||
case "groupOfUniqueNames":
|
||||
fallthrough
|
||||
case "goauthentik.io/ldap/group":
|
||||
fallthrough
|
||||
case "goauthentik.io/ldap/virtual-group":
|
||||
fallthrough
|
||||
case GroupObjectClass:
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
gEntries := make([]*ldap.Entry, 0)
|
||||
uEntries := make([]*ldap.Entry, 0)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
gapisp := sentry.StartSpan(req.ctx, "authentik.providers.ldap.search.api_group")
|
||||
searchReq, skip := parseFilterForGroup(c.CoreApi.CoreGroupsList(gapisp.Context()), parsedFilter, false)
|
||||
if skip {
|
||||
pi.log.Trace("Skip backend request")
|
||||
return
|
||||
}
|
||||
groups, _, err := searchReq.Execute()
|
||||
gapisp.Finish()
|
||||
if err != nil {
|
||||
req.log.WithError(err).Warning("failed to get groups")
|
||||
return
|
||||
}
|
||||
pi.log.WithField("count", len(groups.Results)).Trace("Got results from API")
|
||||
|
||||
for _, g := range groups.Results {
|
||||
gEntries = append(gEntries, pi.GroupEntry(pi.APIGroupToLDAPGroup(g)))
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
uapisp := sentry.StartSpan(req.ctx, "authentik.providers.ldap.search.api_user")
|
||||
searchReq, skip := parseFilterForUser(c.CoreApi.CoreUsersList(uapisp.Context()), parsedFilter, false)
|
||||
if skip {
|
||||
pi.log.Trace("Skip backend request")
|
||||
return
|
||||
}
|
||||
users, _, err := searchReq.Execute()
|
||||
uapisp.Finish()
|
||||
if err != nil {
|
||||
req.log.WithError(err).Warning("failed to get users")
|
||||
return
|
||||
}
|
||||
|
||||
for _, u := range users.Results {
|
||||
uEntries = append(uEntries, pi.GroupEntry(pi.APIUserToLDAPGroup(u)))
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
entries = append(gEntries, uEntries...)
|
||||
case "":
|
||||
fallthrough
|
||||
case "organizationalPerson":
|
||||
fallthrough
|
||||
case "inetOrgPerson":
|
||||
fallthrough
|
||||
case "goauthentik.io/ldap/user":
|
||||
fallthrough
|
||||
case UserObjectClass:
|
||||
uapisp := sentry.StartSpan(req.ctx, "authentik.providers.ldap.search.api_user")
|
||||
searchReq, skip := parseFilterForUser(c.CoreApi.CoreUsersList(uapisp.Context()), parsedFilter, false)
|
||||
if skip {
|
||||
pi.log.Trace("Skip backend request")
|
||||
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
|
||||
}
|
||||
users, _, err := searchReq.Execute()
|
||||
uapisp.Finish()
|
||||
|
||||
if err != nil {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err)
|
||||
}
|
||||
for _, u := range users.Results {
|
||||
entries = append(entries, pi.UserEntry(u))
|
||||
}
|
||||
}
|
||||
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
|
||||
dn := pi.GetUserDN(u.Username)
|
||||
attrs := AKAttrsToLDAP(u.Attributes)
|
||||
|
||||
attrs = pi.ensureAttributes(attrs, map[string][]string{
|
||||
"memberOf": pi.GroupsForUser(u),
|
||||
// Old fields for backwards compatibility
|
||||
"accountStatus": {BoolToString(*u.IsActive)},
|
||||
"superuser": {BoolToString(u.IsSuperuser)},
|
||||
"goauthentik.io/ldap/active": {BoolToString(*u.IsActive)},
|
||||
"goauthentik.io/ldap/superuser": {BoolToString(u.IsSuperuser)},
|
||||
"cn": {u.Username},
|
||||
"sAMAccountName": {u.Username},
|
||||
"uid": {u.Uid},
|
||||
"name": {u.Name},
|
||||
"displayName": {u.Name},
|
||||
"mail": {*u.Email},
|
||||
"objectClass": {UserObjectClass, "organizationalPerson", "inetOrgPerson", "goauthentik.io/ldap/user"},
|
||||
"uidNumber": {pi.GetUidNumber(u)},
|
||||
"gidNumber": {pi.GetUidNumber(u)},
|
||||
})
|
||||
return &ldap.Entry{DN: dn, Attributes: attrs}
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GroupEntry(g LDAPGroup) *ldap.Entry {
|
||||
attrs := AKAttrsToLDAP(g.akAttributes)
|
||||
|
||||
objectClass := []string{GroupObjectClass, "groupOfUniqueNames", "goauthentik.io/ldap/group"}
|
||||
if g.isVirtualGroup {
|
||||
objectClass = append(objectClass, "goauthentik.io/ldap/virtual-group")
|
||||
}
|
||||
|
||||
attrs = pi.ensureAttributes(attrs, map[string][]string{
|
||||
"objectClass": objectClass,
|
||||
"member": g.member,
|
||||
"goauthentik.io/ldap/superuser": {BoolToString(g.isSuperuser)},
|
||||
"cn": {g.cn},
|
||||
"uid": {g.uid},
|
||||
"sAMAccountName": {g.cn},
|
||||
"gidNumber": {g.gidNumber},
|
||||
})
|
||||
return &ldap.Entry{DN: g.dn, Attributes: attrs}
|
||||
}
|
|
@ -2,50 +2,18 @@ package ldap
|
|||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/pires/go-proxyproto"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/crypto"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
"goauthentik.io/internal/outpost/ldap/metrics"
|
||||
|
||||
"github.com/nmcclain/ldap"
|
||||
)
|
||||
|
||||
const GroupObjectClass = "group"
|
||||
const UserObjectClass = "user"
|
||||
|
||||
type ProviderInstance struct {
|
||||
BaseDN string
|
||||
|
||||
UserDN string
|
||||
|
||||
VirtualGroupDN string
|
||||
GroupDN string
|
||||
|
||||
appSlug string
|
||||
flowSlug string
|
||||
s *LDAPServer
|
||||
log *log.Entry
|
||||
|
||||
tlsServerName *string
|
||||
cert *tls.Certificate
|
||||
outpostName string
|
||||
searchAllowedGroups []*strfmt.UUID
|
||||
boundUsersMutex sync.RWMutex
|
||||
boundUsers map[string]UserFlags
|
||||
|
||||
uidStartNumber int32
|
||||
gidStartNumber int32
|
||||
}
|
||||
|
||||
type UserFlags struct {
|
||||
UserInfo *api.User
|
||||
UserPk int32
|
||||
CanSearch bool
|
||||
}
|
||||
|
||||
type LDAPServer struct {
|
||||
s *ldap.Server
|
||||
log *log.Entry
|
||||
|
@ -55,17 +23,6 @@ type LDAPServer struct {
|
|||
providers []*ProviderInstance
|
||||
}
|
||||
|
||||
type LDAPGroup struct {
|
||||
dn string
|
||||
cn string
|
||||
uid string
|
||||
gidNumber string
|
||||
member []string
|
||||
isSuperuser bool
|
||||
isVirtualGroup bool
|
||||
akAttributes interface{}
|
||||
}
|
||||
|
||||
func NewServer(ac *ak.APIController) *LDAPServer {
|
||||
s := ldap.NewServer()
|
||||
s.EnforceLDAP = true
|
||||
|
@ -90,3 +47,54 @@ func NewServer(ac *ak.APIController) *LDAPServer {
|
|||
func (ls *LDAPServer) Type() string {
|
||||
return "ldap"
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) StartLDAPServer() error {
|
||||
listen := "0.0.0.0:3389"
|
||||
|
||||
ln, err := net.Listen("tcp", listen)
|
||||
if err != nil {
|
||||
ls.log.WithField("listen", listen).WithError(err).Fatalf("FATAL: listen failed")
|
||||
}
|
||||
proxyListener := &proxyproto.Listener{Listener: ln}
|
||||
defer proxyListener.Close()
|
||||
|
||||
ls.log.WithField("listen", listen).Info("Starting ldap server")
|
||||
err = ls.s.Serve(proxyListener)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ls.log.Printf("closing %s", ln.Addr())
|
||||
return ls.s.ListenAndServe(listen)
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) Start() error {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
metrics.RunServer()
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := ls.StartLDAPServer()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := ls.StartLDAPTLSServer()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) TimerFlowCacheExpiry() {
|
||||
for _, p := range ls.providers {
|
||||
ls.log.WithField("flow", p.flowSlug).Debug("Pre-heating flow cache")
|
||||
p.binder.TimerFlowCacheExpiry()
|
||||
}
|
||||
}
|
||||
|
|
55
internal/outpost/ldap/ldap_tls.go
Normal file
55
internal/outpost/ldap/ldap_tls.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package ldap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
|
||||
"github.com/pires/go-proxyproto"
|
||||
)
|
||||
|
||||
func (ls *LDAPServer) getCertificates(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if len(ls.providers) == 1 {
|
||||
if ls.providers[0].cert != nil {
|
||||
ls.log.WithField("server-name", info.ServerName).Debug("We only have a single provider, using their cert")
|
||||
return ls.providers[0].cert, nil
|
||||
}
|
||||
}
|
||||
for _, provider := range ls.providers {
|
||||
if provider.tlsServerName == &info.ServerName {
|
||||
if provider.cert == nil {
|
||||
ls.log.WithField("server-name", info.ServerName).Debug("Handler does not have a certificate")
|
||||
return ls.defaultCert, nil
|
||||
}
|
||||
return provider.cert, nil
|
||||
}
|
||||
}
|
||||
ls.log.WithField("server-name", info.ServerName).Debug("Fallback to default cert")
|
||||
return ls.defaultCert, nil
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) StartLDAPTLSServer() error {
|
||||
listen := "0.0.0.0:6636"
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
MaxVersion: tls.VersionTLS12,
|
||||
GetCertificate: ls.getCertificates,
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", listen)
|
||||
if err != nil {
|
||||
ls.log.WithField("listen", listen).WithError(err).Fatalf("FATAL: listen failed")
|
||||
}
|
||||
|
||||
proxyListener := &proxyproto.Listener{Listener: ln}
|
||||
defer proxyListener.Close()
|
||||
|
||||
tln := tls.NewListener(proxyListener, tlsConfig)
|
||||
|
||||
ls.log.WithField("listen", listen).Info("Starting ldap tls server")
|
||||
err = ls.s.Serve(tln)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ls.log.Printf("closing %s", ln.Addr())
|
||||
return ls.s.ListenAndServe(listen)
|
||||
}
|
|
@ -2,23 +2,19 @@ package ldap
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/pires/go-proxyproto"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/ldap/metrics"
|
||||
)
|
||||
|
||||
const (
|
||||
UsersOU = "users"
|
||||
GroupsOU = "groups"
|
||||
VirtualGroupsOU = "virtual-groups"
|
||||
"goauthentik.io/api"
|
||||
directbind "goauthentik.io/internal/outpost/ldap/bind/direct"
|
||||
"goauthentik.io/internal/outpost/ldap/constants"
|
||||
"goauthentik.io/internal/outpost/ldap/flags"
|
||||
directsearch "goauthentik.io/internal/outpost/ldap/search/direct"
|
||||
memorysearch "goauthentik.io/internal/outpost/ldap/search/memory"
|
||||
)
|
||||
|
||||
func (ls *LDAPServer) Refresh() error {
|
||||
|
@ -31,9 +27,9 @@ func (ls *LDAPServer) Refresh() error {
|
|||
}
|
||||
providers := make([]*ProviderInstance, len(outposts.Results))
|
||||
for idx, provider := range outposts.Results {
|
||||
userDN := strings.ToLower(fmt.Sprintf("ou=%s,%s", UsersOU, *provider.BaseDn))
|
||||
groupDN := strings.ToLower(fmt.Sprintf("ou=%s,%s", GroupsOU, *provider.BaseDn))
|
||||
virtualGroupDN := strings.ToLower(fmt.Sprintf("ou=%s,%s", VirtualGroupsOU, *provider.BaseDn))
|
||||
userDN := strings.ToLower(fmt.Sprintf("ou=%s,%s", constants.OUUsers, *provider.BaseDn))
|
||||
groupDN := strings.ToLower(fmt.Sprintf("ou=%s,%s", constants.OUGroups, *provider.BaseDn))
|
||||
virtualGroupDN := strings.ToLower(fmt.Sprintf("ou=%s,%s", constants.OUVirtualGroups, *provider.BaseDn))
|
||||
logger := log.WithField("logger", "authentik.outpost.ldap").WithField("provider", provider.Name)
|
||||
providers[idx] = &ProviderInstance{
|
||||
BaseDN: *provider.BaseDn,
|
||||
|
@ -44,7 +40,7 @@ func (ls *LDAPServer) Refresh() error {
|
|||
flowSlug: provider.BindFlowSlug,
|
||||
searchAllowedGroups: []*strfmt.UUID{(*strfmt.UUID)(provider.SearchGroup.Get())},
|
||||
boundUsersMutex: sync.RWMutex{},
|
||||
boundUsers: make(map[string]UserFlags),
|
||||
boundUsers: make(map[string]flags.UserFlags),
|
||||
s: ls,
|
||||
log: logger,
|
||||
tlsServerName: provider.TlsServerName,
|
||||
|
@ -60,79 +56,14 @@ func (ls *LDAPServer) Refresh() error {
|
|||
}
|
||||
providers[idx].cert = ls.cs.Get(*kp)
|
||||
}
|
||||
if *provider.SearchMode.Ptr() == api.SEARCHMODEENUM_CACHED {
|
||||
providers[idx].searcher = memorysearch.NewMemorySearcher(providers[idx])
|
||||
} else if *provider.SearchMode.Ptr() == api.SEARCHMODEENUM_DIRECT {
|
||||
providers[idx].searcher = directsearch.NewDirectSearcher(providers[idx])
|
||||
}
|
||||
providers[idx].binder = directbind.NewDirectBinder(providers[idx])
|
||||
}
|
||||
ls.providers = providers
|
||||
ls.log.Info("Update providers")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) StartLDAPServer() error {
|
||||
listen := "0.0.0.0:3389"
|
||||
|
||||
ln, err := net.Listen("tcp", listen)
|
||||
if err != nil {
|
||||
ls.log.Fatalf("FATAL: listen (%s) failed - %s", listen, err)
|
||||
}
|
||||
proxyListener := &proxyproto.Listener{Listener: ln}
|
||||
defer proxyListener.Close()
|
||||
|
||||
ls.log.WithField("listen", listen).Info("Starting ldap server")
|
||||
err = ls.s.Serve(proxyListener)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ls.log.Printf("closing %s", ln.Addr())
|
||||
return ls.s.ListenAndServe(listen)
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) StartLDAPTLSServer() error {
|
||||
listen := "0.0.0.0:6636"
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
MaxVersion: tls.VersionTLS12,
|
||||
GetCertificate: ls.getCertificates,
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", listen)
|
||||
if err != nil {
|
||||
ls.log.Fatalf("FATAL: listen (%s) failed - %s", listen, err)
|
||||
}
|
||||
|
||||
proxyListener := &proxyproto.Listener{Listener: ln}
|
||||
defer proxyListener.Close()
|
||||
|
||||
tln := tls.NewListener(proxyListener, tlsConfig)
|
||||
|
||||
ls.log.WithField("listen", listen).Info("Starting ldap tls server")
|
||||
err = ls.s.Serve(tln)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ls.log.Printf("closing %s", ln.Addr())
|
||||
return ls.s.ListenAndServe(listen)
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) Start() error {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
metrics.RunServer()
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := ls.StartLDAPServer()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := ls.StartLDAPTLSServer()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,47 +1,21 @@
|
|||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
goldap "github.com/go-ldap/ldap/v3"
|
||||
"github.com/google/uuid"
|
||||
"github.com/nmcclain/ldap"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/ldap/metrics"
|
||||
"goauthentik.io/internal/outpost/ldap/search"
|
||||
"goauthentik.io/internal/utils"
|
||||
)
|
||||
|
||||
type SearchRequest struct {
|
||||
ldap.SearchRequest
|
||||
BindDN string
|
||||
id string
|
||||
conn net.Conn
|
||||
log *log.Entry
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) {
|
||||
span := sentry.StartSpan(context.TODO(), "authentik.providers.ldap.search", sentry.TransactionName("authentik.providers.ldap.search"))
|
||||
rid := uuid.New().String()
|
||||
span.SetTag("request_uid", rid)
|
||||
span.SetTag("user.username", bindDN)
|
||||
span.SetTag("ak_filter", searchReq.Filter)
|
||||
span.SetTag("ak_base_dn", searchReq.BaseDN)
|
||||
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
req := SearchRequest{
|
||||
SearchRequest: searchReq,
|
||||
BindDN: bindDN,
|
||||
conn: conn,
|
||||
log: ls.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),
|
||||
id: rid,
|
||||
ctx: span.Context(),
|
||||
}
|
||||
req, span := search.NewRequest(bindDN, searchReq, conn)
|
||||
|
||||
defer func() {
|
||||
span.Finish()
|
||||
|
@ -50,9 +24,9 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n
|
|||
"type": "search",
|
||||
"filter": req.Filter,
|
||||
"dn": req.BindDN,
|
||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
||||
"client": utils.GetIP(conn.RemoteAddr()),
|
||||
}).Observe(float64(span.EndTime.Sub(span.StartTime)))
|
||||
req.log.WithField("took-ms", span.EndTime.Sub(span.StartTime).Milliseconds()).Info("Search request")
|
||||
req.Log().WithField("took-ms", span.EndTime.Sub(span.StartTime).Milliseconds()).Info("Search request")
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
|
@ -69,13 +43,13 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n
|
|||
}
|
||||
bd, err := goldap.ParseDN(searchReq.BaseDN)
|
||||
if err != nil {
|
||||
req.log.WithError(err).Info("failed to parse basedn")
|
||||
req.Log().WithError(err).Info("failed to parse basedn")
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("invalid DN")
|
||||
}
|
||||
for _, provider := range ls.providers {
|
||||
providerBase, _ := goldap.ParseDN(provider.BaseDN)
|
||||
if providerBase.AncestorOf(bd) || providerBase.Equal(bd) {
|
||||
return provider.Search(req)
|
||||
return provider.searcher.Search(req)
|
||||
}
|
||||
}
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("no provider could handle request")
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
package ldap
|
||||
package direct
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/nmcclain/ldap"
|
||||
"goauthentik.io/internal/constants"
|
||||
"goauthentik.io/internal/outpost/ldap/search"
|
||||
)
|
||||
|
||||
func (pi *ProviderInstance) SearchBase(req SearchRequest, authz bool) (ldap.ServerSearchResult, error) {
|
||||
func (ds *DirectSearcher) SearchBase(req *search.Request, authz bool) (ldap.ServerSearchResult, error) {
|
||||
dn := ""
|
||||
if authz {
|
||||
dn = req.SearchRequest.BaseDN
|
||||
|
@ -19,7 +20,7 @@ func (pi *ProviderInstance) SearchBase(req SearchRequest, authz bool) (ldap.Serv
|
|||
Attributes: []*ldap.EntryAttribute{
|
||||
{
|
||||
Name: "distinguishedName",
|
||||
Values: []string{pi.BaseDN},
|
||||
Values: []string{ds.si.GetBaseDN()},
|
||||
},
|
||||
{
|
||||
Name: "objectClass",
|
||||
|
@ -32,9 +33,9 @@ func (pi *ProviderInstance) SearchBase(req SearchRequest, authz bool) (ldap.Serv
|
|||
{
|
||||
Name: "namingContexts",
|
||||
Values: []string{
|
||||
pi.BaseDN,
|
||||
pi.GroupDN,
|
||||
pi.UserDN,
|
||||
ds.si.GetBaseDN(),
|
||||
ds.si.GetBaseUserDN(),
|
||||
ds.si.GetBaseGroupDN(),
|
||||
},
|
||||
},
|
||||
{
|
219
internal/outpost/ldap/search/direct/direct.go
Normal file
219
internal/outpost/ldap/search/direct/direct.go
Normal file
|
@ -0,0 +1,219 @@
|
|||
package direct
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/nmcclain/ldap"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost/ldap/constants"
|
||||
"goauthentik.io/internal/outpost/ldap/flags"
|
||||
"goauthentik.io/internal/outpost/ldap/group"
|
||||
"goauthentik.io/internal/outpost/ldap/metrics"
|
||||
"goauthentik.io/internal/outpost/ldap/search"
|
||||
"goauthentik.io/internal/outpost/ldap/server"
|
||||
"goauthentik.io/internal/outpost/ldap/utils"
|
||||
)
|
||||
|
||||
type DirectSearcher struct {
|
||||
si server.LDAPServerInstance
|
||||
log *log.Entry
|
||||
}
|
||||
|
||||
func NewDirectSearcher(si server.LDAPServerInstance) *DirectSearcher {
|
||||
ds := &DirectSearcher{
|
||||
si: si,
|
||||
log: log.WithField("logger", "authentik.outpost.ldap.searcher.direct"),
|
||||
}
|
||||
ds.log.Info("initialised direct searcher")
|
||||
return ds
|
||||
}
|
||||
|
||||
func (ds *DirectSearcher) SearchMe(req *search.Request, f flags.UserFlags) (ldap.ServerSearchResult, error) {
|
||||
if f.UserInfo == nil {
|
||||
u, _, err := ds.si.GetAPIClient().CoreApi.CoreUsersRetrieve(req.Context(), f.UserPk).Execute()
|
||||
if err != nil {
|
||||
req.Log().WithError(err).Warning("Failed to get user info")
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("failed to get userinfo")
|
||||
}
|
||||
f.UserInfo = &u
|
||||
}
|
||||
entries := make([]*ldap.Entry, 1)
|
||||
entries[0] = ds.si.UserEntry(*f.UserInfo)
|
||||
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
|
||||
}
|
||||
|
||||
func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult, error) {
|
||||
accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access")
|
||||
baseDN := strings.ToLower("," + ds.si.GetBaseDN())
|
||||
|
||||
entries := []*ldap.Entry{}
|
||||
filterEntity, err := ldap.GetFilterObjectClass(req.Filter)
|
||||
if err != nil {
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": ds.si.GetOutpostName(),
|
||||
"type": "search",
|
||||
"reason": "filter_parse_fail",
|
||||
"dn": req.BindDN,
|
||||
"client": req.RemoteAddr(),
|
||||
}).Inc()
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", req.Filter)
|
||||
}
|
||||
if len(req.BindDN) < 1 {
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": ds.si.GetOutpostName(),
|
||||
"type": "search",
|
||||
"reason": "empty_bind_dn",
|
||||
"dn": req.BindDN,
|
||||
"client": req.RemoteAddr(),
|
||||
}).Inc()
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", req.BindDN)
|
||||
}
|
||||
if !strings.HasSuffix(req.BindDN, baseDN) {
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": ds.si.GetOutpostName(),
|
||||
"type": "search",
|
||||
"reason": "invalid_bind_dn",
|
||||
"dn": req.BindDN,
|
||||
"client": req.RemoteAddr(),
|
||||
}).Inc()
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: BindDN %s not in our BaseDN %s", req.BindDN, ds.si.GetBaseDN())
|
||||
}
|
||||
|
||||
flags, ok := ds.si.GetFlags(req.BindDN)
|
||||
if !ok {
|
||||
req.Log().Debug("User info not cached")
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": ds.si.GetOutpostName(),
|
||||
"type": "search",
|
||||
"reason": "user_info_not_cached",
|
||||
"dn": req.BindDN,
|
||||
"client": req.RemoteAddr(),
|
||||
}).Inc()
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied")
|
||||
}
|
||||
|
||||
if req.Scope == ldap.ScopeBaseObject {
|
||||
req.Log().Debug("base scope, showing domain info")
|
||||
return ds.SearchBase(req, flags.CanSearch)
|
||||
}
|
||||
if !flags.CanSearch {
|
||||
req.Log().Debug("User can't search, showing info about user")
|
||||
return ds.SearchMe(req, flags)
|
||||
}
|
||||
accsp.Finish()
|
||||
|
||||
parsedFilter, err := ldap.CompileFilter(req.Filter)
|
||||
if err != nil {
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": ds.si.GetOutpostName(),
|
||||
"type": "search",
|
||||
"reason": "filter_parse_fail",
|
||||
"dn": req.BindDN,
|
||||
"client": req.RemoteAddr(),
|
||||
}).Inc()
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", req.Filter)
|
||||
}
|
||||
|
||||
// Create a custom client to set additional headers
|
||||
c := api.NewAPIClient(ds.si.GetAPIClient().GetConfig())
|
||||
c.GetConfig().AddDefaultHeader("X-authentik-outpost-ldap-query", req.Filter)
|
||||
|
||||
switch filterEntity {
|
||||
default:
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": ds.si.GetOutpostName(),
|
||||
"type": "search",
|
||||
"reason": "unhandled_filter_type",
|
||||
"dn": req.BindDN,
|
||||
"client": req.RemoteAddr(),
|
||||
}).Inc()
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: unhandled filter type: %s [%s]", filterEntity, req.Filter)
|
||||
case constants.OCGroupOfUniqueNames:
|
||||
fallthrough
|
||||
case constants.OCAKGroup:
|
||||
fallthrough
|
||||
case constants.OCAKVirtualGroup:
|
||||
fallthrough
|
||||
case constants.OCGroup:
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
gEntries := make([]*ldap.Entry, 0)
|
||||
uEntries := make([]*ldap.Entry, 0)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
gapisp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.api_group")
|
||||
searchReq, skip := utils.ParseFilterForGroup(c.CoreApi.CoreGroupsList(gapisp.Context()), parsedFilter, false)
|
||||
if skip {
|
||||
req.Log().Trace("Skip backend request")
|
||||
return
|
||||
}
|
||||
groups, _, err := searchReq.Execute()
|
||||
gapisp.Finish()
|
||||
if err != nil {
|
||||
req.Log().WithError(err).Warning("failed to get groups")
|
||||
return
|
||||
}
|
||||
req.Log().WithField("count", len(groups.Results)).Trace("Got results from API")
|
||||
|
||||
for _, g := range groups.Results {
|
||||
gEntries = append(gEntries, group.FromAPIGroup(g, ds.si).Entry())
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
uapisp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.api_user")
|
||||
searchReq, skip := utils.ParseFilterForUser(c.CoreApi.CoreUsersList(uapisp.Context()), parsedFilter, false)
|
||||
if skip {
|
||||
req.Log().Trace("Skip backend request")
|
||||
return
|
||||
}
|
||||
users, _, err := searchReq.Execute()
|
||||
uapisp.Finish()
|
||||
if err != nil {
|
||||
req.Log().WithError(err).Warning("failed to get users")
|
||||
return
|
||||
}
|
||||
|
||||
for _, u := range users.Results {
|
||||
uEntries = append(uEntries, group.FromAPIUser(u, ds.si).Entry())
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
entries = append(gEntries, uEntries...)
|
||||
case "":
|
||||
fallthrough
|
||||
case constants.OCOrgPerson:
|
||||
fallthrough
|
||||
case constants.OCInetOrgPerson:
|
||||
fallthrough
|
||||
case constants.OCAKUser:
|
||||
fallthrough
|
||||
case constants.OCUser:
|
||||
uapisp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.api_user")
|
||||
searchReq, skip := utils.ParseFilterForUser(c.CoreApi.CoreUsersList(uapisp.Context()), parsedFilter, false)
|
||||
if skip {
|
||||
req.Log().Trace("Skip backend request")
|
||||
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
|
||||
}
|
||||
users, _, err := searchReq.Execute()
|
||||
uapisp.Finish()
|
||||
|
||||
if err != nil {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err)
|
||||
}
|
||||
for _, u := range users.Results {
|
||||
entries = append(entries, ds.si.UserEntry(u))
|
||||
}
|
||||
}
|
||||
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
|
||||
}
|
54
internal/outpost/ldap/search/memory/base.go
Normal file
54
internal/outpost/ldap/search/memory/base.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/nmcclain/ldap"
|
||||
"goauthentik.io/internal/constants"
|
||||
"goauthentik.io/internal/outpost/ldap/search"
|
||||
)
|
||||
|
||||
func (ms *MemorySearcher) SearchBase(req *search.Request, authz bool) (ldap.ServerSearchResult, error) {
|
||||
dn := ""
|
||||
if authz {
|
||||
dn = req.SearchRequest.BaseDN
|
||||
}
|
||||
return ldap.ServerSearchResult{
|
||||
Entries: []*ldap.Entry{
|
||||
{
|
||||
DN: dn,
|
||||
Attributes: []*ldap.EntryAttribute{
|
||||
{
|
||||
Name: "distinguishedName",
|
||||
Values: []string{ms.si.GetBaseDN()},
|
||||
},
|
||||
{
|
||||
Name: "objectClass",
|
||||
Values: []string{"top", "domain"},
|
||||
},
|
||||
{
|
||||
Name: "supportedLDAPVersion",
|
||||
Values: []string{"3"},
|
||||
},
|
||||
{
|
||||
Name: "namingContexts",
|
||||
Values: []string{
|
||||
ms.si.GetBaseDN(),
|
||||
ms.si.GetBaseUserDN(),
|
||||
ms.si.GetBaseGroupDN(),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "vendorName",
|
||||
Values: []string{"goauthentik.io"},
|
||||
},
|
||||
{
|
||||
Name: "vendorVersion",
|
||||
Values: []string{fmt.Sprintf("authentik LDAP Outpost Version %s (build %s)", constants.VERSION, constants.BUILD())},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess,
|
||||
}, nil
|
||||
}
|
63
internal/outpost/ldap/search/memory/fetch.go
Normal file
63
internal/outpost/ldap/search/memory/fetch.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"goauthentik.io/api"
|
||||
)
|
||||
|
||||
const pageSize = 100
|
||||
|
||||
func (ms *MemorySearcher) FetchUsers() []api.User {
|
||||
fetchUsersOffset := func(page int) (*api.PaginatedUserList, error) {
|
||||
users, _, err := ms.si.GetAPIClient().CoreApi.CoreUsersList(context.TODO()).Page(int32(page)).PageSize(pageSize).Execute()
|
||||
if err != nil {
|
||||
ms.log.WithError(err).Warning("failed to update users")
|
||||
return nil, err
|
||||
}
|
||||
ms.log.WithField("page", page).Debug("fetched users")
|
||||
return &users, nil
|
||||
}
|
||||
page := 1
|
||||
users := make([]api.User, 0)
|
||||
for {
|
||||
apiUsers, err := fetchUsersOffset(page)
|
||||
if err != nil {
|
||||
return users
|
||||
}
|
||||
if apiUsers.Pagination.Next > 0 {
|
||||
page += 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
users = append(users, apiUsers.Results...)
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
func (ms *MemorySearcher) FetchGroups() []api.Group {
|
||||
fetchGroupsOffset := func(page int) (*api.PaginatedGroupList, error) {
|
||||
groups, _, err := ms.si.GetAPIClient().CoreApi.CoreGroupsList(context.TODO()).Page(int32(page)).PageSize(pageSize).Execute()
|
||||
if err != nil {
|
||||
ms.log.WithError(err).Warning("failed to update groups")
|
||||
return nil, err
|
||||
}
|
||||
ms.log.WithField("page", page).Debug("fetched groups")
|
||||
return &groups, nil
|
||||
}
|
||||
page := 1
|
||||
groups := make([]api.Group, 0)
|
||||
for {
|
||||
apiGroups, err := fetchGroupsOffset(page)
|
||||
if err != nil {
|
||||
return groups
|
||||
}
|
||||
if apiGroups.Pagination.Next > 0 {
|
||||
page += 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
groups = append(groups, apiGroups.Results...)
|
||||
}
|
||||
return groups
|
||||
}
|
182
internal/outpost/ldap/search/memory/memory.go
Normal file
182
internal/outpost/ldap/search/memory/memory.go
Normal file
|
@ -0,0 +1,182 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/nmcclain/ldap"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost/ldap/constants"
|
||||
"goauthentik.io/internal/outpost/ldap/flags"
|
||||
"goauthentik.io/internal/outpost/ldap/group"
|
||||
"goauthentik.io/internal/outpost/ldap/metrics"
|
||||
"goauthentik.io/internal/outpost/ldap/search"
|
||||
"goauthentik.io/internal/outpost/ldap/server"
|
||||
)
|
||||
|
||||
type MemorySearcher struct {
|
||||
si server.LDAPServerInstance
|
||||
log *log.Entry
|
||||
|
||||
users []api.User
|
||||
groups []api.Group
|
||||
}
|
||||
|
||||
func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher {
|
||||
ms := &MemorySearcher{
|
||||
si: si,
|
||||
log: log.WithField("logger", "authentik.outpost.ldap.searcher.memory"),
|
||||
}
|
||||
ms.log.Info("initialised memory searcher")
|
||||
ms.users = ms.FetchUsers()
|
||||
ms.groups = ms.FetchGroups()
|
||||
return ms
|
||||
}
|
||||
|
||||
func (ms *MemorySearcher) SearchMe(req *search.Request, f flags.UserFlags) (ldap.ServerSearchResult, error) {
|
||||
if f.UserInfo == nil {
|
||||
for _, u := range ms.users {
|
||||
if u.Pk == f.UserPk {
|
||||
f.UserInfo = &u
|
||||
}
|
||||
}
|
||||
if f.UserInfo == nil {
|
||||
req.Log().WithField("pk", f.UserPk).Warning("User with pk is not in local cache")
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("failed to get userinfo")
|
||||
}
|
||||
}
|
||||
entries := make([]*ldap.Entry, 1)
|
||||
entries[0] = ms.si.UserEntry(*f.UserInfo)
|
||||
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
|
||||
}
|
||||
|
||||
func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, error) {
|
||||
accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access")
|
||||
baseDN := strings.ToLower("," + ms.si.GetBaseDN())
|
||||
|
||||
entries := []*ldap.Entry{}
|
||||
filterEntity, err := ldap.GetFilterObjectClass(req.Filter)
|
||||
if err != nil {
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": ms.si.GetOutpostName(),
|
||||
"type": "search",
|
||||
"reason": "filter_parse_fail",
|
||||
"dn": req.BindDN,
|
||||
"client": req.RemoteAddr(),
|
||||
}).Inc()
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", req.Filter)
|
||||
}
|
||||
if len(req.BindDN) < 1 {
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": ms.si.GetOutpostName(),
|
||||
"type": "search",
|
||||
"reason": "empty_bind_dn",
|
||||
"dn": req.BindDN,
|
||||
"client": req.RemoteAddr(),
|
||||
}).Inc()
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", req.BindDN)
|
||||
}
|
||||
if !strings.HasSuffix(req.BindDN, baseDN) {
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": ms.si.GetOutpostName(),
|
||||
"type": "search",
|
||||
"reason": "invalid_bind_dn",
|
||||
"dn": req.BindDN,
|
||||
"client": req.RemoteAddr(),
|
||||
}).Inc()
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: BindDN %s not in our BaseDN %s", req.BindDN, ms.si.GetBaseDN())
|
||||
}
|
||||
|
||||
flags, ok := ms.si.GetFlags(req.BindDN)
|
||||
if !ok {
|
||||
req.Log().Debug("User info not cached")
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": ms.si.GetOutpostName(),
|
||||
"type": "search",
|
||||
"reason": "user_info_not_cached",
|
||||
"dn": req.BindDN,
|
||||
"client": req.RemoteAddr(),
|
||||
}).Inc()
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied")
|
||||
}
|
||||
|
||||
if req.Scope == ldap.ScopeBaseObject {
|
||||
req.Log().Debug("base scope, showing domain info")
|
||||
return ms.SearchBase(req, flags.CanSearch)
|
||||
}
|
||||
if !flags.CanSearch {
|
||||
req.Log().Debug("User can't search, showing info about user")
|
||||
return ms.SearchMe(req, flags)
|
||||
}
|
||||
accsp.Finish()
|
||||
|
||||
// parsedFilter, err := ldap.CompileFilter(req.Filter)
|
||||
// if err != nil {
|
||||
// metrics.RequestsRejected.With(prometheus.Labels{
|
||||
// "outpost_name": ms.si.GetOutpostName(),
|
||||
// "type": "search",
|
||||
// "reason": "filter_parse_fail",
|
||||
// "dn": req.BindDN,
|
||||
// "client": req.RemoteAddr(),
|
||||
// }).Inc()
|
||||
// return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", req.Filter)
|
||||
// }
|
||||
|
||||
switch filterEntity {
|
||||
default:
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": ms.si.GetOutpostName(),
|
||||
"type": "search",
|
||||
"reason": "unhandled_filter_type",
|
||||
"dn": req.BindDN,
|
||||
"client": req.RemoteAddr(),
|
||||
}).Inc()
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: unhandled filter type: %s [%s]", filterEntity, req.Filter)
|
||||
case constants.OCGroupOfUniqueNames:
|
||||
fallthrough
|
||||
case constants.OCAKGroup:
|
||||
fallthrough
|
||||
case constants.OCAKVirtualGroup:
|
||||
fallthrough
|
||||
case constants.OCGroup:
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
gEntries := make([]*ldap.Entry, 0)
|
||||
uEntries := make([]*ldap.Entry, 0)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for _, g := range ms.groups {
|
||||
gEntries = append(gEntries, group.FromAPIGroup(g, ms.si).Entry())
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for _, u := range ms.users {
|
||||
uEntries = append(uEntries, group.FromAPIUser(u, ms.si).Entry())
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
entries = append(gEntries, uEntries...)
|
||||
case "":
|
||||
fallthrough
|
||||
case constants.OCOrgPerson:
|
||||
fallthrough
|
||||
case constants.OCInetOrgPerson:
|
||||
fallthrough
|
||||
case constants.OCAKUser:
|
||||
fallthrough
|
||||
case constants.OCUser:
|
||||
for _, u := range ms.users {
|
||||
entries = append(entries, ms.si.UserEntry(u))
|
||||
}
|
||||
}
|
||||
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
|
||||
}
|
53
internal/outpost/ldap/search/request.go
Normal file
53
internal/outpost/ldap/search/request.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/google/uuid"
|
||||
"github.com/nmcclain/ldap"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/utils"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
ldap.SearchRequest
|
||||
BindDN string
|
||||
log *log.Entry
|
||||
|
||||
id string
|
||||
conn net.Conn
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func NewRequest(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (*Request, *sentry.Span) {
|
||||
rid := uuid.New().String()
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
span := sentry.StartSpan(context.TODO(), "authentik.providers.ldap.search", sentry.TransactionName("authentik.providers.ldap.search"))
|
||||
span.SetTag("request_uid", rid)
|
||||
span.SetTag("user.username", bindDN)
|
||||
span.SetTag("ak_filter", searchReq.Filter)
|
||||
span.SetTag("ak_base_dn", searchReq.BaseDN)
|
||||
return &Request{
|
||||
SearchRequest: searchReq,
|
||||
BindDN: bindDN,
|
||||
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),
|
||||
id: rid,
|
||||
ctx: span.Context(),
|
||||
}, span
|
||||
}
|
||||
|
||||
func (r *Request) Context() context.Context {
|
||||
return r.ctx
|
||||
}
|
||||
|
||||
func (r *Request) Log() *log.Entry {
|
||||
return r.log
|
||||
}
|
||||
|
||||
func (r *Request) RemoteAddr() string {
|
||||
return utils.GetIP(r.conn.RemoteAddr())
|
||||
}
|
7
internal/outpost/ldap/search/searcher.go
Normal file
7
internal/outpost/ldap/search/searcher.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package search
|
||||
|
||||
import "github.com/nmcclain/ldap"
|
||||
|
||||
type Searcher interface {
|
||||
Search(req *Request) (ldap.ServerSearchResult, error)
|
||||
}
|
35
internal/outpost/ldap/server/base.go
Normal file
35
internal/outpost/ldap/server/base.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/nmcclain/ldap"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost/ldap/flags"
|
||||
)
|
||||
|
||||
type LDAPServerInstance interface {
|
||||
GetAPIClient() *api.APIClient
|
||||
GetOutpostName() string
|
||||
|
||||
GetFlowSlug() string
|
||||
GetAppSlug() string
|
||||
GetSearchAllowedGroups() []*strfmt.UUID
|
||||
|
||||
UserEntry(u api.User) *ldap.Entry
|
||||
|
||||
GetBaseDN() string
|
||||
GetBaseGroupDN() string
|
||||
GetBaseUserDN() string
|
||||
|
||||
GetUserDN(string) string
|
||||
GetGroupDN(string) string
|
||||
GetVirtualGroupDN(string) string
|
||||
|
||||
GetUidNumber(api.User) string
|
||||
GetGidNumber(api.Group) string
|
||||
|
||||
UsersForGroup(api.Group) []string
|
||||
|
||||
GetFlags(string) (flags.UserFlags, bool)
|
||||
SetFlags(string, flags.UserFlags)
|
||||
}
|
|
@ -3,70 +3,12 @@ package ldap
|
|||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/nmcclain/ldap"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/api"
|
||||
)
|
||||
|
||||
func BoolToString(in bool) string {
|
||||
if in {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
func ldapResolveTypeSingle(in interface{}) *string {
|
||||
switch t := in.(type) {
|
||||
case string:
|
||||
return &t
|
||||
case *string:
|
||||
return t
|
||||
case bool:
|
||||
s := BoolToString(t)
|
||||
return &s
|
||||
case *bool:
|
||||
s := BoolToString(*t)
|
||||
return &s
|
||||
default:
|
||||
log.WithField("type", reflect.TypeOf(in).String()).Warning("Type can't be mapped to LDAP yet")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func AKAttrsToLDAP(attrs interface{}) []*ldap.EntryAttribute {
|
||||
attrList := []*ldap.EntryAttribute{}
|
||||
if attrs == nil {
|
||||
return attrList
|
||||
}
|
||||
a := attrs.(*map[string]interface{})
|
||||
for attrKey, attrValue := range *a {
|
||||
entry := &ldap.EntryAttribute{Name: attrKey}
|
||||
switch t := attrValue.(type) {
|
||||
case []string:
|
||||
entry.Values = t
|
||||
case *[]string:
|
||||
entry.Values = *t
|
||||
case []interface{}:
|
||||
entry.Values = make([]string, len(t))
|
||||
for idx, v := range t {
|
||||
v := ldapResolveTypeSingle(v)
|
||||
entry.Values[idx] = *v
|
||||
}
|
||||
default:
|
||||
v := ldapResolveTypeSingle(t)
|
||||
if v != nil {
|
||||
entry.Values = []string{*v}
|
||||
}
|
||||
}
|
||||
attrList = append(attrList, entry)
|
||||
}
|
||||
return attrList
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GroupsForUser(user api.User) []string {
|
||||
groups := make([]string, len(user.Groups))
|
||||
for i, group := range user.GroupsObj {
|
||||
|
@ -83,32 +25,6 @@ func (pi *ProviderInstance) UsersForGroup(group api.Group) []string {
|
|||
return users
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) APIGroupToLDAPGroup(g api.Group) LDAPGroup {
|
||||
return LDAPGroup{
|
||||
dn: pi.GetGroupDN(g.Name),
|
||||
cn: g.Name,
|
||||
uid: string(g.Pk),
|
||||
gidNumber: pi.GetGidNumber(g),
|
||||
member: pi.UsersForGroup(g),
|
||||
isVirtualGroup: false,
|
||||
isSuperuser: *g.IsSuperuser,
|
||||
akAttributes: g.Attributes,
|
||||
}
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) APIUserToLDAPGroup(u api.User) LDAPGroup {
|
||||
return LDAPGroup{
|
||||
dn: pi.GetVirtualGroupDN(u.Username),
|
||||
cn: u.Username,
|
||||
uid: u.Uid,
|
||||
gidNumber: pi.GetUidNumber(u),
|
||||
member: []string{pi.GetUserDN(u.Username)},
|
||||
isVirtualGroup: true,
|
||||
isSuperuser: false,
|
||||
akAttributes: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetUserDN(user string) string {
|
||||
return fmt.Sprintf("cn=%s,%s", user, pi.UserDN)
|
||||
}
|
||||
|
@ -155,26 +71,3 @@ func (pi *ProviderInstance) GetRIDForGroup(uid string) int32 {
|
|||
|
||||
return int32(gid)
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) ensureAttributes(attrs []*ldap.EntryAttribute, shouldHave map[string][]string) []*ldap.EntryAttribute {
|
||||
for name, values := range shouldHave {
|
||||
attrs = pi.mustHaveAttribute(attrs, name, values)
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) mustHaveAttribute(attrs []*ldap.EntryAttribute, name string, value []string) []*ldap.EntryAttribute {
|
||||
shouldSet := true
|
||||
for _, attr := range attrs {
|
||||
if attr.Name == name {
|
||||
shouldSet = false
|
||||
}
|
||||
}
|
||||
if shouldSet {
|
||||
return append(attrs, &ldap.EntryAttribute{
|
||||
Name: name,
|
||||
Values: value,
|
||||
})
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
|
86
internal/outpost/ldap/utils/utils.go
Normal file
86
internal/outpost/ldap/utils/utils.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/nmcclain/ldap"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func BoolToString(in bool) string {
|
||||
if in {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
func ldapResolveTypeSingle(in interface{}) *string {
|
||||
switch t := in.(type) {
|
||||
case string:
|
||||
return &t
|
||||
case *string:
|
||||
return t
|
||||
case bool:
|
||||
s := BoolToString(t)
|
||||
return &s
|
||||
case *bool:
|
||||
s := BoolToString(*t)
|
||||
return &s
|
||||
default:
|
||||
log.WithField("type", reflect.TypeOf(in).String()).Warning("Type can't be mapped to LDAP yet")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func AKAttrsToLDAP(attrs interface{}) []*ldap.EntryAttribute {
|
||||
attrList := []*ldap.EntryAttribute{}
|
||||
if attrs == nil {
|
||||
return attrList
|
||||
}
|
||||
a := attrs.(*map[string]interface{})
|
||||
for attrKey, attrValue := range *a {
|
||||
entry := &ldap.EntryAttribute{Name: attrKey}
|
||||
switch t := attrValue.(type) {
|
||||
case []string:
|
||||
entry.Values = t
|
||||
case *[]string:
|
||||
entry.Values = *t
|
||||
case []interface{}:
|
||||
entry.Values = make([]string, len(t))
|
||||
for idx, v := range t {
|
||||
v := ldapResolveTypeSingle(v)
|
||||
entry.Values[idx] = *v
|
||||
}
|
||||
default:
|
||||
v := ldapResolveTypeSingle(t)
|
||||
if v != nil {
|
||||
entry.Values = []string{*v}
|
||||
}
|
||||
}
|
||||
attrList = append(attrList, entry)
|
||||
}
|
||||
return attrList
|
||||
}
|
||||
|
||||
func EnsureAttributes(attrs []*ldap.EntryAttribute, shouldHave map[string][]string) []*ldap.EntryAttribute {
|
||||
for name, values := range shouldHave {
|
||||
attrs = MustHaveAttribute(attrs, name, values)
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
func MustHaveAttribute(attrs []*ldap.EntryAttribute, name string, value []string) []*ldap.EntryAttribute {
|
||||
shouldSet := true
|
||||
for _, attr := range attrs {
|
||||
if attr.Name == name {
|
||||
shouldSet = false
|
||||
}
|
||||
}
|
||||
if shouldSet {
|
||||
return append(attrs, &ldap.EntryAttribute{
|
||||
Name: name,
|
||||
Values: value,
|
||||
})
|
||||
}
|
||||
return attrs
|
||||
}
|
|
@ -1,19 +1,20 @@
|
|||
package ldap
|
||||
package utils
|
||||
|
||||
import (
|
||||
goldap "github.com/go-ldap/ldap/v3"
|
||||
ber "github.com/nmcclain/asn1-ber"
|
||||
"github.com/nmcclain/ldap"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost/ldap/constants"
|
||||
)
|
||||
|
||||
func parseFilterForGroup(req api.ApiCoreGroupsListRequest, f *ber.Packet, skip bool) (api.ApiCoreGroupsListRequest, bool) {
|
||||
func ParseFilterForGroup(req api.ApiCoreGroupsListRequest, f *ber.Packet, skip bool) (api.ApiCoreGroupsListRequest, bool) {
|
||||
switch f.Tag {
|
||||
case ldap.FilterEqualityMatch:
|
||||
return parseFilterForGroupSingle(req, f)
|
||||
case ldap.FilterAnd:
|
||||
for _, child := range f.Children {
|
||||
r, s := parseFilterForGroup(req, child, skip)
|
||||
r, s := ParseFilterForGroup(req, child, skip)
|
||||
skip = skip || s
|
||||
req = r
|
||||
}
|
||||
|
@ -53,7 +54,7 @@ func parseFilterForGroupSingle(req api.ApiCoreGroupsListRequest, f *ber.Packet)
|
|||
username := userDN.RDNs[0].Attributes[0].Value
|
||||
// If the DN's first ou is virtual-groups, ignore this filter
|
||||
if len(userDN.RDNs) > 1 {
|
||||
if userDN.RDNs[1].Attributes[0].Value == VirtualGroupsOU || userDN.RDNs[1].Attributes[0].Value == GroupsOU {
|
||||
if userDN.RDNs[1].Attributes[0].Value == constants.OUVirtualGroups || userDN.RDNs[1].Attributes[0].Value == constants.OUGroups {
|
||||
// Since we know we're not filtering anything, skip this request
|
||||
return req, true
|
||||
}
|
|
@ -1,19 +1,20 @@
|
|||
package ldap
|
||||
package utils
|
||||
|
||||
import (
|
||||
goldap "github.com/go-ldap/ldap/v3"
|
||||
ber "github.com/nmcclain/asn1-ber"
|
||||
"github.com/nmcclain/ldap"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost/ldap/constants"
|
||||
)
|
||||
|
||||
func parseFilterForUser(req api.ApiCoreUsersListRequest, f *ber.Packet, skip bool) (api.ApiCoreUsersListRequest, bool) {
|
||||
func ParseFilterForUser(req api.ApiCoreUsersListRequest, f *ber.Packet, skip bool) (api.ApiCoreUsersListRequest, bool) {
|
||||
switch f.Tag {
|
||||
case ldap.FilterEqualityMatch:
|
||||
return parseFilterForUserSingle(req, f)
|
||||
case ldap.FilterAnd:
|
||||
for _, child := range f.Children {
|
||||
r, s := parseFilterForUser(req, child, skip)
|
||||
r, s := ParseFilterForUser(req, child, skip)
|
||||
skip = skip || s
|
||||
req = r
|
||||
}
|
||||
|
@ -58,7 +59,7 @@ func parseFilterForUserSingle(req api.ApiCoreUsersListRequest, f *ber.Packet) (a
|
|||
name := groupDN.RDNs[0].Attributes[0].Value
|
||||
// If the DN's first ou is virtual-groups, ignore this filter
|
||||
if len(groupDN.RDNs) > 1 {
|
||||
if groupDN.RDNs[1].Attributes[0].Value == UsersOU || groupDN.RDNs[1].Attributes[0].Value == VirtualGroupsOU {
|
||||
if groupDN.RDNs[1].Attributes[0].Value == constants.OUUsers || groupDN.RDNs[1].Attributes[0].Value == constants.OUVirtualGroups {
|
||||
// Since we know we're not filtering anything, skip this request
|
||||
return req, true
|
||||
}
|
14
schema.yml
14
schema.yml
|
@ -22035,6 +22035,8 @@ components:
|
|||
generated from the group.Pk to make sure that the numbers aren't too low
|
||||
for POSIX groups. Default is 4000 to ensure that we don't collide with
|
||||
local groups or users primary groups gidNumber
|
||||
search_mode:
|
||||
$ref: '#/components/schemas/SearchModeEnum'
|
||||
required:
|
||||
- application_slug
|
||||
- bind_flow_slug
|
||||
|
@ -22173,6 +22175,8 @@ components:
|
|||
items:
|
||||
type: string
|
||||
readOnly: true
|
||||
search_mode:
|
||||
$ref: '#/components/schemas/SearchModeEnum'
|
||||
required:
|
||||
- assigned_application_name
|
||||
- assigned_application_slug
|
||||
|
@ -22228,6 +22232,8 @@ components:
|
|||
generated from the group.Pk to make sure that the numbers aren't too low
|
||||
for POSIX groups. Default is 4000 to ensure that we don't collide with
|
||||
local groups or users primary groups gidNumber
|
||||
search_mode:
|
||||
$ref: '#/components/schemas/SearchModeEnum'
|
||||
required:
|
||||
- authorization_flow
|
||||
- name
|
||||
|
@ -26858,6 +26864,8 @@ components:
|
|||
generated from the group.Pk to make sure that the numbers aren't too low
|
||||
for POSIX groups. Default is 4000 to ensure that we don't collide with
|
||||
local groups or users primary groups gidNumber
|
||||
search_mode:
|
||||
$ref: '#/components/schemas/SearchModeEnum'
|
||||
PatchedLDAPSourceRequest:
|
||||
type: object
|
||||
description: LDAP Source Serializer
|
||||
|
@ -29419,6 +29427,11 @@ components:
|
|||
- expression
|
||||
- name
|
||||
- scope_name
|
||||
SearchModeEnum:
|
||||
enum:
|
||||
- direct
|
||||
- cached
|
||||
type: string
|
||||
ServiceConnection:
|
||||
type: object
|
||||
description: ServiceConnection Serializer
|
||||
|
@ -30715,3 +30728,4 @@ components:
|
|||
servers:
|
||||
- url: /api/v3/
|
||||
- url: /api/v2beta/
|
||||
|
||||
|
|
|
@ -625,6 +625,10 @@ msgstr "Cached flows"
|
|||
msgid "Cached policies"
|
||||
msgstr "Cached policies"
|
||||
|
||||
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
||||
msgid "Cached querying, the outpost holds all users and groups in-memory and will refresh every 5 Minutes."
|
||||
msgstr "Cached querying, the outpost holds all users and groups in-memory and will refresh every 5 Minutes."
|
||||
|
||||
#: src/pages/sources/oauth/OAuthSourceViewPage.ts
|
||||
msgid "Callback URL"
|
||||
msgstr "Callback URL"
|
||||
|
@ -913,6 +917,10 @@ msgstr "Configure how the flow executor should handle an invalid response to a c
|
|||
msgid "Configure how the issuer field of the ID Token should be filled."
|
||||
msgstr "Configure how the issuer field of the ID Token should be filled."
|
||||
|
||||
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
||||
msgid "Configure how the outpost queries the core authentik server's users."
|
||||
msgstr "Configure how the outpost queries the core authentik server's users."
|
||||
|
||||
#:
|
||||
#:
|
||||
#~ msgid "Configure settings relevant to your user profile."
|
||||
|
@ -1416,6 +1424,10 @@ msgstr "Digest algorithm"
|
|||
msgid "Digits"
|
||||
msgstr "Digits"
|
||||
|
||||
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
||||
msgid "Direct querying, always returns the latest data, but slower than cached querying."
|
||||
msgstr "Direct querying, always returns the latest data, but slower than cached querying."
|
||||
|
||||
#:
|
||||
#:
|
||||
#~ msgid "Disable"
|
||||
|
@ -3802,6 +3814,10 @@ msgstr "Score"
|
|||
msgid "Search group"
|
||||
msgstr "Search group"
|
||||
|
||||
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
||||
msgid "Search mode"
|
||||
msgstr "Search mode"
|
||||
|
||||
#: src/elements/table/TableSearch.ts
|
||||
#: src/user/LibraryPage.ts
|
||||
msgid "Search..."
|
||||
|
|
|
@ -627,6 +627,10 @@ msgstr "Flux mis en cache"
|
|||
msgid "Cached policies"
|
||||
msgstr "Politiques mises en cache"
|
||||
|
||||
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
||||
msgid "Cached querying, the outpost holds all users and groups in-memory and will refresh every 5 Minutes."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/sources/oauth/OAuthSourceViewPage.ts
|
||||
msgid "Callback URL"
|
||||
msgstr "URL de rappel"
|
||||
|
@ -913,6 +917,10 @@ msgstr "Configure comment l'exécuteur de flux gère une réponse invalide à un
|
|||
msgid "Configure how the issuer field of the ID Token should be filled."
|
||||
msgstr "Configure comment le champ émetteur du jeton ID sera rempli."
|
||||
|
||||
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
||||
msgid "Configure how the outpost queries the core authentik server's users."
|
||||
msgstr ""
|
||||
|
||||
#~ msgid "Configure settings relevant to your user profile."
|
||||
#~ msgstr "Configure les paramètre applicable à votre profil."
|
||||
|
||||
|
@ -1406,6 +1414,10 @@ msgstr "Algorithme d'empreinte"
|
|||
msgid "Digits"
|
||||
msgstr "Chiffres"
|
||||
|
||||
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
||||
msgid "Direct querying, always returns the latest data, but slower than cached querying."
|
||||
msgstr ""
|
||||
|
||||
#~ msgid "Disable"
|
||||
#~ msgstr "Désactiver"
|
||||
|
||||
|
@ -3770,6 +3782,10 @@ msgstr "Note"
|
|||
msgid "Search group"
|
||||
msgstr "Rechercher un groupe"
|
||||
|
||||
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
||||
msgid "Search mode"
|
||||
msgstr ""
|
||||
|
||||
#: src/elements/table/TableSearch.ts
|
||||
#: src/user/LibraryPage.ts
|
||||
msgid "Search..."
|
||||
|
|
|
@ -621,6 +621,10 @@ msgstr ""
|
|||
msgid "Cached policies"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
||||
msgid "Cached querying, the outpost holds all users and groups in-memory and will refresh every 5 Minutes."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/sources/oauth/OAuthSourceViewPage.ts
|
||||
msgid "Callback URL"
|
||||
msgstr ""
|
||||
|
@ -907,6 +911,10 @@ msgstr ""
|
|||
msgid "Configure how the issuer field of the ID Token should be filled."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
||||
msgid "Configure how the outpost queries the core authentik server's users."
|
||||
msgstr ""
|
||||
|
||||
#:
|
||||
#:
|
||||
#~ msgid "Configure settings relevant to your user profile."
|
||||
|
@ -1408,6 +1416,10 @@ msgstr ""
|
|||
msgid "Digits"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
||||
msgid "Direct querying, always returns the latest data, but slower than cached querying."
|
||||
msgstr ""
|
||||
|
||||
#:
|
||||
#:
|
||||
#~ msgid "Disable"
|
||||
|
@ -3794,6 +3806,10 @@ msgstr ""
|
|||
msgid "Search group"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
||||
msgid "Search mode"
|
||||
msgstr ""
|
||||
|
||||
#: src/elements/table/TableSearch.ts
|
||||
#: src/user/LibraryPage.ts
|
||||
msgid "Search..."
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
FlowsInstancesListDesignationEnum,
|
||||
LDAPProvider,
|
||||
ProvidersApi,
|
||||
SearchModeEnum,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { DEFAULT_CONFIG, tenant } from "../../../api/Config";
|
||||
|
@ -118,6 +119,25 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
|
|||
${t`Users in the selected group can do search queries. If no group is selected, no LDAP Searches are allowed.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${t`Search mode`} name="searchMode">
|
||||
<select class="pf-c-form-control">
|
||||
<option
|
||||
value="${SearchModeEnum.Cached}"
|
||||
?selected=${this.instance?.searchMode === SearchModeEnum.Cached}
|
||||
>
|
||||
${t`Cached querying, the outpost holds all users and groups in-memory and will refresh every 5 Minutes.`}
|
||||
</option>
|
||||
<option
|
||||
value="${SearchModeEnum.Direct}"
|
||||
?selected=${this.instance?.searchMode === SearchModeEnum.Direct}
|
||||
>
|
||||
${t`Direct querying, always returns the latest data, but slower than cached querying.`}
|
||||
</option>
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Configure how the outpost queries the core authentik server's users.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${t`Protocol settings`} </span>
|
||||
|
|
|
@ -152,3 +152,13 @@ Configure how authentik should show avatars for users. Following values can be s
|
|||
- `%(username)s`: The user's username
|
||||
- `%(mail_hash)s`: The email address, md5 hashed
|
||||
- `%(upn)s`: The user's UPN, if set (otherwise an empty string)
|
||||
|
||||
## Debugging
|
||||
|
||||
To check if your config has been applied correctly, you can run the following command to output the full config:
|
||||
|
||||
```
|
||||
docker-compose run --rm worker dump_config
|
||||
# Or for kubernetes
|
||||
kubectl exec -it deployment/authentik-worker -c authentik -- ak dump_config
|
||||
```
|
||||
|
|
58
website/docs/integrations/services/proxmox-ve/index.md
Normal file
58
website/docs/integrations/services/proxmox-ve/index.md
Normal file
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
title: Proxmox VE
|
||||
---
|
||||
|
||||
## What is Proxmox VE
|
||||
|
||||
From https://pve.proxmox.com/wiki/Main_Page
|
||||
|
||||
:::note
|
||||
Proxmox Virtual Environment is an open source server virtualization management solution based on QEMU/KVM and LXC. You can manage virtual machines, containers, highly available clusters, storage and networks with an integrated, easy-to-use web interface or via CLI. Proxmox VE code is licensed under the GNU Affero General Public License, version 3. The project is developed and maintained by Proxmox Server Solutions GmbH.
|
||||
:::
|
||||
|
||||
:::warning
|
||||
This requires Proxmox VE 7.0 or newer.
|
||||
:::
|
||||
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `proxmox.company` is the FQDN of the Proxmox VE server.
|
||||
- `authentik.company` is the FQDN of the authentik install.
|
||||
|
||||
### Step 1
|
||||
|
||||
Under _Providers_, create an OAuth2/OpenID provider with these settings:
|
||||
|
||||
- Name: proxmox
|
||||
- Client Type: Confidential
|
||||
- JWT Algorithm: RS256
|
||||
- Redirect URI: `https://proxmox.company:8006` (Note the absence of the trailing slash, and the inclusion of the webinterface port)
|
||||
|
||||
### Step 2
|
||||
|
||||
Create an application which uses this provider. Optionally apply access restrictions to the application.
|
||||
|
||||
Set the Launch URL to `https://promox.company:8006`.
|
||||
|
||||
## Proxmox VE Setup
|
||||
|
||||
Proxmox VE allows configuration of authentication sources using the web interface (under Datacenter -> Permissions -> Authentication).
|
||||
|
||||
![](proxmox-source.png)
|
||||
|
||||
Another way is to use the CLI. SSH into any Proxmox cluster node, and issue the following command:
|
||||
|
||||
`pveum realm add authentik --type openid --issuer-url https://authentik.company/application/o/proxmox/ --client-id xxx --client-key xxx --username-claim username --autocreate 1`
|
||||
|
||||
You can find the Issuer URL on the Provider Metadata tab in Authentik. You can find the Client ID and Key on the Provider Edit dialog in Authentik.
|
||||
|
||||
After configuring the source in Proxmox, any user that logs in to Proxmox for the first time automatically gets an user named `<authentik username>@<pve realm name>`. In this example,
|
||||
Authentik user `bob` will get an user named `bob@authentik` in Proxmox. You can then assign Permissions as normally in Proxmox. You can also pre-create the users in Proxmox if you want
|
||||
the user to be able to perform actions immediately after first login.
|
||||
|
||||
There is no way to directly trigger an OpenID Connect login in Proxmox, but if you set the source as 'default', it will be automatically selected on the Proxmox login screen.
|
||||
|
||||
![](proxmox-login.png)
|
BIN
website/docs/integrations/services/proxmox-ve/proxmox-login.png
Normal file
BIN
website/docs/integrations/services/proxmox-ve/proxmox-login.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.4 KiB |
BIN
website/docs/integrations/services/proxmox-ve/proxmox-source.png
Normal file
BIN
website/docs/integrations/services/proxmox-ve/proxmox-source.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
|
@ -17,7 +17,7 @@ docker-compose run --rm server create_recovery_key 10 akadmin
|
|||
or, for Kubernetes, run
|
||||
|
||||
```
|
||||
kubectl exec -it deployment/authentik-web -c authentik -- ak create_recovery_key 10 akadmin
|
||||
kubectl exec -it deployment/authentik-worker -c authentik -- ak create_recovery_key 10 akadmin
|
||||
```
|
||||
|
||||
This will output a link, that can be used to instantly gain access to authentik as the user specified above. The link is valid for amount of years specified above, in this case, 10 years.
|
||||
|
|
17
website/docs/troubleshooting/missing_admin_group.md
Normal file
17
website/docs/troubleshooting/missing_admin_group.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
title: Missing admin group
|
||||
---
|
||||
|
||||
If all of the Admin groups have been deleted, or misconfigured during sync, you can use the following command to gain access back.
|
||||
|
||||
Run the following command, where *username* is the user you want to add to the newly created group:
|
||||
|
||||
```
|
||||
docker-compose run --rm server create_admin_group username
|
||||
```
|
||||
|
||||
or, for Kubernetes, run
|
||||
|
||||
```
|
||||
kubectl exec -it deployment/authentik-worker -c authentik -- ak create_admin_group username
|
||||
```
|
|
@ -100,6 +100,7 @@ module.exports = {
|
|||
"integrations/services/minio/index",
|
||||
"integrations/services/nextcloud/index",
|
||||
"integrations/services/portainer/index",
|
||||
"integrations/services/proxmox-ve/index",
|
||||
"integrations/services/rancher/index",
|
||||
"integrations/services/sentry/index",
|
||||
"integrations/services/sonarr/index",
|
||||
|
@ -209,6 +210,7 @@ module.exports = {
|
|||
"troubleshooting/login",
|
||||
"troubleshooting/image_upload_backup",
|
||||
"troubleshooting/missing_permission",
|
||||
"troubleshooting/missing_admin_group",
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
Reference in a new issue