Hopefully, fix most case-sensitivity issues

- DNs are always used in canonical form: lowercase, no spaces. This is
  how they are internally handled and stored in paths and fields such as
  member and memberof
- Attribute names now can have any combination of lower/uppercase and
  stuff should work
- When modifying an attribute with a name that hase a different
  lower/upper combination than the previously stored value, keep the
  previous attribute name
- Trim spaces from values and do not store empty values
This commit is contained in:
Alex 2020-02-15 12:04:06 +01:00
parent 0c4d55895c
commit 825aa77089
5 changed files with 163 additions and 101 deletions

View file

@ -1,6 +1,2 @@
- Switch to `go mod` for building Bottin
- Implement missing search filters (in applyFilter) - Implement missing search filters (in applyFilter)
- Add an initial prefix to the consul key value - Add an initial prefix to the consul key value
- Potential bugs with different combinations of lower/uppercase names

View file

@ -12,7 +12,7 @@ job "directory" {
task "server" { task "server" {
driver = "docker" driver = "docker"
config { config {
image = "lxpz/bottin_amd64:13" image = "lxpz/bottin_amd64:14"
readonly_rootfs = true readonly_rootfs = true
port_map { port_map {
ldap_port = 389 ldap_port = 389

59
main.go
View file

@ -10,6 +10,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
ldap "bottin/ldapserver" ldap "bottin/ldapserver"
@ -19,15 +20,18 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
const ATTR_USERPASSWORD = "userpassword" // System managed attributes (cannot be changed by user, see checkRestrictedAttr)
const ATTR_MEMBER = "member"
const ATTR_MEMBEROF = "memberof" const ATTR_MEMBEROF = "memberof"
const ATTR_ENTRYUUID = "entryuuid" const ATTR_ENTRYUUID = "entryuuid"
const ATTR_CREATORSNAME = "creatorsname" const ATTR_CREATORSNAME = "creatorsname"
const ATTR_CREATETIMESTAMP = "createtimestamp" const ATTR_CREATETIMESTAMP = "createtimestamp"
const ATTR_MODIFIERSNAME = "modifiersname" const ATTR_MODIFIERSNAME = "modifiersname"
const ATTR_MODIFYTIMESTAMP = "modifytimestamp" const ATTR_MODIFYTIMESTAMP = "modifytimestamp"
// Attributes that we are interested in at various points
const ATTR_OBJECTCLASS = "objectclass" const ATTR_OBJECTCLASS = "objectclass"
const ATTR_MEMBER = "member"
const ATTR_USERPASSWORD = "userpassword"
type ConfigFile struct { type ConfigFile struct {
Suffix string `json:"suffix"` Suffix string `json:"suffix"`
@ -260,20 +264,27 @@ func (server *Server) newUserState() ldap.UserState {
} }
func (server *Server) init() error { func (server *Server) init() error {
path, err := dnToConsul(server.config.Suffix) // Check that suffix is in canonical format in config file
suffix_canonical, err := server.checkDN(server.config.Suffix, false)
if err != nil { if err != nil {
return err return err
} }
if suffix_canonical != server.config.Suffix {
return fmt.Errorf("Please write suffix in canonical format: %s", suffix_canonical)
}
pair, _, err := server.kv.Get(path+"/attribute="+ATTR_OBJECTCLASS, nil) // Check that root object exists.
// If it does, we're done. Otherwise, we have some initialization to do.
exists, err := server.objectExists(server.config.Suffix)
if err != nil { if err != nil {
return err return err
} }
if exists {
if pair != nil {
return nil return nil
} }
// We have to initialize the server.
// Create a root object and an admin object.
base_attributes := Entry{ base_attributes := Entry{
ATTR_OBJECTCLASS: []string{"top", "dcObject", "organization"}, ATTR_OBJECTCLASS: []string{"top", "dcObject", "organization"},
"structuralobjectclass": []string{"organization"}, "structuralobjectclass": []string{"organization"},
@ -333,16 +344,27 @@ func (server *Server) addElements(dn string, attrs Entry) error {
return err return err
} }
for k, v := range attrs { for k, valuesNC := range attrs {
path := prefix + "/attribute=" + k path := prefix + "/attribute=" + k
if len(v) == 0 {
// If we have zero values, delete associated k/v pair // Trim spaces and remove empty values
values := []string{}
for _, v := range valuesNC {
vv := strings.TrimSpace(v)
if len(vv) > 0 {
values = append(values, vv)
}
}
// If we have zero values, delete associated k/v pair
// Otherwise, write new values
if len(values) == 0 {
_, err := server.kv.Delete(path, nil) _, err := server.kv.Delete(path, nil)
if err != nil { if err != nil {
return err return err
} }
} else { } else {
json, err := json.Marshal(v) json, err := json.Marshal(values)
if err != nil { if err != nil {
return err return err
} }
@ -362,16 +384,23 @@ func (server *Server) getAttribute(dn string, attr string) ([]string, error) {
return nil, err return nil, err
} }
pair, _, err := server.kv.Get(path+"/attribute="+attr, nil) pairs, _, err := server.kv.List(path+"/attribute=", nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if pair == nil { values := []string{}
return []string{}, nil 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 parseValue(pair.Value) return values, nil
} }
func (server *Server) objectExists(dn string) (bool, error) { func (server *Server) objectExists(dn string) (bool, error) {
@ -388,7 +417,7 @@ func (server *Server) objectExists(dn string) (bool, error) {
} }
func (server *Server) checkDN(dn string, allow_extend bool) (string, error) { func (server *Server) checkDN(dn string, allow_extend bool) (string, error) {
// 1. Canonicalize: remove spaces between things // 1. Canonicalize: remove spaces between things and put all in lower case
dn, err := canonicalDN(dn) dn, err := canonicalDN(dn)
if err != nil { if err != nil {
return "", err return "", err

33
util.go
View file

@ -98,27 +98,31 @@ func parseDN(dn string) ([]DNComponent, error) {
return nil, fmt.Errorf("Wrong DN component: %s (expected type=value)", rdn) return nil, fmt.Errorf("Wrong DN component: %s (expected type=value)", rdn)
} }
ret = append(ret, DNComponent{ ret = append(ret, DNComponent{
Type: strings.TrimSpace(splits[0]), Type: strings.ToLower(strings.TrimSpace(splits[0])),
Value: strings.TrimSpace(splits[1]), Value: strings.ToLower(strings.TrimSpace(splits[1])),
}) })
} }
return ret, nil return ret, nil
} }
func unparseDN(path []DNComponent) string {
ret := ""
for _, c := range path {
if ret != "" {
ret = ret + ","
}
ret = ret + c.Type + "=" + c.Value
}
return ret
}
func canonicalDN(dn string) (string, error) { func canonicalDN(dn string) (string, error) {
path, err := parseDN(dn) path, err := parseDN(dn)
if err != nil { if err != nil {
return "", err return "", err
} }
ret := "" return unparseDN(path), nil
for _, c := range path {
if ret != "" {
ret = ret + ","
}
ret = ret + c.Type + "=" + c.Value
}
return ret, nil
} }
func checkRestrictedAttr(attr string) error { func checkRestrictedAttr(attr string) error {
@ -162,3 +166,12 @@ func valueMatch(attr, val1, val2 string) bool {
return strings.EqualFold(val1, val2) return strings.EqualFold(val1, val2)
} }
} }
func listContains(list []string, key string) bool {
for _, v := range list {
if key == v {
return true
}
}
return false
}

166
write.go
View file

@ -58,7 +58,16 @@ func (server *Server) handleAddInternal(state *State, r *message.AddRequest) (in
return ldap.LDAPResultEntryAlreadyExists, nil return ldap.LDAPResultEntryAlreadyExists, nil
} }
// TODO: check that parent object exists // Check that parent object exists
parentDn := unparseDN(dnSplit[1:])
parentExists, err := server.objectExists(parentDn)
if err != nil {
return ldap.LDAPResultOperationsError, err
}
if !parentExists {
return ldap.LDAPResultNoSuchObject, fmt.Errorf(
"Parent object %s does not exist", parentDn)
}
// If adding a group, track of who the members will be so that their memberOf field can be updated later // If adding a group, track of who the members will be so that their memberOf field can be updated later
members := []string{} members := []string{}
@ -77,8 +86,9 @@ func (server *Server) handleAddInternal(state *State, r *message.AddRequest) (in
if err != nil { if err != nil {
return ldap.LDAPResultObjectClassViolation, err return ldap.LDAPResultObjectClassViolation, err
} }
// If they are writing a member key, we have to check they are adding valid members
if strings.EqualFold(key, ATTR_MEMBER) { if strings.EqualFold(key, ATTR_MEMBER) {
// If they are writing a member list, we have to check they are adding valid members
// Also, rewrite member list to use canonical DN syntax (no spaces, all lowercase)
for _, member := range vals_str { for _, member := range vals_str {
member_canonical, err := server.checkDN(member, false) member_canonical, err := server.checkDN(member, false)
if err != nil { if err != nil {
@ -93,19 +103,31 @@ func (server *Server) handleAddInternal(state *State, r *message.AddRequest) (in
"Cannot add %s to members, it does not exist!", "Cannot add %s to members, it does not exist!",
member_canonical) member_canonical)
} }
members = append(members, member_canonical)
} }
members = append(members, vals_str...) entry[key] = members
}
if prev, ok := entry[key]; ok {
entry[key] = append(prev, vals_str...)
} else { } else {
entry[key] = vals_str if prev, ok := entry[key]; ok {
entry[key] = append(prev, vals_str...)
} else {
entry[key] = vals_str
}
} }
} }
if _, ok := entry[ATTR_OBJECTCLASS]; !ok { // Ensure object has at least one objectclass value
hasObjectClass := false
for k := range entry {
if strings.EqualFold(k, ATTR_OBJECTCLASS) {
hasObjectClass = true
break
}
}
if !hasObjectClass {
entry[ATTR_OBJECTCLASS] = []string{"top"} entry[ATTR_OBJECTCLASS] = []string{"top"}
} }
// Write system attributes
entry[ATTR_CREATORSNAME] = []string{state.login.user} entry[ATTR_CREATORSNAME] = []string{state.login.user}
entry[ATTR_CREATETIMESTAMP] = []string{genTimestamp()} entry[ATTR_CREATETIMESTAMP] = []string{genTimestamp()}
entry[ATTR_ENTRYUUID] = []string{genUuid()} entry[ATTR_ENTRYUUID] = []string{genUuid()}
@ -306,11 +328,25 @@ func (server *Server) handleModifyInternal(state *State, r *message.ModifyReques
addMembers, delMembers := []string{}, []string{} addMembers, delMembers := []string{}, []string{}
// Produce new entry values to be saved // Produce new entry values to be saved
newEntry := Entry{} entry := Entry{}
for _, change := range r.Changes() { for _, change := range r.Changes() {
attr := string(change.Modification().Type_()) attr := string(change.Modification().Type_())
values := change.Modification().Vals() changeValues := []string{}
for _, v := range change.Modification().Vals() {
changeValues = append(changeValues, string(v))
}
// If we already had an attribute with this name before,
// make sure we are using the same lowercase/uppercase
for prevAttr := range prevEntry {
if strings.EqualFold(attr, prevAttr) {
attr = prevAttr
break
}
}
// Check that this attribute is not system-managed thus restricted
err = checkRestrictedAttr(attr) err = checkRestrictedAttr(attr)
if err != nil { if err != nil {
return ldap.LDAPResultObjectClassViolation, err return ldap.LDAPResultObjectClassViolation, err
@ -326,112 +362,100 @@ func (server *Server) handleModifyInternal(state *State, r *message.ModifyReques
return ldap.LDAPResultInsufficientAccessRights, nil return ldap.LDAPResultInsufficientAccessRights, nil
} }
if change.Operation() == ldap.ModifyRequestChangeOperationAdd { // If we are changing ATTR_MEMBER, rewrite all values to canonical form
newEntry[attr] = prevEntry[attr] if strings.EqualFold(attr, ATTR_MEMBER) {
for _, val := range values { for i := range changeValues {
present := false canonical_val, err := server.checkDN(changeValues[i], false)
for _, prevVal := range newEntry[attr] { if err != nil {
if prevVal == string(val) { return ldap.LDAPResultInvalidDNSyntax, err
present = true
break
}
} }
if !present { changeValues[i] = canonical_val
newEntry[attr] = append(newEntry[attr], string(val)) }
}
// If we don't yet have a new value for this attr,
// but one existed before, initialize entry[attr] to the old value
// so that later on what we do is simply modify entry[attr] in place
// (this allows to handle sequences of several changes on the same attr)
if _, ok := entry[attr]; !ok {
if _, ok := prevEntry[attr]; ok {
entry[attr] = prevEntry[attr]
}
}
// Apply effective modification on entry[attr]
if change.Operation() == ldap.ModifyRequestChangeOperationAdd {
for _, val := range changeValues {
if !listContains(entry[attr], val) {
entry[attr] = append(entry[attr], val)
if strings.EqualFold(attr, ATTR_MEMBER) { if strings.EqualFold(attr, ATTR_MEMBER) {
addMembers = append(addMembers, string(val)) addMembers = append(addMembers, val)
} }
} }
} }
} else if change.Operation() == ldap.ModifyRequestChangeOperationDelete { } else if change.Operation() == ldap.ModifyRequestChangeOperationDelete {
if len(values) == 0 { if len(changeValues) == 0 {
// Delete everything // Delete everything
newEntry[attr] = []string{}
if strings.EqualFold(attr, ATTR_MEMBER) { if strings.EqualFold(attr, ATTR_MEMBER) {
delMembers = append(delMembers, prevEntry[attr]...) delMembers = append(delMembers, entry[attr]...)
} }
entry[attr] = []string{}
} else { } else {
// Delete only those specified // Delete only those specified
newEntry[attr] = []string{} newList := []string{}
for _, prevVal := range prevEntry[attr] { for _, prevVal := range entry[attr] {
keep := true if !listContains(changeValues, prevVal) {
for _, delVal := range values { newList = append(newList, prevVal)
if string(delVal) == prevVal {
keep = false
break
}
}
if keep {
newEntry[attr] = append(newEntry[attr], prevVal)
} else { } else {
if strings.EqualFold(attr, ATTR_MEMBER) { if strings.EqualFold(attr, ATTR_MEMBER) {
delMembers = append(delMembers, prevVal) delMembers = append(delMembers, prevVal)
} }
} }
} }
entry[attr] = newList
} }
} else if change.Operation() == ldap.ModifyRequestChangeOperationReplace { } else if change.Operation() == ldap.ModifyRequestChangeOperationReplace {
newEntry[attr] = []string{}
for _, newVal := range values {
newEntry[attr] = append(newEntry[attr], string(newVal))
}
if strings.EqualFold(attr, ATTR_MEMBER) { if strings.EqualFold(attr, ATTR_MEMBER) {
for _, newMem := range newEntry[attr] { for _, newMem := range changeValues {
mustAdd := true if !listContains(entry[attr], newMem) {
for _, prevMem := range prevEntry[attr] {
if prevMem == newMem {
mustAdd = false
break
}
}
if mustAdd {
addMembers = append(addMembers, newMem) addMembers = append(addMembers, newMem)
} }
} }
for _, prevMem := range prevEntry[attr] { for _, prevMem := range entry[attr] {
mustDel := true if !listContains(changeValues, prevMem) {
for _, newMem := range newEntry[attr] {
if newMem == prevMem {
mustDel = false
break
}
}
if mustDel {
delMembers = append(delMembers, prevMem) delMembers = append(delMembers, prevMem)
} }
} }
} }
entry[attr] = changeValues
} }
} }
// Check that added members actually exist // Check that added members actually exist
for i := range addMembers { for i := range addMembers {
addMem, err := server.checkDN(addMembers[i], false) exists, err := server.objectExists(addMembers[i])
if err != nil {
return ldap.LDAPResultInvalidDNSyntax, err
}
exists, err := server.objectExists(addMem)
if err != nil { if err != nil {
return ldap.LDAPResultOperationsError, err return ldap.LDAPResultOperationsError, err
} }
if !exists { if !exists {
return ldap.LDAPResultNoSuchObject, fmt.Errorf( return ldap.LDAPResultNoSuchObject, fmt.Errorf(
"Cannot add member %s, it does not exist", addMem) "Cannot add member %s, it does not exist", addMembers[i])
} }
addMembers[i] = addMem
} }
if v, ok := newEntry[ATTR_OBJECTCLASS]; ok && len(v) == 0 { for k, v := range entry {
return ldap.LDAPResultInsufficientAccessRights, fmt.Errorf( if strings.EqualFold(k, ATTR_OBJECTCLASS) && len(v) == 0 {
"Cannot remove all objectclass values") return ldap.LDAPResultInsufficientAccessRights, fmt.Errorf(
"Cannot remove all objectclass values")
}
} }
// Now, the modification has been processed and accepted and we want to commit it // Now, the modification has been processed and accepted and we want to commit it
newEntry[ATTR_MODIFIERSNAME] = []string{state.login.user} entry[ATTR_MODIFIERSNAME] = []string{state.login.user}
newEntry[ATTR_MODIFYTIMESTAMP] = []string{genTimestamp()} entry[ATTR_MODIFYTIMESTAMP] = []string{genTimestamp()}
// Save the edited values // Save the edited values
err = server.addElements(dn, newEntry) err = server.addElements(dn, entry)
if err != nil { if err != nil {
return ldap.LDAPResultOperationsError, err return ldap.LDAPResultOperationsError, err
} }