Rush has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/129501

Change subject: admin module for user/group/permissions cleanup
......................................................................

admin module for user/group/permissions cleanup

Users/groups/perms are defined are meant to be defined in yaml.

Applied like:

yaml:

  groups:
    test1:
      gid: 1
      members: [foo]
    test2:
      gid: 2
      members: []

  users:
    foo:
      uid: 1
      ...

.pp
  node /foo/ {
    #groups create their users (can overlap) and permissions
    #as well as system groups and handling user membership
    class { 'admin': groups => ['test', 'test2'], }
  }

More info see: admin/README

Change-Id: I6982bfced50a22faac37246dff4d52ff163a34ed
---
A modules/admin/README
A modules/admin/data/.gitignore
A modules/admin/files/home/skel/.gitignore
A modules/admin/files/sudoers
A modules/admin/files/user_cleanup.sh
A modules/admin/lib/puppet/parser/functions/unique_users.rb
A modules/admin/manifests/group.pp
A modules/admin/manifests/groupmembers.pp
A modules/admin/manifests/init.pp
A modules/admin/manifests/sudo.pp
A modules/admin/manifests/user.pp
A modules/admin/manifests/yamlgroup.pp
A modules/admin/manifests/yamluser.pp
A modules/admin/templates/sudoers.erb
14 files changed, 686 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/operations/puppet 
refs/changes/01/129501/1

