forked from Deuxfleurs/bottin
9ce0d22c99
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.
284 lines
7.3 KiB
Go
284 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)
|
|
}
|
|
}
|