chore: move the go code into a subfolder

This commit is contained in:
Florian Forster
2025-08-05 15:20:32 -07:00
parent 4ad22ba456
commit cd2921de26
2978 changed files with 373 additions and 300 deletions

View File

@@ -0,0 +1,290 @@
package ldap
import (
"context"
"time"
"github.com/zitadel/zitadel/internal/idp"
)
const DefaultPort = "389"
var _ idp.Provider = (*Provider)(nil)
// Provider is the [idp.Provider] implementation for a generic LDAP provider
type Provider struct {
name string
servers []string
startTLS bool
baseDN string
bindDN string
bindPassword string
userBase string
userObjectClasses []string
userFilters []string
timeout time.Duration
rootCA []byte
loginUrl string
isLinkingAllowed bool
isCreationAllowed bool
isAutoCreation bool
isAutoUpdate bool
idAttribute string
firstNameAttribute string
lastNameAttribute string
displayNameAttribute string
nickNameAttribute string
preferredUsernameAttribute string
emailAttribute string
emailVerifiedAttribute string
phoneAttribute string
phoneVerifiedAttribute string
preferredLanguageAttribute string
avatarURLAttribute string
profileAttribute string
}
type ProviderOpts func(provider *Provider)
// WithLinkingAllowed allows end users to link the federated user to an existing one.
func WithLinkingAllowed() ProviderOpts {
return func(p *Provider) {
p.isLinkingAllowed = true
}
}
// WithCreationAllowed allows end users to create a new user using the federated information.
func WithCreationAllowed() ProviderOpts {
return func(p *Provider) {
p.isCreationAllowed = true
}
}
// WithAutoCreation enables that federated users are automatically created if not already existing.
func WithAutoCreation() ProviderOpts {
return func(p *Provider) {
p.isAutoCreation = true
}
}
// WithAutoUpdate enables that information retrieved from the provider is automatically used to update
// the existing user on each authentication.
func WithAutoUpdate() ProviderOpts {
return func(p *Provider) {
p.isAutoUpdate = true
}
}
// WithoutStartTLS configures to communication insecure with the LDAP server without startTLS
func WithoutStartTLS() ProviderOpts {
return func(p *Provider) {
p.startTLS = false
}
}
// WithCustomIDAttribute configures to map the LDAP attribute to the user, default is the uniqueUserAttribute
func WithCustomIDAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.idAttribute = name
}
}
// WithFirstNameAttribute configures to map the LDAP attribute to the user
func WithFirstNameAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.firstNameAttribute = name
}
}
// WithLastNameAttribute configures to map the LDAP attribute to the user
func WithLastNameAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.lastNameAttribute = name
}
}
// WithDisplayNameAttribute configures to map the LDAP attribute to the user
func WithDisplayNameAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.displayNameAttribute = name
}
}
// WithNickNameAttribute configures to map the LDAP attribute to the user
func WithNickNameAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.nickNameAttribute = name
}
}
// WithPreferredUsernameAttribute configures to map the LDAP attribute to the user
func WithPreferredUsernameAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.preferredUsernameAttribute = name
}
}
// WithEmailAttribute configures to map the LDAP attribute to the user
func WithEmailAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.emailAttribute = name
}
}
// WithEmailVerifiedAttribute configures to map the LDAP attribute to the user
func WithEmailVerifiedAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.emailVerifiedAttribute = name
}
}
// WithPhoneAttribute configures to map the LDAP attribute to the user
func WithPhoneAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.phoneAttribute = name
}
}
// WithPhoneVerifiedAttribute configures to map the LDAP attribute to the user
func WithPhoneVerifiedAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.phoneVerifiedAttribute = name
}
}
// WithPreferredLanguageAttribute configures to map the LDAP attribute to the user
func WithPreferredLanguageAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.preferredLanguageAttribute = name
}
}
// WithAvatarURLAttribute configures to map the LDAP attribute to the user
func WithAvatarURLAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.avatarURLAttribute = name
}
}
// WithProfileAttribute configures to map the LDAP attribute to the user
func WithProfileAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.profileAttribute = name
}
}
func New(
name string,
servers []string,
baseDN string,
bindDN string,
bindPassword string,
userBase string,
userObjectClasses []string,
userFilters []string,
timeout time.Duration,
rootCA []byte,
loginUrl string,
options ...ProviderOpts,
) *Provider {
provider := &Provider{
name: name,
servers: servers,
startTLS: true,
baseDN: baseDN,
bindDN: bindDN,
bindPassword: bindPassword,
userBase: userBase,
userObjectClasses: userObjectClasses,
userFilters: userFilters,
timeout: timeout,
rootCA: rootCA,
loginUrl: loginUrl,
}
for _, option := range options {
option(provider)
}
return provider
}
func (p *Provider) Name() string {
return p.name
}
func (p *Provider) BeginAuth(ctx context.Context, state string, _ ...idp.Parameter) (idp.Session, error) {
return &Session{
Provider: p,
loginUrl: p.loginUrl + state,
}, nil
}
func (p *Provider) GetSession(username, password string) *Session {
return &Session{
Provider: p,
User: username,
Password: password,
}
}
func (p *Provider) IsLinkingAllowed() bool {
return p.isLinkingAllowed
}
func (p *Provider) IsCreationAllowed() bool {
return p.isCreationAllowed
}
func (p *Provider) IsAutoCreation() bool {
return p.isAutoCreation
}
func (p *Provider) IsAutoUpdate() bool {
return p.isAutoUpdate
}
func (p *Provider) getNecessaryAttributes() []string {
attributes := []string{p.userBase}
if p.idAttribute != "" {
attributes = append(attributes, p.idAttribute)
}
if p.firstNameAttribute != "" {
attributes = append(attributes, p.firstNameAttribute)
}
if p.lastNameAttribute != "" {
attributes = append(attributes, p.lastNameAttribute)
}
if p.displayNameAttribute != "" {
attributes = append(attributes, p.displayNameAttribute)
}
if p.nickNameAttribute != "" {
attributes = append(attributes, p.nickNameAttribute)
}
if p.preferredUsernameAttribute != "" {
attributes = append(attributes, p.preferredUsernameAttribute)
}
if p.emailAttribute != "" {
attributes = append(attributes, p.emailAttribute)
}
if p.emailVerifiedAttribute != "" {
attributes = append(attributes, p.emailVerifiedAttribute)
}
if p.phoneAttribute != "" {
attributes = append(attributes, p.phoneAttribute)
}
if p.phoneVerifiedAttribute != "" {
attributes = append(attributes, p.phoneVerifiedAttribute)
}
if p.preferredLanguageAttribute != "" {
attributes = append(attributes, p.preferredLanguageAttribute)
}
if p.avatarURLAttribute != "" {
attributes = append(attributes, p.avatarURLAttribute)
}
if p.profileAttribute != "" {
attributes = append(attributes, p.profileAttribute)
}
return attributes
}