diff --git a/modules/admin/README b/modules/admin/README
new file mode 100644
index 0000000..a5e6e50
--- /dev/null
+++ b/modules/admin/README
@@ -0,0 +1,170 @@
+This module is meant to manage all users, groups, and permissions (sudo).
+
+All managed resources should be defined in yaml.
+
+see: admin/data/data.yaml
+
+-- Examples --
+
+Adding a group:
+
+    groups:
+      mygroup:
+        ensure: present
+        gid: 551
+        members: [foo, bar]
+
+Managing members for a default system group:
+
+-> For groups without a set GID we do not attempt creation
+
+    groups:
+      adm:
+        members: [foo, bar]
+
+Removing a member from a group:
+
+-> Removing 'bar' user from mygroup means removal from members array
+
+    groups:
+      mygroup:
+        ensure: present
+        gid: 551
+        members: [foo, bar] -> members: [foo]
+
+Removing a group:
+
+-> absenting a group will remove it where it was applied
+
+    groups:
+      mygroup:
+        foo:
+          ensure: absent
+          gid: 679
+          members: []
+
+Adding user 'foo':
+
+-> Since assignment is group centric this user won't be created anywhere yet
+
+    users:
+        foo:
+        uid: 1146
+        gid: 500
+        realname: Foo Bar
+        ssh_keys: [ssh-rsa mykeyhash foobar@mac]
+
+Adding user 'foo' to adm:
+
+    groups:
+        adm:
+            members: [foo]
+
+Removing user foo:
+
+-> absented users cannot be members of a group -- other than absent --
+-> users who are not a member of a supplemtary group are removed
+-> Therefore, removing a user from all groups means they will be removed
+   everywhere they existed because of those groups.
+
+    groups:
+        adm:
+          members: [foo, bar] -> members: [bar]
+
+-> User garbage collection logs to syslog and console:
+
+    logger: /usr/local/bin/user_cleanup.sh removing 
user:foo:x:1001:4183::/home/foo:/bin/sh
+
+    notice: /Stage[main]/Admin/Exec[user_cleanup]/returns: 
/usr/local/bin/user_cleanup.sh \
+        removing user:foo:x:1146:1146::/home/foo:/bin/sh
+
+-> However, if you want to ensure a user is especially missing globally
+-> add the user to the meta 'absent' group
+
+    groups:
+      absent:
+        members: [foo]
+
+    users:
+        foo:
+        ensure: absent
+        uid: 510
+        gid: 500
+        realname: Foo Bar
+        ssh_keys: [ssh-rsa mykeyhash foobar@mac]
+
+-> absent group users:
+-> * are _always_ included in every batch of assignments
+-> * should never have 'ensure: present'
+-> * cannot be a member of any other group
+
+Assigning groups/users:
+
+        node /myhost/ {
+            class { 'admin': groups => ['mygroup'], }
+        }
+
+    or (including managed members of a system group):
+
+        node /myhost/ {
+            class { 'admin': groups => ['mygroup', 'adm'], }
+        }
+
+Assigning sudo permissions to a group:
+
+    groups:
+        adm:
+        members: [foo, bar]
+        privs: [ALL=(ALL:ALL) ALL]
+
+    Creates: '/etc/sudoers.d/adm'
+
+        # This file is managed by Puppet!
+        %adm ALL=(ALL:ALL) ALL
+
+Removing sudo permissions from a group:
+
+-> if you remove a group the permissions are removed as well
+-> the 'absent' keyword will also remove all sudo permissions while
+-> retaining the group and members
+
+    bar:
+      gid: 680
+      privs: [absent]
+      members: [a, b, c]
+
+Users can be given sudo permissions in the same way:
+
+-> this is a limited use approach.  these permissions would apply across the 
entire env.
+
+  foo:
+    ensure: present
+    privs: [ALL=(ALL:ALL) ALL]
+
+Assigning one-off (single user, single case) sudo permissions:
+
+    admin::sudo { "foo_user_only_should_do_x":
+        user=>'bob',
+        comment=>'this is good karma',
+        privs=>['ALL = NOPASSWD: ALL'],
+    }
+
+    Creates '/etc/sudoers.d/foo_user_only_should_do_x':
+
+    # This file is managed by Puppet!
+    #this is good karma
+    bob ALL = NOPASSWD: ALL
+
+Getting your /home/ stuff wherever you are:
+
+-> if you define a dir for your username in '${module}/files/home' all 
contents are managed
+
+    ├── files
+    │   ├── home
+    │   │   ├── foo
+    │   │   │   └── .vimrc
+
+
+#Notes:
+#* Groups with no members get root by default
+#* admin::user and admin::group are not dependent on yaml
diff --git a/modules/admin/data/.gitignore b/modules/admin/data/.gitignore
new file mode 100644
index 0000000..a82d1b4
--- /dev/null
+++ b/modules/admin/data/.gitignore
@@ -0,0 +1 @@
+#placeholder
diff --git a/modules/admin/files/home/skel/.gitignore 
b/modules/admin/files/home/skel/.gitignore
new file mode 100644
index 0000000..ad8fd3b
--- /dev/null
+++ b/modules/admin/files/home/skel/.gitignore
@@ -0,0 +1 @@
+#place holder for git
diff --git a/modules/admin/files/sudoers b/modules/admin/files/sudoers
new file mode 100644
index 0000000..7e90fa6
--- /dev/null
+++ b/modules/admin/files/sudoers
@@ -0,0 +1,29 @@
+#
+# This file MUST be edited with the 'visudo' command as root.
+#
+# Please consider adding local content in /etc/sudoers.d/ instead of
+# directly modifying this file.
+#
+# See the man page for details on how to write a sudoers file.
+#
+Defaults    env_reset
+Defaults    
secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+
+# Host alias specification
+
+# User alias specification
+
+# Cmnd alias specification
+
+# User privilege specification
+root    ALL=(ALL:ALL) ALL
+
+# Members of the admin group may gain root privileges
+%admin ALL=(ALL) ALL
+
+# Allow members of group sudo to execute any command
+%sudo   ALL=(ALL:ALL) ALL
+
+# See sudoers(5) for more information on "#include" directives:
+
+#includedir /etc/sudoers.d
diff --git a/modules/admin/files/user_cleanup.sh 
b/modules/admin/files/user_cleanup.sh
new file mode 100644
index 0000000..bebe8b5
--- /dev/null
+++ b/modules/admin/files/user_cleanup.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+# This is a user garbage collection script that removes
+# users who do not have a supplementary group that also have
+# a UID above the ID_BOUNDARY. Removals are logged to syslog.
+# with 'dryrun' as first arg exits 1 if cleanup is needed
+
+PASSWD='/etc/passwd'
+ID_BOUNDRY='500'
+
+function log() {
+    logger $1
+    echo $1
+}
+
+IFS=$'\r\n' PASSWD_USERS=($(cat $PASSWD))
+for var in "${PASSWD_USERS[@]}"
+do
+  username=`grep ${var} /etc/passwd | cut -d ':' -f 1`
+  uid=`grep ${var} /etc/passwd | cut -d ':' -f 3`
+  if [[ "$uid" -gt "$ID_BOUNDRY" ]]; then
+    if [[ `id $username` != *","* ]]; then
+      if [ "${1}" == "dryrun" ]
+        then
+          exit 1
+      fi
+      log "${0} removing user:""${var}"
+      /usr/sbin/deluser --remove-home --backup-to=/tmp $username &> /dev/null
+    fi
+  fi
+done
diff --git a/modules/admin/lib/puppet/parser/functions/unique_users.rb 
b/modules/admin/lib/puppet/parser/functions/unique_users.rb
new file mode 100644
index 0000000..541868e
--- /dev/null
+++ b/modules/admin/lib/puppet/parser/functions/unique_users.rb
@@ -0,0 +1,13 @@
+    module Puppet::Parser::Functions
+      newfunction(:unique_users, :type => :rvalue) do |args|
+        myhash = args[0]
+        applied_groups = args[1]
+        users = Array.new
+        for group in applied_groups
+            if myhash['groups'].key?(group)
+                users.push(myhash['groups'][group]['members'])
+            end
+        end
+        return users.flatten(1).uniq
+      end
+    end
diff --git a/modules/admin/manifests/group.pp b/modules/admin/manifests/group.pp
new file mode 100644
index 0000000..af45090
--- /dev/null
+++ b/modules/admin/manifests/group.pp
@@ -0,0 +1,42 @@
+# A defined type for system group mangement
+#
+# === Parameters
+#
+# [*name*]
+#  Group name
+#
+# [*ensure*]
+#  Add or remove the user group [ "present" | "absent"]
+#
+# [*gid*]
+#  Sets the group id
+#
+# [*privs*]
+#  An array of priviledges to setup via admin::sudo
+#
+
+define admin::group(
+    $ensure         = 'present',
+    $gid            = undef,
+    $privs          = [],
+)
+    {
+
+    #sans specified $gid we assume system group and do not create
+    if ($ensure == 'absent') or ($gid) {
+        group { $name:
+            ensure    => $ensure,
+            name      => $name,
+            allowdupe => false,
+            gid       => $gid,
+        }
+    }
+
+    if !empty($privs) {
+        admin::sudo { $name:
+            ensure     => $ensure,
+            privs      => $privs,
+            is_group   => true,
+        }
+    }
+}
diff --git a/modules/admin/manifests/groupmembers.pp 
b/modules/admin/manifests/groupmembers.pp
new file mode 100644
index 0000000..efbe50e
--- /dev/null
+++ b/modules/admin/manifests/groupmembers.pp
@@ -0,0 +1,42 @@
+# A defined type for managing system group members
+#
+# === Parameters
+#
+# [*name*]
+#  Group name
+#
+# [*yamlhash*]
+#  Gash that contains valid group data
+#
+# [*default_member*]
+#  User to be added to a group when explicit members array is empty
+
+define admin::groupmembers(
+    $yamlhash={},
+    $default_member='root',
+)
+    {
+    include admin
+
+    $gdata = $yamlhash['groups'][$name]
+    $members = $gdata['members']
+
+    if !empty($members) {
+        $joined_user_list = join($members,",")
+    }
+    else {
+        $joined_user_list = $default_member
+    }
+
+    #this list is inclusive.  anyone not defined is removed.
+    #check for group existence and if so compare current users
+    $group_nonexistent="getent group ${name} | xargs test -z"
+    $members_match="getent group ${name} | cut -d ':' -f 4 | grep -E 
^${joined_user_list}$"
+    exec { "${name}_ensure_members":
+        command   => "/usr/bin/gpasswd ${name} -M ${joined_user_list}",
+        path      => "/usr/bin:/bin",
+        unless    => "$group_nonexistent || $members_match",
+        logoutput => true,
+    }
+
+}
diff --git a/modules/admin/manifests/init.pp b/modules/admin/manifests/init.pp
new file mode 100644
index 0000000..44ac5ff
--- /dev/null
+++ b/modules/admin/manifests/init.pp
@@ -0,0 +1,65 @@
+# Creates groups, users, and sudo permissions all from yaml for valid passed 
group name
+#
+# === Parameters
+#
+# [*$groups*]
+#  Array of valid groups (defined in yaml) to create with associated members
+#
+# [*$always_groups*]
+#  Array of valid groups to always run
+#
+
+class admin(
+    $groups=[],
+    $always_groups=['absent', 'ops'],
+)
+    {
+
+    $module_path = get_module_path($module_name)
+    $data = loadyaml("${module_path}/data/data.yaml") 
+    $uinfo = $data['users']
+    $users = keys($uinfo)
+
+    #making sure to include always_groups
+    $all_groups = split(inline_template("<%= (always_groups+groups).join(',') 
%>"),',')
+
+    #this custom function eliminates the need for virtual users
+    $user_set = unique_users($data, $all_groups)
+
+    file { '/usr/local/bin/user_cleanup.sh':
+        ensure => file,
+        mode   => '0755',
+        source => 'puppet:///modules/admin/user_cleanup.sh',
+        before => Exec[user_cleanup],
+    }
+
+    file { '/etc/sudoers':
+        ensure => file,
+        mode   => '0440',
+        source => 'puppet:///modules/admin/sudoers',
+        tag    => 'sudoers',
+    }
+
+    admin::yamlgroup { $all_groups:
+        yamlhash => $data,
+        before   => Admin::Yamluser[$user_set],
+    }
+
+    admin::yamluser { $user_set:
+        yamlhash => $data,
+        before   => Admin::Groupmembers[$all_groups],
+    }
+
+    admin::groupmembers { $all_groups:
+        yamlhash => $data,
+        before   => Exec[user_cleanup],
+    }
+
+    #declarative gotcha: non-defined users can get left behind
+    #here we cleanup anyone not in a supplementary group above a certain UID
+    exec { "user_cleanup":
+        command   => "/usr/local/bin/user_cleanup.sh",
+        unless    => "/usr/local/bin/user_cleanup.sh dryrun",
+        logoutput => true,
+    }
+}
diff --git a/modules/admin/manifests/sudo.pp b/modules/admin/manifests/sudo.pp
new file mode 100644
index 0000000..fca748b
--- /dev/null
+++ b/modules/admin/manifests/sudo.pp
@@ -0,0 +1,76 @@
+# A defined type for user/group sudo privilege management
+#
+# === Parameters
+#
+# [*name*]
+#  If this is for a user or group this is the user or group name.
+#  If this is a one-off it is the name of the on-system sudo file
+#
+# [*ensure*]
+#  Add or remove the priv definition in /etc/sudoers.d [ "present" | "absent"]
+#
+# [*user*]
+#  The username of the user to be given priviledges.
+#
+#  WARNING:  Use for user oneoffs.  Sudo privs should be handled in
+#            the main user/group definition in almost all cases.
+#
+# [*comment*]
+#  In case of a non-user definition/non-group definition priv a comment
+#  can be provided.
+#
+# [*privs*]
+#  An array of lines to be included in a sudoers.d/ file
+#
+# [*is_group*]
+#  Boolean value to determine if this is a group.
+#  Group declarations in sudo terms need a '%' prepend
+#
+
+define admin::sudo(
+    $ensure='present',
+    $user=undef,
+    $comment=undef,
+    $privs=[],
+    $is_group=false,
+)
+    {
+
+    if ($user) and ($is_group) {
+        fail("${user} specified as group")
+    }
+
+    if ($user) {
+        $priv_holder = $user
+    }
+    else {
+        $priv_holder = $name
+    }
+
+    if member($privs, 'absent') {
+        $final_ensure = 'absent'
+    }
+    else {
+        $final_ensure = $ensure
+    }
+
+    #WARNING: if path supplied is an existing dir Puppet will swallow this 
silently
+    $filepath = "/etc/sudoers.d/${name}"
+    file { $filepath:
+        ensure  => $final_ensure,
+        owner   => 'root',
+        group   => 'root',
+        mode    => '0440',
+        content => template('admin/sudoers.erb'),
+        tag     => 'sudoers',
+    }
+
+    #messing up sudo can have dire consquences.  here we are linting
+    #the final sudo file.  if bad, remove and throw an exception.
+    exec { "${name}_sudo_linting":
+        command    => "rm -f /etc/sudoers.d/${name} && false",
+        unless     => "test -e ${filepath} && /usr/sbin/visudo -cf ${filepath} 
|| exit 0",
+        path       => '/bin:/usr/bin',
+        subscribe  => File[$filepath],
+    }
+}
diff --git a/modules/admin/manifests/user.pp b/modules/admin/manifests/user.pp
new file mode 100644
index 0000000..bda23a1
--- /dev/null
+++ b/modules/admin/manifests/user.pp
@@ -0,0 +1,133 @@
+# A defined type for user account management.
+#
+# WARNING: this is designed to NOT play well with local modifications.
+#
+# === Parameters
+#
+# [*name*]
+#  The user of the user to be created.
+#
+# [*ensure*]
+#  Add or remove the user account [ "present" | "absent"]
+#
+# [*uid*]
+#  The UID to set for the new account. Must be globally unique.
+#
+# [*gid*]
+#  Sets the primary group of this user.
+#
+#  NOTE: User created files default to this group
+#
+# [*groups*]
+#  An array of additional groups to add the user to.
+#
+#  NOTE: user membership should almost exclusively be handled in the
+#  external definition format (yaml)
+#
+#  WARNING: setting a group here means anywhere this user exists the
+#           group _has_ to exist also.  More than likely they should be added
+#           to the appropriate group in Admin::Groups
+#
+# [*comment*]
+#  Typicaly the realname for the user.
+#
+# [*shell*]
+#  The login shell.
+#
+# [*privs*]
+#  An array of priviledges to setup via admin::sudo
+#  Rarely should a user differ from an established group.
+#
+# [*ssh_keys*]
+#  An array of strings containing the SSH public keys.
+#
+
+define admin::user (
+    $ensure   = 'present',
+    $uid      = undef,
+    $gid      = undef,
+    $groups   = [],
+    $comment  = '',
+    $shell    = '/bin/bash',
+    $privs    = undef,
+    $ssh_keys = [],
+    )
+{
+    validate_re($ensure, '^(present|absent)$')
+
+    $ensure_dir = $ensure ? {
+        'absent'   => 'absent',
+        'present'  => 'directory',
+    }
+
+    user { $name:
+        ensure     => $ensure,
+        name       => $name,
+        uid        => $uid,
+        comment    => $realname,
+        gid        => $gid,
+        groups     => [],
+        shell      => $shell,
+        managehome => false, # we do it manually below
+        allowdupe  => false,
+    }
+
+    #This is all absented by the above /home/${user} cleanup
+    #Puppet chokes if we try to absent subfiles to /home/${user}
+    if $ensure == 'present' {
+
+        file { "/home/${name}":
+            ensure       => $ensure_dir,
+            source       => [
+            "puppet:///modules/admin/home/${name}/",
+            'puppet:///modules/admin/home/skel/',
+            ],
+            sourceselect => 'first',
+            recurse      => 'remote',
+            mode         => '0644',
+            owner        => $name,
+            group        => $gid,
+            force        => true,
+            tag          => 'user-home',
+            require      => User[$name],
+        }
+
+        # XXX: move under /etc/ssh/userkeys
+        # we want to exclusively manage ssh keys in puppet
+        if !empty($ssh_keys) {
+        
+            if !is_array($ssh_keys) {
+                fail("${name} does not have a correct ssh_keys array: 
${ssh_keys}")
+            }
+
+            $ssh_authorized_keys = join($ssh_keys, "\n")
+
+            file { "/home/${name}/.ssh":
+                ensure  => $ensure_dir,
+                owner   => $name,
+                group   => $gid,
+                mode    => '0700',
+                force   => true,
+                tag     => 'user-ssh',
+                require => File["/home/${name}"],
+            }
+
+            file { "/home/${name}/.ssh/authorized_keys":
+                ensure  => $ensure,
+                owner   => $name,
+                group   => $gid,
+                mode    => '0600',
+                content => $ssh_authorized_keys,
+                force   => true,
+                tag     => 'user-ssh',
+                require => File["/home/${name}/.ssh"],
+            }
+        }
+    }
+    if !empty($privs) {
+        admin::sudo { $name:
+            ensure => $ensure,
+            privs  => $privs,
+        }
+    }
+}
diff --git a/modules/admin/manifests/yamlgroup.pp 
b/modules/admin/manifests/yamlgroup.pp
new file mode 100644
index 0000000..b064adb
--- /dev/null
+++ b/modules/admin/manifests/yamlgroup.pp
@@ -0,0 +1,29 @@
+# A defined type for group creation / user realization from yaml
+#
+# === Parameters
+#
+# [*name*]
+#  Yaml group name
+#
+# [*yamlhash*]
+#  Hash that contains valid group data
+
+define admin::yamlgroup(
+    $yamlhash={},
+)
+    {
+    include admin
+
+    #explicit error as otherwise it goes forward later
+    #complaining of 'invalid hash' which is hard to track down
+    if !has_key($yamlhash['groups'], $name) {
+        fail("${name} is not a valid group name")
+    }
+
+    $gdata = $yamlhash['groups'][$name]
+    admin::group { $name:
+        ensure => $gdata['ensure'],
+        gid    => $gdata['gid'],
+        privs  => $gdata['privs'],
+    }
+}
diff --git a/modules/admin/manifests/yamluser.pp 
b/modules/admin/manifests/yamluser.pp
new file mode 100644
index 0000000..75dbf02
--- /dev/null
+++ b/modules/admin/manifests/yamluser.pp
@@ -0,0 +1,42 @@
+# A defined type for user creation from yaml
+#
+# === Parameters
+#
+# [*name*]
+#  Yaml user name
+#
+# [*yamlhash*]
+#  Hash with valid user data
+
+define admin::yamluser(
+    $yamlhash={},
+)
+    {
+
+    $uinfo = $yamlhash['users'][$name]
+
+    if has_key($uinfo, 'gid') {
+        $group_id = $uinfo['gid']
+    }
+    else {
+        $group_id = $uinfo['uid']
+    }
+
+    if has_key($uinfo, 'ssh_keys') {
+        $key_set = $uinfo['ssh_keys']
+    }
+    else {
+        $key_set = []
+    }
+
+    admin::user { $name:
+        ensure      => $uinfo['ensure'],
+        uid         => $uinfo['uid'],
+        gid         => $group_id,
+        groups      => $uinfo['groups'],
+        comment     => $uinfo['realname'],
+        shell       => $uinfo['shell'],
+        privs       => $uinfo['privs'],
+        ssh_keys    => $key_set,
+   }
+}
diff --git a/modules/admin/templates/sudoers.erb 
b/modules/admin/templates/sudoers.erb
new file mode 100644
index 0000000..e8a83b8
--- /dev/null
+++ b/modules/admin/templates/sudoers.erb
@@ -0,0 +1,12 @@
+# This file is managed by Puppet!
+
+<%- if @comment %>
+#<%= @comment %>
+<%- end %>
+<%- privs.each do |privilege| -%>
+<%- if @is_group == true %>
+%<%= @priv_holder %> <%= privilege %>
+<%- else %>
+<%= @priv_holder %> <%= privilege %>
+<%- end -%>
+<%- end -%>

-- 
To view, visit https://gerrit.wikimedia.org/r/129501
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I6982bfced50a22faac37246dff4d52ff163a34ed
Gerrit-PatchSet: 1
Gerrit-Project: operations/puppet
Gerrit-Branch: production
Gerrit-Owner: Rush <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to