update alias lookahead to respect case (#31352)

* userpass is not case sensitive
* ldap is case sensitive when it is configured that way

---------

Co-authored-by: Ben Ash <bash@hashicorp.com>
This commit is contained in:
mickael-hc 2025-07-23 12:28:05 -04:00 committed by GitHub
parent 8f7d76d781
commit 881febbf98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 201 additions and 5 deletions

View File

@ -948,6 +948,121 @@ func TestBackend_configDefaultsAfterUpdate(t *testing.T) {
})
}
// TestLdapAuthBackend_AliasLookaheadDefault will test the alias lookahead functionality
// for the LDAP auth backend with a default configuration (i.e. case insensitive if the flag is
// not set or is set to false).
func TestLdapAuthBackend_AliasLookaheadDefault(t *testing.T) {
// create config and storage
b, storage := createBackendWithStorage(t)
// Create user "testuser"
resp, err := b.HandleRequest(namespace.RootContext(nil), &logical.Request{
Path: "users/testuser",
Operation: logical.UpdateOperation,
Storage: storage,
Data: map[string]interface{}{
"policies": []string{"default"},
"groups": "testgroup,nested/testgroup",
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
}
// List users
resp, err = b.HandleRequest(namespace.RootContext(nil), &logical.Request{
Path: "users/",
Operation: logical.ListOperation,
Storage: storage,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
}
expected := []string{"testuser"}
if !reflect.DeepEqual(expected, resp.Data["keys"].([]string)) {
t.Fatalf("bad: listed users; expected: %#v actual: %#v", expected, resp.Data["keys"].([]string))
}
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.AliasLookaheadOperation,
Storage: storage,
Path: "login/testuser",
Data: map[string]interface{}{
"Raw": map[string]interface{}{
"username": "testuser",
},
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
}
if resp.Auth.Alias.Name != "testuser" {
t.Fatalf("bad: alias lookahead did not return expected alias name; expected: 'testuser', actual: '%s'", resp.Auth.Alias.Name)
}
}
// TestLdapAuthBackend_AliasLookaheadCaseSensitive will test the alias lookahead functionality
// for the LDAP auth backend when a case sensitive configuration flag is set
func TestLdapAuthBackend_AliasLookaheadCaseSensitive(t *testing.T) {
// create config and storage
b, storage := createBackendWithStorage(t)
// make the backend case sensitive
resp, err := b.HandleRequest(namespace.RootContext(nil), &logical.Request{
Operation: logical.UpdateOperation,
Path: "config",
Data: map[string]interface{}{
"case_sensitive_names": true,
},
Storage: storage,
})
// Create user "teSTuser"
resp, err = b.HandleRequest(namespace.RootContext(nil), &logical.Request{
Path: "users/teSTuser",
Operation: logical.UpdateOperation,
Storage: storage,
Data: map[string]interface{}{
"policies": []string{"default"},
"groups": "testgroup,nested/testgroup",
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
}
// List users
resp, err = b.HandleRequest(namespace.RootContext(nil), &logical.Request{
Path: "users/",
Operation: logical.ListOperation,
Storage: storage,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
}
expected := []string{"teSTuser"}
if !reflect.DeepEqual(expected, resp.Data["keys"].([]string)) {
t.Fatalf("bad: listed users; expected: %#v actual: %#v", expected, resp.Data["keys"].([]string))
}
resp, err = b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.AliasLookaheadOperation,
Storage: storage,
Path: "login/teSTuser",
Data: map[string]interface{}{
"Raw": map[string]interface{}{
"username": "teSTuser",
},
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
}
if resp.Auth.Alias.Name != "teSTuser" {
t.Fatalf("bad: alias lookahead did not return expected alias name; expected: 'teSTuser', actual: '%s'", resp.Auth.Alias.Name)
}
}
func testAccStepConfigUrl(t *testing.T, cfg *ldaputil.ConfigEntry) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,

View File

@ -6,6 +6,7 @@ package ldap
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/cidrutil"
@ -50,6 +51,16 @@ func (b *backend) pathLoginAliasLookahead(ctx context.Context, req *logical.Requ
return nil, fmt.Errorf("missing username")
}
cfg, err := b.Config(ctx, req)
if err != nil {
return nil, err
}
caseSensitive := cfg != nil && cfg.CaseSensitiveNames != nil && *cfg.CaseSensitiveNames
// if the username is not configured to be case-sensitive, we normalize it to lower case.
if !caseSensitive {
username = strings.ToLower(username)
}
return &logical.Response{
Auth: &logical.Auth{
Alias: &logical.Alias{

View File

@ -8,6 +8,7 @@ import (
"crypto/tls"
"fmt"
"reflect"
"strings"
"testing"
"time"
@ -201,6 +202,35 @@ func TestBackend_userCreateOperation(t *testing.T) {
})
}
// TestBackend_userCreateOperationCheckCase will test the alias lookahead functionality
// for the userpass auth method to ensure the alias returned is always lowercase due to
// the effective case insensitivity of the auth method
func TestBackend_userCreateOperationCheckCase(t *testing.T) {
b, err := Factory(context.Background(), &logical.BackendConfig{
Logger: nil,
System: &logical.StaticSystemView{
DefaultLeaseTTLVal: testSysTTL,
MaxLeaseTTLVal: testSysMaxTTL,
},
})
if err != nil {
t.Fatalf("Unable to create backend: %s", err)
}
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: b,
Steps: []logicaltest.TestStep{
testUserCreateOperation(t, "web", "password", "foo"),
testAccStepLogin(t, "web", "password", []string{"default", "foo"}),
testAccStepLoginAlias(t, "web", "password"),
testAccStepLogin(t, "WEb", "password", []string{"default", "foo"}),
testAccStepLoginAlias(t, "wEb", "password"),
testAccStepLogin(t, "WEb", "password", []string{"default", "foo"}),
testAccStepLoginAlias(t, "WEb", "password"),
},
})
}
func TestBackend_passwordUpdate(t *testing.T) {
b, err := Factory(context.Background(), &logical.BackendConfig{
Logger: nil,
@ -302,6 +332,30 @@ func testAccStepLogin(t *testing.T, user string, pass string, policies []string)
}
}
// userpass is case insentive, so we need to ensure that the alias lookahead returns all lowercase username
// even if the username is provided in a different case.
func testAccStepLoginAlias(t *testing.T, user string, pass string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.AliasLookaheadOperation,
Path: "login/" + user,
Data: map[string]interface{}{
"Raw": map[string]interface{}{
"username": user,
},
},
Check: func(resp *logical.Response) error {
if resp.Auth.Alias.Name != strings.ToLower(user) {
return fmt.Errorf("expected alias name to be '%s', got: %s", strings.ToLower(user), resp.Auth.Alias.Name)
}
if resp.IsError() {
return fmt.Errorf("bad: %#v", resp)
}
return nil
},
ConnState: &tls.ConnectionState{},
}
}
func testUserCreateOperation(
t *testing.T, name string, password string, policies string,
) logicaltest.TestStep {

View File

@ -47,10 +47,10 @@ func pathLogin(b *backend) *framework.Path {
}
}
func (b *backend) pathLoginAliasLookahead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
username := d.Get("username").(string)
if username == "" {
return nil, fmt.Errorf("missing username")
func (b *backend) pathLoginAliasLookahead(_ context.Context, _ *logical.Request, d *framework.FieldData) (*logical.Response, error) {
username, err := b.getUsername(d)
if err != nil {
return nil, err
}
return &logical.Response{
@ -62,8 +62,21 @@ func (b *backend) pathLoginAliasLookahead(ctx context.Context, req *logical.Requ
}, nil
}
// getUsername retrieves the username from the field data, ensuring it is not
// empty. The username is always returned in lowercase.
func (b *backend) getUsername(d *framework.FieldData) (string, error) {
username := d.Get("username").(string)
if username == "" {
return "", fmt.Errorf("missing username")
}
return strings.ToLower(username), nil
}
func (b *backend) pathLogin(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
username := strings.ToLower(d.Get("username").(string))
username, err := b.getUsername(d)
if err != nil {
return nil, err
}
password := d.Get("password").(string)
if password == "" {

3
changelog/31352.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
auth: update alias lookahead to respect username case for LDAP and username/password
```