AD attribute based authorization for device groups

Hi Team!

I have trouble with setting up AD attribute based authorization for users whom only can see a specific group of devices. I created a static device group, assigned some device to it. In the AD we store one of the following attributes and arranged togerther with these LibreNMS roles:

{
    "LibreNMS-readonly": {
        "roles": [
            "global-read"
        ]
    },
    "LibreNMS-admin": {
        "roles": [
            "admin"
        ]
    },
    "LibreNMS-groupRW": {
        "roles": [
            "user"
        ]
    }
}

After created an AD user and added LibreNMS-groupRW attribute to them, tried to login and we saw that on Manage Users pages, user has User and Global-Read role at the same time, however AD provides only LibreNMS-groupRW attribute. In fact, Admin users also have the global read role, even though they did not receive the LibreNMS-readonly attribute from AD. The goal would be for external users to only be able to view the devices of their own interest. Any hints?

$ lnms config:get auth_ad_global_read
false

Thanks!

Sir, these “LibreNMS-readonly, LibreNMS-admin, LibreNMS-groupRW” are groups not attributes.

Hi Murray! Thanks for quick answer.

admin, user, global-read are roles, right? The attribute name is “memberOf” and it contains groups (LibreNMS-readonly, LibreNMS-admin, LibreNMS-groupRW) as DN, right? The rule assignment is works, as admin users gets admin rights, but the problem is, all user gets global-read role, even if they does not have “LibreNMS-readonly” group in their memberOf attrib lists and even auth_ad_global_read is false. So every user can see all of the devices, and this is not good. And I can’t figure out, where is the problem, in the config? in the code? is it by design?

Thanks!

As I digging into it deeper, I found that in LibreNMS/Authentication/ActiveDirectoryAuthorizer.php at line 108 there is an ldap_search filter, which is contain a 1.2.840.113556.1.4.1941 OID which is refers to LDAP_MATCHING_RULE_IN_CHAIN. It is a Microsoft Active Directory matching rule used to recursively search through nested group memberships. If I change this filter, skip that OID, then our AD is just works fine with the modified code.

I don’t know if this is a strange AD behavior, checked the groups are security groups, but somehow our AD is gets back result with count=1 in the outer, and count=0 in the inner, even if user is not member of that group:

>>> GROUP: LibreNMS-admin,	    Outer count: 1, Inner count: 0
>>> GROUP: LibreNMS-readonly,	Outer count: 1, Inner count: 0
>>> GROUP: LibreNMS-group-RW,	Outer count: 0, Inner count: 0

If I change “CN” to “memberOf” in attributes, then inner count is 1 also:

>>> GROUP: LibreNMS-admin,	    Outer count: 1, Inner count: 1
>>> GROUP: LibreNMS-readonly,	Outer count: 1, Inner count: 1
>>> GROUP: LibreNMS-group-RW,	Outer count: 0, Inner count: 0

The test_user is member of LibreNMS-admin group and only, not the other two.

The test PHP code:

$groups = array('LibreNMS-admin','LibreNMS-readonly','LibreNMS-group-RW');
foreach ($groups as $group) {
        $ldap_filter = "(&(&(samaccountname=test_user)(!(useraccountcontrol:1.2.840.113556.1.4.803:=2))(objectclass=user))(memberOf:1.2.840.113556.1.4.1941:=cn=".$group.",".$group_dn."))";
        $attributes = array("DN");
#       $attributes = array("memberOf");
        $search_result = @ldap_search($ldap_conn, $ldap_base_dn, $ldap_filter, $attributes);
        if (!$search_result) { die("LDAP search error: " . ldap_error($ldap_conn)); }

        $entries = ldap_get_entries($ldap_conn, $search_result);

        if ($entries["count"] > 0) {
                $co = $entries["count"];
                $ci = $entries["0"]['count'];
                echo ">>> GROUP: ".$group.",\tOuter count: ".$co.", Inner count: ".$ci."\n";
        } else {
                echo ">>> GROUP: ".$group.",\tOuter count: 0, Inner count: 0\n";
        }
}

After I change the filter to:

