diff --git a/README-user.md b/README-user.md index e7267c522..a03fa2169 100644 --- a/README-user.md +++ b/README-user.md @@ -353,6 +353,33 @@ Example playbook to ensure users are absent: state: absent ``` +When using FreeIPA 4.8.0+, SMB logon script, profile, home directory and home drive can be set for users. + +In the example playbook to set SMB attributes note that `smb_profile_path` and `smb_home_dir` use paths in UNC format, which includes backslashes ('\\`). If the paths are quoted, the backslash needs to be escaped becoming "\\", so the path `\\server\dir` becomes `"\\\\server\\dir"`. If the paths are unquoted the slashes do not have to be escaped. + +The YAML specification states that a colon (':') is a key separator and a dashe ('-') is an item marker, only with a space after them, so using both unquoted as part of a path should not be a problem. If a space is needed after a colon or a dash, then a quoted string must be used as in `"user - home"`. For the `smb_home_drive` attribute is is recomended that a quoted string is used, to improve readability. + +Example playbook to set SMB attributes: + +```yaml +--- +- name: Plabook to handle users + hosts: ipaserver + become: false + + tasks: + - name: Ensure user 'smbuser' is present with 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:" +``` + Variables ========= @@ -425,6 +452,10 @@ Variable | Description | Required   | `subject` - Subject of the certificate, only usable together with `issuer` option. | no   | `data` - Certmap data, not usable with other certmapdata options. | no `noprivate` | Do not create user private group. (bool) | no +`smb_logon_script` \| `ipantlogonscript` | SMB logon script path. Requires FreeIPA version 4.8.0+. | no +`smb_profile_path:` \| `ipantprofilepath` | SMB profile path, in UNC format. Requires FreeIPA version 4.8.0+. | no +`smb_home_dir` \| `ipanthomedirectory` | SMB Home Directory, in UNC format. Requires FreeIPA version 4.8.0+. | no +`smb_home_drive` \| `ipanthomedirectorydrive` | SMB Home Directory Drive, a single upercase letter (A-Z) followed by a colon (:), for example "U:". Requires FreeIPA version 4.8.0+. | no `nomembers` | Suppress processing of membership attributes. (bool) | no diff --git a/playbooks/user/smb-attributes.yml b/playbooks/user/smb-attributes.yml new file mode 100644 index 000000000..81c7e6afa --- /dev/null +++ b/playbooks/user/smb-attributes.yml @@ -0,0 +1,17 @@ +--- +- name: Plabook to handle users + hosts: ipaserver + become: false + gather_facts: false + + tasks: + - name: Ensure user 'smbuser' is present with 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:" diff --git a/plugins/modules/ipauser.py b/plugins/modules/ipauser.py index 6441259eb..c4961cc16 100644 --- a/plugins/modules/ipauser.py +++ b/plugins/modules/ipauser.py @@ -242,6 +242,31 @@ 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 @@ -474,6 +499,31 @@ 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 @@ -613,6 +663,17 @@ 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:" """ RETURN = """ @@ -673,7 +734,8 @@ def gen_args(first, last, fullname, displayname, initials, homedir, gecos, random, uid, gid, street, city, userstate, postalcode, phone, mobile, pager, fax, orgunit, title, carlicense, sshpubkey, userauthtype, userclass, radius, radiususer, departmentnumber, - employeenumber, employeetype, preferredlanguage, noprivate, + employeenumber, employeetype, preferredlanguage, smb_logon_script, + smb_profile_path, smb_home_dir, smb_home_drive, noprivate, nomembers): # principal, manager, certificate and certmapdata are handled not in here _args = {} @@ -751,6 +813,14 @@ def gen_args(first, last, fullname, displayname, initials, homedir, gecos, _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 @@ -761,7 +831,9 @@ def check_parameters( # pylint: disable=unused-argument mobile, pager, fax, orgunit, title, manager, carlicense, sshpubkey, userauthtype, userclass, radius, radiususer, departmentnumber, employeenumber, employeetype, preferredlanguage, certificate, - certmapdata, noprivate, nomembers, preserve, update_password): + certmapdata, noprivate, nomembers, preserve, update_password, + smb_logon_script, smb_profile_path, smb_home_dir, smb_home_drive, +): if state == "present" and action == "user": invalid = ["preserve"] else: @@ -773,7 +845,8 @@ def check_parameters( # pylint: disable=unused-argument "sshpubkey", "userauthtype", "userclass", "radius", "radiususer", "departmentnumber", "employeenumber", "employeetype", "preferredlanguage", "noprivate", "nomembers", "update_password", - "gecos", + "gecos", "smb_logon_script", "smb_profile_path", "smb_home_dir", + "smb_home_drive", ] if state == "present" and action == "member": @@ -961,6 +1034,17 @@ def main(): 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), @@ -1073,6 +1157,10 @@ def main(): 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") certificate = ansible_module.params_get("certificate") certmapdata = ansible_module.params_get("certmapdata") noprivate = ansible_module.params_get("noprivate") @@ -1105,7 +1193,8 @@ def main(): manager, carlicense, sshpubkey, userauthtype, userclass, radius, radiususer, departmentnumber, employeenumber, employeetype, preferredlanguage, certificate, certmapdata, noprivate, nomembers, - preserve, update_password) + preserve, update_password, smb_logon_script, smb_profile_path, + smb_home_dir, smb_home_drive) certmapdata = convert_certmapdata(certmapdata) # Use users if names is None @@ -1191,6 +1280,10 @@ def main(): 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") certificate = user.get("certificate") certmapdata = user.get("certmapdata") noprivate = user.get("noprivate") @@ -1206,7 +1299,8 @@ def main(): radiususer, departmentnumber, employeenumber, employeetype, preferredlanguage, certificate, certmapdata, noprivate, nomembers, preserve, - update_password) + update_password, smb_logon_script, smb_profile_path, + smb_home_dir, smb_home_drive) certmapdata = convert_certmapdata(certmapdata) # Extend email addresses @@ -1248,6 +1342,21 @@ def main(): 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") + # Make sure user exists res_find = find_user(ansible_module, name) @@ -1262,7 +1371,8 @@ def main(): postalcode, phone, mobile, pager, fax, orgunit, title, carlicense, sshpubkey, userauthtype, userclass, radius, radiususer, departmentnumber, employeenumber, employeetype, - preferredlanguage, noprivate, nomembers) + preferredlanguage, smb_logon_script, smb_profile_path, + smb_home_dir, smb_home_drive, noprivate, nomembers) if action == "user": # Found the user @@ -1298,8 +1408,21 @@ def main(): 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: diff --git a/tests/user/test_user_smb_attrs.yml b/tests/user/test_user_smb_attrs.yml new file mode 100644 index 000000000..3953e13e0 --- /dev/null +++ b/tests/user/test_user_smb_attrs.yml @@ -0,0 +1,253 @@ +--- +- name: Test users + hosts: "{{ ipa_test_host | default('ipaserver') }}" + become: no + gather_facts: no + + tasks: + - name: Set FreeIPA environment facts. + ansible.builtin.include_tasks: ../env_freeipa_facts.yml + + - name: Only run tests for IPA 4.8.0+ + when: ipa_version is version('4.8.0', '>=') + block: + # SETUP + - name: Remove test users + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + state: absent + + # TESTS + - name: Ensure user testuser exists with all smb paramters + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + first: test + last: user + smb_profile_path: "/some/profile/path" + smb_home_dir: "/some/home/dir" + smb_home_drive: "U{{ ':' }}" + smb_logon_script: "/some/profile/script.sh" + register: result + failed_when: not result.changed or result.failed + + - name: Ensure user testuser exists all smb paramters, again + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + first: test + last: user + smb_logon_script: "/some/profile/script.sh" + smb_profile_path: "/some/profile/path" + smb_home_dir: "/some/home/dir" + smb_home_drive: "U{{ ':' }}" + register: result + failed_when: result.changed or result.failed + + - name: Check SMB logon script is correct + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + smb_logon_script: "/some/profile/script.sh" + register: result + check_mode: true + failed_when: result.changed or result.failed + + - name: Check SMB profile path is correct + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + smb_profile_path: "/some/profile/path" + register: result + check_mode: true + failed_when: result.changed or result.failed + + - name: Check SMB Home Directory is correct + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + smb_home_dir: "/some/home/dir" + register: result + check_mode: true + failed_when: result.changed or result.failed + + - name: Check SMB Home Drive is correct + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + first: test + last: user + smb_home_drive: "U{{ ':' }}" + register: result + check_mode: true + failed_when: result.changed or result.failed + + - name: Set SMB logon script + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + smb_logon_script: "/some/profile/another_script.sh" + register: result + failed_when: not result.changed or result.failed + + - name: Set SMB logon script, again + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + smb_logon_script: "/some/profile/another_script.sh" + register: result + failed_when: result.changed or result.failed + + - name: Clear SMB logon script + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + smb_logon_script: "" + register: result + failed_when: not result.changed or result.failed + + - name: Clear SMB logon script, again + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + smb_logon_script: "" + register: result + failed_when: result.changed or result.failed + + - name: Set SMB profile path + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + smb_profile_path: "/some/profile/another_path" + register: result + failed_when: not result.changed or result.failed + + - name: Set SMB profile path, again + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + smb_profile_path: "/some/profile/another_path" + register: result + failed_when: result.changed or result.failed + + - name: Clear SMB profile path + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + smb_profile_path: "" + register: result + failed_when: not result.changed or result.failed + + - name: Clear SMB profile, again + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + smb_profile_path: "" + register: result + failed_when: result.changed or result.failed + + - name: Set SMB home directory + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + smb_home_dir: "/some/other/home" + register: result + failed_when: not result.changed or result.failed + + - name: Set SMB home directory, again + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + smb_home_dir: "/some/other/home" + register: result + failed_when: result.changed or result.failed + + - name: Clear SMB home directory + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + smb_home_dir: "" + register: result + failed_when: not result.changed or result.failed + + - name: Clear SMB home directory, again + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + smb_home_dir: "" + register: result + failed_when: result.changed or result.failed + + - name: Set SMB home drive + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + smb_home_drive: "Z{{ ':' }}" + register: result + failed_when: not result.changed or result.failed + + - name: Set SMB home drive, again + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + smb_home_drive: "Z{{ ':' }}" + register: result + failed_when: result.changed or result.failed + + - name: Set SMB home drive to invalid value + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + smb_home_drive: "INVALID:" + register: result + failed_when: not result.failed or "value of smb_home_drive must be one of" not in result.msg + + - name: Clear SMB home drive + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + smb_home_drive: "" + register: result + failed_when: not result.changed or result.failed + + - name: Clear SMB home drive, again + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + smb_home_drive: "" + register: result + failed_when: result.changed or result.failed + + always: + # CLEANUP + - name: Remove test users + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: testuser + state: absent