mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 05:17:33 +00:00
feat: add management for ldap idp template (#5220)
Add management functionality for LDAP idps with templates and the basic functionality for the LDAP provider, which can then be used with a separate login page in the future. --------- Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
237
internal/idp/providers/ldap/ldap.go
Normal file
237
internal/idp/providers/ldap/ldap.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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
|
||||
host string
|
||||
port string
|
||||
tls bool
|
||||
baseDN string
|
||||
userObjectClass string
|
||||
userUniqueAttribute string
|
||||
admin string
|
||||
password string
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// WithCustomPort configures a custom port used for the communication instead of :389 as per default
|
||||
func WithCustomPort(port string) ProviderOpts {
|
||||
return func(p *Provider) {
|
||||
p.port = port
|
||||
}
|
||||
}
|
||||
|
||||
// Insecure configures to communication insecure with the LDAP server without TLS
|
||||
func Insecure() ProviderOpts {
|
||||
return func(p *Provider) {
|
||||
p.tls = 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,
|
||||
host string,
|
||||
baseDN string,
|
||||
userObjectClass string,
|
||||
userUniqueAttribute string,
|
||||
admin string,
|
||||
password string,
|
||||
loginUrl string,
|
||||
options ...ProviderOpts,
|
||||
) *Provider {
|
||||
provider := &Provider{
|
||||
name: name,
|
||||
host: host,
|
||||
port: DefaultPort,
|
||||
tls: true,
|
||||
baseDN: baseDN,
|
||||
userObjectClass: userObjectClass,
|
||||
userUniqueAttribute: userUniqueAttribute,
|
||||
admin: admin,
|
||||
password: password,
|
||||
loginUrl: loginUrl,
|
||||
idAttribute: userUniqueAttribute,
|
||||
}
|
||||
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, params ...any) (idp.Session, error) {
|
||||
return &Session{
|
||||
Provider: p,
|
||||
loginUrl: p.loginUrl + "?state=" + state,
|
||||
}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
185
internal/idp/providers/ldap/ldap_test.go
Normal file
185
internal/idp/providers/ldap/ldap_test.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestProvider_Options(t *testing.T) {
|
||||
type fields struct {
|
||||
name string
|
||||
host string
|
||||
baseDN string
|
||||
userObjectClass string
|
||||
userUniqueAttribute string
|
||||
admin string
|
||||
password string
|
||||
loginUrl string
|
||||
opts []ProviderOpts
|
||||
}
|
||||
type want struct {
|
||||
name string
|
||||
port string
|
||||
tls 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",
|
||||
host: "host",
|
||||
baseDN: "base",
|
||||
userObjectClass: "class",
|
||||
userUniqueAttribute: "attr",
|
||||
admin: "admin",
|
||||
password: "password",
|
||||
loginUrl: "url",
|
||||
opts: nil,
|
||||
},
|
||||
want: want{
|
||||
name: "ldap",
|
||||
port: DefaultPort,
|
||||
tls: true,
|
||||
linkingAllowed: false,
|
||||
creationAllowed: false,
|
||||
autoCreation: false,
|
||||
autoUpdate: false,
|
||||
idAttribute: "attr",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all true",
|
||||
fields: fields{
|
||||
name: "ldap",
|
||||
host: "host",
|
||||
baseDN: "base",
|
||||
userObjectClass: "class",
|
||||
userUniqueAttribute: "attr",
|
||||
admin: "admin",
|
||||
password: "password",
|
||||
loginUrl: "url",
|
||||
opts: []ProviderOpts{
|
||||
WithLinkingAllowed(),
|
||||
WithCreationAllowed(),
|
||||
WithAutoCreation(),
|
||||
WithAutoUpdate(),
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
name: "ldap",
|
||||
port: DefaultPort,
|
||||
tls: true,
|
||||
linkingAllowed: true,
|
||||
creationAllowed: true,
|
||||
autoCreation: true,
|
||||
autoUpdate: true,
|
||||
idAttribute: "attr",
|
||||
},
|
||||
}, {
|
||||
name: "all true, attributes set",
|
||||
fields: fields{
|
||||
name: "ldap",
|
||||
host: "host",
|
||||
baseDN: "base",
|
||||
userObjectClass: "class",
|
||||
userUniqueAttribute: "attr",
|
||||
admin: "admin",
|
||||
password: "password",
|
||||
loginUrl: "url",
|
||||
opts: []ProviderOpts{
|
||||
Insecure(),
|
||||
WithCustomPort("port"),
|
||||
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",
|
||||
port: "port",
|
||||
tls: 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.host, tt.fields.baseDN, tt.fields.userObjectClass, tt.fields.userUniqueAttribute, tt.fields.admin, tt.fields.password, tt.fields.loginUrl, tt.fields.opts...)
|
||||
|
||||
a.Equal(tt.want.name, provider.Name())
|
||||
a.Equal(tt.want.port, provider.port)
|
||||
a.Equal(tt.want.tls, provider.tls)
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
98
internal/idp/providers/ldap/session.go
Normal file
98
internal/idp/providers/ldap/session.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/idp"
|
||||
)
|
||||
|
||||
var ErrNoSingleUser = errors.New("user does not exist or too many entries returned")
|
||||
|
||||
var _ idp.Session = (*Session)(nil)
|
||||
|
||||
type Session struct {
|
||||
Provider *Provider
|
||||
loginUrl string
|
||||
user string
|
||||
password string
|
||||
}
|
||||
|
||||
func (s *Session) GetAuthURL() string {
|
||||
return s.loginUrl
|
||||
}
|
||||
func (s *Session) FetchUser(_ context.Context) (idp.User, error) {
|
||||
l, err := ldap.DialURL("ldap://" + s.Provider.host + ":" + s.Provider.port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
if s.Provider.tls {
|
||||
err = l.StartTLS(&tls.Config{ServerName: s.Provider.host})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Bind as the admin to search for user
|
||||
err = l.Bind("cn="+s.Provider.admin+","+s.Provider.baseDN, s.Provider.password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Search for user with the unique attribute for the userDN
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
s.Provider.baseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
fmt.Sprintf("(&(objectClass="+s.Provider.userObjectClass+")("+s.Provider.userUniqueAttribute+"=%s))", ldap.EscapeFilter(s.user)),
|
||||
[]string{"dn"},
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := l.Search(searchRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(sr.Entries) != 1 {
|
||||
return nil, ErrNoSingleUser
|
||||
}
|
||||
|
||||
user := sr.Entries[0]
|
||||
// Bind as the user to verify their password
|
||||
err = l.Bind(user.DN, s.password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
emailVerified, err := strconv.ParseBool(user.GetAttributeValue(s.Provider.emailVerifiedAttribute))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
phoneVerified, err := strconv.ParseBool(user.GetAttributeValue(s.Provider.phoneVerifiedAttribute))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewUser(
|
||||
user.GetAttributeValue(s.Provider.idAttribute),
|
||||
user.GetAttributeValue(s.Provider.firstNameAttribute),
|
||||
user.GetAttributeValue(s.Provider.lastNameAttribute),
|
||||
user.GetAttributeValue(s.Provider.displayNameAttribute),
|
||||
user.GetAttributeValue(s.Provider.nickNameAttribute),
|
||||
user.GetAttributeValue(s.Provider.preferredUsernameAttribute),
|
||||
user.GetAttributeValue(s.Provider.emailAttribute),
|
||||
emailVerified,
|
||||
user.GetAttributeValue(s.Provider.phoneAttribute),
|
||||
phoneVerified,
|
||||
language.Make(user.GetAttributeValue(s.Provider.preferredLanguageAttribute)),
|
||||
user.GetAttributeValue(s.Provider.avatarURLAttribute),
|
||||
user.GetAttributeValue(s.Provider.profileAttribute),
|
||||
), nil
|
||||
}
|
91
internal/idp/providers/ldap/user.go
Normal file
91
internal/idp/providers/ldap/user.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package ldap
|
||||
|
||||
import "golang.org/x/text/language"
|
||||
|
||||
type User struct {
|
||||
id string
|
||||
firstName string
|
||||
lastName string
|
||||
displayName string
|
||||
nickName string
|
||||
preferredUsername string
|
||||
email string
|
||||
emailVerified bool
|
||||
phone string
|
||||
phoneVerified bool
|
||||
preferredLanguage language.Tag
|
||||
avatarURL string
|
||||
profile string
|
||||
}
|
||||
|
||||
func NewUser(
|
||||
id string,
|
||||
firstName string,
|
||||
lastName string,
|
||||
displayName string,
|
||||
nickName string,
|
||||
preferredUsername string,
|
||||
email string,
|
||||
emailVerified bool,
|
||||
phone string,
|
||||
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() string {
|
||||
return u.email
|
||||
}
|
||||
func (u *User) IsEmailVerified() bool {
|
||||
return u.emailVerified
|
||||
}
|
||||
func (u *User) GetPhone() string {
|
||||
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
|
||||
}
|
Reference in New Issue
Block a user