$ldap_filter = "(&(&(samaccountname=test_user)(!(useraccountcontrol:1.2.840.113556.1.4.803:=2))(objectclass=user))(memberOf=cn=".$group.",".$group_dn."))";

and attributes is “DN”, then:

>>> GROUP: LibreNMS-admin,	    Outer count: 1, Inner count: 0
>>> GROUP: LibreNMS-readonly,	Outer count: 0, Inner count: 0
>>> GROUP: LibreNMS-group-RW,	Outer count: 0, Inner count: 0

If I change attributes to “memberOf”, then:

>>> GROUP: LibreNMS-admin,	    Outer count: 1, Inner count: 1
>>> GROUP: LibreNMS-readonly,	Outer count: 0, Inner count: 0
>>> GROUP: LibreNMS-group-RW,	Outer count: 0, Inner count: 0

As we see, current code could gets back false positive authorization to unauthorized users. Any hints how to fix the code?

I don’t understand your data. what is inner, outer, and count? What do you expect them to be?

Is one group a member of another group? It sounds like that is your issue that some group has membership in another group and you don’t expect that.

Hi Murray,

I copied a test PHP code from the ActiveDirectoryAuthorizer.php file from the end of the userInGroup function, see above, so we can reproduce the issue without changing the running LibreNMS code.

There is a test_user in AD, which is only member of the LibreNMS-admin group. I simply playing with the ldap_search filter, AI helped to identify and hotfix this issue, and it seems current LibreNMS code gets back false positive authorization for users that otherwise has not.

This is because of our ActiveDirectory also gets back result for group what the user not member of, because of the filter(?), in this example for LibreNMS-readonly.

In other words, if filter contains that LDAP_MATCHING_RULE_IN_CHAIN OID, then AD gets back false results, it says, there is result in “LibreNMS-admin” and “LibreNMS-readonly” groups (count=1), and no result for “LibreNMS-group-RW” group (count=0), so gives the user two permissions while being a member of only one group:

>>> GROUP: LibreNMS-admin:
>>> RESULT: {"count":1,"0":{"count":0,"dn":"CN=Test User,OU=Organization Unit,DC=Company,DC=net"}}

>>> GROUP: LibreNMS-readonly:
>>> RESULT: {"count":1,"0":{"count":0,"dn":"CN=Test User,OU=Organization Unit,DC=Company,DC=net"}}

>>> GROUP: LibreNMS-group-RW:
>>> RESULT: {"count":0}

If I remove the OID from ldap_search filter, and using only “memberOf=$group_dn” then the results are:

>>> GROUP: LibreNMS-admin:
>>> RESULT: {"count":1,"0":{"count":0,"dn":"CN=Test User,OU=Organization Unit,DC=Company,DC=net"}}

>>> GROUP: LibreNMS-readonly:
>>> RESULT: {"count":0}

>>> GROUP: LibreNMS-group-RW:
>>> RESULT: {"count":0}

Best Regards,

Tibor

Thinking about this, and maybe the best resolution for this issue is a new toggle switch for something like “Nested Group Memberships“, and if this is true, then ldap_search filter would be with OID, if false, then filter would be without OID. I don’t know how right AI is in this, but it thinks that if there is no embedded group membership, then it is forbidden to use OID, for this very reason. Thanks!

Best Regards,

Tibor

If your active directory is saying that you are in that group with the nested group membership filter. Then this is an active directory bug as far as I can tell.

Yes, you are right, BUT! I don’t think we are the one and only who fell into this trap, this is a security risk, more or less. I mean, if there are no nested group memberships in AD and librenms is hardcoded to search using LDAP_MATCHING_RULE_IN_CHAIN ​​anyway.

As I wrote we don’t use nested group memberships yet, our librenms authorization groups are fresh new, and checked a user’s member and dynamic memberOf attribute lists, those contains only one attribute, AD nevertheless gives a hit in two groups.

Without LDAP_MATCHING_RULE_IN_CHAIN, it works as it gets. So our opinion is, the best approach for everyone is would be to supplement the existing AD author code with a choice, for security reason. The default would be the simplest where there is no nested group memberships, and on the GUI it has a flip button to activate this feature in search for those whom really using nested group memberships feature in their ADs.

Thanks!

Kind Regards,

Tibor