File: aix.rb

package info (click to toggle)
puppet-agent 7.23.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 19,092 kB
  • sloc: ruby: 245,074; sh: 456; makefile: 38; xml: 33
file content (361 lines) | stat: -rw-r--r-- 13,598 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
# User Puppet provider for AIX. It uses standard commands to manage users:
#  mkuser, rmuser, lsuser, chuser
#
# Notes:
# - AIX users can have expiry date defined with minute granularity,
#   but Puppet does not allow it. There is a ticket open for that (#5431)
#
# - AIX maximum password age is in WEEKs, not days
#
# See https://puppet.com/docs/puppet/latest/provider_development.html
# for more information
require_relative '../../../puppet/provider/aix_object'
require_relative '../../../puppet/util/posix'
require 'tempfile'
require 'date'

Puppet::Type.type(:user).provide :aix, :parent => Puppet::Provider::AixObject do
  desc "User management for AIX."

  defaultfor :operatingsystem => :aix
  confine :operatingsystem => :aix

  # Commands that manage the element
  commands :list      => "/usr/sbin/lsuser"
  commands :add       => "/usr/bin/mkuser"
  commands :delete    => "/usr/sbin/rmuser"
  commands :modify    => "/usr/bin/chuser"

  commands :chpasswd  => "/bin/chpasswd"

  # Provider features
  has_features :manages_aix_lam
  has_features :manages_homedir, :manages_passwords, :manages_shell
  has_features :manages_expiry,  :manages_password_age
  has_features :manages_local_users_and_groups

  class << self
    def group_provider
      @group_provider ||= Puppet::Type.type(:group).provider(:aix)
    end

    # Define some Puppet Property => AIX Attribute (and vice versa)
    # conversion functions here.

    def gid_to_pgrp(provider, gid)
      group = group_provider.find(gid, provider.ia_module_args)

      group[:name]
    end

    def pgrp_to_gid(provider, pgrp)
      group = group_provider.find(pgrp, provider.ia_module_args)

      group[:gid]
    end

    def expiry_to_expires(expiry)
      return '0' if expiry == "0000-00-00" || expiry.to_sym == :absent
      
      DateTime.parse(expiry, "%Y-%m-%d %H:%M")
        .strftime("%m%d%H%M%y")
    end

    def expires_to_expiry(provider, expires)
      return :absent if expires == '0'

      unless (match_obj = /\A(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)\z/.match(expires))
        #TRANSLATORS 'AIX' is the name of an operating system and should not be translated
        Puppet.warning(_("Could not convert AIX expires date '%{expires}' on %{class_name}[%{resource_name}]") % { expires: expires, class_name: provider.resource.class.name, resource_name: provider.resource.name })
        return :absent
      end

      month, day, year = match_obj[1], match_obj[2], match_obj[-1]
      return "20#{year}-#{month}-#{day}"
    end

    # We do some validation before-hand to ensure the value's an Array,
    # a String, etc. in the property. This routine does a final check to
    # ensure our value doesn't have whitespace before we convert it to
    # an attribute.
    def groups_property_to_attribute(groups)
      if groups =~ /\s/
        raise ArgumentError, _("Invalid value %{groups}: Groups must be comma separated!") % { groups: groups }
      end

      groups
    end

    # We do not directly use the groups attribute value because that will
    # always include the primary group, even if our user is not one of its
    # members. Instead, we retrieve our property value by parsing the etc/group file,
    # which matches what we do on our other POSIX platforms like Linux and Solaris.
    #
    # See https://www.ibm.com/support/knowledgecenter/en/ssw_aix_72/com.ibm.aix.files/group_security.htm
    def groups_attribute_to_property(provider, _groups)
      Puppet::Util::POSIX.groups_of(provider.resource[:name]).join(',')
    end
  end

  mapping puppet_property: :comment,
          aix_attribute: :gecos

  mapping puppet_property: :expiry,
          aix_attribute: :expires,
          property_to_attribute: method(:expiry_to_expires),
          attribute_to_property: method(:expires_to_expiry)

  mapping puppet_property: :gid,
          aix_attribute: :pgrp,
          property_to_attribute: method(:gid_to_pgrp),
          attribute_to_property: method(:pgrp_to_gid)

  mapping puppet_property: :groups, 
          property_to_attribute: method(:groups_property_to_attribute),
          attribute_to_property: method(:groups_attribute_to_property)

  mapping puppet_property: :home
  mapping puppet_property: :shell

  numeric_mapping puppet_property: :uid,
                  aix_attribute: :id

  numeric_mapping puppet_property: :password_max_age,
                  aix_attribute: :maxage

  numeric_mapping puppet_property: :password_min_age,
                  aix_attribute: :minage

  numeric_mapping puppet_property: :password_warn_days,
                  aix_attribute: :pwdwarntime

  # Now that we have all of our mappings, let's go ahead and make
  # the resource methods (property getters + setters for our mapped
  # properties + a getter for the attributes property).
  mk_resource_methods

  # Setting the primary group (pgrp attribute) on AIX causes both the
  # current and new primary groups to be included in our user's groups,
  # which is undesirable behavior. Thus, this custom setter resets the
  # 'groups' property back to its previous value after setting the primary
  # group.
  def gid=(value)
    old_pgrp = gid
    cur_groups = groups

    set(:gid, value)

    begin
      self.groups = cur_groups
    rescue Puppet::Error => detail
      raise Puppet::Error, _("Could not reset the groups property back to %{cur_groups} after setting the primary group on %{resource}[%{name}]. This means that the previous primary group of %{old_pgrp} and the new primary group of %{new_pgrp} have been added to %{cur_groups}. You will need to manually reset the groups property if this is undesirable behavior. Detail: %{detail}") % { cur_groups: cur_groups, resource: @resource.class.name, name: @resource.name, old_pgrp: old_pgrp, new_pgrp: value, detail: detail }, detail.backtrace
    end
  end

  # Helper function that parses the password from the given
  # password filehandle. This is here to make testing easier
  # for #password since we cannot configure Mocha to mock out
  # a method and have it return a block's value, meaning we
  # cannot test #password directly (not in a simple and obvious
  # way, at least).
  # @api private
  def parse_password(f)
    # From the docs, a user stanza is formatted as (newlines are explicitly
    # stated here for clarity):
    #   <user>:\n
    #     <attribute1>=<value1>\n
    #     <attribute2>=<value2>\n
    #
    # First, find our user stanza
    stanza = f.each_line.find { |line| line =~ /\A#{@resource[:name]}:/ }
    return :absent unless stanza

    # Now find the password line, if it exists. Note our call to each_line here
    # will pick up right where we left off.
    match_obj = nil
    f.each_line.find do |line|
      # Break if we find another user stanza. This means our user
      # does not have a password.
      break if line =~ /^\S+:$/

      match_obj = /password\s+=\s+(\S+)/.match(line)
    end
    return :absent unless match_obj

    match_obj[1]
  end

  #- **password**
  #    The user's password, in whatever encrypted format the local machine
  #    requires. Be sure to enclose any value that includes a dollar sign ($)
  #    in single quotes (').  Requires features manages_passwords.
  #
  # Retrieve the password parsing the /etc/security/passwd file.
  def password
    # AIX reference indicates this file is ASCII
    # https://www.ibm.com/support/knowledgecenter/en/ssw_aix_72/com.ibm.aix.files/passwd_security.htm
    Puppet::FileSystem.open("/etc/security/passwd", nil, "r:ASCII") do |f|
      parse_password(f)
    end
  end

  def password=(value)
    user = @resource[:name]

    begin
      # Puppet execute does not support strings as input, only files.
      # The password is expected to be in an encrypted format given -e is specified:
      # https://www.ibm.com/support/knowledgecenter/ssw_aix_71/com.ibm.aix.cmds1/chpasswd.htm
      # /etc/security/passwd is specified as an ASCII file per the AIX documentation
      tempfile = nil
      tempfile = Tempfile.new("puppet_#{user}_pw", :encoding => Encoding::ASCII)
      tempfile << "#{user}:#{value}\n"
      tempfile.close()

      # Options '-e', '-c', use encrypted password and clear flags
      # Must receive "user:enc_password" as input
      # command, arguments = {:failonfail => true, :combine => true}
      # Fix for bugs #11200 and #10915
      cmd = [self.class.command(:chpasswd), *ia_module_args, '-e', '-c']
      execute_options = {
        :failonfail => false,
        :combine => true,
        :stdinfile => tempfile.path
      }
      output = execute(cmd, execute_options)

      # chpasswd can return 1, even on success (at least on AIX 6.1); empty output
      # indicates success
      if output != ""
        raise Puppet::ExecutionFailure, "chpasswd said #{output}"
      end
    rescue Puppet::ExecutionFailure  => detail
      raise Puppet::Error, "Could not set password on #{@resource.class.name}[#{@resource.name}]: #{detail}", detail.backtrace
    ensure
      if tempfile
        # Extra close will noop. This is in case the write to our tempfile
        # fails.
        tempfile.close()
        tempfile.delete()
      end
    end
  end

  def create
    super

    # We specify the 'groups' AIX attribute in AixObject's create method
    # when creating our user. However, this does not always guarantee that
    # our 'groups' property is set to the right value. For example, the
    # primary group will always be included in the 'groups' property. This is
    # bad if we're explicitly managing the 'groups' property under inclusive
    # membership, and we are not specifying the primary group in the 'groups'
    # property value.
    #
    # Setting the groups property here a second time will ensure that our user is
    # created and in the right state. Note that this is an idempotent operation,
    # so if AixObject's create method already set it to the right value, then this
    # will noop.
    if (groups = @resource.should(:groups))
      self.groups = groups
    end

    if (password = @resource.should(:password))
      self.password = password
    end
  end

  # Lists all instances of the given object, taking in an optional set
  # of ia_module arguments. Returns an array of hashes, each hash
  # having the schema
  #   {
  #     :name => <object_name>
  #     :home => <object_home>
  #   }
  def list_all_homes(ia_module_args = [])
    cmd = [command(:list), '-c', *ia_module_args, '-a', 'home', 'ALL']
    parse_aix_objects(execute(cmd)).to_a.map do |object|
      name = object[:name]
      home = object[:attributes].delete(:home)

      { name: name, home: home }
    end
  rescue => e
    Puppet.debug("Could not list home of all users: #{e.message}")
    {}
  end

  # Deletes this instance resource
  def delete
    homedir = home
    super
    return unless @resource.managehome?

    if !Puppet::Util.absolute_path?(homedir) || File.realpath(homedir) == '/' || Puppet::FileSystem.symlink?(homedir)
      Puppet.debug("Can not remove home directory '#{homedir}' of user '#{@resource[:name]}'. Please make sure the path is not relative, symlink or '/'.")
      return
    end

    affected_home = list_all_homes.find { |info| info[:home].start_with?(File.realpath(homedir)) }
    if affected_home
      Puppet.debug("Can not remove home directory '#{homedir}' of user '#{@resource[:name]}' as it would remove the home directory '#{affected_home[:home]}' of user '#{affected_home[:name]}' also.")
      return
    end

    FileUtils.remove_entry_secure(homedir, true)
  end

  def deletecmd
    [self.class.command(:delete), '-p'] + ia_module_args + [@resource[:name]]
  end

  # UNSUPPORTED
  #- **profile_membership**
  #    Whether specified roles should be treated as the only roles
  #    of which the user is a member or whether they should merely
  #    be treated as the minimum membership list.  Valid values are
  #    `inclusive`, `minimum`.
  # UNSUPPORTED
  #- **profiles**
  #    The profiles the user has.  Multiple profiles should be
  #    specified as an array.  Requires features manages_solaris_rbac.
  # UNSUPPORTED
  #- **project**
  #    The name of the project associated with a user  Requires features
  #    manages_solaris_rbac.
  # UNSUPPORTED
  #- **role_membership**
  #    Whether specified roles should be treated as the only roles
  #    of which the user is a member or whether they should merely
  #    be treated as the minimum membership list.  Valid values are
  #    `inclusive`, `minimum`.
  # UNSUPPORTED
  #- **roles**
  #    The roles the user has.  Multiple roles should be
  #    specified as an array.  Requires features manages_roles.
  # UNSUPPORTED
  #- **key_membership**
  #    Whether specified key value pairs should be treated as the only
  #    attributes
  #    of the user or whether they should merely
  #    be treated as the minimum list.  Valid values are `inclusive`,
  #    `minimum`.
  # UNSUPPORTED
  #- **keys**
  #    Specify user attributes in an array of keyvalue pairs  Requires features
  #    manages_solaris_rbac.
  # UNSUPPORTED
  #- **allowdupe**
  #  Whether to allow duplicate UIDs.  Valid values are `true`, `false`.
  # UNSUPPORTED
  #- **auths**
  #    The auths the user has.  Multiple auths should be
  #    specified as an array.  Requires features manages_solaris_rbac.
  # UNSUPPORTED
  #- **auth_membership**
  #    Whether specified auths should be treated as the only auths
  #    of which the user is a member or whether they should merely
  #    be treated as the minimum membership list.  Valid values are
  #    `inclusive`, `minimum`.
  # UNSUPPORTED
end