View File

@@ -0,0 +1,207 @@
package ldap
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestProvider_Options(t *testing.T) {
type fields struct {
name string
servers []string
baseDN string
bindDN string
bindPassword string
userBase string
userObjectClasses []string
userFilters []string
timeout time.Duration
rootCA []byte
loginUrl string
opts []ProviderOpts
}
type want struct {
name string
rootCA []byte
startTls bool
linkingAllowed bool
creationAllowed bool
autoCreation bool
autoUpdate bool
idAttribute string
firstNameAttribute string
lastNameAttribute string
displayNameAttribute string
nickNameAttribute string
preferredUsernameAttribute string
emailAttribute string
emailVerifiedAttribute string
phoneAttribute string
phoneVerifiedAttribute string
preferredLanguageAttribute string
avatarURLAttribute string
profileAttribute string
}
tests := []struct {
name string
fields fields
want want
}{
{
name: "default",
fields: fields{
name: "ldap",
servers: []string{"server"},
baseDN: "base",
bindDN: "binddn",
bindPassword: "password",
userBase: "user",
userObjectClasses: []string{"object"},
userFilters: []string{"filter"},
timeout: 30 * time.Second,
loginUrl: "url",
opts: nil,
},
want: want{
name: "ldap",
startTls: true,
linkingAllowed: false,
creationAllowed: false,
autoCreation: false,
autoUpdate: false,
idAttribute: "",
},
},
{
name: "all true",
fields: fields{
name: "ldap",
servers: []string{"server"},
baseDN: "base",
bindDN: "binddn",
bindPassword: "password",
userBase: "user",
userObjectClasses: []string{"object"},
userFilters: []string{"filter"},
timeout: 30 * time.Second,
loginUrl: "url",
opts: []ProviderOpts{
WithoutStartTLS(),
WithLinkingAllowed(),
WithCreationAllowed(),
WithAutoCreation(),
WithAutoUpdate(),
},
},
want: want{
name: "ldap",
startTls: false,
linkingAllowed: true,
creationAllowed: true,
autoCreation: true,
autoUpdate: true,
idAttribute: "",
},
}, {
name: "all true, attributes set",
fields: fields{
name: "ldap",
servers: []string{"server"},
baseDN: "base",
bindDN: "binddn",
bindPassword: "password",
userBase: "user",
userObjectClasses: []string{"object"},
userFilters: []string{"filter"},
timeout: 30 * time.Second,
rootCA: []byte("certificate"),
loginUrl: "url",
opts: []ProviderOpts{
WithoutStartTLS(),
WithLinkingAllowed(),
WithCreationAllowed(),
WithAutoCreation(),
WithAutoUpdate(),
WithCustomIDAttribute("id"),
WithFirstNameAttribute("first"),
WithLastNameAttribute("last"),
WithDisplayNameAttribute("display"),
WithNickNameAttribute("nick"),
WithPreferredUsernameAttribute("prefUser"),
WithEmailAttribute("email"),
WithEmailVerifiedAttribute("emailVerified"),
WithPhoneAttribute("phone"),
WithPhoneVerifiedAttribute("phoneVerified"),
WithPreferredLanguageAttribute("prefLang"),
WithAvatarURLAttribute("avatar"),
WithProfileAttribute("profile"),
},
},
want: want{
name: "ldap",
rootCA: []byte("certificate"),
startTls: false,
linkingAllowed: true,
creationAllowed: true,
autoCreation: true,
autoUpdate: true,
idAttribute: "id",
firstNameAttribute: "first",
lastNameAttribute: "last",
displayNameAttribute: "display",
nickNameAttribute: "nick",
preferredUsernameAttribute: "prefUser",
emailAttribute: "email",
emailVerifiedAttribute: "emailVerified",
phoneAttribute: "phone",
phoneVerifiedAttribute: "phoneVerified",
preferredLanguageAttribute: "prefLang",
avatarURLAttribute: "avatar",
profileAttribute: "profile",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := assert.New(t)
provider := New(
tt.fields.name,
tt.fields.servers,
tt.fields.baseDN,
tt.fields.bindDN,
tt.fields.bindPassword,
tt.fields.userBase,
tt.fields.userObjectClasses,
tt.fields.userFilters,
tt.fields.timeout,
tt.fields.rootCA,
tt.fields.loginUrl,
tt.fields.opts...,
)
a.Equal(tt.want.name, provider.Name())
a.Equal(tt.want.rootCA, provider.rootCA)
a.Equal(tt.want.startTls, provider.startTLS)
a.Equal(tt.want.linkingAllowed, provider.IsLinkingAllowed())
a.Equal(tt.want.creationAllowed, provider.IsCreationAllowed())
a.Equal(tt.want.autoCreation, provider.IsAutoCreation())
a.Equal(tt.want.autoUpdate, provider.IsAutoUpdate())
a.Equal(tt.want.idAttribute, provider.idAttribute)
a.Equal(tt.want.firstNameAttribute, provider.firstNameAttribute)
a.Equal(tt.want.lastNameAttribute, provider.lastNameAttribute)
a.Equal(tt.want.displayNameAttribute, provider.displayNameAttribute)
a.Equal(tt.want.nickNameAttribute, provider.nickNameAttribute)
a.Equal(tt.want.preferredUsernameAttribute, provider.preferredUsernameAttribute)
a.Equal(tt.want.emailAttribute, provider.emailAttribute)
a.Equal(tt.want.emailVerifiedAttribute, provider.emailVerifiedAttribute)
a.Equal(tt.want.phoneAttribute, provider.phoneAttribute)
a.Equal(tt.want.phoneVerifiedAttribute, provider.phoneVerifiedAttribute)
a.Equal(tt.want.preferredLanguageAttribute, provider.preferredLanguageAttribute)
a.Equal(tt.want.avatarURLAttribute, provider.avatarURLAttribute)
a.Equal(tt.want.profileAttribute, provider.profileAttribute)
})
}
}

