LDAP Provider: TLS support (#1137)
This commit is contained in:
parent
cd0a6f2d7c
commit
7dfc621ae4
|
@ -51,6 +51,7 @@ COPY --from=website-builder /static/build_docs/ /work/website/build_docs/
|
||||||
|
|
||||||
COPY ./cmd /work/cmd
|
COPY ./cmd /work/cmd
|
||||||
COPY ./web/static.go /work/web/static.go
|
COPY ./web/static.go /work/web/static.go
|
||||||
|
COPY ./website/static.go /work/website/static.go
|
||||||
COPY ./internal /work/internal
|
COPY ./internal /work/internal
|
||||||
COPY ./go.mod /work/go.mod
|
COPY ./go.mod /work/go.mod
|
||||||
COPY ./go.sum /work/go.sum
|
COPY ./go.sum /work/go.sum
|
||||||
|
|
|
@ -1,24 +1,60 @@
|
||||||
"""Groups API Viewset"""
|
"""Groups API Viewset"""
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from rest_framework.fields import JSONField
|
from rest_framework.fields import BooleanField, CharField, JSONField
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ListSerializer, ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
||||||
|
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import is_dict
|
from authentik.core.api.utils import is_dict
|
||||||
from authentik.core.models import Group
|
from authentik.core.models import Group, User
|
||||||
|
|
||||||
|
|
||||||
|
class GroupMemberSerializer(ModelSerializer):
|
||||||
|
"""Stripped down user serializer to show relevant users for groups"""
|
||||||
|
|
||||||
|
is_superuser = BooleanField(read_only=True)
|
||||||
|
avatar = CharField(read_only=True)
|
||||||
|
attributes = JSONField(validators=[is_dict], required=False)
|
||||||
|
uid = CharField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
model = User
|
||||||
|
fields = [
|
||||||
|
"pk",
|
||||||
|
"username",
|
||||||
|
"name",
|
||||||
|
"is_active",
|
||||||
|
"last_login",
|
||||||
|
"is_superuser",
|
||||||
|
"email",
|
||||||
|
"avatar",
|
||||||
|
"attributes",
|
||||||
|
"uid",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class GroupSerializer(ModelSerializer):
|
class GroupSerializer(ModelSerializer):
|
||||||
"""Group Serializer"""
|
"""Group Serializer"""
|
||||||
|
|
||||||
attributes = JSONField(validators=[is_dict], required=False)
|
attributes = JSONField(validators=[is_dict], required=False)
|
||||||
|
users_obj = ListSerializer(
|
||||||
|
child=GroupMemberSerializer(), read_only=True, source="users", required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = Group
|
model = Group
|
||||||
fields = ["pk", "name", "is_superuser", "parent", "users", "attributes"]
|
fields = [
|
||||||
|
"pk",
|
||||||
|
"name",
|
||||||
|
"is_superuser",
|
||||||
|
"parent",
|
||||||
|
"users",
|
||||||
|
"attributes",
|
||||||
|
"users_obj",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class GroupViewSet(UsedByMixin, ModelViewSet):
|
class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||||
|
|
|
@ -17,6 +17,8 @@ class LDAPProviderSerializer(ProviderSerializer):
|
||||||
fields = ProviderSerializer.Meta.fields + [
|
fields = ProviderSerializer.Meta.fields + [
|
||||||
"base_dn",
|
"base_dn",
|
||||||
"search_group",
|
"search_group",
|
||||||
|
"certificate",
|
||||||
|
"tls_server_name",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,6 +46,8 @@ class LDAPOutpostConfigSerializer(ModelSerializer):
|
||||||
"bind_flow_slug",
|
"bind_flow_slug",
|
||||||
"application_slug",
|
"application_slug",
|
||||||
"search_group",
|
"search_group",
|
||||||
|
"certificate",
|
||||||
|
"tls_server_name",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,4 +11,5 @@ class LDAPDockerController(DockerController):
|
||||||
super().__init__(outpost, connection)
|
super().__init__(outpost, connection)
|
||||||
self.deployment_ports = [
|
self.deployment_ports = [
|
||||||
DeploymentPort(389, "ldap", "tcp", 3389),
|
DeploymentPort(389, "ldap", "tcp", 3389),
|
||||||
|
DeploymentPort(636, "ldaps", "tcp", 6636),
|
||||||
]
|
]
|
||||||
|
|
|
@ -11,4 +11,5 @@ class LDAPKubernetesController(KubernetesController):
|
||||||
super().__init__(outpost, connection)
|
super().__init__(outpost, connection)
|
||||||
self.deployment_ports = [
|
self.deployment_ports = [
|
||||||
DeploymentPort(389, "ldap", "tcp", 3389),
|
DeploymentPort(389, "ldap", "tcp", 3389),
|
||||||
|
DeploymentPort(636, "ldaps", "tcp", 6636),
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Generated by Django 3.2.5 on 2021-07-13 11:38
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_crypto", "0002_create_self_signed_kp"),
|
||||||
|
("authentik_providers_ldap", "0002_ldapprovider_search_group"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="ldapprovider",
|
||||||
|
name="certificate",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="authentik_crypto.certificatekeypair",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="ldapprovider",
|
||||||
|
name="tls_server_name",
|
||||||
|
field=models.TextField(blank=True, default=""),
|
||||||
|
),
|
||||||
|
]
|
|
@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.serializers import Serializer
|
from rest_framework.serializers import Serializer
|
||||||
|
|
||||||
from authentik.core.models import Group, Provider
|
from authentik.core.models import Group, Provider
|
||||||
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.outposts.models import OutpostModel
|
from authentik.outposts.models import OutpostModel
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,6 +29,17 @@ class LDAPProvider(OutpostModel, Provider):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
tls_server_name = models.TextField(
|
||||||
|
default="",
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
certificate = models.ForeignKey(
|
||||||
|
CertificateKeyPair,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def launch_url(self) -> Optional[str]:
|
def launch_url(self) -> Optional[str]:
|
||||||
"""LDAP never has a launch URL"""
|
"""LDAP never has a launch URL"""
|
||||||
|
|
|
@ -37,7 +37,7 @@ func GenerateSelfSignedCert() (tls.Certificate, error) {
|
||||||
SerialNumber: serialNumber,
|
SerialNumber: serialNumber,
|
||||||
Subject: pkix.Name{
|
Subject: pkix.Name{
|
||||||
Organization: []string{"authentik"},
|
Organization: []string{"authentik"},
|
||||||
CommonName: "authentik Proxy default certificate",
|
CommonName: "authentik Outpost default certificate",
|
||||||
},
|
},
|
||||||
NotBefore: notBefore,
|
NotBefore: notBefore,
|
||||||
NotAfter: notAfter,
|
NotAfter: notAfter,
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package ak
|
package ak
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -9,6 +11,7 @@ import (
|
||||||
"github.com/getsentry/sentry-go"
|
"github.com/getsentry/sentry-go"
|
||||||
httptransport "github.com/go-openapi/runtime/client"
|
httptransport "github.com/go-openapi/runtime/client"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"goauthentik.io/outpost/api"
|
||||||
"goauthentik.io/outpost/pkg"
|
"goauthentik.io/outpost/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -66,3 +69,21 @@ func GetTLSTransport() http.RoundTripper {
|
||||||
}
|
}
|
||||||
return tlsTransport
|
return tlsTransport
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseCertificate Load certificate from Keyepair UUID and parse it into a go Certificate
|
||||||
|
func ParseCertificate(kpUuid string, cryptoApi *api.CryptoApiService) (*tls.Certificate, error) {
|
||||||
|
cert, _, err := cryptoApi.CryptoCertificatekeypairsViewCertificateRetrieve(context.Background(), kpUuid).Execute()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
key, _, err := cryptoApi.CryptoCertificatekeypairsViewPrivateKeyRetrieve(context.Background(), kpUuid).Execute()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
x509cert, err := tls.X509KeyPair([]byte(cert.Data), []byte(key.Data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &x509cert, nil
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package ldap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -10,6 +11,7 @@ import (
|
||||||
|
|
||||||
"github.com/go-openapi/strfmt"
|
"github.com/go-openapi/strfmt"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"goauthentik.io/outpost/pkg/ak"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ls *LDAPServer) Refresh() error {
|
func (ls *LDAPServer) Refresh() error {
|
||||||
|
@ -24,6 +26,7 @@ func (ls *LDAPServer) Refresh() error {
|
||||||
for idx, provider := range outposts.Results {
|
for idx, provider := range outposts.Results {
|
||||||
userDN := strings.ToLower(fmt.Sprintf("ou=users,%s", *provider.BaseDn))
|
userDN := strings.ToLower(fmt.Sprintf("ou=users,%s", *provider.BaseDn))
|
||||||
groupDN := strings.ToLower(fmt.Sprintf("ou=groups,%s", *provider.BaseDn))
|
groupDN := strings.ToLower(fmt.Sprintf("ou=groups,%s", *provider.BaseDn))
|
||||||
|
logger := log.WithField("logger", "authentik.outpost.ldap").WithField("provider", provider.Name)
|
||||||
providers[idx] = &ProviderInstance{
|
providers[idx] = &ProviderInstance{
|
||||||
BaseDN: *provider.BaseDn,
|
BaseDN: *provider.BaseDn,
|
||||||
GroupDN: groupDN,
|
GroupDN: groupDN,
|
||||||
|
@ -34,7 +37,18 @@ func (ls *LDAPServer) Refresh() error {
|
||||||
boundUsersMutex: sync.RWMutex{},
|
boundUsersMutex: sync.RWMutex{},
|
||||||
boundUsers: make(map[string]UserFlags),
|
boundUsers: make(map[string]UserFlags),
|
||||||
s: ls,
|
s: ls,
|
||||||
log: log.WithField("logger", "authentik.outpost.ldap").WithField("provider", provider.Name),
|
log: logger,
|
||||||
|
tlsServerName: provider.TlsServerName,
|
||||||
|
}
|
||||||
|
if provider.Certificate.Get() != nil {
|
||||||
|
logger.WithField("provider", provider.Name).Debug("Enabling TLS")
|
||||||
|
cert, err := ak.ParseCertificate(*provider.Certificate.Get(), ls.ac.Client.CryptoApi)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithField("provider", provider.Name).WithError(err).Warning("Failed to fetch certificate")
|
||||||
|
} else {
|
||||||
|
providers[idx].cert = cert
|
||||||
|
logger.WithField("provider", provider.Name).Debug("Loaded certificates")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ls.providers = providers
|
ls.providers = providers
|
||||||
|
@ -58,9 +72,30 @@ func (ls *LDAPServer) StartLDAPServer() error {
|
||||||
return ls.s.ListenAndServe(listen)
|
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 := tls.Listen("tcp", listen, tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
ls.log.Fatalf("FATAL: listen (%s) failed - %s", listen, err)
|
||||||
|
}
|
||||||
|
ls.log.WithField("listen", listen).Info("Starting ldap tls server")
|
||||||
|
err = ls.s.Serve(ln)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ls.log.Printf("closing %s", ln.Addr())
|
||||||
|
return ls.s.ListenAndServe(listen)
|
||||||
|
}
|
||||||
|
|
||||||
func (ls *LDAPServer) Start() error {
|
func (ls *LDAPServer) Start() error {
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
wg.Add(2)
|
wg.Add(3)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
err := ls.StartHTTPServer()
|
err := ls.StartHTTPServer()
|
||||||
|
@ -75,6 +110,13 @@ func (ls *LDAPServer) Start() error {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
err := ls.StartLDAPTLSServer()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -109,7 +109,7 @@ func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
|
||||||
|
|
||||||
attrs = append(attrs, AKAttrsToLDAP(u.Attributes)...)
|
attrs = append(attrs, AKAttrsToLDAP(u.Attributes)...)
|
||||||
|
|
||||||
dn := fmt.Sprintf("cn=%s,%s", u.Username, pi.UserDN)
|
dn := pi.GetUserDN(u.Username)
|
||||||
|
|
||||||
return &ldap.Entry{DN: dn, Attributes: attrs}
|
return &ldap.Entry{DN: dn, Attributes: attrs}
|
||||||
}
|
}
|
||||||
|
@ -129,6 +129,9 @@ func (pi *ProviderInstance) GroupEntry(g api.Group) *ldap.Entry {
|
||||||
Values: []string{GroupObjectClass, "goauthentik.io/ldap/group"},
|
Values: []string{GroupObjectClass, "goauthentik.io/ldap/group"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "member", Values: pi.UsersForGroup(g)})
|
||||||
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "goauthentik.io/ldap/superuser", Values: []string{BoolToString(*g.IsSuperuser)}})
|
||||||
|
|
||||||
attrs = append(attrs, AKAttrsToLDAP(g.Attributes)...)
|
attrs = append(attrs, AKAttrsToLDAP(g.Attributes)...)
|
||||||
|
|
||||||
dn := pi.GetGroupDN(g)
|
dn := pi.GetGroupDN(g)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package ldap
|
package ldap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/go-openapi/strfmt"
|
"github.com/go-openapi/strfmt"
|
||||||
|
@ -25,6 +26,9 @@ type ProviderInstance struct {
|
||||||
s *LDAPServer
|
s *LDAPServer
|
||||||
log *log.Entry
|
log *log.Entry
|
||||||
|
|
||||||
|
tlsServerName *string
|
||||||
|
cert *tls.Certificate
|
||||||
|
|
||||||
searchAllowedGroups []*strfmt.UUID
|
searchAllowedGroups []*strfmt.UUID
|
||||||
boundUsersMutex sync.RWMutex
|
boundUsersMutex sync.RWMutex
|
||||||
boundUsers map[string]UserFlags
|
boundUsers map[string]UserFlags
|
||||||
|
@ -39,7 +43,7 @@ type LDAPServer struct {
|
||||||
s *ldap.Server
|
s *ldap.Server
|
||||||
log *log.Entry
|
log *log.Entry
|
||||||
ac *ak.APIController
|
ac *ak.APIController
|
||||||
|
defaultCert *tls.Certificate
|
||||||
providers []*ProviderInstance
|
providers []*ProviderInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +56,11 @@ func NewServer(ac *ak.APIController) *LDAPServer {
|
||||||
ac: ac,
|
ac: ac,
|
||||||
providers: []*ProviderInstance{},
|
providers: []*ProviderInstance{},
|
||||||
}
|
}
|
||||||
|
defaultCert, err := ak.GenerateSelfSignedCert()
|
||||||
|
if err != nil {
|
||||||
|
log.Warning(err)
|
||||||
|
}
|
||||||
|
ls.defaultCert = &defaultCert
|
||||||
s.BindFunc("", ls)
|
s.BindFunc("", ls)
|
||||||
s.SearchFunc("", ls)
|
s.SearchFunc("", ls)
|
||||||
return ls
|
return ls
|
||||||
|
|
|
@ -2,8 +2,10 @@ package ldap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"github.com/nmcclain/ldap"
|
"github.com/nmcclain/ldap"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
"goauthentik.io/outpost/api"
|
"goauthentik.io/outpost/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -14,6 +16,24 @@ func BoolToString(in bool) string {
|
||||||
return "false"
|
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 {
|
func AKAttrsToLDAP(attrs interface{}) []*ldap.EntryAttribute {
|
||||||
attrList := []*ldap.EntryAttribute{}
|
attrList := []*ldap.EntryAttribute{}
|
||||||
a := attrs.(*map[string]interface{})
|
a := attrs.(*map[string]interface{})
|
||||||
|
@ -22,10 +42,19 @@ func AKAttrsToLDAP(attrs interface{}) []*ldap.EntryAttribute {
|
||||||
switch t := attrValue.(type) {
|
switch t := attrValue.(type) {
|
||||||
case []string:
|
case []string:
|
||||||
entry.Values = t
|
entry.Values = t
|
||||||
case string:
|
case *[]string:
|
||||||
entry.Values = []string{t}
|
entry.Values = *t
|
||||||
case bool:
|
case []interface{}:
|
||||||
entry.Values = []string{BoolToString(t)}
|
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)
|
attrList = append(attrList, entry)
|
||||||
}
|
}
|
||||||
|
@ -40,6 +69,18 @@ func (pi *ProviderInstance) GroupsForUser(user api.User) []string {
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pi *ProviderInstance) UsersForGroup(group api.Group) []string {
|
||||||
|
users := make([]string, len(group.UsersObj))
|
||||||
|
for i, user := range group.UsersObj {
|
||||||
|
users[i] = pi.GetUserDN(user.Username)
|
||||||
|
}
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pi *ProviderInstance) GetUserDN(user string) string {
|
||||||
|
return fmt.Sprintf("cn=%s,%s", user, pi.UserDN)
|
||||||
|
}
|
||||||
|
|
||||||
func (pi *ProviderInstance) GetGroupDN(group api.Group) string {
|
func (pi *ProviderInstance) GetGroupDN(group api.Group) string {
|
||||||
return fmt.Sprintf("cn=%s,%s", group.Name, pi.GroupDN)
|
return fmt.Sprintf("cn=%s,%s", group.Name, pi.GroupDN)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -16,6 +15,7 @@ import (
|
||||||
"github.com/oauth2-proxy/oauth2-proxy/pkg/validation"
|
"github.com/oauth2-proxy/oauth2-proxy/pkg/validation"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"goauthentik.io/outpost/api"
|
"goauthentik.io/outpost/api"
|
||||||
|
"goauthentik.io/outpost/pkg/ak"
|
||||||
)
|
)
|
||||||
|
|
||||||
type providerBundle struct {
|
type providerBundle struct {
|
||||||
|
@ -90,23 +90,12 @@ func (pb *providerBundle) prepareOpts(provider api.ProxyOutpostConfig) *options.
|
||||||
|
|
||||||
if provider.Certificate.Get() != nil {
|
if provider.Certificate.Get() != nil {
|
||||||
pb.log.WithField("provider", provider.Name).Debug("Enabling TLS")
|
pb.log.WithField("provider", provider.Name).Debug("Enabling TLS")
|
||||||
cert, _, err := pb.s.ak.Client.CryptoApi.CryptoCertificatekeypairsViewCertificateRetrieve(context.Background(), *provider.Certificate.Get()).Execute()
|
cert, err := ak.ParseCertificate(*provider.Certificate.Get(), pb.s.ak.Client.CryptoApi)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pb.log.WithField("provider", provider.Name).WithError(err).Warning("Failed to fetch certificate")
|
pb.log.WithField("provider", provider.Name).WithError(err).Warning("Failed to fetch certificate")
|
||||||
return providerOpts
|
return providerOpts
|
||||||
}
|
}
|
||||||
key, _, err := pb.s.ak.Client.CryptoApi.CryptoCertificatekeypairsViewPrivateKeyRetrieve(context.Background(), *provider.Certificate.Get()).Execute()
|
pb.cert = cert
|
||||||
if err != nil {
|
|
||||||
pb.log.WithField("provider", provider.Name).WithError(err).Warning("Failed to fetch private key")
|
|
||||||
return providerOpts
|
|
||||||
}
|
|
||||||
|
|
||||||
x509cert, err := tls.X509KeyPair([]byte(cert.Data), []byte(key.Data))
|
|
||||||
if err != nil {
|
|
||||||
pb.log.WithField("provider", provider.Name).WithError(err).Warning("Failed to parse certificate")
|
|
||||||
return providerOpts
|
|
||||||
}
|
|
||||||
pb.cert = &x509cert
|
|
||||||
pb.log.WithField("provider", provider.Name).Debug("Loaded certificates")
|
pb.log.WithField("provider", provider.Name).Debug("Loaded certificates")
|
||||||
}
|
}
|
||||||
return providerOpts
|
return providerOpts
|
||||||
|
|
113
schema.yml
113
schema.yml
|
@ -19927,11 +19927,100 @@ components:
|
||||||
attributes:
|
attributes:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: {}
|
additionalProperties: {}
|
||||||
|
users_obj:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/GroupMember'
|
||||||
|
readOnly: true
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
- parent
|
- parent
|
||||||
- pk
|
- pk
|
||||||
- users
|
- users
|
||||||
|
- users_obj
|
||||||
|
GroupMember:
|
||||||
|
type: object
|
||||||
|
description: Stripped down user serializer to show relevant users for groups
|
||||||
|
properties:
|
||||||
|
pk:
|
||||||
|
type: integer
|
||||||
|
readOnly: true
|
||||||
|
title: ID
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_
|
||||||
|
only.
|
||||||
|
pattern: ^[\w.@+-]+$
|
||||||
|
maxLength: 150
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: User's display name.
|
||||||
|
is_active:
|
||||||
|
type: boolean
|
||||||
|
title: Active
|
||||||
|
description: Designates whether this user should be treated as active. Unselect
|
||||||
|
this instead of deleting accounts.
|
||||||
|
last_login:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
nullable: true
|
||||||
|
is_superuser:
|
||||||
|
type: boolean
|
||||||
|
readOnly: true
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
title: Email address
|
||||||
|
maxLength: 254
|
||||||
|
avatar:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
|
uid:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
required:
|
||||||
|
- avatar
|
||||||
|
- is_superuser
|
||||||
|
- name
|
||||||
|
- pk
|
||||||
|
- uid
|
||||||
|
- username
|
||||||
|
GroupMemberRequest:
|
||||||
|
type: object
|
||||||
|
description: Stripped down user serializer to show relevant users for groups
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_
|
||||||
|
only.
|
||||||
|
pattern: ^[\w.@+-]+$
|
||||||
|
maxLength: 150
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: User's display name.
|
||||||
|
is_active:
|
||||||
|
type: boolean
|
||||||
|
title: Active
|
||||||
|
description: Designates whether this user should be treated as active. Unselect
|
||||||
|
this instead of deleting accounts.
|
||||||
|
last_login:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
nullable: true
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
title: Email address
|
||||||
|
maxLength: 254
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- username
|
||||||
GroupRequest:
|
GroupRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Group Serializer
|
description: Group Serializer
|
||||||
|
@ -20402,6 +20491,12 @@ components:
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Users in this group can do search queries. If not set, every
|
description: Users in this group can do search queries. If not set, every
|
||||||
user can execute search queries.
|
user can execute search queries.
|
||||||
|
certificate:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: true
|
||||||
|
tls_server_name:
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- application_slug
|
- application_slug
|
||||||
- bind_flow_slug
|
- bind_flow_slug
|
||||||
|
@ -20514,6 +20609,12 @@ components:
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Users in this group can do search queries. If not set, every
|
description: Users in this group can do search queries. If not set, every
|
||||||
user can execute search queries.
|
user can execute search queries.
|
||||||
|
certificate:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: true
|
||||||
|
tls_server_name:
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- assigned_application_name
|
- assigned_application_name
|
||||||
- assigned_application_slug
|
- assigned_application_slug
|
||||||
|
@ -20547,6 +20648,12 @@ components:
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Users in this group can do search queries. If not set, every
|
description: Users in this group can do search queries. If not set, every
|
||||||
user can execute search queries.
|
user can execute search queries.
|
||||||
|
certificate:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: true
|
||||||
|
tls_server_name:
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- authorization_flow
|
- authorization_flow
|
||||||
- name
|
- name
|
||||||
|
@ -24883,6 +24990,12 @@ components:
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Users in this group can do search queries. If not set, every
|
description: Users in this group can do search queries. If not set, every
|
||||||
user can execute search queries.
|
user can execute search queries.
|
||||||
|
certificate:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: true
|
||||||
|
tls_server_name:
|
||||||
|
type: string
|
||||||
PatchedLDAPSourceRequest:
|
PatchedLDAPSourceRequest:
|
||||||
type: object
|
type: object
|
||||||
description: LDAP Source Serializer
|
description: LDAP Source Serializer
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { FlowsApi, ProvidersApi, LDAPProvider, CoreApi, FlowsInstancesListDesignationEnum } from "authentik-api";
|
import { FlowsApi, ProvidersApi, LDAPProvider, CoreApi, FlowsInstancesListDesignationEnum, CryptoApi } from "authentik-api";
|
||||||
import { t } from "@lingui/macro";
|
import { t } from "@lingui/macro";
|
||||||
import { customElement } from "lit-element";
|
import { customElement } from "lit-element";
|
||||||
import { html, TemplateResult } from "lit-html";
|
import { html, TemplateResult } from "lit-html";
|
||||||
|
@ -90,6 +90,27 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
|
||||||
<input type="text" value="${first(this.instance?.baseDn, "DC=ldap,DC=goauthentik,DC=io")}" class="pf-c-form-control" required>
|
<input type="text" value="${first(this.instance?.baseDn, "DC=ldap,DC=goauthentik,DC=io")}" class="pf-c-form-control" required>
|
||||||
<p class="pf-c-form__helper-text">${t`LDAP DN under which bind requests and search requests can be made.`}</p>
|
<p class="pf-c-form__helper-text">${t`LDAP DN under which bind requests and search requests can be made.`}</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${t`TLS Server name`}
|
||||||
|
name="baseDn">
|
||||||
|
<input type="text" value="${first(this.instance?.tlsServerName, "")}" class="pf-c-form-control">
|
||||||
|
<p class="pf-c-form__helper-text">${t`Server name for which this provider's certificate is valid for.`}</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${t`Certificate`}
|
||||||
|
name="certificate">
|
||||||
|
<select class="pf-c-form-control">
|
||||||
|
<option value="" ?selected=${this.instance?.certificate === undefined}>---------</option>
|
||||||
|
${until(new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsList({
|
||||||
|
ordering: "pk",
|
||||||
|
hasKey: true,
|
||||||
|
}).then(keys => {
|
||||||
|
return keys.results.map(key => {
|
||||||
|
return html`<option value=${ifDefined(key.pk)} ?selected=${this.instance?.certificate === key.pk}>${key.name}</option>`;
|
||||||
|
});
|
||||||
|
}), html`<option>${t`Loading...`}</option>`)}
|
||||||
|
</select>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
</div>
|
</div>
|
||||||
</ak-form-group>
|
</ak-form-group>
|
||||||
</form>`;
|
</form>`;
|
||||||
|
|
|
@ -22,7 +22,7 @@ You can bind using the DN `cn=<username>,ou=users,<base DN>`, or using the follo
|
||||||
ldapsearch \
|
ldapsearch \
|
||||||
-x \ # Only simple binds are currently supported
|
-x \ # Only simple binds are currently supported
|
||||||
-h *ip* \
|
-h *ip* \
|
||||||
-p 3389 \
|
-p 389 \
|
||||||
-D 'cn=*user*,ou=users,DC=ldap,DC=goauthentik,DC=io' \ # Bind user and password
|
-D 'cn=*user*,ou=users,DC=ldap,DC=goauthentik,DC=io' \ # Bind user and password
|
||||||
-w '*password*' \
|
-w '*password*' \
|
||||||
-b 'ou=users,DC=ldap,DC=goauthentik,DC=io' \ # The search base
|
-b 'ou=users,DC=ldap,DC=goauthentik,DC=io' \ # The search base
|
||||||
|
@ -48,8 +48,15 @@ The following fields are current set for groups:
|
||||||
|
|
||||||
- `cn`: The group's name
|
- `cn`: The group's name
|
||||||
- `uid`: Unique group identifier
|
- `uid`: Unique group identifier
|
||||||
|
- `member`: A list of all DNs of the group's members
|
||||||
- `objectClass`: A list of these strings:
|
- `objectClass`: A list of these strings:
|
||||||
- "group"
|
- "group"
|
||||||
- "goauthentik.io/ldap/group"
|
- "goauthentik.io/ldap/group"
|
||||||
|
|
||||||
**Additionally**, for both users and groups, any attributes you set are also present as LDAP Attributes.
|
**Additionally**, for both users and groups, any attributes you set are also present as LDAP Attributes.
|
||||||
|
|
||||||
|
## SSL
|
||||||
|
|
||||||
|
You can also configure SSL for your LDAP Providers by selecting a certificate and a server name in the provider settings.
|
||||||
|
|
||||||
|
This enables you to bind on port 636 using LDAPS, StartTLS is not supported.
|
||||||
|
|
Reference in New Issue