diff --git a/changelogs/fragments/81770-add-uid-guid-minmax-keys.yml b/changelogs/fragments/81770-add-uid-guid-minmax-keys.yml new file mode 100644 index 00000000000..e617e6e22b4 --- /dev/null +++ b/changelogs/fragments/81770-add-uid-guid-minmax-keys.yml @@ -0,0 +1,3 @@ +minor_changes: + - Add ``uid_min``, ``uid_max`` to the user plugin to overwrite the defaults provided by the ``/etc/login.defs`` file (https://github.com/ansible/ansible/pull/81770). + - Add ``gid_min``, ``gid_max`` to the group plugin to overwrite the defaults provided by the ``/etc/login.defs`` file (https://github.com/ansible/ansible/pull/81770). diff --git a/lib/ansible/modules/group.py b/lib/ansible/modules/group.py index a838db4a5c2..716e7e0a515 100644 --- a/lib/ansible/modules/group.py +++ b/lib/ansible/modules/group.py @@ -62,6 +62,22 @@ options: type: bool default: no version_added: "2.8" + gid_min: + description: + - Sets the GID_MIN value for group creation. + - Overwrites /etc/login.defs default value. + - Currently supported on Linux. Does nothing when used with other platforms. + - Requires O(local) is omitted or V(False). + type: int + version_added: "2.18" + gid_max: + description: + - Sets the GID_MAX value for group creation. + - Overwrites /etc/login.defs default value. + - Currently supported on Linux. Does nothing when used with other platforms. + - Requires O(local) is omitted or V(False). + type: int + version_added: "2.18" extends_documentation_fragment: action_common_attributes attributes: check_mode: @@ -151,6 +167,14 @@ class Group(object): self.system = module.params['system'] self.local = module.params['local'] self.non_unique = module.params['non_unique'] + self.gid_min = module.params['gid_min'] + self.gid_max = module.params['gid_max'] + + if self.local: + if self.gid_min is not None: + module.fail_json(msg="'gid_min' can not be used with 'local'") + if self.gid_max is not None: + module.fail_json(msg="'gid_max' can not be used with 'local'") def execute_command(self, cmd): return self.module.run_command(cmd) @@ -184,6 +208,12 @@ class Group(object): cmd.append('-o') elif key == 'system' and kwargs[key] is True: cmd.append('-r') + if self.gid_min is not None: + cmd.append('-K') + cmd.append('GID_MIN=' + str(self.gid_min)) + if self.gid_max is not None: + cmd.append('-K') + cmd.append('GID_MAX=' + str(self.gid_max)) cmd.append(self.name) return self.execute_command(cmd) @@ -292,6 +322,12 @@ class SunOS(Group): cmd.append(str(kwargs[key])) if self.non_unique: cmd.append('-o') + if self.gid_min is not None: + cmd.append('-K') + cmd.append('GID_MIN=' + str(self.gid_min)) + if self.gid_max is not None: + cmd.append('-K') + cmd.append('GID_MAX=' + str(self.gid_max)) cmd.append(self.name) return self.execute_command(cmd) @@ -323,6 +359,12 @@ class AIX(Group): cmd.append('id=' + str(kwargs[key])) elif key == 'system' and kwargs[key] is True: cmd.append('-a') + if self.gid_min is not None: + cmd.append('-K') + cmd.append('GID_MIN=' + str(self.gid_min)) + if self.gid_max is not None: + cmd.append('-K') + cmd.append('GID_MAX=' + str(self.gid_max)) cmd.append(self.name) return self.execute_command(cmd) @@ -368,6 +410,12 @@ class FreeBsdGroup(Group): cmd.append(str(self.gid)) if self.non_unique: cmd.append('-o') + if self.gid_min is not None: + cmd.append('-K') + cmd.append('GID_MIN=' + str(self.gid_min)) + if self.gid_max is not None: + cmd.append('-K') + cmd.append('GID_MAX=' + str(self.gid_max)) return self.execute_command(cmd) def group_mod(self, **kwargs): @@ -492,6 +540,12 @@ class OpenBsdGroup(Group): cmd.append(str(self.gid)) if self.non_unique: cmd.append('-o') + if self.gid_min is not None: + cmd.append('-K') + cmd.append('GID_MIN=' + str(self.gid_min)) + if self.gid_max is not None: + cmd.append('-K') + cmd.append('GID_MAX=' + str(self.gid_max)) cmd.append(self.name) return self.execute_command(cmd) @@ -538,6 +592,12 @@ class NetBsdGroup(Group): cmd.append(str(self.gid)) if self.non_unique: cmd.append('-o') + if self.gid_min is not None: + cmd.append('-K') + cmd.append('GID_MIN=' + str(self.gid_min)) + if self.gid_max is not None: + cmd.append('-K') + cmd.append('GID_MAX=' + str(self.gid_max)) cmd.append(self.name) return self.execute_command(cmd) @@ -578,6 +638,14 @@ class BusyBoxGroup(Group): if self.system: cmd.append('-S') + if self.gid_min is not None: + cmd.append('-K') + cmd.append('GID_MIN=' + str(self.gid_min)) + + if self.gid_max is not None: + cmd.append('-K') + cmd.append('GID_MAX=' + str(self.gid_max)) + cmd.append(self.name) return self.execute_command(cmd) @@ -626,6 +694,8 @@ def main(): system=dict(type='bool', default=False), local=dict(type='bool', default=False), non_unique=dict(type='bool', default=False), + gid_min=dict(type='int'), + gid_max=dict(type='int'), ), supports_check_mode=True, required_if=[ diff --git a/lib/ansible/modules/user.py b/lib/ansible/modules/user.py index 8cf27b37b2d..a9fd393925d 100644 --- a/lib/ansible/modules/user.py +++ b/lib/ansible/modules/user.py @@ -275,6 +275,23 @@ options: - Currently supported on AIX, Linux, NetBSD, OpenBSD. type: int version_added: "2.18" + uid_min: + description: + - Sets the UID_MIN value for user creation. + - Overwrites /etc/login.defs default value. + - Currently supported on Linux. Does nothing when used with other platforms. + - Requires O(local) is omitted or V(False). + type: int + version_added: "2.18" + uid_max: + description: + - Sets the UID_MAX value for user creation. + - Overwrites /etc/login.defs default value. + - Currently supported on Linux. Does nothing when used with other platforms. + - Requires O(local) is omitted or V(False). + type: int + version_added: "2.18" + extends_documentation_fragment: action_common_attributes attributes: check_mode: @@ -595,9 +612,16 @@ class User(object): self.password_expire_warn = module.params['password_expire_warn'] self.umask = module.params['umask'] self.inactive = module.params['password_expire_account_disable'] + self.uid_min = module.params['uid_min'] + self.uid_max = module.params['uid_max'] - if self.umask is not None and self.local: - module.fail_json(msg="'umask' can not be used with 'local'") + if self.local: + if self.umask is not None: + module.fail_json(msg="'umask' can not be used with 'local'") + if self.uid_min is not None: + module.fail_json(msg="'uid_min' can not be used with 'local'") + if self.uid_max is not None: + module.fail_json(msg="'uid_max' can not be used with 'local'") if module.params['groups'] is not None: self.groups = ','.join(module.params['groups']) @@ -798,6 +822,14 @@ class User(object): if self.system: cmd.append('-r') + if self.uid_min is not None: + cmd.append('-K') + cmd.append('UID_MIN=' + str(self.uid_min)) + + if self.uid_max is not None: + cmd.append('-K') + cmd.append('UID_MAX=' + str(self.uid_max)) + cmd.append(self.name) (rc, out, err) = self.execute_command(cmd) if not self.local or rc != 0: @@ -1465,6 +1497,14 @@ class FreeBsdUser(User): else: cmd.append(str(calendar.timegm(self.expires))) + if self.uid_min is not None: + cmd.append('-K') + cmd.append('UID_MIN=' + str(self.uid_min)) + + if self.uid_max is not None: + cmd.append('-K') + cmd.append('UID_MAX=' + str(self.uid_max)) + # system cannot be handled currently - should we error if its requested? # create the user (rc, out, err) = self.execute_command(cmd) @@ -1718,6 +1758,13 @@ class OpenBSDUser(User): if self.inactive is not None: cmd.append('-f') cmd.append(self.inactive) + if self.uid_min is not None: + cmd.append('-K') + cmd.append('UID_MIN=' + str(self.uid_min)) + + if self.uid_max is not None: + cmd.append('-K') + cmd.append('UID_MAX=' + str(self.uid_max)) cmd.append(self.name) return self.execute_command(cmd) @@ -1904,6 +1951,14 @@ class NetBSDUser(User): cmd.append('-K') cmd.append('UMASK=' + self.umask) + if self.uid_min is not None: + cmd.append('-K') + cmd.append('UID_MIN=' + str(self.uid_min)) + + if self.uid_max is not None: + cmd.append('-K') + cmd.append('UID_MAX=' + str(self.uid_max)) + cmd.append(self.name) return self.execute_command(cmd) @@ -2112,6 +2167,13 @@ class SunOS(User): if self.inactive is not None: cmd.append('-f') cmd.append(self.inactive) + if self.uid_min is not None: + cmd.append('-K') + cmd.append('UID_MIN=' + str(self.uid_min)) + + if self.uid_max is not None: + cmd.append('-K') + cmd.append('UID_MAX=' + str(self.uid_max)) cmd.append(self.name) @@ -2722,6 +2784,13 @@ class AIX(User): if self.inactive is not None: cmd.append('-f') cmd.append(self.inactive) + if self.uid_min is not None: + cmd.append('-K') + cmd.append('UID_MIN=' + str(self.uid_min)) + + if self.uid_max is not None: + cmd.append('-K') + cmd.append('UID_MAX=' + str(self.uid_max)) cmd.append(self.name) (rc, out, err) = self.execute_command(cmd) @@ -3059,6 +3128,14 @@ class BusyBox(User): if self.system: cmd.append('-S') + if self.uid_min is not None: + cmd.append('-K') + cmd.append('UID_MIN=' + str(self.uid_min)) + + if self.uid_max is not None: + cmd.append('-K') + cmd.append('UID_MAX=' + str(self.uid_max)) + cmd.append(self.name) rc, out, err = self.execute_command(cmd) @@ -3204,6 +3281,8 @@ def main(): role=dict(type='str'), umask=dict(type='str'), password_expire_account_disable=dict(type='int', no_log=False), + uid_min=dict(type='int'), + uid_max=dict(type='int'), ), supports_check_mode=True, ) diff --git a/test/integration/targets/group/tasks/main.yml b/test/integration/targets/group/tasks/main.yml index 21235240354..dc0619a16d6 100644 --- a/test/integration/targets/group/tasks/main.yml +++ b/test/integration/targets/group/tasks/main.yml @@ -16,4 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +- name: skip broken distros + meta: end_host + when: ansible_distribution == 'Alpine' + - import_tasks: tests.yml +- import_tasks: test_create_group_min_max.yml + when: ansible_facts.system == 'Linux' \ No newline at end of file diff --git a/test/integration/targets/group/tasks/test_create_group_min_max.yml b/test/integration/targets/group/tasks/test_create_group_min_max.yml new file mode 100644 index 00000000000..73d882db8e2 --- /dev/null +++ b/test/integration/targets/group/tasks/test_create_group_min_max.yml @@ -0,0 +1,73 @@ +--- +- name: create a group with a specific gid + group: + name: testgroupone + gid: 8000 + state: present + register: group_test0_1 + +- name: create another group without a specific gid + group: + name: testgrouptwo + state: present + register: group_test0_2 + +- name: show that the last group gets an id higher than the previous highest one + assert: + that: + group_test0_1.gid < group_test0_2.gid + +- name: create a group within gid_max range + group: + name: testgroupthree + gid_max: 1999 + state: present + register: group_test0_3 + +- name: assert that a group with gid_max gets a lower gid + assert: + that: + group_test0_2.gid > group_test0_3.gid + +- name: proof of range limits + block: + - name: create group 1 within min 1500 and max 1501 + group: + name: testgroupfour + gid_min: 1500 + gid_max: 1501 + state: present + register: group_test0_4 + + - name: create group 2 within min 1500 and max 1501 + group: + name: testgroupfive + gid_min: 1500 + gid_max: 1501 + state: present + register: group_test0_5 + + - name: create group 3 within min 1500 and max 1501 and show that the range applies + group: + name: testgroupsix + gid_min: 1500 + gid_max: 1501 + state: present + register: group_test0_6 + failed_when: not group_test0_6.failed + +- name: show that creating a group by setting both gid_min and local is not possible + group: + name: gidminlocalgroup_test_1 + gid_min: 1000 + local: true + register: gidminlocalgroup_test_1 + failed_when: not gidminlocalgroup_test_1.failed + +- name: show that creating a group by setting both gid_max and local is not possible + group: + name: gidmaxlocalgroup_test_1 + gid_max: 2000 + local: true + register: gidmaxlocalgroup_test_1 + failed_when: not gidmaxlocalgroup_test_1.failed diff --git a/test/integration/targets/user/tasks/main.yml b/test/integration/targets/user/tasks/main.yml index aefd359ff56..bb4b261b75a 100644 --- a/test/integration/targets/user/tasks/main.yml +++ b/test/integration/targets/user/tasks/main.yml @@ -43,3 +43,5 @@ - import_tasks: test_umask.yml when: ansible_facts.system == 'Linux' - import_tasks: test_inactive_new_account.yml +- import_tasks: test_create_user_min_max.yml + when: ansible_facts.system == 'Linux' diff --git a/test/integration/targets/user/tasks/test_create_user_min_max.yml b/test/integration/targets/user/tasks/test_create_user_min_max.yml new file mode 100644 index 00000000000..21a41f50f60 --- /dev/null +++ b/test/integration/targets/user/tasks/test_create_user_min_max.yml @@ -0,0 +1,73 @@ +--- +- name: create a user with a specific uid + user: + name: testuserone + uid: 8000 + state: present + register: user_test0_1 + +- name: create another user without a specific uid + user: + name: testusertwo + state: present + register: user_test0_2 + +- name: show that the last user gets an id higher than the previous highest one + assert: + that: + user_test0_1.uid < user_test0_2.uid + +- name: create a user within max range + user: + name: testuserthree + uid_max: 1999 + state: present + register: user_test0_3 + +- name: assert that user with uid_max gets a lower uid + assert: + that: + user_test0_2.uid > user_test0_3.uid + +- name: proof of range limits + block: + - name: create user 1 within min 1500 and max 1501 + user: + name: testuserfour + uid_min: 1500 + uid_max: 1501 + state: present + register: user_test0_4 + + - name: create user 2 within min 1500 and max 1501 + user: + name: testuserfive + uid_min: 1500 + uid_max: 1501 + state: present + register: user_test0_5 + + - name: create user 3 within min 1500 and max 1501 and show that the range applies + user: + name: testusersix + uid_min: 1500 + uid_max: 1501 + state: present + register: user_test0_6 + failed_when: not user_test0_6.failed + +- name: show that creating a group by setting both uid_min and local is not possible + user: + name: uidminlocaluser_test_1 + uid_min: 1000 + local: true + register: uidminlocaluser_test_1 + failed_when: not uidminlocaluser_test_1.failed + +- name: show that creating a group by setting both uid_max and local is not possible + user: + name: uidmaxlocaluser_test_1 + uid_max: 2000 + local: true + register: uidmaxlocaluser_test_1 + failed_when: not uidmaxlocaluser_test_1.failed