diff --git a/roles/proxmox/tasks/main.yml b/roles/proxmox/tasks/main.yml index 8fdc07d..6b87986 100644 --- a/roles/proxmox/tasks/main.yml +++ b/roles/proxmox/tasks/main.yml @@ -52,3 +52,5 @@ - include_tasks: firewall.yml - include_tasks: frr.yml + +- include_tasks: user.yml diff --git a/roles/proxmox/tasks/user.yml b/roles/proxmox/tasks/user.yml new file mode 100644 index 0000000..cf0a250 --- /dev/null +++ b/roles/proxmox/tasks/user.yml @@ -0,0 +1,27 @@ +- block: + - set_fact: + primary: '{{ nodes | map(attribute="inventory_hostname") | sort | first }}' + + - name: Install LDAP sync script + template: + dest: /usr/local/bin/sync-ldap.py + src: sync-ldap.py.j2 + mode: 0700 + when: primary == inventory_hostname + + - name: Remove LDAP sync script + file: + path: /usr/local/bin/sync-ldap.py + state: absent + when: primary != inventory_hostname + + - name: Configure cronjob + cron: + name: 'sync LDAP users and groups' + job: 'ip vrf exec default /usr/local/bin/sync-ldap.py' + user: root + cron_file: sync-ldap + hour: "2" + minute: "51" + state: '{{ "present" if inventory_hostname == primary else "absent" }}' + when: '"sync-ldap" in hostvars[inventory_hostname]' diff --git a/roles/proxmox/templates/sync-ldap.py.j2 b/roles/proxmox/templates/sync-ldap.py.j2 new file mode 100644 index 0000000..1cb0a7e --- /dev/null +++ b/roles/proxmox/templates/sync-ldap.py.j2 @@ -0,0 +1,57 @@ +#!/usr/bin/python3 + +import collections +import os +import re + +import ldap3 + +{% set password = lookup('passwordstore', "cluster/"+cluster, returnall=true) | from_yaml %} +realm = '{{ hostvars[inventory_hostname]["sync-ldap"] }}' +ldap_host = '{{ domain }}' +ldap_user = '{{ password.ldap_user }}' +ldap_pass = '{{ password.ldap_pass }}' +ldap_base = '{{ domain | split(".") | map("regex_replace", "^", "dc=") | join(",") }}' + +# build LDAP query for users +filters = [ + '(objectClass=user)', # only users + '(objectCategory=person)', # that are people + '(schacHomeOrganization=*)', # presumably + '(!(userAccountControl:1.2.840.113556.1.4.803:=2))' # with enabled accounts +] + +# run query +server = ldap3.Server(ldap_host, use_ssl=True) +ldap = ldap3.Connection(server, ldap_user, ldap_pass, auto_bind=True) +ldap.search(ldap_base, + f'(&{"".join(filters)})', # conjuction (&(…)(…)(…)) of queries + attributes=['userPrincipalName', 'givenName', 'sn', 'mail', 'memberOf']) + +# build user and group dicts +all_users = {} +all_groups = collections.defaultdict(set) +for e in ldap.entries: + user = f'{e.userPrincipalName.value}@{realm}' + all_users[user] = { k: e[k].value for k in e.entry_attributes } + for group in e.memberOf: + if m := re.match(r'^CN=([^,]*)', group.replace('\\,', '-')): + group = re.sub(r'[^A-Za-z0-9_.-]', '-', m[1]) + all_groups[group].add(user) + +with open('/etc/pve/user.cfg.new', 'w') as f: + # user:{username}@{realm}:1:0:{name}:{surname}:{mail}:AD sync:: + for user, info in sorted(all_users.items()): + print(f'user:{user}:1:0:{info["givenName"]}:{info["sn"]}:{info["mail"]}:AD sync::', file=f) + + # group:{name}:{users}:AD sync: + print(f'group:ALL:{",".join(sorted(all_users))}:AD sync:', file=f) + for group, users in all_groups.items(): + print(f'group:{group}:{",".join(sorted(users))}:AD sync:', file=f) + + # keep everything not added by us + for line in open('/etc/pve/user.cfg'): + if not re.match('^(user|group):.*:AD sync:', line): + print(line, end='', file=f) + +os.rename('/etc/pve/user.cfg.new', '/etc/pve/user.cfg')