diff --git a/README-user.md b/README-user.md index da04fc3d5518f38d2d1501c0dc0886670df214dd..78b258045cf8c5a2db1684e64067877ab3f14f83 100644 --- a/README-user.md +++ b/README-user.md @@ -445,6 +445,8 @@ Variable | Description | Required `employeenumber` | Employee Number | no `employeetype` | Employee Type | no `preferredlanguage` | Preferred Language | no +`idp` \| `ipaidpconfiglink` | External IdP configuration | no +`idp_user_id` \| `ipaidpsub` | A string that identifies the user at external IdP | no `certificate` | List of base-64 encoded user certificates. | no `certmapdata` | List of certificate mappings. Either `data` or `certificate` or `issuer` together with `subject` need to be specified. Only usable with IPA versions 4.5 and up. <br>Options: | no | `certificate` - Base-64 encoded user certificate, not usable with other certmapdata options. | no diff --git a/playbooks/user/add-user-external-idp.yml b/playbooks/user/add-user-external-idp.yml new file mode 100644 index 0000000000000000000000000000000000000000..894878b252f31af8a7c9b6312d23b4f607768867 --- /dev/null +++ b/playbooks/user/add-user-external-idp.yml @@ -0,0 +1,12 @@ +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + + tasks: + - name: Create user associated with an external IdP + ipauser: + ipaadmin_password: SomeADMINpassword + name: idpuser + idp: keycloak + idp_user_id: idpuser@exemple.com diff --git a/plugins/modules/ipauser.py b/plugins/modules/ipauser.py index 6059829c252e8d4b1f7474715a0addc76d293141..dcea92f4678184cf1ee1f1dadf3428cbb30cb70e 100644 --- a/plugins/modules/ipauser.py +++ b/plugins/modules/ipauser.py @@ -271,6 +271,16 @@ options: 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 @@ -528,6 +538,16 @@ options: 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 @@ -735,8 +755,8 @@ def gen_args(first, last, fullname, displayname, initials, homedir, gecos, 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, noprivate, - nomembers): + 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: @@ -809,6 +829,10 @@ def gen_args(first, last, fullname, displayname, initials, homedir, gecos, _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: @@ -833,6 +857,7 @@ def check_parameters( # pylint: disable=unused-argument 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, ): if state == "present" and action == "user": invalid = ["preserve"] @@ -846,7 +871,7 @@ def check_parameters( # pylint: disable=unused-argument "departmentnumber", "employeenumber", "employeetype", "preferredlanguage", "noprivate", "nomembers", "update_password", "gecos", "smb_logon_script", "smb_profile_path", "smb_home_dir", - "smb_home_drive", + "smb_home_drive", "idp", "idp_user_id" ] if state == "present" and action == "member": @@ -1069,6 +1094,9 @@ def main(): 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=['ipaidpconfiglink']), ) ansible_module = IPAAnsibleModule( @@ -1171,6 +1199,8 @@ def main(): 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") @@ -1204,7 +1234,7 @@ def main(): radiususer, departmentnumber, employeenumber, employeetype, preferredlanguage, certificate, certmapdata, noprivate, nomembers, preserve, update_password, smb_logon_script, smb_profile_path, - smb_home_dir, smb_home_drive) + smb_home_dir, smb_home_drive, idp, idp_user_id) certmapdata = convert_certmapdata(certmapdata) # Use users if names is None @@ -1298,6 +1328,8 @@ def main(): 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") certificate = user.get("certificate") certmapdata = user.get("certmapdata") noprivate = user.get("noprivate") @@ -1314,7 +1346,7 @@ def main(): employeetype, preferredlanguage, certificate, certmapdata, noprivate, nomembers, preserve, update_password, smb_logon_script, smb_profile_path, - smb_home_dir, smb_home_drive) + smb_home_dir, smb_home_drive, idp, idp_user_id) certmapdata = convert_certmapdata(certmapdata) # Check API specific parameters @@ -1375,6 +1407,19 @@ def main(): "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) @@ -1390,7 +1435,9 @@ def main(): carlicense, sshpubkey, userauthtype, userclass, radius, radiususer, departmentnumber, employeenumber, employeetype, preferredlanguage, smb_logon_script, smb_profile_path, - smb_home_dir, smb_home_drive, noprivate, nomembers) + smb_home_dir, smb_home_drive, idp, idp_user_id, noprivate, + nomembers, + ) if action == "user": # Found the user diff --git a/tests/user/test_user_idp_attrs.yml b/tests/user/test_user_idp_attrs.yml new file mode 100644 index 0000000000000000000000000000000000000000..def267b8197bae83d8bd983a8a31b88161105bf5 --- /dev/null +++ b/tests/user/test_user_idp_attrs.yml @@ -0,0 +1,107 @@ +--- +- name: Test user + hosts: "{{ ipa_test_host | default('ipaserver') }}" + become: false + gather_facts: false + module_defaults: + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + + tasks: + - name: Include tasks ../env_freeipa_facts.yml + ansible.builtin.include_tasks: ../env_freeipa_facts.yml + + # CLEANUP TEST ITEMS + + - name: Ensure user idpuser is absent + ipauser: + name: idpuser + state: absent + + # CREATE TEST ITEMS + - name: Run tests if FreeIPA 4.10.0+ is installed + when: ipa_version is version('4.10.0', '>=') + block: + - name: Ensure IDP provider is present + # TODO: Use an ansible-freeipa plugin instead of 'shell' + ansible.builtin.shell: + cmd: | + kinit -c test_krb5_cache admin <<< SomeADMINpassword + KRB5CCNAME=test_krb5_cache ipa idp-add keycloak --provider keycloak \ + --org master \ + --base-url https://client.ipademo.local:8443/auth \ + --client-id ipa_oidc_client \ + --secret <<< $(echo -e "Secret123\nSecret123") + kdestroy -c test_krb5_cache -q -A + register: addidp + failed_when: + - '"Added Identity Provider" not in addidp.stdout' + - '"already exists" not in addidp.stderr' + + # TESTS + + - name: Ensure user idpuser is present + ipauser: + name: idpuser + first: IDP + last: User + userauthtype: idp + idp: keycloak + idp_user_id: "idpuser@ipademo.local" + register: result + failed_when: not result.changed or result.failed + + - name: Ensure user idpuser is present again + ipauser: + name: idpuser + first: IDP + last: User + userauthtype: idp + idp: keycloak + idp_user_id: "idpuser@ipademo.local" + register: result + failed_when: result.changed or result.failed + + - name: Clear 'idp_user_id' + ipauser: + name: idpuser + idp_user_id: "" + register: result + failed_when: not result.changed or result.failed + + - name: Clear 'idp' + ipauser: + name: idpuser + idp: "" + register: result + failed_when: not result.changed or result.failed + + - name: Ensure user idpuser is absent + ipauser: + name: idpuser + state: absent + register: result + failed_when: not result.changed or result.failed + + - name: Ensure user idpuser is absent again + ipauser: + name: idpuser + state: absent + register: result + failed_when: result.changed or result.failed + + + # CLEANUP TEST ITEMS + - name: Ensure IDP provider is absent + # TODO: Use an ansible-freeipa plugin instead of 'shell' + ansible.builtin.shell: + cmd: | + kinit -c test_krb5_cache admin <<< SomeADMINpassword + ipa idp-del keycloak + kdestroy -c test_krb5_cache -q -A + always: + - name: Ensure user idpuser is absent + ipauser: + name: idpuser + state: absent