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 }