Skip to content
Snippets Groups Projects
Select Git revision
  • 85006d611fe55f554bf39a63a8a4e2a95be9eee1
  • master default protected
  • v1.15.0
  • v1.14.7
  • v1.14.6
  • v1.14.5
  • v1.14.4
  • v1.14.3
  • v1.14.2
  • v1.14.1
  • v1.14.0
  • v1.13.2
  • v1.13.1
  • v1.13.0
  • v1.12.1
  • v1.12.0
  • v1.11.1
  • v1.11.0
  • v1.10.0
  • v1.9.2
  • v1.9.1
  • v1.9.0
22 results

ipasudorule.py

Blame
  • ipauser.py 55.55 KiB
    #!/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
    
        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(  # pylint: disable=unused-argument
            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)   # 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)}
    
    
    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 = []
            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")
                    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, 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
                            # 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")
    
                            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)
    
            del user_set
    
            # Check mode exit
            if ansible_module.check_mode:
                ansible_module.exit_json(changed=len(commands) > 0, **exit_args)
    
            # 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()