Skip to content
Snippets Groups Projects
Commit a36e8e08 authored by Thomas Woerner's avatar Thomas Woerner
Browse files

New user management module

There is a new user management module placed in the plugins folder:

  plugins/modules/ipauser.py

The user module allows to add, remove, enable, disable, unlock und undelete
users.

The user module is as compatible as possible to the Ansible upstream
`ipa_user` module, but addtionally offers to preserve delete, enable,
disable, unlock and undelete users.

Here is the documentation for the module:

  README-user.md

New example playbooks have been added:

  playbooks/user/add-user.yml
  playbooks/user/delete-user.yml
  playbooks/user/enable-user.yml
  playbooks/user/disable-user.yml
  playbooks/user/delete-preserve--user.yml
  playbooks/user/undelete-user.yml
parent 1cb0ac67
Branches
Tags
No related merge requests found
User module
===========
Description
-----------
The user module allows to add, remove, enable, disable, unlock und undelete users.
The user module is as compatible as possible to the Ansible upstream `ipa_user` module, but addtionally offers to preserve delete, enable, disable, unlock and undelete users.
Features
--------
* User management
Supported FreeIPA Versions
--------------------------
FreeIPA versions 4.4.0 and up are supported by the ipauser module.
Requirements
------------
**Controller**
* Ansible version: 2.8+
**Node**
* Supported FreeIPA version (see above)
Usage
=====
Example inventory file
```ini
[ipaserver]
ipaserver.test.local
```
Example playbook to add users:
```yaml
---
- name: Playbook to handle users
hosts: ipaserver
become: true
tasks:
# Create user pinky
- ipauser:
ipaadmin_password: MyPassword123
name: pinky
first: pinky
last: Acme
uid: 10001
gid: 100
phone: "+555123457"
email: pinky@acme.com
passwordexpiration: "2023-01-19 23:59:59"
password: "no-brain"
update_password: on_create
# Create user brain
- ipauser:
ipaadmin_password: MyPassword123
name: brain
first: brain
last: Acme
```
`update_password` controls if a password for a user will be set in present state only on creation or every time (always).
Example playbook to delete a user, but preserve it:
```yaml
---
- name: Playbook to handle users
hosts: ipaserver
become: true
tasks:
# Remove user pinky and brain
- ipauser:
ipaadmin_password: MyPassword123
name: pinky
preserve: yes
state: disabled
```
Example playbook to undelete a user.
```yaml
---
- name: Playbook to handle users
hosts: ipaserver
become: true
tasks:
# Remove user pinky and brain
- ipauser:
ipaadmin_password: MyPassword123
name: pinky
state: undeleted
```
Example playbook to disable a user:
```yaml
---
- name: Playbook to handle users
hosts: ipaserver
become: true
tasks:
# Remove user pinky and brain
- ipauser:
ipaadmin_password: MyPassword123
name: pinky
state: disabled
```
Example playbook to enable a users:
```yaml
---
- name: Playbook to handle users
hosts: ipaserver
become: true
tasks:
# Remove user pinky and brain
- ipauser:
ipaadmin_password: MyPassword123
name: pinky,brain
state: disabled
```
Example playbook to delete users:
```yaml
---
- name: Playbook to handle users
hosts: ipaserver
become: true
tasks:
# Remove user pinky and brain
- ipauser:
ipaadmin_password: MyPassword123
name: pinky,brain
state: disabled
```
Variables
=========
ipauser
-------
Variable | Description | Required
-------- | ----------- | --------
`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no
`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no
`name` | The list of user name strings. | no
`first` \| ¸givenname` | The first name string. | no
`last` | The last name | no
`fullname` \| `cn` | The full name string. | no
`displayname` | The display name string. | no
`homedir` | The home directory string. | no
`shell` \| `loginshell` | The login shell string. | no
`email` | List of email address strings. | no
`principalname` \| `krbprincipalname` | The kerberos principal sptring. | no
`passwordexpiration` \| `krbpasswordexpiration` | The kerberos password expiration date. Possible formats: `YYYYMMddHHmmssZ`, `YYYY-MM-ddTHH:mm:ssZ`, `YYYY-MM-ddTHH:mmZ`, `YYYY-MM-ddZ`, `YYYY-MM-dd HH:mm:ssZ` or `YYYY-MM-dd HH:mmZ`. The trailing 'Z' can be skipped. | no
`password` | The user password string. | no
`uid` \| `uidnumber` | The UID integer. | no
`gid` \| `gidnumber` | The GID integer. | no
`phone` \| `telephonenumber` | List of telephone number strings, | no
`title` | The job title string. | no
~~`sshpubkey` \| `ipasshpubkey`~~ | ~~List of SSH public keys.~~ | ~~no~~
`update_password` | Set password for a user in present state only on creation or always. It can be one of `always` or `on_create` and defaults to `always`. | no
`preserve` | Delete a user, keeping the entry available for future use. (bool) | no
`state` | The state to ensure. It can be one of `present`, `absent`, `enabled`, `disabled`, `unlocked` or `undeleted`, default: `present`. | yes
Authors
=======
Thomas Woerner
---
- name: Playbook to handle users
hosts: ipaserver
become: true
tasks:
# Create user pinky
- ipauser:
ipaadmin_password: MyPassword123
name: pinky
first: pinky
last: Acme
uid: 10001
gid: 100
phone: "+555123457"
email: pinky@acme.com
passwordexpiration: "2023-01-19 23:59:59"
password: "no-brain"
update_password: on_create
---
- name: Playbook to handle users
hosts: ipaserver
become: true
tasks:
# Remove user pinky and brain
- ipauser:
ipaadmin_password: MyPassword123
name: pinky
preserve: yes
state: disabled
---
- name: Playbook to handle users
hosts: ipaserver
become: true
tasks:
# Remove user pinky and brain
- ipauser:
ipaadmin_password: MyPassword123
name: pinky
state: disabled
---
- name: Playbook to handle users
hosts: ipaserver
become: true
tasks:
# Remove user pinky and brain
- ipauser:
ipaadmin_password: MyPassword123
name: pinky
state: disabled
---
- name: Playbook to handle users
hosts: ipaserver
become: true
tasks:
# Remove user pinky and brain
- ipauser:
ipaadmin_password: MyPassword123
name: pinky
state: disabled
---
- name: Playbook to handle users
hosts: ipaserver
become: true
tasks:
# Remove user pinky and brain
- ipauser:
ipaadmin_password: MyPassword123
name: pinky
state: undeleted
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Authors:
# Thomas Woerner <twoerner@redhat.com>
#
# Copyright (C) 2019 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
ANSIBLE_METADATA = {
"metadata_version": "1.0",
"supported_by": "community",
"status": ["preview"],
}
DOCUMENTATION = """
---
module: ipauser
short description: Manage FreeIPA users
description: Manage FreeIPA users
options:
ipaadmin_principal:
description: The admin principal
default: admin
ipaadmin_password:
description: The admin password
required: false
name:
description: The list of users (internally uid).
required: false
first:
description: The first name
required: false
aliases: ["givenname"]
last:
description: The last name
required: false
fullname:
description: The full name
required: false
aliases: ["cn"]
displayname:
description: The display name
required: false
homedir:
description: The home directory
required: false
shell:
description: The login shell
required: false
aliases: ["loginshell"]
email:
description: List of email addresses
required: false
principalname:
description: The kerberos principal
required: false
aliases: ["krbprincipalname"]
passwordexpiration:
description:
- The kerberos password expiration date
- (possible formats: YYYYMMddHHmmssZ, YYYY-MM-ddTHH:mm:ssZ,
- YYYY-MM-ddTHH:mmZ, YYYY-MM-ddZ, YYYY-MM-dd HH:mm:ssZ,
- YYYY-MM-dd HH:mmZ) The trailing 'Z' can be skipped.
required: false
aliases: ["krbpasswordexpiration"]
password:
description: The user password
required: false
uid:
description: The UID
required: false
aliases: ["uidnumber"]
gid:
description: The GID
required: false
aliases: ["gidnumber"]
phone:
description: List of telephone numbers
required: false
aliases: ["telephonenumber"]
title:
description: The job title
required: false
#sshpubkey:
# description: List of SSH public keys
# required: false
# aliases: ["ipasshpubkey"]
# ..
update_password:
description: Set password for a user in present state only on creation or always
default: 'always'
choices: ["always", "on_create"]
preserve:
description: Delete a user, keeping the entry available for future use
required: false
state:
description: State to ensure
default: present
choices: ["present", "absent",
"enabled", "disabled",
"unlocked", "undeleted"]
author:
- Thomas Woerner
"""
EXAMPLES = """
# Create user pinky
- ipauser:
ipaadmin_password: MyPassword123
name: pinky
first: pinky
last: Acme
uid: 10001
gid: 100
phone: "+555123457"
email: pinky@acme.com
passwordexpiration: "2023-01-19 23:59:59"
password: "no-brain"
update_password: on_create
# Create user brain
- ipauser:
ipaadmin_password: MyPassword123
name: brain
first: brain
last: Acme
# Delete user pinky, but preserved
- ipauser:
ipaadmin_password: MyPassword123
name: pinky
preserve: yes
state: absent
# Undelete user pinky
- ipauser:
ipaadmin_password: MyPassword123
name: pinky
state: undeleted
# Disable user pinky
- ipauser:
ipaadmin_password: MyPassword123
name: pinky,brain
state: disabled
# Enable user pinky and brain
- ipauser:
ipaadmin_password: MyPassword123
name: pinky,brain
state: enabled
# Remove user pinky and brain
- ipauser:
ipaadmin_password: MyPassword123
name: pinky,brain
state: disabled
"""
RETURN = """
"""
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_bytes, to_native, to_text
from ansible.module_utils.ansible_freeipa_module import temp_kinit, \
temp_kdestroy, valid_creds, api_connect, api_command, date_format, \
compare_args_ipa
def find_user(module, name, preserved=False):
#module.warn("find_user(.., %s)" % to_text(name))
_args = {
"all": True,
"uid": to_text(name),
}
if preserved:
_args["preserved"] = preserved
_result = api_command(module, "user_find", to_text(name), _args)
if len(_result["result"]) > 1:
module.fail_json(
msg="There is more than one user '%s'" % (name))
elif len(_result["result"]) == 1:
return _result["result"][0]
else:
return None
def gen_args(first, last, fullname, displayname, homedir, shell, emails,
principalname, passwordexpiration, password, uid, gid,
phones, title, sshpubkey):
_args = {}
if first is not None:
_args["givenname"] = first
if last is not None:
_args["sn"] = last
if fullname is not None:
_args["cn"] = fullname
if displayname is not None:
_args["displayname"] = displayname
if homedir is not None:
_args["homedirectory"] = homedir
if shell is not None:
_args["loginshell"] = shell
if emails is not None and len(emails) > 0:
_args["mail"] = emails
if principalname is not None:
_args["krbprincipalname"] = principalname
if passwordexpiration is not None:
_args["krbpasswordexpiration"] = passwordexpiration
if password is not None:
_args["userpassword"] = password
if uid is not None:
_args["uidnumber"] = str(uid)
if gid is not None:
_args["gidnumber"] = str(gid)
if phones is not None and len(phones) > 0:
_args["telephonenumber"] = phones
if title is not None:
_args["title"] = title
if sshpubkey is not None:
_args["ipasshpubkey"] = sshpubkey
return _args
def main():
ansible_module = AnsibleModule(
argument_spec=dict(
# general
ipaadmin_principal=dict(type="str", default="admin"),
ipaadmin_password=dict(type="str", required=False, no_log=True),
name=dict(type="list", aliases=["login"], default=None,
required=True),
# present
first=dict(type="str", aliases=["givenname"], default=None),
last=dict(type="str", default=None),
fullname=dict(type="str", aliases=["cn"], default=None),
displayname=dict(type="str", default=None),
homedir=dict(type="str", default=None),
shell=dict(type="str", aliases=["loginshell"], default=None),
email=dict(type="list", default=None),
principalname=dict(type="str", aliases=["krbprincipalname"],
default=None),
passwordexpiration=dict(type="str",
aliases=["krbpasswordexpiration"],
default=None),
password=dict(type="str", default=None, no_log=True),
uid=dict(type="int", aliases=["uidnumber"], default=None),
gid=dict(type="int", aliases=["gidnumber"], default=None),
phone=dict(type="list", aliases=["telephonenumber"], default=None),
title=dict(type="str", default=None),
#sshpubkey=dict(type="list", aliases=["ipasshpubkey"],
# default=None),
update_password=dict(type='str', default=None,
choices=['always', 'on_create']),
# deleted
preserve=dict(required=False, type='bool', default=None),
# state
state=dict(type="str", default="present",
choices=["present", "absent", "enabled", "disabled",
"unlocked", "undeleted"]),
),
supports_check_mode=True,
)
ansible_module._ansible_debug = True
# Get parameters
# general
ipaadmin_principal = ansible_module.params.get("ipaadmin_principal")
ipaadmin_password = ansible_module.params.get("ipaadmin_password")
names = ansible_module.params.get("name")
# present
first = ansible_module.params.get("first")
last = ansible_module.params.get("last")
fullname = ansible_module.params.get("fullname")
displayname = ansible_module.params.get("displayname")
homedir = ansible_module.params.get("homedir")
shell = ansible_module.params.get("shell")
emails = ansible_module.params.get("email")
principalname = ansible_module.params.get("principalname")
passwordexpiration = ansible_module.params.get("passwordexpiration")
if passwordexpiration is not None:
if passwordexpiration[:-1] != "Z":
passwordexpiration = "%sZ" % passwordexpiration
passwordexpiration = date_format(passwordexpiration)
password = ansible_module.params.get("password")
uid = ansible_module.params.get("uid")
gid = ansible_module.params.get("gid")
phones = ansible_module.params.get("phone")
title = ansible_module.params.get("title")
sshpubkey = ansible_module.params.get("sshpubkey")
update_password = ansible_module.params.get("update_password")
# deleted
preserve = ansible_module.params.get("preserve")
# state
state = ansible_module.params.get("state")
# Check parameters
if state == "present":
if len(names) != 1:
ansible_module.fail_json(
msg="Onle one user can be added at a time.")
if first is None:
ansible_module.fail_json(msg="First name is needed")
if last is None:
ansible_module.fail_json(msg="Last name is needed")
if state == "absent":
if len(names) < 1:
ansible_module.fail_json(
msg="No name given.")
for x in [ "first", "last", "fullname", "displayname", "homedir",
"shell", "emails", "principalname", "passwordexpiration",
"password", "uid", "gid", "phones", "title", "sshpubkey",
"update_password" ]:
if vars()[x] is not None:
ansible_module.fail_json(
msg="Argument '%s' can not be used with state '%s'" % \
(x, state))
else:
if preserve is not None:
ansible_module.fail_json(
msg="Preserve is only possible for state=absent")
if update_password is None:
update_password = "always"
# Init
changed = False
exit_args = { }
ccache_dir = None
ccache_name = None
try:
if not valid_creds(ipaadmin_principal):
ccache_dir, ccache_name = temp_kinit(ipaadmin_principal,
ipaadmin_password)
api_connect()
commands = []
for name in names:
# Make sure user exists
res_find = find_user(ansible_module, name)
# Also search for preserved user
res_find_preserved = find_user(ansible_module, name,
preserved=True)
#ansible_module.warn("res_find: %s" % repr(res_find))
# Create command
if state == "present":
# Generate args
args = gen_args(
first, last, fullname, displayname, homedir, shell, emails,
principalname, passwordexpiration, password, uid, gid,
phones, title, sshpubkey)
# Also check preserved users
if res_find is None and res_find_preserved is not None:
res_find = res_find_preserved
# Found the user
if res_find is not None:
# Ignore password with update_password == on_create
if update_password == "on_create" and \
"userpassword" in args:
del args["userpassword"]
# For all settings is args, check if there are
# different settings in the find result.
# If yes: modify
if not compare_args_ipa(ansible_module, args, res_find):
commands.append([name, "user_mod", args])
else:
commands.append([name, "user_add", args])
elif state == "absent":
# Also check preserved users
if res_find is None and res_find_preserved is not None:
res_find = res_find_preserved
if res_find is not None:
args = {}
if preserve is not None:
args["preserve"] = preserve
commands.append([name, "user_del", args])
elif state == "undeleted":
if res_find_preserved is not None:
commands.append([name, "user_undel", {}])
else:
raise ValueError("No preserved user '%s'" % name)
elif state == "enabled":
if res_find is not None:
if res_find["nsaccountlock"] == True:
commands.append([name, "user_enable", {}])
else:
raise ValueError("No disabled user '%s'" % name)
elif state == "disabled":
if res_find is not None:
if res_find["nsaccountlock"] == False:
commands.append([name, "user_disable", {}])
else:
raise ValueError("No user '%s'" % name)
elif state == "unlocked":
if res_find is not None:
commands.append([name, "user_unlock", {}])
else:
ansible_module.fail_json(msg="Unkown state '%s'" % state)
# Execute commands
for name, command, args in commands:
try:
result = api_command(ansible_module, command,
to_text(name), args)
changed = True
except Exception as e:
ansible_module.fail_json(msg="%s: %s: %s" % (command, name,
str(e)))
except Exception as e:
ansible_module.fail_json(msg=str(e))
finally:
temp_kdestroy(ccache_dir, ccache_name)
# Done
ansible_module.exit_json(changed=changed, **exit_args)
if __name__ == "__main__":
main()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment