github.com/greenpau/go-authcrunch@v1.1.4/pkg/ids/ldap/store.go (about)

     1  // Copyright 2022 Paul Greenberg greenpau@outlook.com
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package ldap
    16  
    17  import (
    18  	"encoding/json"
    19  	"github.com/greenpau/go-authcrunch/pkg/authn/enums/operator"
    20  	"github.com/greenpau/go-authcrunch/pkg/authn/icons"
    21  	"github.com/greenpau/go-authcrunch/pkg/errors"
    22  	"github.com/greenpau/go-authcrunch/pkg/requests"
    23  	"go.uber.org/zap"
    24  	"net/url"
    25  	"regexp"
    26  	"strings"
    27  )
    28  
    29  const (
    30  	storeKind = "ldap"
    31  )
    32  
    33  var (
    34  	emailRegexPattern    = regexp.MustCompile("^[a-zA-Z0-9.+\\._~-]{1,61}@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
    35  	usernameRegexPattern = regexp.MustCompile("^[a-zA-Z0-9.+\\._~-]{1,61}$")
    36  )
    37  
    38  // Config holds the configuration for the IdentityStore.
    39  type Config struct {
    40  	Name               string         `json:"name,omitempty" xml:"name,omitempty" yaml:"name,omitempty"`
    41  	Realm              string         `json:"realm,omitempty" xml:"realm,omitempty" yaml:"realm,omitempty"`
    42  	Servers            []AuthServer   `json:"servers,omitempty" xml:"servers,omitempty" yaml:"servers,omitempty"`
    43  	BindUsername       string         `json:"bind_username,omitempty" xml:"bind_username,omitempty" yaml:"bind_username,omitempty"`
    44  	BindPassword       string         `json:"bind_password,omitempty" xml:"bind_password,omitempty" yaml:"bind_password,omitempty"`
    45  	Attributes         UserAttributes `json:"attributes,omitempty" xml:"attributes,omitempty" yaml:"attributes,omitempty"`
    46  	SearchBaseDN       string         `json:"search_base_dn,omitempty" xml:"search_base_dn,omitempty" yaml:"search_base_dn,omitempty"`
    47  	SearchUserFilter   string         `json:"search_user_filter,omitempty" xml:"search_user_filter,omitempty" yaml:"search_user_filter,omitempty"`
    48  	SearchGroupFilter  string         `json:"search_group_filter,omitempty" xml:"search_group_filter,omitempty" yaml:"search_group_filter,omitempty"`
    49  	Groups             []UserGroup    `json:"groups,omitempty" xml:"groups,omitempty" yaml:"groups,omitempty"`
    50  	TrustedAuthorities []string       `json:"trusted_authorities,omitempty" xml:"trusted_authorities,omitempty" yaml:"trusted_authorities,omitempty"`
    51  
    52  	// LoginIcon is the UI login icon attributes.
    53  	LoginIcon *icons.LoginIcon `json:"login_icon,omitempty" xml:"login_icon,omitempty" yaml:"login_icon,omitempty"`
    54  
    55  	// RegistrationEnabled controls whether visitors can registers.
    56  	RegistrationEnabled bool `json:"registration_enabled,omitempty" xml:"registration_enabled,omitempty" yaml:"registration_enabled,omitempty"`
    57  	// UsernameRecoveryEnabled controls whether a user could recover username by providing an email address.
    58  	UsernameRecoveryEnabled bool `json:"username_recovery_enabled,omitempty" xml:"username_recovery_enabled,omitempty" yaml:"username_recovery_enabled,omitempty"`
    59  	// PasswordRecoveryEnabled controls whether a user could recover password by providing an email address.
    60  	PasswordRecoveryEnabled bool `json:"password_recovery_enabled,omitempty" xml:"password_recovery_enabled,omitempty" yaml:"password_recovery_enabled,omitempty"`
    61  	// ContactSupportEnabled controls whether contact support link is available.
    62  	ContactSupportEnabled bool `json:"contact_support_enabled,omitempty" xml:"contact_support_enabled,omitempty" yaml:"contact_support_enabled,omitempty"`
    63  
    64  	// SupportLink is the link to the support portal.
    65  	SupportLink string `json:"support_link,omitempty" xml:"support_link,omitempty" yaml:"support_link,omitempty"`
    66  	// SupportEmail is the email address to reach support.
    67  	SupportEmail string `json:"support_email,omitempty" xml:"support_email,omitempty" yaml:"support_email,omitempty"`
    68  
    69  	// The roles assigned to a user when no matching LDAP groups found.
    70  	FallbackRoles []string `json:"fallback_roles,omitempty" xml:"fallback_roles,omitempty" yaml:"fallback_roles,omitempty"`
    71  }
    72  
    73  // UserGroup represent the binding between BaseDN and a serarch filter.
    74  // Upon successful authentation for the combination, a user gets
    75  // assigned the roles associated with the binding.
    76  type UserGroup struct {
    77  	GroupDN string   `json:"dn,omitempty" xml:"dn,omitempty" yaml:"dn,omitempty"`
    78  	Roles   []string `json:"roles,omitempty" xml:"roles,omitempty" yaml:"roles,omitempty"`
    79  }
    80  
    81  // AuthServer represents an instance of LDAP server.
    82  type AuthServer struct {
    83  	Address          string   `json:"address,omitempty" xml:"address,omitempty" yaml:"address,omitempty"`
    84  	URL              *url.URL `json:"-"`
    85  	Port             string   `json:"-"`
    86  	Encrypted        bool     `json:"-"`
    87  	IgnoreCertErrors bool     `json:"ignore_cert_errors,omitempty" xml:"ignore_cert_errors,omitempty" yaml:"ignore_cert_errors,omitempty"`
    88  	PosixGroups      bool     `json:"posix_groups,omitempty" xml:"posix_groups,omitempty" yaml:"posix_groups,omitempty"`
    89  	Timeout          int      `json:"timeout,omitempty" xml:"timeout,omitempty" yaml:"timeout,omitempty"`
    90  }
    91  
    92  // UserAttributes represent the mapping of LDAP attributes
    93  // to JWT fields.
    94  type UserAttributes struct {
    95  	Name     string `json:"name,omitempty" xml:"name,omitempty" yaml:"name,omitempty"`
    96  	Surname  string `json:"surname,omitempty" xml:"surname,omitempty" yaml:"surname,omitempty"`
    97  	Username string `json:"username,omitempty" xml:"username,omitempty" yaml:"username,omitempty"`
    98  	MemberOf string `json:"member_of,omitempty" xml:"member_of,omitempty" yaml:"member_of,omitempty"`
    99  	Email    string `json:"email,omitempty" xml:"email,omitempty" yaml:"email,omitempty"`
   100  }
   101  
   102  // IdentityStore represents authentication provider with LDAP identity store.
   103  type IdentityStore struct {
   104  	config        *Config        `json:"-"`
   105  	authenticator *Authenticator `json:"-"`
   106  	logger        *zap.Logger
   107  	configured    bool
   108  }
   109  
   110  // NewIdentityStore return an instance of LDAP-based identity store.
   111  func NewIdentityStore(cfg *Config, logger *zap.Logger) (*IdentityStore, error) {
   112  	if logger == nil {
   113  		return nil, errors.ErrIdentityStoreConfigureLoggerNotFound
   114  	}
   115  
   116  	b := &IdentityStore{
   117  		config:        cfg,
   118  		authenticator: NewAuthenticator(),
   119  		logger:        logger,
   120  	}
   121  
   122  	if err := b.config.Validate(); err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	return b, nil
   127  }
   128  
   129  // GetRealm return authentication realm.
   130  func (b *IdentityStore) GetRealm() string {
   131  	return b.config.Realm
   132  }
   133  
   134  // GetName return the name associated with this identity store.
   135  func (b *IdentityStore) GetName() string {
   136  	return b.config.Name
   137  }
   138  
   139  // GetKind returns the authentication method associated with this identity store.
   140  func (b *IdentityStore) GetKind() string {
   141  	return storeKind
   142  }
   143  
   144  // Configured returns true if the identity store was configured.
   145  func (b *IdentityStore) Configured() bool {
   146  	return b.configured
   147  }
   148  
   149  // Request performs the requested identity store operation.
   150  func (b *IdentityStore) Request(op operator.Type, r *requests.Request) error {
   151  	switch op {
   152  	case operator.Authenticate:
   153  		return b.Authenticate(r)
   154  	case operator.IdentifyUser:
   155  		return b.IdentifyUser(r)
   156  	case operator.ChangePassword:
   157  		return errors.ErrOperatorNotAvailable.WithArgs(op)
   158  	}
   159  	return errors.ErrOperatorNotSupported.WithArgs(op)
   160  }
   161  
   162  // Authenticate performs authentication.
   163  func (b *IdentityStore) Authenticate(r *requests.Request) error {
   164  	if strings.Contains(r.User.Username, "@") {
   165  		if !emailRegexPattern.MatchString(r.User.Username) {
   166  			return errors.ErrIdentityStoreLdapAuthenticateInvalidUserEmail
   167  		}
   168  	} else {
   169  		if !usernameRegexPattern.MatchString(r.User.Username) {
   170  			return errors.ErrIdentityStoreLdapAuthenticateInvalidUsername
   171  		}
   172  	}
   173  	if len(r.User.Password) < 3 {
   174  		return errors.ErrIdentityStoreLdapAuthenticateInvalidPassword
   175  	}
   176  	return b.authenticator.AuthenticateUser(r)
   177  }
   178  
   179  // IdentifyUser  performs user identification.
   180  func (b *IdentityStore) IdentifyUser(r *requests.Request) error {
   181  	if strings.Contains(r.User.Username, "@") {
   182  		if !emailRegexPattern.MatchString(r.User.Username) {
   183  			return errors.ErrIdentityStoreLdapAuthenticateInvalidUserEmail
   184  		}
   185  	} else {
   186  		if !usernameRegexPattern.MatchString(r.User.Username) {
   187  			return errors.ErrIdentityStoreLdapAuthenticateInvalidUsername
   188  		}
   189  	}
   190  	return b.authenticator.IdentifyUser(r)
   191  }
   192  
   193  // Configure configures IdentityStore.
   194  func (b *IdentityStore) Configure() error {
   195  	b.authenticator.logger = b.logger
   196  
   197  	if err := b.authenticator.ConfigureRealm(b.config); err != nil {
   198  		b.logger.Error("failed configuring realm (domain) for LDAP authentication",
   199  			zap.String("error", err.Error()))
   200  		return err
   201  	}
   202  
   203  	if err := b.authenticator.ConfigureSearch(b.config); err != nil {
   204  		b.logger.Error("failed configuring base DN, search filter, attributes for LDAP queries",
   205  			zap.String("error", err.Error()))
   206  		return err
   207  	}
   208  
   209  	if err := b.authenticator.ConfigureServers(b.config); err != nil {
   210  		b.logger.Error("failed to configure LDAP server addresses",
   211  			zap.String("error", err.Error()))
   212  		return err
   213  	}
   214  
   215  	if err := b.authenticator.ConfigureBindCredentials(b.config); err != nil {
   216  		b.logger.Error("failed configuring user credentials for LDAP binding",
   217  			zap.String("error", err.Error()))
   218  		return err
   219  	}
   220  
   221  	if err := b.authenticator.ConfigureUserGroups(b.config); err != nil {
   222  		b.logger.Error("failed configuring user groups for LDAP search",
   223  			zap.String("error", err.Error()))
   224  		return err
   225  	}
   226  	if err := b.authenticator.ConfigureTrustedAuthorities(b.config); err != nil {
   227  		b.logger.Error("failed configuring trusted authorities",
   228  			zap.String("error", err.Error()))
   229  		return err
   230  	}
   231  
   232  	// Configure UI login icon.
   233  	if b.config.LoginIcon == nil {
   234  		b.config.LoginIcon = icons.NewLoginIcon(storeKind)
   235  	} else {
   236  		b.config.LoginIcon.Configure(storeKind)
   237  	}
   238  
   239  	// Add support and credentials recovery to the UI login icon.
   240  	b.config.LoginIcon.RegistrationEnabled = b.config.RegistrationEnabled
   241  	b.config.LoginIcon.UsernameRecoveryEnabled = b.config.UsernameRecoveryEnabled
   242  	b.config.LoginIcon.PasswordRecoveryEnabled = b.config.PasswordRecoveryEnabled
   243  	b.config.LoginIcon.ContactSupportEnabled = b.config.ContactSupportEnabled
   244  	b.config.LoginIcon.SupportLink = b.config.SupportLink
   245  	b.config.LoginIcon.SupportEmail = b.config.SupportEmail
   246  
   247  	b.logger.Info(
   248  		"successfully configured identity store",
   249  		zap.String("name", b.config.Name),
   250  		zap.String("kind", storeKind),
   251  		zap.Any("login_icon", b.config.LoginIcon),
   252  	)
   253  
   254  	b.configured = true
   255  
   256  	return nil
   257  }
   258  
   259  // GetConfig returns IdentityStore configuration.
   260  func (b *IdentityStore) GetConfig() map[string]interface{} {
   261  	var m map[string]interface{}
   262  	j, _ := json.Marshal(b.config)
   263  	json.Unmarshal(j, &m)
   264  	if _, exists := m["bind_password"]; exists {
   265  		m["bind_password"] = "**masked**"
   266  	}
   267  	return m
   268  }
   269  
   270  // Validate validates identity store configuration.
   271  func (cfg *Config) Validate() error {
   272  	if cfg.Name == "" {
   273  		return errors.ErrIdentityStoreConfigureNameEmpty
   274  	}
   275  	if cfg.Realm == "" {
   276  		return errors.ErrIdentityStoreConfigureRealmEmpty
   277  	}
   278  	return nil
   279  }
   280  
   281  // GetLoginIcon returns the instance of the icon associated with the provider.
   282  func (b *IdentityStore) GetLoginIcon() *icons.LoginIcon {
   283  	return b.config.LoginIcon
   284  }