View File

@@ -0,0 +1,333 @@
package ldap
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"net"
"net/url"
"strconv"
"time"
"unicode/utf8"
"github.com/go-ldap/ldap/v3"
"github.com/zitadel/logging"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp"
)
var ErrNoSingleUser = errors.New("user does not exist or too many entries returned")
var ErrFailedLogin = errors.New("user failed to login")
var ErrUnableToAppendRootCA = errors.New("unable to append rootCA")
var _ idp.Session = (*Session)(nil)
type Session struct {
Provider *Provider
loginUrl string
User string
Password string
Entry *ldap.Entry
}
func NewSession(provider *Provider, username, password string) *Session {
return &Session{Provider: provider, User: username, Password: password}
}
// GetAuth implements the [idp.Session] interface.
func (s *Session) GetAuth(ctx context.Context) (idp.Auth, error) {
return idp.Redirect(s.loginUrl)
}
// PersistentParameters implements the [idp.Session] interface.
func (s *Session) PersistentParameters() map[string]any {
return nil
}
// FetchUser implements the [idp.Session] interface.
func (s *Session) FetchUser(_ context.Context) (_ idp.User, err error) {
var user *ldap.Entry
for _, server := range s.Provider.servers {
user, err = tryBind(server,
s.Provider.startTLS,
s.Provider.bindDN,
s.Provider.bindPassword,
s.Provider.baseDN,
s.Provider.getNecessaryAttributes(),
s.Provider.userObjectClasses,
s.Provider.userFilters,
s.User,
s.Password,
s.Provider.timeout,
s.Provider.rootCA)
// If there were invalid credentials or multiple users with the credentials cancel process
if err != nil && (errors.Is(err, ErrFailedLogin) || errors.Is(err, ErrNoSingleUser)) {
return nil, err
}
// If a user bind was successful and user is filled continue with login, otherwise try next server
if err == nil && user != nil {
break
}
}
if err != nil {
return nil, err
}
s.Entry = user
return mapLDAPEntryToUser(
user,
s.Provider.idAttribute,
s.Provider.firstNameAttribute,
s.Provider.lastNameAttribute,
s.Provider.displayNameAttribute,
s.Provider.nickNameAttribute,
s.Provider.preferredUsernameAttribute,
s.Provider.emailAttribute,
s.Provider.emailVerifiedAttribute,
s.Provider.phoneAttribute,
s.Provider.phoneVerifiedAttribute,
s.Provider.preferredLanguageAttribute,
s.Provider.avatarURLAttribute,
s.Provider.profileAttribute,
)
}
func (s *Session) ExpiresAt() time.Time {
return time.Time{} // falls back to the default expiration time
}
func tryBind(
server string,
startTLS bool,
bindDN string,
bindPassword string,
baseDN string,
attributes []string,
objectClasses []string,
userFilters []string,
username string,
password string,
timeout time.Duration,
rootCA []byte,
) (*ldap.Entry, error) {
conn, err := getConnection(server, startTLS, timeout, rootCA)
if err != nil {
return nil, err
}
defer conn.Close()
if err := conn.Bind(bindDN, bindPassword); err != nil {
return nil, err
}
return trySearchAndUserBind(
conn,
baseDN,
attributes,
objectClasses,
userFilters,
username,
password,
timeout,
)
}
func getConnection(
server string,
startTLS bool,
timeout time.Duration,
rootCA []byte,
) (*ldap.Conn, error) {
if timeout == 0 {
timeout = ldap.DefaultTimeout
}
dialer := make([]ldap.DialOpt, 1, 2)
dialer[0] = ldap.DialWithDialer(&net.Dialer{Timeout: timeout})
u, err := url.Parse(server)
if err != nil {
return nil, err
}
if u.Scheme == "ldaps" && len(rootCA) > 0 {
rootCAs := x509.NewCertPool()
if ok := rootCAs.AppendCertsFromPEM(rootCA); !ok {
return nil, ErrUnableToAppendRootCA
}
dialer = append(dialer, ldap.DialWithTLSConfig(&tls.Config{
RootCAs: rootCAs,
}))
}
conn, err := ldap.DialURL(server, dialer...)
if err != nil {
return nil, err
}
if u.Scheme == "ldap" && startTLS {
err = conn.StartTLS(&tls.Config{ServerName: u.Host})
if err != nil {
return nil, err
}
}
return conn, nil
}
func trySearchAndUserBind(
conn *ldap.Conn,
baseDN string,
attributes []string,
objectClasses []string,
userFilters []string,
username string,
password string,
timeout time.Duration,
) (*ldap.Entry, error) {
searchQuery := queriesAndToSearchQuery(
objectClassesToSearchQuery(objectClasses),
queriesOrToSearchQuery(
userFiltersToSearchQuery(userFilters, username)...,
),
)
// Search for user with the unique attribute for the userDN
searchRequest := ldap.NewSearchRequest(
baseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, int(timeout.Seconds()), false,
searchQuery,
attributes,
nil,
)
sr, err := conn.Search(searchRequest)
if err != nil {
return nil, err
}
if len(sr.Entries) != 1 {
logging.WithFields("entries", len(sr.Entries)).Info("ldap: no single user found")
return nil, ErrNoSingleUser
}
user := sr.Entries[0]
// Bind as the user to verify their password
userDN, err := ldap.ParseDN(user.DN)
if err != nil {
logging.WithFields("userDN", user.DN).WithError(err).Info("ldap user parse DN failed")
return nil, err
}
if err = conn.Bind(userDN.String(), password); err != nil {
logging.WithFields("userDN", user.DN).WithError(err).Info("ldap user bind failed")
return nil, ErrFailedLogin
}
return user, nil
}
func queriesAndToSearchQuery(queries ...string) string {
if len(queries) == 0 {
return ""
}
if len(queries) == 1 {
return queries[0]
}
joinQueries := "(&"
for _, s := range queries {
joinQueries += s
}
return joinQueries + ")"
}
func queriesOrToSearchQuery(queries ...string) string {
if len(queries) == 0 {
return ""
}
if len(queries) == 1 {
return queries[0]
}
joinQueries := "(|"
for _, s := range queries {
joinQueries += s
}
return joinQueries + ")"
}
func objectClassesToSearchQuery(classes []string) string {
searchQuery := ""
for _, class := range classes {
searchQuery += "(objectClass=" + class + ")"
}
return searchQuery
}
func userFiltersToSearchQuery(filters []string, username string) []string {
searchQueries := make([]string, len(filters))
for i, filter := range filters {
searchQueries[i] = "(" + filter + "=" + username + ")"
}
return searchQueries
}
func mapLDAPEntryToUser(
user *ldap.Entry,
idAttribute,
firstNameAttribute,
lastNameAttribute,
displayNameAttribute,
nickNameAttribute,
preferredUsernameAttribute,
emailAttribute,
emailVerifiedAttribute,
phoneAttribute,
phoneVerifiedAttribute,
preferredLanguageAttribute,
avatarURLAttribute,
profileAttribute string,
) (_ *User, err error) {
var emailVerified bool
if v := user.GetAttributeValue(emailVerifiedAttribute); v != "" {
emailVerified, err = strconv.ParseBool(v)
if err != nil {
return nil, err
}
}
var phoneVerified bool
if v := user.GetAttributeValue(phoneVerifiedAttribute); v != "" {
phoneVerified, err = strconv.ParseBool(v)
if err != nil {
return nil, err
}
}
return NewUser(
getAttributeValue(user, idAttribute),
getAttributeValue(user, firstNameAttribute),
getAttributeValue(user, lastNameAttribute),
getAttributeValue(user, displayNameAttribute),
getAttributeValue(user, nickNameAttribute),
getAttributeValue(user, preferredUsernameAttribute),
domain.EmailAddress(user.GetAttributeValue(emailAttribute)),
emailVerified,
domain.PhoneNumber(user.GetAttributeValue(phoneAttribute)),
phoneVerified,
language.Make(user.GetAttributeValue(preferredLanguageAttribute)),
user.GetAttributeValue(avatarURLAttribute),
user.GetAttributeValue(profileAttribute),
), nil
}
func getAttributeValue(user *ldap.Entry, attribute string) string {
// return an empty string if no attribute is needed
if attribute == "" {
return ""
}
value := user.GetAttributeValue(attribute)
if utf8.ValidString(value) {
return value
}
return base64.StdEncoding.EncodeToString(user.GetRawAttributeValue(attribute))
}

View File

@@ -0,0 +1,400 @@
package ldap
import (
"testing"
"github.com/go-ldap/ldap/v3"
"github.com/stretchr/testify/assert"
"golang.org/x/text/language"
)
func TestProvider_objectClassesToSearchQuery(t *testing.T) {
tests := []struct {
name string
fields []string
want string
}{
{
name: "zero",
fields: []string{},
want: "",
},
{
name: "one",
fields: []string{"test"},
want: "(objectClass=test)",
},
{
name: "three",
fields: []string{"test1", "test2", "test3"},
want: "(objectClass=test1)(objectClass=test2)(objectClass=test3)",
},
{
name: "five",
fields: []string{"test1", "test2", "test3", "test4", "test5"},
want: "(objectClass=test1)(objectClass=test2)(objectClass=test3)(objectClass=test4)(objectClass=test5)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := assert.New(t)
a.Equal(tt.want, objectClassesToSearchQuery(tt.fields))
})
}
}
func TestProvider_userFiltersToSearchQuery(t *testing.T) {
tests := []struct {
name string
fields []string
username string
want []string
}{
{
name: "zero",
fields: []string{},
username: "user",
want: []string{},
},
{
name: "one",
fields: []string{"test"},
username: "user",
want: []string{"(test=user)"},
},
{
name: "three",
fields: []string{"test1", "test2", "test3"},
username: "user",
want: []string{"(test1=user)", "(test2=user)", "(test3=user)"},
},
{
name: "five",
fields: []string{"test1", "test2", "test3", "test4", "test5"},
username: "user",
want: []string{"(test1=user)", "(test2=user)", "(test3=user)", "(test4=user)", "(test5=user)"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := assert.New(t)
a.Equal(tt.want, userFiltersToSearchQuery(tt.fields, tt.username))
})
}
}
func TestProvider_queriesAndToSearchQuery(t *testing.T) {
tests := []struct {
name string
fields []string
want string
}{
{
name: "zero",
fields: []string{},
want: "",
},
{
name: "one",
fields: []string{"(test)"},
want: "(test)",
},
{
name: "three",
fields: []string{"(test1)", "(test2)", "(test3)"},
want: "(&(test1)(test2)(test3))",
},
{
name: "five",
fields: []string{"(test1)", "(test2)", "(test3)", "(test4)", "(test5)"},
want: "(&(test1)(test2)(test3)(test4)(test5))",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := assert.New(t)
a.Equal(tt.want, queriesAndToSearchQuery(tt.fields...))
})
}
}
func TestProvider_queriesOrToSearchQuery(t *testing.T) {
tests := []struct {
name string
fields []string
want string
}{
{
name: "zero",
fields: []string{},
want: "",
},
{
name: "one",
fields: []string{"(test)"},
want: "(test)",
},
{
name: "three",
fields: []string{"(test1)", "(test2)", "(test3)"},
want: "(|(test1)(test2)(test3))",
},
{
name: "five",
fields: []string{"(test1)", "(test2)", "(test3)", "(test4)", "(test5)"},
want: "(|(test1)(test2)(test3)(test4)(test5))",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := assert.New(t)
a.Equal(tt.want, queriesOrToSearchQuery(tt.fields...))
})
}
}
func TestProvider_mapLDAPEntryToUser(t *testing.T) {
type fields struct {
user *ldap.Entry
idAttribute string
firstNameAttribute string
lastNameAttribute string
displayNameAttribute string
nickNameAttribute string
preferredUsernameAttribute string
emailAttribute string
emailVerifiedAttribute string
phoneAttribute string
phoneVerifiedAttribute string
preferredLanguageAttribute string
avatarURLAttribute string
profileAttribute string
}
type want struct {
user *User
err func(error) bool
}
tests := []struct {
name string
fields fields
want want
}{
{
name: "empty",
fields: fields{
user: &ldap.Entry{
Attributes: []*ldap.EntryAttribute{
{Name: "id", Values: []string{"id"}},
{Name: "first", Values: []string{"first"}},
{Name: "last", Values: []string{"last"}},
{Name: "display", Values: []string{"display"}},
{Name: "nick", Values: []string{"nick"}},
{Name: "preferred", Values: []string{"preferred"}},
{Name: "email", Values: []string{"email"}},
{Name: "emailVerified", Values: []string{"false"}},
{Name: "phone", Values: []string{"phone"}},
{Name: "phoneVerified", Values: []string{"false"}},
{Name: "lang", Values: []string{"und"}},
{Name: "avatar", Values: []string{"avatar"}},
{Name: "profile", Values: []string{"profile"}},
},
},
idAttribute: "",
firstNameAttribute: "",
lastNameAttribute: "",
displayNameAttribute: "",
nickNameAttribute: "",
preferredUsernameAttribute: "",
emailAttribute: "",
emailVerifiedAttribute: "",
phoneAttribute: "",
phoneVerifiedAttribute: "",
preferredLanguageAttribute: "",
avatarURLAttribute: "",
profileAttribute: "",
},
want: want{
user: &User{
ID: "",
FirstName: "",
LastName: "",
DisplayName: "",
NickName: "",
PreferredUsername: "",
Email: "",
EmailVerified: false,
Phone: "",
PhoneVerified: false,
PreferredLanguage: language.Tag{},
AvatarURL: "",
Profile: "",
},
},
},
{
name: "failed parse emailVerified",
fields: fields{
user: &ldap.Entry{
Attributes: []*ldap.EntryAttribute{
{Name: "id", Values: []string{"id"}},
{Name: "first", Values: []string{"first"}},
{Name: "last", Values: []string{"last"}},
{Name: "display", Values: []string{"display"}},
{Name: "nick", Values: []string{"nick"}},
{Name: "preferred", Values: []string{"preferred"}},
{Name: "email", Values: []string{"email"}},
{Name: "emailVerified", Values: []string{"failure"}},
{Name: "phone", Values: []string{"phone"}},
{Name: "phoneVerified", Values: []string{"false"}},
{Name: "lang", Values: []string{"und"}},
{Name: "avatar", Values: []string{"avatar"}},
{Name: "profile", Values: []string{"profile"}},
},
},
idAttribute: "id",
firstNameAttribute: "first",
lastNameAttribute: "last",
displayNameAttribute: "display",
nickNameAttribute: "nick",
preferredUsernameAttribute: "preferred",
emailAttribute: "email",
emailVerifiedAttribute: "emailVerified",
phoneAttribute: "phone",
phoneVerifiedAttribute: "phoneVerified",
preferredLanguageAttribute: "lang",
avatarURLAttribute: "avatar",
profileAttribute: "profile",
},
want: want{
err: func(err error) bool {
return err != nil
},
},
},
{
name: "failed parse phoneVerified",
fields: fields{
user: &ldap.Entry{
Attributes: []*ldap.EntryAttribute{
{Name: "id", Values: []string{"id"}},
{Name: "first", Values: []string{"first"}},
{Name: "last", Values: []string{"last"}},
{Name: "display", Values: []string{"display"}},
{Name: "nick", Values: []string{"nick"}},
{Name: "preferred", Values: []string{"preferred"}},
{Name: "email", Values: []string{"email"}},
{Name: "emailVerified", Values: []string{"false"}},
{Name: "phone", Values: []string{"phone"}},
{Name: "phoneVerified", Values: []string{"failure"}},
{Name: "lang", Values: []string{"und"}},
{Name: "avatar", Values: []string{"avatar"}},
{Name: "profile", Values: []string{"profile"}},
},
},
idAttribute: "id",
firstNameAttribute: "first",
lastNameAttribute: "last",
displayNameAttribute: "display",
nickNameAttribute: "nick",
preferredUsernameAttribute: "preferred",
emailAttribute: "email",
emailVerifiedAttribute: "emailVerified",
phoneAttribute: "phone",
phoneVerifiedAttribute: "phoneVerified",
preferredLanguageAttribute: "lang",
avatarURLAttribute: "avatar",
profileAttribute: "profile",
},
want: want{
err: func(err error) bool {
return err != nil
},
},
},
{
name: "full user",
fields: fields{
user: &ldap.Entry{
Attributes: []*ldap.EntryAttribute{
{Name: "id", Values: []string{"id"}},
{Name: "first", Values: []string{"first"}},
{Name: "last", Values: []string{"last"}},
{Name: "display", Values: []string{"display"}},
{Name: "nick", Values: []string{"nick"}},
{Name: "preferred", Values: []string{"preferred"}},
{Name: "email", Values: []string{"email"}},
{Name: "emailVerified", Values: []string{"false"}},
{Name: "phone", Values: []string{"phone"}},
{Name: "phoneVerified", Values: []string{"false"}},
{Name: "lang", Values: []string{"und"}},
{Name: "avatar", Values: []string{"avatar"}},
{Name: "profile", Values: []string{"profile"}},
},
},
idAttribute: "id",
firstNameAttribute: "first",
lastNameAttribute: "last",
displayNameAttribute: "display",
nickNameAttribute: "nick",
preferredUsernameAttribute: "preferred",
emailAttribute: "email",
emailVerifiedAttribute: "emailVerified",
phoneAttribute: "phone",
phoneVerifiedAttribute: "phoneVerified",
preferredLanguageAttribute: "lang",
avatarURLAttribute: "avatar",
profileAttribute: "profile",
},
want: want{
user: &User{
ID: "id",
FirstName: "first",
LastName: "last",
DisplayName: "display",
NickName: "nick",
PreferredUsername: "preferred",
Email: "email",
EmailVerified: false,
Phone: "phone",
PhoneVerified: false,
PreferredLanguage: language.Make("und"),
AvatarURL: "avatar",
Profile: "profile",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := mapLDAPEntryToUser(
tt.fields.user,
tt.fields.idAttribute,
tt.fields.firstNameAttribute,
tt.fields.lastNameAttribute,
tt.fields.displayNameAttribute,
tt.fields.nickNameAttribute,
tt.fields.preferredUsernameAttribute,
tt.fields.emailAttribute,
tt.fields.emailVerifiedAttribute,
tt.fields.phoneAttribute,
tt.fields.phoneVerifiedAttribute,
tt.fields.preferredLanguageAttribute,
tt.fields.avatarURLAttribute,
tt.fields.profileAttribute,
)
if tt.want.err == nil {
assert.NoError(t, err)
}
if tt.want.err != nil && !tt.want.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.want.err == nil {
assert.Equal(t, tt.want.user, got)
}
})
}
}

View File

@@ -0,0 +1,95 @@
package ldap
import (
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
)
type User struct {
ID string `json:"id,omitempty"`
FirstName string `json:"firstName,omitempty"`
LastName string `json:"lastName,omitempty"`
DisplayName string `json:"displayName,omitempty"`
NickName string `json:"nickName,omitempty"`
PreferredUsername string `json:"preferredUsername,omitempty"`
Email domain.EmailAddress `json:"email,omitempty"`
EmailVerified bool `json:"emailVerified,omitempty"`
Phone domain.PhoneNumber `json:"phone,omitempty"`
PhoneVerified bool `json:"phoneVerified,omitempty"`
PreferredLanguage language.Tag `json:"preferredLanguage,omitempty"`
AvatarURL string `json:"avatarURL,omitempty"`
Profile string `json:"profile,omitempty"`
}
func NewUser(
id string,
firstName string,
lastName string,
displayName string,
nickName string,
preferredUsername string,
email domain.EmailAddress,
emailVerified bool,
phone domain.PhoneNumber,
phoneVerified bool,
preferredLanguage language.Tag,
avatarURL string,
profile string,
) *User {
return &User{
id,
firstName,
lastName,
displayName,
nickName,
preferredUsername,
email,
emailVerified,
phone,
phoneVerified,
preferredLanguage,
avatarURL,
profile,
}
}
func (u *User) GetID() string {
return u.ID
}
func (u *User) GetFirstName() string {
return u.FirstName
}
func (u *User) GetLastName() string {
return u.LastName
}
func (u *User) GetDisplayName() string {
return u.DisplayName
}
func (u *User) GetNickname() string {
return u.NickName
}
func (u *User) GetPreferredUsername() string {
return u.PreferredUsername
}
func (u *User) GetEmail() domain.EmailAddress {
return u.Email
}
func (u *User) IsEmailVerified() bool {
return u.EmailVerified
}
func (u *User) GetPhone() domain.PhoneNumber {
return u.Phone
}
func (u *User) IsPhoneVerified() bool {
return u.PhoneVerified
}
func (u *User) GetPreferredLanguage() language.Tag {
return u.PreferredLanguage
}
func (u *User) GetAvatarURL() string {
return u.AvatarURL
}
func (u *User) GetProfile() string {
return u.Profile
}