Skip to content

Please support nested groups #68

@akorn

Description

@akorn

Currently the code seems to assume that the value of a member attribute is the DN of a user, and treats the RDN as the uid (even if it happens to be called cn and ldap_uidattr is unset, and even if the LDAP member entity doesn't have the posixAccount objectClass, which I'd say is a bug).

Under rfc2307bis, group members can also be groups; libnss-ldap also supports this.

I think nsscache should perform a transitive closure on the groups it obtains from LDAP. I wrote a shell script that does this, as a proof of concept; it's a bit crude, but it works. It allows arbitrary group nesting (even across groups that don't have the posixGroup objectClass). It doesn't prevent infinite recursion though, and it's very slow.

Ironically, getent group gets it right if nss is configured to use libnss-ldap, so that instead of running nsscache, we could just use getent -- if that wouldn't defeat the purpose of using nss-cache. :)

#!/bin/zsh
#
# Purpose: get groups from LDAP recursively and extract their members.

typeset -A isgroup
typeset -A isposixgroup
typeset -A members # Contains DNs
typeset -A memberuids
typeset -A processed_DNs
typeset -A uids
typeset -A groups
typeset -A memberfilter

GROUPBASE="$1"
GROUPFILTER="$2"        # only print groups matching this filter; use objectClass=posixGroup
MEMBERFILTER="$3"       # only print members matching this filter; use objectClass=posixAccount

function process_ldapsearch() {
        local currentdn=""
        local memberdn=""
        local memberuid=""
        while read attrib val; do case "$attrib $val" in
                dn:*)
                        currentdn="$val"
                        ;;
                objectClass:\ groupOfNames)
                        isgroup[$currentdn]=1
                        ;;
                objectClass:\ posixGroup)
                        isposixgroup[$currentdn]=1
                        ;;
                member:\ *)
                        memberdn="$val"
                        # Do we already have this member in the current group? If not, add it.
                        [[ "$members[$currentdn]" =~ \b${memberdn}\b ]] || members[$currentdn]="$members[$currentdn] $memberdn"
                        [[ "${memberdn/,$GROUPBASE/}" = "${memberdn}" ]] && # Is this member located directly under our specified GROUPBASE dn?
                                [[ "${memberdn/uid=/}" = "${memberdn}" ]] && { # Process members that are outside our search base and that are not obviously users
                                [[ $processed_DNs[$memberdn] = "" ]] && ldapsearch -LLL -x -o ldif-wrap=no -b "${memberdn}" | process_ldapsearch
                                processed_DNs[$memberdn]=1
                        }
                        ;;
                memberUid:*)
                        memberuid="$val"
                        # Do we already have this member in the current group? If not, add it.
                        [[ "$memberuids[$currentdn]" =~ \b$memberuid\b ]] || memberuids[$currentdn]="$memberuids[$currentdn] $memberuid"
                        ;;
                *)
                        ;;
        esac; done
}

function memberfilter_match() {
        [[ -z "$MEMBERFILTER" ]] && return 0
        [[ -n "$memberfilter[$1]" ]] && return $memberfilter[$1]
        if [[ $(ldapsearch -s base -LLL -x -o ldif-wrap=no -b "$i" "($MEMBERFILTER)" dn) = "dn: $i" ]]; then
                memberfilter[$1]="0"
        else
                memberfilter[$1]="1"
        fi
        return $memberfilter[$1]
}

function getmembers() {
        local group=$1
        local -A curmembers
        local i
        local j
        for i in ${(z)members[$group]}; do
                if (( ${+isgroup[$i]} || ${+isposixgroup[$i]} )); then
                        for j in $(getmembers $i); do
                                curmembers[$j]=1
                        done
                else
                        if memberfilter_match $i; then
                                i=${i/uid=/}
                                i=${i/,*/}
                                curmembers[$i]=1
                        fi
                fi
        done
        for i in ${(z)memberuids[$group]}; do
                curmembers[$i]=1
        done
        echo ${(k)curmembers}
        return 0;
}

# main()
ldapsearch -LLL -x -b "$GROUPBASE" -o ldif-wrap=no | process_ldapsearch

# Merge the list of posixGroups and groupOfNames groups; the keys of the groups hash will form a list of all groups
for group in ${(k)isposixgroup} ${(k)isgroup}; do
        groups[$group]=1
done

for group in ${(k)groups}; do
        if [[ -z "$GROUPFILTER" ]] || [[ $(ldapsearch -s base -LLL -x -b "$group" -o ldif-wrap=no "($GROUPFILTER)" dn) = "dn: $group" ]] then
                groupname=${group/cn=/}
                if ! [[ "${groupname/,$GROUPBASE/}" = "${groupname}" ]]; then   # Was this particular group inside our search base or outside it?
                        groupname=${groupname/,*/}
                        echo $groupname $(getmembers $group | tr ' ' '\n' | sort | tr '\n' ' ')
                fi
        fi
done

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions