Active Directory/LDAP User & Group Imports via Director

I have successful imports of users from LDAP working to Icinga2 via Director. I also successfully imported a group via LDAP.

My question is, is it possible to sync the LDAP group and all of the users it contains to Icinga2 via Director in order to have users automatically sync’d and created when users are added/changed/removed from the group in Active Directory?

This would be ideal if possible. If not, I will just import the users and manually create groups in icinga2 as I don’t know what the benefit would be of importing a group from LDAP if it doesn’t understand its members/users etc…

Thanks!

This depends a bit on your AD LDAP structure

  • Are CN matching the sAMAccountName
  • Can you filter our certain groups you want
  • Group in Group will be a problem

What can be tricky:

  • New groups must be added before the user
  • Users must be updated before a group can be removed

Here is an example. I won’t describe the LDAP Resource here, but its simple to create and used for auth in Icinga Web as well.

Import source: AD Users

Name: AD Users
Source Type: Ldap
Key column name: sAMAccountName
Resource: Your LDAP resource
Ldap Search Base: ou=User,ou=myorg,dc=int,dc=example,dc=com
Object class: user
Object filter: &(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2))
Properties: sAMAccountName, cn, sn, givenName, mail, mobile, memberOf

Now where the magic happens is modifier:

Property: memberOf
Modifier: Filter Array Values
Method: Simple Match
Filter: *,ou=groups,ou=myorg,dc=int,dc=example,dc=com
Policy: Keep matching
When empty: Return empty array
Property: memberOf
Modifier: Regex Replacement
Pattern: /^cn=(.+?),ou=.*/i
Replacement: \1

The modifier will replace the DN with the simple CN, which should be identical to the sAMAccountName of the group.

Import source: AD Groups

Name: AD Users
Source Type: Ldap
Key column name: cn
Resource: Your LDAP resource
Ldap Search Base: ou=groups,ou=myorg,dc=int,dc=example,dc=com
Object class: group
Object filter:
Properties: cn, name

Sync rule: AD Groups

Type: User group

Properties from AD Groups:

  • ${cn} -> object_name

Sync rule: AD Users

Type: User

Properties from AD Users:

  • ${sAMAccountName} -> object_name
  • ${memberOf} -> groups
  • ${cn} -> display_name
  • ${mail} -> email
  • ${mobile} -> pager

Result

object User "mfrosch" {
    display_name = "Markus Frosch"
    email = "markus.frosch@netways.de"
    groups = [
        "net-auth-consulting",
        // others
    ]
}
object UserGroup "net-auth-consulting" {
}
2 Likes

Thank you! I will play around with this today.

Hi,
We have set up import of users and groups in a certain ou called icinga on our AD server. The import seems to work fine except that although we create the groups and run an import before adding users to the groups on the AD server, the groups are empty in icinga. If we add users to the group on the icinga server, the synchronization rule will remove the users from the group.

Any suggestions to why the groups are empty?

Hello Per,

as far as I understand your problem, you have to add the users in the groups in AD. If you add them in icinga, the director will sync the groups from AD and there they are empty.

I hope that helps.
Oli

Hi again. we do add the users on the AD server, but they do not show up in Icinga. So although the group is created, it shows up as empty in Icinga.

Motivation:

This thread helped me a lot to get started.
Because of this, here is a little write up on where I went with it:

Goal:

Use AD groups to give selective access in Icingaweb2 and use the same groups for Notifications.

Problem:

This is bad! As required by AGDLP, we have business groups as members of permission groups.
Impossible with only the director I would guess:

https://github.com/Icinga/icingaweb2-module-director/issues/2222

My solution for the moment:

https://github.com/Icinga/icingaweb2-module-fileshipper

And this quick hacked together script:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8 ts=4 sw=4 sts=4 et :

from ldap3 import Server, Connection, ALL, NTLM
from ldap3.utils.conv import escape_filter_chars
import json
import sys

server = Server('ADSERVER', use_ssl=True, get_info=ALL)

def get_user_groups(dn):
    with Connection(server, user="BINDUSER", password="BINDPW", authentication=NTLM) as conn:
        dn = escape_filter_chars(dn)
        conn.search(
            search_base='SEARCHBASE',
            search_filter='(member:1.2.840.113556.1.4.1941:=%s)' % dn,)
        return conn.entries

