mirror of
https://github.com/h44z/wg-portal.git
synced 2026-09-30 16:52:12 +02:00
test(ldap): cover entry conversion and group membership helpers (#757)
ldap_utils.go had no tests. LdapConvertEntries, LdapSearchAttributes and LdapIsMemberOf need no server, so cover those three. LdapConnect and LdapFindAllUsers still do. Pins what the sync depends on: a field_map naming an attribute the server does not return yields an empty identifier rather than a missing key. Group membership stays raw for DN parsing, and the attribute list drops unset fields and duplicates. Signed-off-by: clark-ja <37738506+clark-ja@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
)
|
||||
|
||||
func testLdapFields() *config.LdapFields {
|
||||
return &config.LdapFields{
|
||||
BaseFields: config.BaseFields{UserIdentifier: "uid", Email: "mail"},
|
||||
GroupMembership: "memberOf",
|
||||
}
|
||||
}
|
||||
|
||||
func testGroupDN(t *testing.T) *ldap.DN {
|
||||
t.Helper()
|
||||
|
||||
dn, err := ldap.ParseDN("cn=admins,dc=example,dc=com")
|
||||
require.NoError(t, err)
|
||||
|
||||
return dn
|
||||
}
|
||||
|
||||
// testEntry builds a result the way the library's own decoder would, so the
|
||||
// fixtures carry both string and raw values.
|
||||
func testEntry(attrs map[string][]string) *ldap.SearchResult {
|
||||
entry := &ldap.Entry{DN: "uid=alice,dc=example,dc=com"}
|
||||
for name, values := range attrs {
|
||||
entry.Attributes = append(entry.Attributes, ldap.NewEntryAttribute(name, values))
|
||||
}
|
||||
|
||||
return &ldap.SearchResult{Entries: []*ldap.Entry{entry}}
|
||||
}
|
||||
|
||||
// A field_map naming an attribute the server does not return leaves the key in
|
||||
// place with an empty value rather than omitting it. Callers rely on this when
|
||||
// they decide whether a sync learned anything about who is entitled.
|
||||
func TestLdapConvertEntriesKeepsKeyForMissingAttribute(t *testing.T) {
|
||||
sr := testEntry(map[string][]string{"cn": {"alice"}})
|
||||
|
||||
users := LdapConvertEntries(sr, testLdapFields())
|
||||
|
||||
require.Len(t, users, 1)
|
||||
identifier, present := users[0]["uid"]
|
||||
assert.True(t, present, "the identifier key must exist even when the attribute is absent")
|
||||
assert.Equal(t, "", identifier)
|
||||
}
|
||||
|
||||
// Directories may hold several values for an attribute. Only the first is used.
|
||||
func TestLdapConvertEntriesUsesFirstValue(t *testing.T) {
|
||||
sr := testEntry(map[string][]string{"mail": {"first@example.com", "second@example.com"}})
|
||||
|
||||
users := LdapConvertEntries(sr, testLdapFields())
|
||||
|
||||
require.Len(t, users, 1)
|
||||
assert.Equal(t, "first@example.com", users[0]["mail"])
|
||||
}
|
||||
|
||||
// Group membership stays raw because LdapIsMemberOf parses the values as DNs.
|
||||
// Converting it to strings here would break admin group detection.
|
||||
func TestLdapConvertEntriesKeepsGroupMembershipRaw(t *testing.T) {
|
||||
sr := testEntry(map[string][]string{"memberOf": {"cn=admins,dc=example,dc=com"}})
|
||||
|
||||
users := LdapConvertEntries(sr, testLdapFields())
|
||||
|
||||
require.Len(t, users, 1)
|
||||
assert.Equal(t, [][]byte{[]byte("cn=admins,dc=example,dc=com")}, users[0]["memberOf"])
|
||||
}
|
||||
|
||||
// Unset optional fields must not be requested, otherwise the search asks the
|
||||
// server for an attribute named "".
|
||||
func TestLdapSearchAttributesOmitsUnsetFields(t *testing.T) {
|
||||
attrs := LdapSearchAttributes(&config.LdapFields{
|
||||
BaseFields: config.BaseFields{UserIdentifier: "uid"},
|
||||
})
|
||||
|
||||
assert.Equal(t, []string{"dn", "uid"}, attrs)
|
||||
}
|
||||
|
||||
// The default field map maps both the identifier and the email to "mail", so the
|
||||
// attribute list has to be deduplicated.
|
||||
func TestLdapSearchAttributesDeduplicates(t *testing.T) {
|
||||
attrs := LdapSearchAttributes(&config.LdapFields{
|
||||
BaseFields: config.BaseFields{UserIdentifier: "mail", Email: "mail"},
|
||||
})
|
||||
|
||||
assert.Equal(t, []string{"dn", "mail"}, attrs)
|
||||
}
|
||||
|
||||
// Servers are free to return a DN with different spacing or attribute case than
|
||||
// the configured admin group, so the comparison parses both sides.
|
||||
func TestLdapIsMemberOfIgnoresDnFormatting(t *testing.T) {
|
||||
isMember, err := LdapIsMemberOf([][]byte{[]byte("CN=admins, DC=example, DC=com")}, testGroupDN(t))
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, isMember)
|
||||
}
|
||||
|
||||
func TestLdapIsMemberOfReportsNonMember(t *testing.T) {
|
||||
isMember, err := LdapIsMemberOf([][]byte{[]byte("cn=users,dc=example,dc=com")}, testGroupDN(t))
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.False(t, isMember)
|
||||
}
|
||||
|
||||
// An unparseable group value is an error rather than a silent non-match, so a
|
||||
// malformed directory entry cannot quietly drop someone's admin rights.
|
||||
func TestLdapIsMemberOfRejectsMalformedDn(t *testing.T) {
|
||||
_, err := LdapIsMemberOf([][]byte{[]byte("not-a-dn")}, testGroupDN(t))
|
||||
|
||||
assert.Error(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user