#!/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 user (internally uid). required: true first: description: The first name required: false aliases: ["givenname"] last: description: The last name required: false aliases: ["sn"] 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 Use empty string to reset userauthtype to the initial value. choices=['password', 'radius', 'otp', ''] required: false aliases: ["ipauserauthtype"] 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 - Only usable with IPA versions 4.5 and up. 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 data: description: Certmap data 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 aliases: ["sn"] 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 Use empty string to reset userauthtype to the initial value. choices=['password', 'radius', 'otp', ''] required: false aliases: ["ipauserauthtype"] 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 - Only usable with IPA versions 4.5 and up. 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 data: description: Certmap data 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: SomeADMINpassword 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: SomeADMINpassword name: brain first: brain last: Acme # Delete user pinky, but preserved - ipauser: ipaadmin_password: SomeADMINpassword name: pinky preserve: yes state: absent # Undelete user pinky - ipauser: ipaadmin_password: SomeADMINpassword name: pinky state: undeleted # Disable user pinky - ipauser: ipaadmin_password: SomeADMINpassword name: pinky,brain state: disabled # Enable user pinky and brain - ipauser: ipaadmin_password: SomeADMINpassword name: pinky,brain state: enabled # Remove user pinky and brain - ipauser: ipaadmin_password: SomeADMINpassword name: pinky,brain state: disabled """ RETURN = """ user: description: User dict with random password returned: If random is yes and user did not exist or update_password is yes type: dict options: randompassword: description: The generated random password returned: If only one user is handled by the module name: description: The user name of the user that got a new random password returned: If several users are handled by the module type: dict options: randompassword: description: The generated random password returned: always """ 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, \ api_command_no_name, gen_add_del_lists, encode_certificate, \ load_cert_from_str, DN_x500_text, api_check_command 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 certs = _result.get("usercertificate") if certs is not None: _result["usercertificate"] = [encode_certificate(x) for x in certs] 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") data = x.get("data") if data is not None: if certificate is not None or issuer is not None or \ subject is not None: module.fail_json( msg="certmapdata: data can not be used with " "certificate, issuer or subject") check_certmapdata(data) 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 data is None and 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 extend_emails(email, default_email_domain): if email is not None: return ["%s@%s" % (_email, default_email_domain) if "@" not in _email else _email for _email in email] return email def convert_certmapdata(certmapdata): if certmapdata is None: return None _result = [] for x in certmapdata: certificate = x.get("certificate") issuer = x.get("issuer") subject = x.get("subject") data = x.get("data") if data is None: if issuer is None and subject is None: cert = load_cert_from_str(certificate) issuer = cert.issuer subject = cert.subject _result.append("X509:<I>%s<S>%s" % (DN_x500_text(issuer), DN_x500_text(subject))) else: _result.append(data) return _result def check_certmapdata(data): if not data.startswith("X509:"): return False i = data.find("<I>", 4) s = data.find("<S>", i) issuer = data[i+3:s] subject = data[s+3:] if i < 0 or s < 0 or "CN" not in issuer or "CN" not in subject: return False return True def gen_certmapdata_args(certmapdata): return {"ipacertmapdata": to_text(certmapdata)} def main(): user_spec = dict( # present first=dict(type="str", aliases=["givenname"], default=None), last=dict(type="str", aliases=["sn"], 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), data=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, no_log=False, 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.") 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) certmapdata = convert_certmapdata(certmapdata) # 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() # Default email domain result = api_command_no_name(ansible_module, "config_show", {}) default_email_domain = result["result"]["ipadefaultemaildomain"][0] # Extend email addresses email = extend_emails(email, default_email_domain) # commands 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) certmapdata = convert_certmapdata(certmapdata) # Extend email addresses email = extend_emails(email, default_email_domain) 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") # Check certmapdata 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 certmapdata is not None and \ not api_check_command("user_add_certmapdata"): ansible_module.fail_json( msg="The use of certmapdata 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"] # Ignore userauthtype if it is empty (for resetting) # and not set in for the user if "ipauserauthtype" not in res_find and \ "ipauserauthtype" in args and \ args["ipauserauthtype"] == ['']: del args["ipauserauthtype"] # 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: # Make sure we have a first and last name 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") commands.append([name, "user_add", args]) # Handle members: principal, manager, certificate and # certmapdata if res_find is not None: # Generate addition and removal lists manager_add, manager_del = gen_add_del_lists( manager, res_find.get("manager")) principal_add, principal_del = gen_add_del_lists( principal, res_find.get("krbprincipalname")) # 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, certificate_del = gen_add_del_lists( certificate, res_find.get("usercertificate")) certmapdata_add, certmapdata_del = gen_add_del_lists( certmapdata, res_find.get("ipacertmapdata")) 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_remove_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 if "random" in args and command in ["user_add", "user_mod"] \ and "randompassword" in result["result"]: if len(names) == 1: exit_args["randompassword"] = \ result["result"]["randompassword"] else: exit_args.setdefault(name, {})["randompassword"] = \ result["result"]["randompassword"] 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, user=exit_args) if __name__ == "__main__": main()