// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package ldap import ( "context" "fmt" "io" "os" "strings" "github.com/hashicorp/vault/api" ) type LDAPAuth struct { mountPath string username string password string passwordFile string passwordEnv string } type Password struct { // Path on the file system where the LDAP password can be found. FromFile string // The name of the environment variable containing the LDAP password FromEnv string // The password as a plaintext string value. FromString string } var _ api.AuthMethod = (*LDAPAuth)(nil) type LoginOption func(a *LDAPAuth) error const ( defaultMountPath = "ldap" ) // NewLDAPAuth initializes a new LDAP auth method interface to be // passed as a parameter to the client.Auth().Login method. // // Supported options: WithMountPath func NewLDAPAuth(username string, password *Password, opts ...LoginOption) (*LDAPAuth, error) { if username == "" { return nil, fmt.Errorf("no user name provided for login") } if password == nil { return nil, fmt.Errorf("no password provided for login") } err := password.validate() if err != nil { return nil, fmt.Errorf("invalid password: %w", err) } a := &LDAPAuth{ mountPath: defaultMountPath, username: username, } if password.FromFile != "" { a.passwordFile = password.FromFile } if password.FromEnv != "" { a.passwordEnv = password.FromEnv } if password.FromString != "" { a.password = password.FromString } // Loop through each option for _, opt := range opts { // Call the option giving the instantiated // *LDAPAuth as the argument err := opt(a) if err != nil { return nil, fmt.Errorf("error with login option: %w", err) } } // return the modified auth struct instance return a, nil } func (a *LDAPAuth) Login(ctx context.Context, client *api.Client) (*api.Secret, error) { if ctx == nil { ctx = context.Background() } loginData := make(map[string]interface{}) if a.passwordFile != "" { passwordValue, err := a.readPasswordFromFile() if err != nil { return nil, fmt.Errorf("error reading password: %w", err) } loginData["password"] = passwordValue } else if a.passwordEnv != "" { passwordValue := os.Getenv(a.passwordEnv) if passwordValue == "" { return nil, fmt.Errorf("password was specified with an environment variable with an empty value") } loginData["password"] = passwordValue } else { loginData["password"] = a.password } path := fmt.Sprintf("auth/%s/login/%s", a.mountPath, a.username) resp, err := client.Logical().WriteWithContext(ctx, path, loginData) if err != nil { return nil, fmt.Errorf("unable to log in with LDAP auth: %w", err) } return resp, nil } func WithMountPath(mountPath string) LoginOption { return func(a *LDAPAuth) error { a.mountPath = mountPath return nil } } func (a *LDAPAuth) readPasswordFromFile() (string, error) { passwordFile, err := os.Open(a.passwordFile) if err != nil { return "", fmt.Errorf("unable to open file containing password: %w", err) } defer passwordFile.Close() limitedReader := io.LimitReader(passwordFile, 1000) passwordBytes, err := io.ReadAll(limitedReader) if err != nil { return "", fmt.Errorf("unable to read password: %w", err) } passwordValue := strings.TrimSuffix(string(passwordBytes), "\n") return passwordValue, nil } func (password *Password) validate() error { if password.FromFile == "" && password.FromEnv == "" && password.FromString == "" { return fmt.Errorf("password for LDAP auth must be provided with a source file, environment variable, or plaintext string") } if password.FromFile != "" { if password.FromEnv != "" || password.FromString != "" { return fmt.Errorf("only one source for the password should be specified") } } if password.FromEnv != "" { if password.FromFile != "" || password.FromString != "" { return fmt.Errorf("only one source for the password should be specified") } } if password.FromString != "" { if password.FromFile != "" || password.FromEnv != "" { return fmt.Errorf("only one source for the password should be specified") } } return nil }