bottin/read.go
Simon Beck 9ce0d22c99
All checks were successful
continuous-integration/drone/push Build is passing
Fix wrong handling of multi value attributes
While ldapsearch doesn't seem to mind, apps like keycloak seem to have
issues with adding multiple attributes with different values. While
the resulting ldif in ldapsearch is indistinguishable there seems to
be a slight different on the protocol level.

If adding multiple attributes with the same name and different values,
keycloak will only see the last entry. But adding a single attribute
a slice of values is seems to handle it correctly.
2022-02-14 12:13:31 +01:00

285 lines
7.3 KiB
Go

package main
import (
"fmt"
"strings"
ldap "bottin/ldapserver"
message "bottin/goldap"
)
// Generic read utility functions ----------
func (server *Server) getAttribute(dn string, attr string) ([]string, error) {
path, err := dnToConsul(dn)
if err != nil {
return nil, err
}
// List all attributes of the object, this is needed because the attribute we are
// looking for can exist with different cases than the one specified here
pairs, _, err := server.kv.List(path+"/attribute=", &server.readOpts)
if err != nil {
return nil, err
}
// Collect values for the attribute, case-insensitively
values := []string{}
for _, pair := range pairs {
if strings.EqualFold(pair.Key, path+"/attribute="+attr) {
newVals, err := parseValue(pair.Value)
if err != nil {
return nil, err
}
values = append(values, newVals...)
}
}
return values, nil
}
func (server *Server) objectExists(dn string) (bool, error) {
prefix, err := dnToConsul(dn)
if err != nil {
return false, err
}
data, _, err := server.kv.List(prefix+"/attribute=", &server.readOpts)
if err != nil {
return false, err
}
return len(data) > 0, nil
}
// Compare request -------------------------
func (server *Server) handleCompare(s ldap.UserState, w ldap.ResponseWriter, m *ldap.Message) {
state := s.(*State)
r := m.GetCompareRequest()
code, err := server.handleCompareInternal(state, &r)
res := ldap.NewResponse(code)
if err != nil {
res.SetDiagnosticMessage(err.Error())
}
w.Write(message.CompareResponse(res))
}
func (server *Server) handleCompareInternal(state *State, r *message.CompareRequest) (int, error) {
attr := string(r.Ava().AttributeDesc())
expected := string(r.Ava().AssertionValue())
dn, err := server.checkDN(string(r.Entry()), false)
if err != nil {
return ldap.LDAPResultInvalidDNSyntax, err
}
// Check permissions
if !server.config.Acl.Check(&state.login, dn, "read", []string{attr}) {
return ldap.LDAPResultInsufficientAccessRights, nil
}
// Do query
exists, err := server.objectExists(dn)
if err != nil {
return ldap.LDAPResultOperationsError, err
}
if !exists {
return ldap.LDAPResultNoSuchObject, fmt.Errorf("Not found: %s", dn)
}
values, err := server.getAttribute(dn, attr)
if err != nil {
return ldap.LDAPResultOperationsError, err
}
for _, v := range values {
if valueMatch(attr, v, expected) {
return ldap.LDAPResultCompareTrue, nil
}
}
return ldap.LDAPResultCompareFalse, nil
}
// Search request -------------------------
func (server *Server) handleSearch(s ldap.UserState, w ldap.ResponseWriter, m *ldap.Message) {
state := s.(*State)
r := m.GetSearchRequest()
code, err := server.handleSearchInternal(state, w, &r)
res := ldap.NewResponse(code)
if err != nil {
res.SetDiagnosticMessage(err.Error())
}
if code != ldap.LDAPResultSuccess {
server.logger.Printf("Failed to do search %#v (%s)", r, err)
}
w.Write(message.SearchResultDone(res))
}
func (server *Server) handleSearchInternal(state *State, w ldap.ResponseWriter, r *message.SearchRequest) (int, error) {
baseObject, err := server.checkDN(string(r.BaseObject()), true)
if err != nil {
return ldap.LDAPResultInvalidDNSyntax, err
}
server.logger.Tracef("-- SEARCH REQUEST: --")
server.logger.Tracef("Request BaseDn=%s", baseObject)
server.logger.Tracef("Request Filter=%s", r.Filter())
server.logger.Tracef("Request FilterString=%s", r.FilterString())
server.logger.Tracef("Request Attributes=%s", r.Attributes())
server.logger.Tracef("Request TimeLimit=%d", r.TimeLimit().Int())
if !server.config.Acl.Check(&state.login, "read", baseObject, []string{}) {
return ldap.LDAPResultInsufficientAccessRights, fmt.Errorf("Please specify a base object on which you have read rights")
}
baseObjectLevel := len(strings.Split(baseObject, ","))
basePath, err := dnToConsul(baseObject)
if err != nil {
return ldap.LDAPResultInvalidDNSyntax, err
}
if r.Scope() == message.SearchRequestScopeBaseObject {
basePath += "/attribute="
} else {
basePath += "/"
}
data, _, err := server.kv.List(basePath, &server.readOpts)
if err != nil {
return ldap.LDAPResultOperationsError, err
}
entries, err := parseConsulResult(data)
if err != nil {
return ldap.LDAPResultOperationsError, err
}
server.logger.Tracef("in %s: %#v", basePath, data)
server.logger.Tracef("%#v", entries)
for dn, entry := range entries {
if r.Scope() == message.SearchRequestScopeBaseObject {
if dn != baseObject {
continue
}
} else if r.Scope() == message.SearchRequestSingleLevel {
objectLevel := len(strings.Split(dn, ","))
if objectLevel != baseObjectLevel+1 {
continue
}
}
// Filter out if we don't match requested filter
matched, err := applyFilter(entry, r.Filter())
if err != nil {
return ldap.LDAPResultUnwillingToPerform, err
}
if !matched {
continue
}
// Filter out if user is not allowed to read this
if !server.config.Acl.Check(&state.login, "read", dn, []string{}) {
continue
}
e := ldap.NewSearchResultEntry(dn)
for attr, val := range entry {
// If attribute is not in request, exclude it from returned entry
if len(r.Attributes()) > 0 {
found := false
for _, requested := range r.Attributes() {
if string(requested) == "1.1" && len(r.Attributes()) == 1 {
break
}
if string(requested) == "*" || strings.EqualFold(string(requested), attr) {
found = true
break
}
}
if !found {
continue
}
}
// If we are not allowed to read attribute, exclude it from returned entry
if !server.config.Acl.Check(&state.login, "read", dn, []string{attr}) {
continue
}
// Send result
resultVals := []message.AttributeValue{}
for _, v := range val {
resultVals = append(resultVals, message.AttributeValue(v))
}
e.AddAttribute(message.AttributeDescription(attr), resultVals...)
}
w.Write(e)
}
return ldap.LDAPResultSuccess, nil
}
func applyFilter(entry Entry, filter message.Filter) (bool, error) {
if fAnd, ok := filter.(message.FilterAnd); ok {
for _, cond := range fAnd {
res, err := applyFilter(entry, cond)
if err != nil {
return false, err
}
if !res {
return false, nil
}
}
return true, nil
} else if fOr, ok := filter.(message.FilterOr); ok {
for _, cond := range fOr {
res, err := applyFilter(entry, cond)
if err != nil {
return false, err
}
if res {
return true, nil
}
}
return false, nil
} else if fNot, ok := filter.(message.FilterNot); ok {
res, err := applyFilter(entry, fNot.Filter)
if err != nil {
return false, err
}
return !res, nil
} else if fPresent, ok := filter.(message.FilterPresent); ok {
what := string(fPresent)
// Case insensitive search
for desc, values := range entry {
if strings.EqualFold(what, desc) {
return len(values) > 0, nil
}
}
return false, nil
} else if fEquality, ok := filter.(message.FilterEqualityMatch); ok {
desc := string(fEquality.AttributeDesc())
target := string(fEquality.AssertionValue())
// Case insensitive attribute search
for entry_desc, value := range entry {
if strings.EqualFold(entry_desc, desc) {
for _, val := range value {
if valueMatch(entry_desc, val, target) {
return true, nil
}
}
return false, nil
}
}
return false, nil
} else {
return false, fmt.Errorf("Unsupported filter: %#v %T", filter, filter)
}
}