# -*- coding: utf-8 -*- # Authors: # Thomas Woerner <twoerner@redhat.com> # # Copyright (C) 2019-2022 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/>. from __future__ import (absolute_import, division, print_function) __metaclass__ = type ANSIBLE_METADATA = { "metadata_version": "1.0", "supported_by": "community", "status": ["preview"], } DOCUMENTATION = """ --- module: ipauser short_description: Manage FreeIPA users description: Manage FreeIPA users extends_documentation_fragment: - ipamodule_base_docs options: name: description: The list of users (internally uid). type: list elements: str required: false aliases: ["login"] users: description: The list of user dicts (internally uid). type: list elements: dict suboptions: name: description: The user (internally uid). type: str required: true aliases: ["login"] first: description: The first name. Required if user does not exist. type: str required: false aliases: ["givenname"] last: description: The last name. Required if user doesnot exst. type: str required: false aliases: ["sn"] fullname: description: The full name type: str required: false aliases: ["cn"] displayname: description: The display name type: str required: false initials: description: Initials type: str required: false homedir: description: The home directory type: str required: false gecos: description: The GECOS type: str required: false shell: description: The login shell type: str required: false aliases: ["loginshell"] email: description: List of email addresses type: list elements: str required: false principal: description: The kerberos principal type: list elements: str 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. type: str 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. type: str required: false aliases: ["krbpasswordexpiration"] password: description: The user password type: str required: false random: description: Generate a random user password required: false type: bool uid: description: User ID Number (system will assign one if not provided) type: int required: false aliases: ["uidnumber"] gid: description: Group ID Number type: int required: false aliases: ["gidnumber"] street: description: Street address type: str required: false city: description: City type: str required: false userstate: description: State/Province type: str required: false aliases: ["st"] postalcode: description: Postalcode/ZIP type: str required: false aliases: ["zip"] phone: description: List of telephone numbers type: list elements: str required: false aliases: ["telephonenumber"] mobile: description: List of mobile telephone numbers type: list elements: str required: false pager: description: List of pager numbers type: list elements: str required: false fax: description: List of fax numbers type: list elements: str required: false aliases: ["facsimiletelephonenumber"] orgunit: description: Org. Unit type: str required: false aliases: ["ou"] title: description: The job title type: str required: false manager: description: List of managers type: list elements: str required: false carlicense: description: List of car licenses type: list elements: str required: false sshpubkey: description: List of SSH public keys required: false type: list elements: str aliases: ["ipasshpubkey"] userauthtype: description: List of supported user authentication types Use empty string to reset userauthtype to the initial value. type: list elements: str choices: ["password", "radius", "otp", "pkinit", "hardened", "idp", ""] required: false aliases: ["ipauserauthtype"] userclass: description: - User category - (semantics placed on this attribute are for local interpretation) type: list elements: str required: false aliases: ["class"] radius: description: RADIUS proxy configuration type: str required: false aliases: ["ipatokenradiusconfiglink"] radiususer: description: RADIUS proxy username type: str required: false aliases: ["radiususername", "ipatokenradiususername"] departmentnumber: description: Department Number type: list elements: str required: false employeenumber: description: Employee Number type: str required: false employeetype: description: Employee Type type: str required: false smb_logon_script: description: SMB logon script path type: str required: false aliases: ["ipantlogonscript"] smb_profile_path: description: SMB profile path type: str required: false aliases: ["ipantprofilepath"] smb_home_dir: description: SMB Home Directory type: str required: false aliases: ["ipanthomedirectory"] smb_home_drive: description: SMB Home Directory Drive type: str required: false choices: [ 'A:', 'B:', 'C:', 'D:', 'E:', 'F:', 'G:', 'H:', 'I:', 'J:', 'K:', 'L:', 'M:', 'N:', 'O:', 'P:', 'Q:', 'R:', 'S:', 'T:', 'U:', 'V:', 'W:', 'X:', 'Y:', 'Z:', '' ] aliases: ["ipanthomedirectorydrive"] preferredlanguage: description: Preferred Language type: str required: false idp: description: External IdP configuration type: str required: false aliases: ["ipaidpconfiglink"] idp_user_id: description: A string that identifies the user at external IdP type: str required: false aliases: ["ipaidpsub"] certificate: description: List of base-64 encoded user certificates type: list elements: str required: false aliases: ["usercertificate"] certmapdata: description: - List of certificate mappings - Only usable with IPA versions 4.5 and up. type: list elements: dict suboptions: certificate: description: Base-64 encoded user certificate type: str required: false issuer: description: Issuer of the certificate type: str required: false subject: description: Subject of the certificate type: str required: false data: description: Certmap data type: str 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 rename: description: Rename the user object required: false type: str aliases: ["new_name"] required: false first: description: The first name. Required if user does not exist. type: str required: false aliases: ["givenname"] last: description: The last name. Required if user doesnot exst. type: str required: false aliases: ["sn"] fullname: description: The full name type: str required: false aliases: ["cn"] displayname: description: The display name type: str required: false initials: description: Initials type: str required: false homedir: description: The home directory type: str required: false gecos: description: The GECOS type: str required: false shell: description: The login shell type: str required: false aliases: ["loginshell"] email: description: List of email addresses type: list elements: str required: false principal: description: The kerberos principal type: list elements: str 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. type: str 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. type: str required: false aliases: ["krbpasswordexpiration"] password: description: The user password type: str required: false random: description: Generate a random user password required: false type: bool uid: description: User ID Number (system will assign one if not provided) type: int required: false aliases: ["uidnumber"] gid: description: Group ID Number type: int required: false aliases: ["gidnumber"] street: description: Street address type: str required: false city: description: City type: str required: false userstate: description: State/Province type: str required: false aliases: ["st"] postalcode: description: Postalcode/ZIP type: str required: false aliases: ["zip"] phone: description: List of telephone numbers type: list elements: str required: false aliases: ["telephonenumber"] mobile: description: List of mobile telephone numbers type: list elements: str required: false pager: description: List of pager numbers type: list elements: str required: false fax: description: List of fax numbers type: list elements: str required: false aliases: ["facsimiletelephonenumber"] orgunit: description: Org. Unit type: str required: false aliases: ["ou"] title: description: The job title type: str required: false manager: description: List of managers type: list elements: str required: false carlicense: description: List of car licenses type: list elements: str required: false sshpubkey: description: List of SSH public keys required: false type: list elements: str aliases: ["ipasshpubkey"] userauthtype: description: List of supported user authentication types Use empty string to reset userauthtype to the initial value. type: list elements: str choices: ["password", "radius", "otp", "pkinit", "hardened", "idp", ""] required: false aliases: ["ipauserauthtype"] userclass: description: - User category - (semantics placed on this attribute are for local interpretation) type: list elements: str required: false aliases: ["class"] radius: description: RADIUS proxy configuration type: str required: false aliases: ["ipatokenradiusconfiglink"] radiususer: description: RADIUS proxy username type: str required: false aliases: ["radiususername", "ipatokenradiususername"] departmentnumber: description: Department Number type: list elements: str required: false employeenumber: description: Employee Number type: str required: false employeetype: description: Employee Type type: str required: false smb_logon_script: description: SMB logon script path type: str required: false aliases: ["ipantlogonscript"] smb_profile_path: description: SMB profile path type: str required: false aliases: ["ipantprofilepath"] smb_home_dir: description: SMB Home Directory type: str required: false aliases: ["ipanthomedirectory"] smb_home_drive: description: SMB Home Directory Drive type: str required: false choices: [ 'A:', 'B:', 'C:', 'D:', 'E:', 'F:', 'G:', 'H:', 'I:', 'J:', 'K:', 'L:', 'M:', 'N:', 'O:', 'P:', 'Q:', 'R:', 'S:', 'T:', 'U:', 'V:', 'W:', 'X:', 'Y:', 'Z:', '' ] aliases: ["ipanthomedirectorydrive"] preferredlanguage: description: Preferred Language type: str required: false idp: description: External IdP configuration type: str required: false aliases: ["ipaidpconfiglink"] idp_user_id: description: A string that identifies the user at external IdP type: str required: false aliases: ["ipaidpsub"] certificate: description: List of base-64 encoded user certificates type: list elements: str required: false aliases: ["usercertificate"] certmapdata: description: - List of certificate mappings - Only usable with IPA versions 4.5 and up. type: list elements: dict suboptions: certificate: description: Base-64 encoded user certificate type: str required: false issuer: description: Issuer of the certificate type: str required: false subject: description: Subject of the certificate type: str required: false data: description: Certmap data type: str 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 rename: description: Rename the user object required: false type: str aliases: ["new_name"] preserve: description: Delete a user, keeping the entry available for future use required: false type: bool update_password: description: Set password for a user in present state only on creation or always type: str choices: ["always", "on_create"] required: false action: description: Work on user or member level type: str default: "user" choices: ["member", "user"] state: description: State to ensure type: str default: present choices: ["present", "absent", "enabled", "disabled", "unlocked", "undeleted", "renamed"] author: - Thomas Woerner (@t-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 # Create multiple users pinky and brain - ipauser: ipaadmin_password: SomeADMINpassword users: - name: pinky first: pinky last: Acme - 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 but preserve user pinky - ipauser: ipaadmin_password: SomeADMINpassword users: - name: pinky preserve: yes state: absent # Remove user pinky and brain - ipauser: ipaadmin_password: SomeADMINpassword name: pinky,brain state: disabled # Ensure a user has SMB attributes - ipauser: ipaadmin_password: SomeADMINpassword name: smbuser first: SMB last: User smb_logon_script: N:\\logonscripts\\startup smb_profile_path: \\\\server\\profiles\\some_profile smb_home_dir: \\\\users\\home\\smbuser smb_home_drive: "U:" # Rename an existing user - ipauser: ipaadmin_password: SomeADMINpassword name: someuser rename: anotheruser state: renamed """ 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 contains: randompassword: description: The generated random password type: str returned: | If only one user is handled by the module without using users parameter name: description: The user name of the user that got a new random password returned: | If several users are handled by the module with the users parameter type: dict contains: randompassword: description: The generated random password type: str returned: always """ from ansible.module_utils.ansible_freeipa_module import \ IPAAnsibleModule, compare_args_ipa, gen_add_del_lists, date_format, \ encode_certificate, load_cert_from_str, DN_x500_text, to_text, \ ipalib_errors, gen_add_list, gen_intersection_list, \ convert_input_certificates from ansible.module_utils import six if six.PY3: unicode = str def find_user(module, name): _args = { "all": True, } try: _result = module.ipa_command("user_show", name, _args).get("result") except ipalib_errors.NotFound: return None # Transform each principal to a string _result["krbprincipalname"] = [ to_text(x) for x in (_result.get("krbprincipalname") or []) ] _result["usercertificate"] = [ encode_certificate(x) for x in (_result.get("usercertificate") or []) ] return _result def gen_args(first, last, fullname, displayname, initials, homedir, gecos, shell, email, principalexpiration, passwordexpiration, password, random, uid, gid, street, city, userstate, postalcode, phone, mobile, pager, fax, orgunit, title, carlicense, sshpubkey, userauthtype, userclass, radius, radiususer, departmentnumber, employeenumber, employeetype, preferredlanguage, smb_logon_script, smb_profile_path, smb_home_dir, smb_home_drive, idp, idp_user_id, 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 gecos is not None: _args["gecos"] = gecos 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 street is not None: _args["street"] = street 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 idp is not None: _args["ipaidpconfiglink"] = idp if idp_user_id is not None: _args["ipaidpsub"] = idp_user_id if noprivate is not None: _args["noprivate"] = noprivate if nomembers is not None: _args["no_members"] = nomembers if smb_logon_script is not None: _args["ipantlogonscript"] = smb_logon_script if smb_profile_path is not None: _args["ipantprofilepath"] = smb_profile_path if smb_home_dir is not None: _args["ipanthomedirectory"] = smb_home_dir if smb_home_drive is not None: _args["ipanthomedirectorydrive"] = smb_home_drive return _args def check_parameters( # pylint: disable=unused-argument module, state, action, first, last, fullname, displayname, initials, homedir, gecos, shell, email, principal, principalexpiration, passwordexpiration, password, random, uid, gid, street, 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, smb_logon_script, smb_profile_path, smb_home_dir, smb_home_drive, idp, ipa_user_id, rename ): if state == "present" and action == "user": invalid = ["preserve"] else: invalid = [ "first", "last", "fullname", "displayname", "initials", "homedir", "shell", "email", "principalexpiration", "passwordexpiration", "password", "random", "uid", "gid", "street", "city", "phone", "mobile", "pager", "fax", "orgunit", "title", "carlicense", "sshpubkey", "userauthtype", "userclass", "radius", "radiususer", "departmentnumber", "employeenumber", "employeetype", "preferredlanguage", "noprivate", "nomembers", "update_password", "gecos", "smb_logon_script", "smb_profile_path", "smb_home_dir", "smb_home_drive", "idp", "idp_user_id" ] if state == "present" and action == "member": invalid.append("preserve") else: if action == "user": invalid.extend( ["principal", "manager", "certificate", "certmapdata"]) if state != "absent" and preserve is not None: module.fail_json( msg="Preserve is only possible for state=absent") if state != "renamed": invalid.append("rename") else: invalid.extend([ "preserve", "principal", "manager", "certificate", "certmapdata", ]) if not rename: module.fail_json( msg="A value for attribute 'rename' must be provided.") if action == "member": module.fail_json( msg="Action member can not be used with state: renamed.") module.params_fail_used_invalid(invalid, state, action) 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 check_userauthtype(module, userauthtype): _invalid = module.ipa_command_invalid_param_choices( "user_add", "ipauserauthtype", userauthtype) if _invalid: module.fail_json( msg="The use of userauthtype '%s' is not supported " "by your IPA version" % "','".join(_invalid)) 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) # pylint: disable=invalid-name 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)} # pylint: disable=unused-argument def result_handler(module, result, command, name, args, exit_args, errors, single_user): if "random" in args and command in ["user_add", "user_mod"] \ and "randompassword" in result["result"]: if single_user: exit_args["randompassword"] = \ result["result"]["randompassword"] else: exit_args.setdefault(name, {})["randompassword"] = \ result["result"]["randompassword"] IPAAnsibleModule.member_error_handler(module, result, command, name, args, errors) 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), gecos=dict(type="str", default=None), shell=dict(type="str", aliases=["loginshell"], default=None), email=dict(type="list", elements="str", default=None), principal=dict(type="list", elements="str", aliases=["principalname", "krbprincipalname"], default=None), principalexpiration=dict(type="str", aliases=["krbprincipalexpiration"], default=None), passwordexpiration=dict(type="str", aliases=["krbpasswordexpiration"], default=None, no_log=False), 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), street=dict(type="str", 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", elements="str", aliases=["telephonenumber"], default=None), mobile=dict(type="list", elements="str", default=None), pager=dict(type="list", elements="str", default=None), fax=dict(type="list", elements="str", aliases=["facsimiletelephonenumber"], default=None), orgunit=dict(type="str", aliases=["ou"], default=None), title=dict(type="str", default=None), manager=dict(type="list", elements="str", default=None), carlicense=dict(type="list", elements="str", default=None), sshpubkey=dict(type="list", elements="str", aliases=["ipasshpubkey"], default=None), userauthtype=dict(type='list', elements="str", aliases=["ipauserauthtype"], default=None, choices=["password", "radius", "otp", "pkinit", "hardened", "idp", ""]), userclass=dict(type="list", elements="str", 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", elements="str", default=None), employeenumber=dict(type="str", default=None), employeetype=dict(type="str", default=None), smb_logon_script=dict(type="str", default=None, aliases=["ipantlogonscript"]), smb_profile_path=dict(type="str", default=None, aliases=["ipantprofilepath"]), smb_home_dir=dict(type="str", default=None, aliases=["ipanthomedirectory"]), smb_home_drive=dict(type="str", default=None, choices=[ ("%c:" % chr(x)) for x in range(ord('A'), ord('Z') + 1) ] + [""], aliases=["ipanthomedirectorydrive"]), preferredlanguage=dict(type="str", default=None), certificate=dict(type="list", elements="str", 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), idp=dict(type="str", default=None, aliases=['ipaidpconfiglink']), idp_user_id=dict(type="str", default=None, aliases=['ipaidpsub']), rename=dict(type="str", required=False, default=None, aliases=["new_name"]), ) ansible_module = IPAAnsibleModule( argument_spec=dict( # general name=dict(type="list", elements="str", aliases=["login"], default=None, required=False), users=dict(type="list", default=None, options=dict( # Here name is a simple string name=dict(type="str", required=True, aliases=["login"]), # 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", "renamed"]), # 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 names = ansible_module.params_get("name") users = ansible_module.params_get("users") # 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") initials = ansible_module.params_get("initials") homedir = ansible_module.params_get("homedir") gecos = ansible_module.params_get("gecos") shell = ansible_module.params_get("shell") email = ansible_module.params_get("email") principal = ansible_module.params_get("principal") principalexpiration = ansible_module.params_get( "principalexpiration") if principalexpiration is not None: if principalexpiration[:-1] != "Z": principalexpiration = principalexpiration + "Z" principalexpiration = date_format(principalexpiration) passwordexpiration = ansible_module.params_get("passwordexpiration") if passwordexpiration is not None: if passwordexpiration[:-1] != "Z": passwordexpiration = passwordexpiration + "Z" passwordexpiration = date_format(passwordexpiration) password = ansible_module.params_get("password") random = ansible_module.params_get("random") uid = ansible_module.params_get("uid") gid = ansible_module.params_get("gid") street = ansible_module.params_get("street") city = ansible_module.params_get("city") userstate = ansible_module.params_get("userstate") postalcode = ansible_module.params_get("postalcode") phone = ansible_module.params_get("phone") mobile = ansible_module.params_get("mobile") pager = ansible_module.params_get("pager") fax = ansible_module.params_get("fax") orgunit = ansible_module.params_get("orgunit") title = ansible_module.params_get("title") manager = ansible_module.params_get("manager") carlicense = ansible_module.params_get("carlicense") sshpubkey = ansible_module.params_get("sshpubkey", allow_empty_list_item=True) userauthtype = ansible_module.params_get("userauthtype", allow_empty_list_item=True) userclass = ansible_module.params_get("userclass") radius = ansible_module.params_get("radius") radiususer = ansible_module.params_get("radiususer") departmentnumber = ansible_module.params_get("departmentnumber") employeenumber = ansible_module.params_get("employeenumber") employeetype = ansible_module.params_get("employeetype") preferredlanguage = ansible_module.params_get("preferredlanguage") smb_logon_script = ansible_module.params_get("smb_logon_script") smb_profile_path = ansible_module.params_get("smb_profile_path") smb_home_dir = ansible_module.params_get("smb_home_dir") smb_home_drive = ansible_module.params_get("smb_home_drive") idp = ansible_module.params_get("idp") idp_user_id = ansible_module.params_get("idp_user_id") certificate = ansible_module.params_get("certificate") certmapdata = ansible_module.params_get("certmapdata") noprivate = ansible_module.params_get("noprivate") nomembers = ansible_module.params_get("nomembers") # deleted preserve = ansible_module.params_get("preserve") # mod update_password = ansible_module.params_get("update_password") # rename rename = ansible_module.params_get("rename") # general action = ansible_module.params_get("action") state = ansible_module.params_get("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 in ["present", "renamed"]: if names is not None and len(names) != 1: act = "renamed" if state == "renamed" else "added" ansible_module.fail_json( msg="Only one user can be %s at a time using name." % (act)) # Use users if names is None if users is not None: names = users else: check_parameters( ansible_module, state, action, first, last, fullname, displayname, initials, homedir, gecos, shell, email, principal, principalexpiration, passwordexpiration, password, random, uid, gid, street, 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, smb_logon_script, smb_profile_path, smb_home_dir, smb_home_drive, idp, idp_user_id, rename, ) certificate = convert_input_certificates(ansible_module, certificate, state) certmapdata = convert_certmapdata(certmapdata) # Init changed = False exit_args = {} # Connect to IPA API with ansible_module.ipa_connect(): # Check version specific settings server_realm = ansible_module.ipa_get_realm() # Check API specific parameters check_userauthtype(ansible_module, userauthtype) # Default email domain result = ansible_module.ipa_command_no_name("config_show", {}) default_email_domain = result["result"]["ipadefaultemaildomain"][0] # Extend email addresses email = extend_emails(email, default_email_domain) # commands commands = [] user_set = set() for user in names: if isinstance(user, dict): name = user.get("name") if name in user_set: ansible_module.fail_json( msg="user '%s' is used more than once" % name) user_set.add(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") gecos = user.get("gecos") 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") street = user.get("street") 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") smb_logon_script = user.get("smb_logon_script") smb_profile_path = user.get("smb_profile_path") smb_home_dir = user.get("smb_home_dir") smb_home_drive = user.get("smb_home_drive") idp = user.get("idp") idp_user_id = user.get("idp_user_id") rename = user.get("rename") 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, gecos, shell, email, principal, principalexpiration, passwordexpiration, password, random, uid, gid, street, 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, smb_logon_script, smb_profile_path, smb_home_dir, smb_home_drive, idp, idp_user_id, rename, ) certificate = convert_input_certificates(ansible_module, certificate, state) certmapdata = convert_certmapdata(certmapdata) # Check API specific parameters check_userauthtype(ansible_module, userauthtype) # Extend email addresses email = extend_emails(email, default_email_domain) elif ( isinstance( user, (str, unicode) # pylint: disable=W0012,E0606 ) ): 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 ansible_module.ipa_command_param_exists( "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 ansible_module.ipa_command_exists("user_add_certmapdata"): ansible_module.fail_json( msg="The use of certmapdata is not supported by " "your IPA version") # Check if SMB attributes are available if ( any([ smb_logon_script, smb_profile_path, smb_home_dir, smb_home_drive ]) and not ansible_module.ipa_command_param_exists( "user_mod", "ipanthomedirectory" ) ): ansible_module.fail_json( msg="The use of smb_logon_script, smb_profile_path, " "smb_profile_path, and smb_home_drive is not supported " "by your IPA version") # Check if IdP support is available require_idp = ( idp is not None or idp_user_id is not None or userauthtype == "idp" ) has_idp_support = ansible_module.ipa_command_param_exists( "user_add", "ipaidpconfiglink" ) if require_idp and not has_idp_support: ansible_module.fail_json( msg="Your IPA version does not support External IdP.") # Make sure user exists res_find = find_user(ansible_module, name) # Create command if state == "present": # Generate args args = gen_args( first, last, fullname, displayname, initials, homedir, gecos, shell, email, principalexpiration, passwordexpiration, password, random, uid, gid, street, city, userstate, postalcode, phone, mobile, pager, fax, orgunit, title, carlicense, sshpubkey, userauthtype, userclass, radius, radiususer, departmentnumber, employeenumber, employeetype, preferredlanguage, smb_logon_script, smb_profile_path, smb_home_dir, smb_home_drive, idp, idp_user_id, noprivate, nomembers, ) 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 using "random:false" password should not be # generated. if not args.get("random", True): 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 # The nomembers parameter is added to args for the # api command. But no_members is never part of # res_find from user-show, therefore this parameter # needs to be ignored in compare_args_ipa. if not compare_args_ipa( ansible_module, args, res_find, ignore=["no_members"]): 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") smb_attrs = { k: args[k] for k in [ "ipanthomedirectory", "ipanthomedirectorydrive", "ipantlogonscript", "ipantprofilepath", ] if k in args } for key in smb_attrs.keys(): del args[key] commands.append([name, "user_add", args]) if smb_attrs: commands.append([name, "user_mod", smb_attrs]) # 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 manager_add = gen_add_list( manager, res_find.get("manager")) if manager_add is not None and len(manager_add) > 0: commands.append([name, "user_add_manager", { "user": manager_add, }]) # 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 principal_add = gen_add_list( principal, res_find.get("krbprincipalname")) if principal_add is not None and len(principal_add) > 0: for _principal in principal_add: 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 certificate_add = gen_add_list( certificate, res_find.get("usercertificate")) if certificate_add is not None and \ len(certificate_add) > 0: for _certificate in certificate_add: 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 certmapdata_add = gen_add_list( certmapdata, res_find.get("ipacertmapdata")) if certmapdata_add is not None and \ len(certmapdata_add) > 0: for _data in certmapdata_add: commands.append([name, "user_add_certmapdata", gen_certmapdata_args(_data)]) elif state == "absent": if action == "user": if res_find is not None: args = {} if preserve is not None: args["preserve"] = preserve if ( not res_find.get("preserved", False) or not args.get("preserve", False) ): 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 manager_del = gen_intersection_list( manager, res_find.get("manager")) if manager_del is not None and 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. # Ensure principals are absent principal_del = gen_intersection_list( principal, res_find.get("krbprincipalname")) if principal_del is not None and len(principal_del) > 0: commands.append([name, "user_remove_principal", { "krbprincipalname": principal_del, }]) # 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 certificate_del = gen_intersection_list( certificate, res_find.get("usercertificate")) if certificate_del is not None and \ 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) # Ensure certmapdata are absent certmapdata_del = gen_intersection_list( certmapdata, res_find.get("ipacertmapdata")) if certmapdata_del is not None and \ len(certmapdata_del) > 0: # Using issuer and subject can only be done one by # one reliably (https://pagure.io/freeipa/issue/8097) for _data in certmapdata_del: commands.append([name, "user_remove_certmapdata", gen_certmapdata_args(_data)]) elif state == "undeleted": if res_find is not None: if res_find.get("preserved", False): commands.append([name, "user_undel", {}]) else: raise ValueError("No 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 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: raise ValueError("No user '%s'" % name) elif state == "renamed": if res_find is None: ansible_module.fail_json(msg="No user '%s'" % name) else: if rename != name: commands.append([name, 'user_mod', {"rename": rename}]) else: ansible_module.fail_json(msg="Unkown state '%s'" % state) del user_set # Execute commands changed = ansible_module.execute_ipa_commands( commands, result_handler, batch=True, keeponly=["randompassword"], exit_args=exit_args, single_user=users is None) # Done ansible_module.exit_json(changed=changed, user=exit_args) if __name__ == "__main__": main()