with Connection(server, user="BINDUSER", password="BINDPW", authentication=NTLM) as conn:
    conn.search(
        search_base='SEARCHBASE',
        search_filter='\
        (&\
            (memberOf:1.2.840.113556.1.4.1941:=DNOFMAINGROUP)\
                    (!(userAccountControl:1.2.840.113556.1.4.803:=2))\
                        (mail=*)\
                        (objectClass=user)\
        )',
        attributes=[
            'SamAccountName',
            'displayName',
            'mail',
        ]
    )

    #print(conn.entries[0].entry_dn)
    icinga_users = []
    for user in conn.entries:
        user_json = user.entry_to_json()
        user_dict = json.loads(user_json)
        user_dict.update(user_dict['attributes'])
        del user_dict['attributes']
        user_dict['groups'] = []
        #print(user_dict)
        groups = get_user_groups(user.entry_dn)
        for group in groups:
            group_json = group.entry_to_json()
            group_dict = json.loads(group_json)
            #print(group_dict)
            user_dict['groups'].append(group_dict['dn'])
        #print(json.dumps(user_dict, indent=4, sort_keys=True))
        icinga_users.append(user_dict)
        #break
    #print(json.dumps(icinga_users, indent=4, sort_keys=True))
    with open('icinga_users.json', 'w') as outfile:
        json.dump(icinga_users, outfile)

Replace the following with your values:

  • ADSERVER
  • BINDUSER
  • BINDPW
  • DNOFMAINGROUP - I had to put all my icinga permisson groups in to one group - this is the one to rule them all

Next some cleanup. My Icingaweb2 permission groups have a ICT_Icinga_P prefix.
Excerpt from Basket:

"ImportSource": {
        "Json from icinga_aduser.py": {
            "description": null,
            "key_column": "sAMAccountName",
            "modifiers": [
                {
                    "description": null,
                    "priority": "1",
                    "property_name": "sAMAccountName",
                    "provider_class": "Icinga\\Module\\Director\\PropertyModifier\\PropertyModifierJoin",
                    "settings": {
                        "glue": ""
                    },
                    "target_property": null
                },
                {
                    "description": null,
                    "priority": "2",
                    "property_name": "displayName",
                    "provider_class": "Icinga\\Module\\Director\\PropertyModifier\\PropertyModifierJoin",
                    "settings": {
                        "glue": ""
                    },
                    "target_property": null
                },
                {
                    "description": null,
                    "priority": "3",
                    "property_name": "groups",
                    "provider_class": "Icinga\\Module\\Director\\PropertyModifier\\PropertyModifierArrayFilter",
                    "settings": {
                        "filter_method": "wildcard",
                        "filter_string": "*ICT_P_Icinga*",
                        "policy": "keep",
                        "when_empty": "empty_array"
                    },
                    "target_property": null
                },
                {
                    "description": null,
                    "priority": "4",
                    "property_name": "groups",
                    "provider_class": "Icinga\\Module\\Director\\PropertyModifier\\PropertyModifierRegexReplace",
                    "settings": {
                        "pattern": "\/^cn=(.+?),ou=.*\/i",
                        "replacement": "\\1"
                    },
                    "target_property": null
                },
                {
                    "description": null,
                    "priority": "5",
                    "property_name": "groups",
                    "provider_class": "Icinga\\Module\\Director\\PropertyModifier\\PropertyModifierRegexReplace",
                    "settings": {
                        "pattern": "\/^ICT_P_Icinga_(.+?)$\/i",
                        "replacement": "\\1"
                    },
                    "target_property": null
                },
                {
                    "description": null,
                    "priority": "6",
                    "property_name": "groups",
                    "provider_class": "Icinga\\Module\\Director\\PropertyModifier\\PropertyModifierArrayFilter",
                    "settings": {
                        "filter_method": "wildcard",
                        "filter_string": "ICT_P_Icinga",
                        "policy": "reject",
                        "when_empty": "empty_array"
                    },
                    "target_property": null
                },
                {
                    "description": null,
                    "priority": "7",
                    "property_name": "mail",
                    "provider_class": "Icinga\\Module\\Director\\PropertyModifier\\PropertyModifierJoin",
                    "settings": {
                        "glue": ""
                    },
                    "target_property": null
                }
            ],
            "originalId": "6",
            "provider_class": "Icinga\\Module\\Fileshipper\\ProvidedHook\\Director\\ImportSource",
            "settings": {
                "basedir": "\/var\/cache\/icinga_aduser",
                "file_format": "json",
                "file_name": "icinga_users.json"
            },
            "source_name": "Json from icinga_aduser.py"
        },

Groups are easy and and don’t requre a external script The use of:

(&(memberOf:1.2.840.113556.1.4.1941:=DNOFMAINGROUP)(objectClass=group))

in the disector source ldap query was enough for me. The cleanup details are left as excercise to the reader…