mirror of
https://github.com/freeipa/ansible-freeipa.git
synced 2026-03-27 05:43:05 +00:00
The information about the version limitation of the passwordexpiration parameter has been missing. The parameter is only usable for IPA versions 4.7 and up.
1318 lines
51 KiB
Python
1318 lines
51 KiB
Python
#!/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
|
|
users:
|
|
description: The list of user dicts (internally uid).
|
|
options:
|
|
name:
|
|
description: The ser (internally uid).
|
|
required: true
|
|
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
|
|
initials:
|
|
description: Initials
|
|
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
|
|
principal:
|
|
description: The kerberos principal
|
|
required: false
|
|
aliases: ["principalname", "krbprincipalname"]
|
|
principalexpiration:
|
|
description:
|
|
- The kerberos principal 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: ["krbprincipalexpiration"]
|
|
passwordexpiration:
|
|
description:
|
|
- The kerberos password expiration date (FreeIPA-4.7+)
|
|
- (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.
|
|
- Only usable with IPA versions 4.7 and up.
|
|
required: false
|
|
aliases: ["krbpasswordexpiration"]
|
|
password:
|
|
description: The user password
|
|
required: false
|
|
random:
|
|
description: Generate a random user password
|
|
required: false
|
|
type: bool
|
|
uid:
|
|
description: The UID
|
|
required: false
|
|
aliases: ["uidnumber"]
|
|
gid:
|
|
description: The GID
|
|
required: false
|
|
aliases: ["gidnumber"]
|
|
city:
|
|
description: City
|
|
required: false
|
|
userstate:
|
|
description: State/Province
|
|
required: false
|
|
aliases: ["st"]
|
|
postalcode:
|
|
description: Postalcode/ZIP
|
|
required: false
|
|
aliases: ["zip"]
|
|
phone:
|
|
description: List of telephone numbers
|
|
required: false
|
|
aliases: ["telephonenumber"]
|
|
mobile:
|
|
description: List of mobile telephone numbers
|
|
required: false
|
|
pager:
|
|
description: List of pager numbers
|
|
required: false
|
|
fax:
|
|
description: List of fax numbers
|
|
required: false
|
|
aliases: ["facsimiletelephonenumber"]
|
|
orgunit:
|
|
description: Org. Unit
|
|
required: false
|
|
title:
|
|
description: The job title
|
|
required: false
|
|
manager:
|
|
description: List of managers
|
|
required: false
|
|
carlicense:
|
|
description: List of car licenses
|
|
required: false
|
|
sshpubkey:
|
|
description: List of SSH public keys
|
|
required: false
|
|
aliases: ["ipasshpubkey"]
|
|
userauthtype:
|
|
description: List of supported user authentication types
|
|
choices=['password', 'radius', 'otp']
|
|
required: false
|
|
userclass:
|
|
description:
|
|
- User category
|
|
- (semantics placed on this attribute are for local interpretation)
|
|
required: false
|
|
radius:
|
|
description: RADIUS proxy configuration
|
|
required: false
|
|
radiususer:
|
|
description: RADIUS proxy username
|
|
required: false
|
|
departmentnumber:
|
|
description: Department Number
|
|
required: false
|
|
employeenumber:
|
|
description: Employee Number
|
|
required: false
|
|
employeetype:
|
|
description: Employee Type
|
|
required: false
|
|
preferredlanguage:
|
|
description: Preferred Language
|
|
required: false
|
|
certificate:
|
|
description: List of base-64 encoded user certificates
|
|
required: false
|
|
certmapdata:
|
|
description: List of certificate mappings
|
|
options:
|
|
certificate:
|
|
description: Base-64 encoded user certificate
|
|
required: false
|
|
issuer:
|
|
description: Issuer of the certificate
|
|
required: false
|
|
subject:
|
|
description: Subject of the certificate
|
|
required: false
|
|
required: false
|
|
noprivate:
|
|
description: Don't create user private group
|
|
required: false
|
|
type: bool
|
|
nomembers:
|
|
description: Suppress processing of membership attributes
|
|
required: false
|
|
type: bool
|
|
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
|
|
initials:
|
|
description: Initials
|
|
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
|
|
principal:
|
|
description: The kerberos principal
|
|
required: false
|
|
aliases: ["principalname", "krbprincipalname"]
|
|
principalexpiration:
|
|
description:
|
|
- The kerberos principal 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: ["krbprincipalexpiration"]
|
|
passwordexpiration:
|
|
description:
|
|
- The kerberos password expiration date (FreeIPA-4.7+)
|
|
- (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.
|
|
- Only usable with IPA versions 4.7 and up.
|
|
required: false
|
|
aliases: ["krbpasswordexpiration"]
|
|
password:
|
|
description: The user password
|
|
required: false
|
|
random:
|
|
description: Generate a random user password
|
|
required: false
|
|
type: bool
|
|
uid:
|
|
description: The UID
|
|
required: false
|
|
aliases: ["uidnumber"]
|
|
gid:
|
|
description: The GID
|
|
required: false
|
|
aliases: ["gidnumber"]
|
|
city:
|
|
description: City
|
|
required: false
|
|
userstate:
|
|
description: State/Province
|
|
required: false
|
|
aliases: ["st"]
|
|
postalcode:
|
|
description: ZIP
|
|
required: false
|
|
aliases: ["zip"]
|
|
phone:
|
|
description: List of telephone numbers
|
|
required: false
|
|
aliases: ["telephonenumber"]
|
|
mobile:
|
|
description: List of mobile telephone numbers
|
|
required: false
|
|
pager:
|
|
description: List of pager numbers
|
|
required: false
|
|
fax:
|
|
description: List of fax numbers
|
|
required: false
|
|
aliases: ["facsimiletelephonenumber"]
|
|
orgunit:
|
|
description: Org. Unit
|
|
required: false
|
|
title:
|
|
description: The job title
|
|
required: false
|
|
manager:
|
|
description: List of managers
|
|
required: false
|
|
carlicense:
|
|
description: List of car licenses
|
|
required: false
|
|
sshpubkey:
|
|
description: List of SSH public keys
|
|
required: false
|
|
aliases: ["ipasshpubkey"]
|
|
userauthtype:
|
|
description: List of supported user authentication types
|
|
choices=['password', 'radius', 'otp']
|
|
required: false
|
|
userclass:
|
|
description:
|
|
- User category
|
|
- (semantics placed on this attribute are for local interpretation)
|
|
required: false
|
|
radius:
|
|
description: RADIUS proxy configuration
|
|
required: false
|
|
radiususer:
|
|
description: RADIUS proxy username
|
|
required: false
|
|
departmentnumber:
|
|
description: Department Number
|
|
required: false
|
|
employeenumber:
|
|
description: Employee Number
|
|
required: false
|
|
employeetype:
|
|
description: Employee Type
|
|
required: false
|
|
preferredlanguage:
|
|
description: Preferred Language
|
|
required: false
|
|
certificate:
|
|
description: List of base-64 encoded user certificates
|
|
required: false
|
|
certmapdata:
|
|
description: List of certificate mappings
|
|
options:
|
|
certificate:
|
|
description: Base-64 encoded user certificate
|
|
required: false
|
|
issuer:
|
|
description: Issuer of the certificate
|
|
required: false
|
|
subject:
|
|
description: Subject of the certificate
|
|
required: false
|
|
required: false
|
|
noprivate:
|
|
description: Don't create user private group
|
|
required: false
|
|
type: bool
|
|
nomembers:
|
|
description: Suppress processing of membership attributes
|
|
required: false
|
|
type: bool
|
|
preserve:
|
|
description: Delete a user, keeping the entry available for future use
|
|
required: false
|
|
update_password:
|
|
description:
|
|
Set password for a user in present state only on creation or always
|
|
default: "always"
|
|
choices: ["always", "on_create"]
|
|
required: false
|
|
action:
|
|
description: Work on user or member level
|
|
default: "user"
|
|
choices: ["member", "user"]
|
|
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_text
|
|
from ansible.module_utils.ansible_freeipa_module import temp_kinit, \
|
|
temp_kdestroy, valid_creds, api_connect, api_command, date_format, \
|
|
compare_args_ipa, module_params_get, api_check_param, api_get_realm
|
|
import six
|
|
|
|
|
|
if six.PY3:
|
|
unicode = str
|
|
|
|
|
|
def find_user(module, name, preserved=False):
|
|
_args = {
|
|
"all": True,
|
|
"uid": name,
|
|
}
|
|
if preserved:
|
|
_args["preserved"] = preserved
|
|
|
|
_result = api_command(module, "user_find", name, _args)
|
|
|
|
if len(_result["result"]) > 1:
|
|
module.fail_json(
|
|
msg="There is more than one user '%s'" % (name))
|
|
elif len(_result["result"]) == 1:
|
|
# Transform each principal to a string
|
|
_result = _result["result"][0]
|
|
if "krbprincipalname" in _result \
|
|
and _result["krbprincipalname"] is not None:
|
|
_list = []
|
|
for x in _result["krbprincipalname"]:
|
|
_list.append(str(x))
|
|
_result["krbprincipalname"] = _list
|
|
return _result
|
|
else:
|
|
return None
|
|
|
|
|
|
def gen_args(first, last, fullname, displayname, initials, homedir, shell,
|
|
email, principalexpiration, passwordexpiration, password,
|
|
random, uid, gid, city, userstate, postalcode, phone, mobile,
|
|
pager, fax, orgunit, title, carlicense, sshpubkey, userauthtype,
|
|
userclass, radius, radiususer, departmentnumber, employeenumber,
|
|
employeetype, preferredlanguage, noprivate, nomembers):
|
|
# principal, manager, certificate and certmapdata are handled not in here
|
|
_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 initials is not None:
|
|
_args["initials"] = initials
|
|
if homedir is not None:
|
|
_args["homedirectory"] = homedir
|
|
if shell is not None:
|
|
_args["loginshell"] = shell
|
|
if email is not None and len(email) > 0:
|
|
_args["mail"] = email
|
|
if principalexpiration is not None:
|
|
_args["krbprincipalexpiration"] = principalexpiration
|
|
if passwordexpiration is not None:
|
|
_args["krbpasswordexpiration"] = passwordexpiration
|
|
if password is not None:
|
|
_args["userpassword"] = password
|
|
if random is not None:
|
|
_args["random"] = random
|
|
if uid is not None:
|
|
_args["uidnumber"] = to_text(str(uid))
|
|
if gid is not None:
|
|
_args["gidnumber"] = to_text(str(gid))
|
|
if city is not None:
|
|
_args["l"] = city
|
|
if userstate is not None:
|
|
_args["st"] = userstate
|
|
if postalcode is not None:
|
|
_args["postalcode"] = postalcode
|
|
if phone is not None and len(phone) > 0:
|
|
_args["telephonenumber"] = phone
|
|
if mobile is not None and len(mobile) > 0:
|
|
_args["mobile"] = mobile
|
|
if pager is not None and len(pager) > 0:
|
|
_args["pager"] = pager
|
|
if fax is not None and len(fax) > 0:
|
|
_args["facsimiletelephonenumber"] = fax
|
|
if orgunit is not None:
|
|
_args["ou"] = orgunit
|
|
if title is not None:
|
|
_args["title"] = title
|
|
if carlicense is not None and len(carlicense) > 0:
|
|
_args["carlicense"] = carlicense
|
|
if sshpubkey is not None and len(sshpubkey) > 0:
|
|
_args["ipasshpubkey"] = sshpubkey
|
|
if userauthtype is not None and len(userauthtype) > 0:
|
|
_args["ipauserauthtype"] = userauthtype
|
|
if userclass is not None:
|
|
_args["userclass"] = userclass
|
|
if radius is not None:
|
|
_args["ipatokenradiusconfiglink"] = radius
|
|
if radiususer is not None:
|
|
_args["ipatokenradiususername"] = radiususer
|
|
if departmentnumber is not None:
|
|
_args["departmentnumber"] = departmentnumber
|
|
if employeenumber is not None:
|
|
_args["employeenumber"] = employeenumber
|
|
if employeetype is not None:
|
|
_args["employeetype"] = employeetype
|
|
if preferredlanguage is not None:
|
|
_args["preferredlanguage"] = preferredlanguage
|
|
if noprivate is not None:
|
|
_args["noprivate"] = noprivate
|
|
if nomembers is not None:
|
|
_args["no_members"] = nomembers
|
|
return _args
|
|
|
|
|
|
def check_parameters(module, state, action,
|
|
first, last, fullname, displayname, initials, homedir,
|
|
shell, email, principal, principalexpiration,
|
|
passwordexpiration, password, random, uid, gid, city,
|
|
phone, mobile, pager, fax, orgunit, title, manager,
|
|
carlicense, sshpubkey, userauthtype, userclass, radius,
|
|
radiususer, departmentnumber, employeenumber,
|
|
employeetype, preferredlanguage, certificate,
|
|
certmapdata, noprivate, nomembers, preserve,
|
|
update_password):
|
|
|
|
if state == "present":
|
|
if action == "member":
|
|
invalid = ["first", "last", "fullname", "displayname", "initials",
|
|
"homedir", "shell", "email", "principalexpiration",
|
|
"passwordexpiration", "password", "random", "uid",
|
|
"gid", "city", "phone", "mobile", "pager", "fax",
|
|
"orgunit", "title", "carlicense", "sshpubkey",
|
|
"userauthtype", "userclass", "radius", "radiususer",
|
|
"departmentnumber", "employeenumber", "employeetype",
|
|
"preferredlanguage", "noprivate", "nomembers",
|
|
"preserve", "update_password"]
|
|
for x in invalid:
|
|
if vars()[x] is not None:
|
|
module.fail_json(
|
|
msg="Argument '%s' can not be used with action "
|
|
"'%s'" % (x, action))
|
|
|
|
else:
|
|
invalid = ["first", "last", "fullname", "displayname", "initials",
|
|
"homedir", "shell", "email", "principalexpiration",
|
|
"passwordexpiration", "password", "random", "uid",
|
|
"gid", "city", "phone", "mobile", "pager", "fax",
|
|
"orgunit", "title", "carlicense", "sshpubkey",
|
|
"userauthtype", "userclass", "radius", "radiususer",
|
|
"departmentnumber", "employeenumber", "employeetype",
|
|
"preferredlanguage", "noprivate", "nomembers",
|
|
"update_password"]
|
|
if action == "user":
|
|
invalid.extend(["principal", "manager",
|
|
"certificate", "certmapdata",
|
|
])
|
|
for x in invalid:
|
|
if vars()[x] is not None:
|
|
module.fail_json(
|
|
msg="Argument '%s' can not be used with state '%s'" %
|
|
(x, state))
|
|
|
|
if state != "absent" and preserve is not None:
|
|
module.fail_json(
|
|
msg="Preserve is only possible for state=absent")
|
|
|
|
if certmapdata is not None:
|
|
for x in certmapdata:
|
|
certificate = x.get("certificate")
|
|
issuer = x.get("issuer")
|
|
subject = x.get("subject")
|
|
|
|
if certificate is not None \
|
|
and (issuer is not None or subject is not None):
|
|
module.fail_json(
|
|
msg="certmapdata: certificate can not be used with "
|
|
"issuer or subject")
|
|
if certificate is None:
|
|
if issuer is None:
|
|
module.fail_json(msg="certmapdata: issuer is missing")
|
|
if subject is None:
|
|
module.fail_json(msg="certmapdata: subject is missing")
|
|
|
|
|
|
def gen_certmapdata_args(certmapdata):
|
|
certificate = certmapdata.get("certificate")
|
|
issuer = certmapdata.get("issuer")
|
|
subject = certmapdata.get("subject")
|
|
|
|
_args = {}
|
|
if certificate is not None:
|
|
_args["certificate"] = certificate
|
|
if issuer is not None:
|
|
_args["issuer"] = issuer
|
|
if subject is not None:
|
|
_args["subject"] = subject
|
|
return _args
|
|
|
|
|
|
def main():
|
|
user_spec = dict(
|
|
# 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),
|
|
initials=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),
|
|
principal=dict(type="list", aliases=["principalname",
|
|
"krbprincipalname"],
|
|
default=None),
|
|
principalexpiration=dict(type="str",
|
|
aliases=["krbprincipalexpiration"],
|
|
default=None),
|
|
passwordexpiration=dict(type="str",
|
|
aliases=["krbpasswordexpiration"],
|
|
default=None),
|
|
password=dict(type="str", default=None, no_log=True),
|
|
random=dict(type='bool', default=None),
|
|
uid=dict(type="int", aliases=["uidnumber"], default=None),
|
|
gid=dict(type="int", aliases=["gidnumber"], default=None),
|
|
city=dict(type="str", default=None),
|
|
userstate=dict(type="str", aliases=["st"], default=None),
|
|
postalcode=dict(type="str", aliases=["zip"], default=None),
|
|
phone=dict(type="list", aliases=["telephonenumber"], default=None),
|
|
mobile=dict(type="list", default=None),
|
|
pager=dict(type="list", default=None),
|
|
fax=dict(type="list", aliases=["facsimiletelephonenumber"],
|
|
default=None),
|
|
orgunit=dict(type="str", aliases=["ou"], default=None),
|
|
title=dict(type="str", default=None),
|
|
manager=dict(type="list", default=None),
|
|
carlicense=dict(type="list", default=None),
|
|
sshpubkey=dict(type="list", aliases=["ipasshpubkey"],
|
|
default=None),
|
|
userauthtype=dict(type='list', aliases=["ipauserauthtype"],
|
|
default=None,
|
|
choices=['password', 'radius', 'otp']),
|
|
userclass=dict(type="list", aliases=["class"],
|
|
default=None),
|
|
radius=dict(type="str", aliases=["ipatokenradiusconfiglink"],
|
|
default=None),
|
|
radiususer=dict(type="str", aliases=["radiususername",
|
|
"ipatokenradiususername"],
|
|
default=None),
|
|
departmentnumber=dict(type="list", default=None),
|
|
employeenumber=dict(type="str", default=None),
|
|
employeetype=dict(type="str", default=None),
|
|
preferredlanguage=dict(type="str", default=None),
|
|
certificate=dict(type="list", aliases=["usercertificate"],
|
|
default=None),
|
|
certmapdata=dict(type="list", default=None,
|
|
options=dict(
|
|
# Here certificate is a simple string
|
|
certificate=dict(type="str", default=None),
|
|
issuer=dict(type="str", default=None),
|
|
subject=dict(type="str", default=None)
|
|
),
|
|
elements='dict', required=False),
|
|
noprivate=dict(type='bool', default=None),
|
|
nomembers=dict(type='bool', default=None),
|
|
)
|
|
|
|
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=False),
|
|
users=dict(type="list", aliases=["login"], default=None,
|
|
options=dict(
|
|
# Here name is a simple string
|
|
name=dict(type="str", required=True),
|
|
# Add user specific parameters
|
|
**user_spec
|
|
),
|
|
elements='dict', required=False),
|
|
|
|
# deleted
|
|
preserve=dict(required=False, type='bool', default=None),
|
|
|
|
# mod
|
|
update_password=dict(type='str', default=None,
|
|
choices=['always', 'on_create']),
|
|
|
|
# general
|
|
action=dict(type="str", default="user",
|
|
choices=["member", "user"]),
|
|
state=dict(type="str", default="present",
|
|
choices=["present", "absent", "enabled", "disabled",
|
|
"unlocked", "undeleted"]),
|
|
|
|
# Add user specific parameters for simple use case
|
|
**user_spec
|
|
),
|
|
mutually_exclusive=[["name", "users"]],
|
|
required_one_of=[["name", "users"]],
|
|
supports_check_mode=True,
|
|
)
|
|
|
|
ansible_module._ansible_debug = True
|
|
|
|
# Get parameters
|
|
|
|
# general
|
|
ipaadmin_principal = module_params_get(ansible_module,
|
|
"ipaadmin_principal")
|
|
ipaadmin_password = module_params_get(ansible_module, "ipaadmin_password")
|
|
names = module_params_get(ansible_module, "name")
|
|
users = module_params_get(ansible_module, "users")
|
|
|
|
# present
|
|
first = module_params_get(ansible_module, "first")
|
|
last = module_params_get(ansible_module, "last")
|
|
fullname = module_params_get(ansible_module, "fullname")
|
|
displayname = module_params_get(ansible_module, "displayname")
|
|
initials = module_params_get(ansible_module, "initials")
|
|
homedir = module_params_get(ansible_module, "homedir")
|
|
shell = module_params_get(ansible_module, "shell")
|
|
email = module_params_get(ansible_module, "email")
|
|
principal = module_params_get(ansible_module, "principal")
|
|
principalexpiration = module_params_get(ansible_module,
|
|
"principalexpiration")
|
|
if principalexpiration is not None:
|
|
if principalexpiration[:-1] != "Z":
|
|
principalexpiration = principalexpiration + "Z"
|
|
principalexpiration = date_format(principalexpiration)
|
|
passwordexpiration = module_params_get(ansible_module,
|
|
"passwordexpiration")
|
|
if passwordexpiration is not None:
|
|
if passwordexpiration[:-1] != "Z":
|
|
passwordexpiration = passwordexpiration + "Z"
|
|
passwordexpiration = date_format(passwordexpiration)
|
|
password = module_params_get(ansible_module, "password")
|
|
random = module_params_get(ansible_module, "random")
|
|
uid = module_params_get(ansible_module, "uid")
|
|
gid = module_params_get(ansible_module, "gid")
|
|
city = module_params_get(ansible_module, "city")
|
|
userstate = module_params_get(ansible_module, "userstate")
|
|
postalcode = module_params_get(ansible_module, "postalcode")
|
|
phone = module_params_get(ansible_module, "phone")
|
|
mobile = module_params_get(ansible_module, "mobile")
|
|
pager = module_params_get(ansible_module, "pager")
|
|
fax = module_params_get(ansible_module, "fax")
|
|
orgunit = module_params_get(ansible_module, "orgunit")
|
|
title = module_params_get(ansible_module, "title")
|
|
manager = module_params_get(ansible_module, "manager")
|
|
carlicense = module_params_get(ansible_module, "carlicense")
|
|
sshpubkey = module_params_get(ansible_module, "sshpubkey")
|
|
userauthtype = module_params_get(ansible_module, "userauthtype")
|
|
userclass = module_params_get(ansible_module, "userclass")
|
|
radius = module_params_get(ansible_module, "radius")
|
|
radiususer = module_params_get(ansible_module, "radiususer")
|
|
departmentnumber = module_params_get(ansible_module, "departmentnumber")
|
|
employeenumber = module_params_get(ansible_module, "employeenumber")
|
|
employeetype = module_params_get(ansible_module, "employeetype")
|
|
preferredlanguage = module_params_get(ansible_module, "preferredlanguage")
|
|
certificate = module_params_get(ansible_module, "certificate")
|
|
certmapdata = module_params_get(ansible_module, "certmapdata")
|
|
noprivate = module_params_get(ansible_module, "noprivate")
|
|
nomembers = module_params_get(ansible_module, "nomembers")
|
|
# deleted
|
|
preserve = module_params_get(ansible_module, "preserve")
|
|
# mod
|
|
update_password = module_params_get(ansible_module, "update_password")
|
|
# general
|
|
action = module_params_get(ansible_module, "action")
|
|
state = module_params_get(ansible_module, "state")
|
|
|
|
# Check parameters
|
|
|
|
if (names is None or len(names) < 1) and \
|
|
(users is None or len(users) < 1):
|
|
ansible_module.fail_json(msg="One of name and users is required")
|
|
|
|
if state == "present":
|
|
if names is not None and len(names) != 1:
|
|
ansible_module.fail_json(
|
|
msg="Only one user can be added at a time using name.")
|
|
if action != "member":
|
|
# Only check first and last here if names is set
|
|
if names is not None:
|
|
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")
|
|
|
|
check_parameters(
|
|
ansible_module, state, action,
|
|
first, last, fullname, displayname, initials, homedir, shell, email,
|
|
principal, principalexpiration, passwordexpiration, password, random,
|
|
uid, gid, city, phone, mobile, pager, fax, orgunit, title, manager,
|
|
carlicense, sshpubkey, userauthtype, userclass, radius, radiususer,
|
|
departmentnumber, employeenumber, employeetype, preferredlanguage,
|
|
certificate, certmapdata, noprivate, nomembers, preserve,
|
|
update_password)
|
|
|
|
# Use users if names is None
|
|
if users is not None:
|
|
names = users
|
|
|
|
# Init
|
|
|
|
changed = False
|
|
exit_args = {}
|
|
ccache_dir = None
|
|
ccache_name = None
|
|
try:
|
|
if not valid_creds(ansible_module, ipaadmin_principal):
|
|
ccache_dir, ccache_name = temp_kinit(ipaadmin_principal,
|
|
ipaadmin_password)
|
|
api_connect()
|
|
|
|
# Check version specific settings
|
|
|
|
server_realm = api_get_realm()
|
|
|
|
commands = []
|
|
|
|
for user in names:
|
|
if isinstance(user, dict):
|
|
name = user.get("name")
|
|
# present
|
|
first = user.get("first")
|
|
last = user.get("last")
|
|
fullname = user.get("fullname")
|
|
displayname = user.get("displayname")
|
|
initials = user.get("initials")
|
|
homedir = user.get("homedir")
|
|
shell = user.get("shell")
|
|
email = user.get("email")
|
|
principal = user.get("principal")
|
|
principalexpiration = user.get("principalexpiration")
|
|
if principalexpiration is not None:
|
|
if principalexpiration[:-1] != "Z":
|
|
principalexpiration = principalexpiration + "Z"
|
|
principalexpiration = date_format(principalexpiration)
|
|
passwordexpiration = user.get("passwordexpiration")
|
|
if passwordexpiration is not None:
|
|
if passwordexpiration[:-1] != "Z":
|
|
passwordexpiration = passwordexpiration + "Z"
|
|
passwordexpiration = date_format(passwordexpiration)
|
|
password = user.get("password")
|
|
random = user.get("random")
|
|
uid = user.get("uid")
|
|
gid = user.get("gid")
|
|
city = user.get("city")
|
|
userstate = user.get("userstate")
|
|
postalcode = user.get("postalcode")
|
|
phone = user.get("phone")
|
|
mobile = user.get("mobile")
|
|
pager = user.get("pager")
|
|
fax = user.get("fax")
|
|
orgunit = user.get("orgunit")
|
|
title = user.get("title")
|
|
manager = user.get("manager")
|
|
carlicense = user.get("carlicense")
|
|
sshpubkey = user.get("sshpubkey")
|
|
userauthtype = user.get("userauthtype")
|
|
userclass = user.get("userclass")
|
|
radius = user.get("radius")
|
|
radiususer = user.get("radiususer")
|
|
departmentnumber = user.get("departmentnumber")
|
|
employeenumber = user.get("employeenumber")
|
|
employeetype = user.get("employeetype")
|
|
preferredlanguage = user.get("preferredlanguage")
|
|
certificate = user.get("certificate")
|
|
certmapdata = user.get("certmapdata")
|
|
noprivate = user.get("noprivate")
|
|
nomembers = user.get("nomembers")
|
|
|
|
check_parameters(
|
|
ansible_module, state, action,
|
|
first, last, fullname, displayname, initials, homedir,
|
|
shell, email, principal, principalexpiration,
|
|
passwordexpiration, password, random, uid, gid, city,
|
|
phone, mobile, pager, fax, orgunit, title, manager,
|
|
carlicense, sshpubkey, userauthtype, userclass, radius,
|
|
radiususer, departmentnumber, employeenumber,
|
|
employeetype, preferredlanguage, certificate,
|
|
certmapdata, noprivate, nomembers, preserve,
|
|
update_password)
|
|
|
|
elif isinstance(user, str) or isinstance(user, unicode):
|
|
name = user
|
|
else:
|
|
ansible_module.fail_json(msg="User '%s' is not valid" %
|
|
repr(user))
|
|
|
|
# Fix principals: add realm if missing
|
|
# We need the connected API for the realm, therefore it can not
|
|
# be part of check_parameters as this is used also before the
|
|
# connection to the API has been established.
|
|
if principal is not None:
|
|
principal = [x if "@" in x else x + "@" + server_realm
|
|
for x in principal]
|
|
|
|
# Check passwordexpiration availability.
|
|
# We need the connected API for this test, therefore it can not
|
|
# be part of check_parameters as this is used also before the
|
|
# connection to the API has been established.
|
|
if passwordexpiration is not None and \
|
|
not api_check_param("user_add", "krbpasswordexpiration"):
|
|
ansible_module.fail_json(
|
|
msg="The use of passwordexpiration is not supported by "
|
|
"your IPA version")
|
|
|
|
# Make sure user exists
|
|
res_find = find_user(ansible_module, name)
|
|
# Also search for preserved user if the user could not be found
|
|
if res_find is None:
|
|
res_find_preserved = find_user(ansible_module, name,
|
|
preserved=True)
|
|
else:
|
|
res_find_preserved = None
|
|
|
|
# Create command
|
|
if state == "present":
|
|
# Generate args
|
|
args = gen_args(
|
|
first, last, fullname, displayname, initials, homedir,
|
|
shell, email, principalexpiration, passwordexpiration,
|
|
password, random, uid, gid, city, userstate, postalcode,
|
|
phone, mobile, pager, fax, orgunit, title, carlicense,
|
|
sshpubkey, userauthtype, userclass, radius, radiususer,
|
|
departmentnumber, employeenumber, employeetype,
|
|
preferredlanguage, noprivate, nomembers)
|
|
|
|
# Also check preserved users
|
|
if res_find is None and res_find_preserved is not None:
|
|
res_find = res_find_preserved
|
|
|
|
if action == "user":
|
|
# Found the user
|
|
if res_find is not None:
|
|
# Ignore password and random with
|
|
# update_password == on_create
|
|
if update_password == "on_create":
|
|
if "userpassword" in args:
|
|
del args["userpassword"]
|
|
if "random" in args:
|
|
del args["random"]
|
|
if "noprivate" in args:
|
|
del args["noprivate"]
|
|
|
|
# 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])
|
|
|
|
# Handle members: principal, manager
|
|
if res_find is not None:
|
|
# Generate addition and removal lists
|
|
manager_add = list(
|
|
set(manager or []) -
|
|
set(res_find.get("manager", [])))
|
|
manager_del = list(
|
|
set(res_find.get("manager", [])) -
|
|
set(manager or []))
|
|
principal_add = list(
|
|
set(principal or []) -
|
|
set(res_find.get("krbprincipalname", [])))
|
|
principal_del = list(
|
|
set(res_find.get("krbprincipalname", [])) -
|
|
set(principal or []))
|
|
|
|
# Principals are not returned as utf8 for IPA using
|
|
# python2 using user_find, therefore we need to
|
|
# convert the principals that we should remove.
|
|
principal_del = [to_text(x) for x in principal_del]
|
|
|
|
certificate_add = list(
|
|
set(certificate or []) -
|
|
set(res_find.get("certificate", [])))
|
|
certificate_del = list(
|
|
set(res_find.get("certificate", [])) -
|
|
set(certificate or []))
|
|
certmapdata_add = list(
|
|
set(certmapdata or []) -
|
|
set(res_find.get("ipaCertMapData", [])))
|
|
certmapdata_del = list(
|
|
set(res_find.get("ipaCertMapData", [])) -
|
|
set(certmapdata or []))
|
|
|
|
else:
|
|
# Use given managers and principals
|
|
manager_add = manager or []
|
|
manager_del = []
|
|
principal_add = principal or []
|
|
principal_del = []
|
|
certificate_add = certificate or []
|
|
certificate_del = []
|
|
certmapdata_add = certmapdata or []
|
|
certmapdata_del = []
|
|
|
|
# Remove canonical principal from principal_del
|
|
canonical_principal = name + "@" + server_realm
|
|
if canonical_principal in principal_del:
|
|
principal_del.remove(canonical_principal)
|
|
|
|
# Add managers
|
|
if len(manager_add) > 0:
|
|
commands.append([name, "user_add_manager",
|
|
{
|
|
"user": manager_add,
|
|
}])
|
|
# Remove managers
|
|
if len(manager_del) > 0:
|
|
commands.append([name, "user_remove_manager",
|
|
{
|
|
"user": manager_del,
|
|
}])
|
|
|
|
# Principals need to be added and removed one by one,
|
|
# because if entry already exists, the processing of
|
|
# the remaining enries is stopped. The same applies to
|
|
# the removal of non-existing entries.
|
|
|
|
# Add principals
|
|
if len(principal_add) > 0:
|
|
for _principal in principal_add:
|
|
commands.append([name, "user_add_principal",
|
|
{
|
|
"krbprincipalname":
|
|
_principal,
|
|
}])
|
|
# Remove principals
|
|
if len(principal_del) > 0:
|
|
for _principal in principal_del:
|
|
commands.append([name, "user_remove_principal",
|
|
{
|
|
"krbprincipalname":
|
|
_principal,
|
|
}])
|
|
|
|
# Certificates need to be added and removed one by one,
|
|
# because if entry already exists, the processing of
|
|
# the remaining enries is stopped. The same applies to
|
|
# the removal of non-existing entries.
|
|
|
|
# Add certificates
|
|
if len(certificate_add) > 0:
|
|
for _certificate in certificate_add:
|
|
commands.append([name, "user_add_cert",
|
|
{
|
|
"usercertificate":
|
|
_certificate,
|
|
}])
|
|
# Remove certificates
|
|
if len(certificate_del) > 0:
|
|
for _certificate in certificate_del:
|
|
commands.append([name, "user_remove_cert",
|
|
{
|
|
"usercertificate":
|
|
_certificate,
|
|
}])
|
|
|
|
# certmapdata need to be added and removed one by one,
|
|
# because issuer and subject can only be done one by
|
|
# one reliably (https://pagure.io/freeipa/issue/8097)
|
|
|
|
# Add certmapdata
|
|
if len(certmapdata_add) > 0:
|
|
for _data in certmapdata_add:
|
|
commands.append([name, "user_add_certmapdata",
|
|
gen_certmapdata_args(_data)])
|
|
# Remove certmapdata
|
|
if len(certmapdata_del) > 0:
|
|
for _data in certmapdata_del:
|
|
commands.append([name, "user_add_certmapdata",
|
|
gen_certmapdata_args(_data)])
|
|
|
|
elif action == "member":
|
|
if res_find is None:
|
|
ansible_module.fail_json(
|
|
msg="No user '%s'" % name)
|
|
|
|
# Ensure managers are present
|
|
if manager is not None and len(manager) > 0:
|
|
commands.append([name, "user_add_manager",
|
|
{
|
|
"user": manager,
|
|
}])
|
|
|
|
# Principals need to be added and removed one by one,
|
|
# because if entry already exists, the processing of
|
|
# the remaining enries is stopped. The same applies to
|
|
# the removal of non-existing entries.
|
|
|
|
# Ensure principals are present
|
|
if principal is not None and len(principal) > 0:
|
|
for _principal in principal:
|
|
commands.append([name, "user_add_principal",
|
|
{
|
|
"krbprincipalname":
|
|
_principal,
|
|
}])
|
|
|
|
# Certificates need to be added and removed one by one,
|
|
# because if entry already exists, the processing of
|
|
# the remaining enries is stopped. The same applies to
|
|
# the removal of non-existing entries.
|
|
|
|
# Ensure certificates are present
|
|
if certificate is not None and len(certificate) > 0:
|
|
for _certificate in certificate:
|
|
commands.append([name, "user_add_cert",
|
|
{
|
|
"usercertificate":
|
|
_certificate,
|
|
}])
|
|
|
|
# certmapdata need to be added and removed one by one,
|
|
# because issuer and subject can only be done one by
|
|
# one reliably (https://pagure.io/freeipa/issue/8097)
|
|
|
|
# Ensure certmapdata are present
|
|
if certmapdata is not None and len(certmapdata) > 0:
|
|
for _data in certmapdata:
|
|
commands.append([name, "user_add_certmapdata",
|
|
gen_certmapdata_args(_data)])
|
|
|
|
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 action == "user":
|
|
if res_find is not None:
|
|
args = {}
|
|
if preserve is not None:
|
|
args["preserve"] = preserve
|
|
commands.append([name, "user_del", args])
|
|
elif action == "member":
|
|
if res_find is None:
|
|
ansible_module.fail_json(
|
|
msg="No user '%s'" % name)
|
|
|
|
# Ensure managers are absent
|
|
if manager is not None and len(manager) > 0:
|
|
commands.append([name, "user_remove_manager",
|
|
{
|
|
"user": manager,
|
|
}])
|
|
|
|
# Principals need to be added and removed one by one,
|
|
# because if entry already exists, the processing of
|
|
# the remaining enries is stopped. The same applies to
|
|
# the removal of non-existing entries.
|
|
|
|
# Ensure principals are absent
|
|
if principal is not None and len(principal) > 0:
|
|
commands.append([name, "user_remove_principal",
|
|
{
|
|
"krbprincipalname": principal,
|
|
}])
|
|
|
|
# Certificates need to be added and removed one by one,
|
|
# because if entry already exists, the processing of
|
|
# the remaining enries is stopped. The same applies to
|
|
# the removal of non-existing entries.
|
|
|
|
# Ensure certificates are absent
|
|
if certificate is not None and len(certificate) > 0:
|
|
for _certificate in certificate:
|
|
commands.append([name, "user_remove_cert",
|
|
{
|
|
"usercertificate":
|
|
_certificate,
|
|
}])
|
|
|
|
# certmapdata need to be added and removed one by one,
|
|
# because issuer and subject can only be done one by
|
|
# one reliably (https://pagure.io/freeipa/issue/8097)
|
|
|
|
# Ensure certmapdata are absent
|
|
if certmapdata is not None and len(certmapdata) > 0:
|
|
# Using issuer and subject can only be done one by
|
|
# one reliably (https://pagure.io/freeipa/issue/8097)
|
|
for _data in certmapdata:
|
|
commands.append([name, "user_remove_certmapdata",
|
|
gen_certmapdata_args(_data)])
|
|
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"]:
|
|
commands.append([name, "user_enable", {}])
|
|
else:
|
|
raise ValueError("No disabled user '%s'" % name)
|
|
|
|
elif state == "disabled":
|
|
if res_find is not None:
|
|
if not res_find["nsaccountlock"]:
|
|
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
|
|
|
|
errors = []
|
|
for name, command, args in commands:
|
|
try:
|
|
result = api_command(ansible_module, command, name,
|
|
args)
|
|
if "completed" in result:
|
|
if result["completed"] > 0:
|
|
changed = True
|
|
else:
|
|
changed = True
|
|
except Exception as e:
|
|
msg = str(e)
|
|
if "already contains" in msg \
|
|
or "does not contain" in msg:
|
|
continue
|
|
# The canonical principal name may not be removed
|
|
if "equal to the canonical principal name must" in msg:
|
|
continue
|
|
ansible_module.fail_json(msg="%s: %s: %s" % (command, name,
|
|
msg))
|
|
|
|
# Get all errors
|
|
# All "already a member" and "not a member" failures in the
|
|
# result are ignored. All others are reported.
|
|
if "failed" in result and len(result["failed"]) > 0:
|
|
for item in result["failed"]:
|
|
failed_item = result["failed"][item]
|
|
for member_type in failed_item:
|
|
for member, failure in failed_item[member_type]:
|
|
if "already a member" in failure \
|
|
or "not a member" in failure:
|
|
continue
|
|
errors.append("%s: %s %s: %s" % (
|
|
command, member_type, member, failure))
|
|
|
|
if len(errors) > 0:
|
|
ansible_module.fail_json(msg=", ".join(errors))
|
|
|
|
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()
|