LDAP Provider: TLS support (#1137)
This commit is contained in:
		| @ -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 | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										23
									
								
								outpost/pkg/ldap/api_tls.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								outpost/pkg/ldap/api_tls.go
									
									
									
									
									
										Normal file
									
								
							| @ -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
	
	Block a user
	 Jens L
					Jens L