Continuing on the trend of posts about AWX, I’ve been working on solving the problem of managing the user lifecycle in our Linux environment for quite a while. I developed some scaffolding in our Ansible environment a while back that proved to be pretty helpful, but it broke quite often and some of my colleagues were not well-versed in the terminal. Now that we have a web interface to work with, I sought to take advantage and develop some tooling that would ease the burden on my teammates. The problem I kept running into was that idempotency would often be a roadblock rather than the “good design” that it usually is.
For example, say you have an existing account using a typical user ID format of first initial-last name, “dberg” for a user named “David Berg.” Now your colleague wants to create a new account for someone named “Derek Berg.” While the email addresses would certainly be different, the IDs would overlap using the format described above. Let’s say your colleague doesn’t know about this existing account and attempts to create it by feeding that user ID into a playbook. If you provide the freeipa.ansible_freeipa.ipauser
module with a user ID that already exists, but an email address that differs from the address currently on record, it will change the email address on the existing user ID to reconcile the difference, not understanding that the goal is to create a new user account. It makes sense from an Ansible perspective, but frustrating to deal with from a user management perspective.
What I needed was some pre-flight checks to ensure:
- we weren’t overwriting data on an existing account
- we weren’t creating a duplicate account for someone who already has one (ie. same email address)
Eventually I discovered the community.general.ldap_search
module, and after taking a few stabs during some free time, I finally found a formula that worked. Keep in mind here that, as alluded to above, we’re using FreeIPA in this example, so while you could likely extrapolate this idea to any IAM system that uses LDAP, some of these tasks may be a bit IPA-specific. See below:
- name: "Create an IPA account"
hosts: ipaserver
vars:
ipa_search_user: # IPA account that can search the LDAP database
ipa_search_pw: # password for above account
target_uid: # user ID you want to create
target_mail: # email address for the target user
pre_tasks:
- name: "Run pre-flight checks for an existing account"
block:
- name: "Check for an existing active user ID"
community.general.ldap_search:
server_uri: ldaps://ipaserver.my.domain:636
dn: cn=users,cn=accounts,dc=my,dc=domain
scope: children
bind_dn: uid={{ ipa_search_user }},cn=users,cn=accounts,dc=ux,dc=kvh
bind_pw: "{{ ipa_search_pw }}"
filter: "(&(objectClass=person)(uid={{ target_uid }}))"
attrs:
- uid
- mail
register: ldap_account
failed_when: ldap_account['results']
# If ldap_account['results'] has something in it, that means we found
# an existing account and we shouldn't move forward.
- name: "Check for a preserved user ID"
# A "preserved" user in FreeIPA is essentially a "deleted" account
# that can be reactivated
community.general.ldap_search:
server_uri: ldaps://ipaserver.my.domain:636
dn: cn=deleted users,cn=accounts,cn=provisioning,dc=my,dc=domain
scope: children
bind_dn: uid={{ ipa_search_user }},cn=users,cn=accounts,dc=ux,dc=kvh
bind_pw: "{{ ipa_search_pw }}"
filter: "(&(objectClass=person)(uid={{ target_uid }}))"
attrs:
- uid
- mail
register: ldap_account
failed_when: ldap_account['results']
- name: "Check for an existing active user email address"
# We run a similar check on email address because this is a unique
# identifier, in the sense that no two users will ever have the same
# email address. Email is managed by a separate domain in our case.
community.general.ldap_search:
server_uri: ldaps://ipaserver.my.domain:636
dn: cn=users,cn=accounts,dc=my,dc=domain
scope: children
bind_dn: uid={{ ipa_search_user }},cn=users,cn=accounts,dc=ux,dc=kvh
bind_pw: "{{ ipa_search_pw }}"
filter: "(&(objectClass=person)(mail={{ target_mail }}))"
attrs:
- uid
- mail
register: ldap_account
failed_when: ldap_account['results']
- name: "Check for a preserved user email address"
community.general.ldap_search:
server_uri:
dn: cn=deleted users,cn=accounts,cn=provisioning,dc=my,dc=domain
scope: children
bind_dn: uid={{ ipa_search_user }},cn=users,cn=accounts,dc=ux,dc=kvh
bind_pw: "{{ ipa_search_pw }}"
filter: "(&(objectClass=person)(mail={{ target_mail }}))"
attrs:
- uid
- mail
register: ldap_account
failed_when: ldap_account['results']
rescue:
- name: "Print error message"
# This rescue is optional, but just emphasizes to the playbook runner
# that they need to choose a different user ID, or confirm that an
# account for the email address they input already exists.
ansible.builtin.fail:
msg: "Oops, we found an account!"
After these pre-flight checks pass, you can run your tasks to create the account, add the account to IPA groups, maybe send out a notification email to certain teams, etc.