Puppet Function: localuser

Defined in:
lib/puppet/parser/functions/localuser.rb
Function type:
Ruby 3.x API

Overview

localuser(Stdlib::Absolutepath $filename, Any $hostname)String

Pull a pre-set password from a password list and return an array of user details associated with the passed hostname.

If the password starts with the string $1$ and the length is 34 characters, then it will be assumed to be an MD5 hash to be directly applied to the system.

If the password is in plain text form, then it will be hashed and stored back into the source file for future use. The plain text version will be commented out in the file.

Lines beginning with the # symbol are ignored and commas , are not allowed in usernames or hostnames though any characters are allowed in passwords.

homedir is the home directory of the user and is optional. By default, the system will choose the home directory.

The function will return a String with the following contents:

[attr]<username>,MD5-based password hash with random salt

Hostname Ruby regular expressions are fully supported. The following formats are allowed:

  • /regex/opts,<username>

  • /regex/,<username>

  • regex,<username>

  • *.<domain>,<username>

  • fqdn,<username>

Examples:

Password Line syntax


[+-!]<fqdn-regex>,<username>,<uid>,<gid>,[<homedir>],<password>

Parameters:

  • filename (Stdlib::Absolutepath)

    path to the file containing the local users

  • hostname (Any)

    host that you are trying to match against

Returns:

  • (String)


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
# File 'lib/puppet/parser/functions/localuser.rb', line 2

newfunction(:localuser, :type => :rvalue, :doc => <<-'ENDHEREDOC') do |args|
  filename = args[0]
  Pull a pre-set password from a password list and return an `array` of
  user details associated with the passed hostname.

  If the password starts with the string `$1$` and the length is `34`
  characters, then it will be assumed to be an `MD5` hash to be directly
  applied to the system.

  If the password is in plain text form, then it will be hashed and stored
  back into the source file for future use.  The plain text version will be
  commented out in the file.

  @example Password Line syntax

    [+-!]<fqdn-regex>,<username>,<uid>,<gid>,[<homedir>],<password>

  Lines beginning with the `#` symbol are ignored and commas `,` are not
  allowed in usernames or hostnames though any characters are allowed in
  passwords.

  `homedir` is the home directory of the user and is optional. By default,
  the system will choose the home directory.

  The function will return a `String` with the following contents:

  `[attr]<username>,MD5-based password hash with random salt`

  Hostname Ruby regular expressions are fully supported. The following
  formats are allowed:

    * /regex/opts,<username>
    * /regex/,<username>
    * regex,<username>
    * *.<domain>,<username>
    * fqdn,<username>

  @param filename [Stdlib::Absolutepath]
    path to the file containing the local users
  @param hostname
    host that you are trying to match against

  @return [String]
  ENDHEREDOC
  hostname = args[1]
  retval = []

  if not FileTest.exists?("#{filename}") then
      return ["### You must have a file on the server at #{filename} from which to read usernames and hashes ###\n### These lines will be ignored ###\n"]
  end

  File.open(filename, 'r+') do |file|
    oldfile = file.readlines.map(&:chomp)

    oldfile.each_with_index do |line,index|

      # If it isn't a comment, do stuff.
      if ( line !~ /^#/ ) then

        host = hostname

        # Chunk the line, the first field is the regex.
        vals = line.split(',')

        orighost = vals.shift
        if ( orighost =~ /^([+-\\!]?)(.*)/ ) then
          extattr = $1
          orighost = $2
        end

        # Copy this to a variable for mangling
        orighost_tmp = orighost

        # This covers the legacy format, which could start with a '*'
        if ( orighost_tmp =~ /^\*/ ) then
            orighost_tmp = ".#{orighost_tmp}"
        end

        # If this is a formatted regex, treat it as such.
        if ( orighost_tmp =~ /^\// ) then
            orighost_tmp = orighost_tmp.split(/\//)
            hostregex = Regexp.new(orighost_tmp[1],orighost_tmp[2])
        else
            hostregex = Regexp.new("^#{orighost_tmp}$")
        end

        # Match against the passed hostname.
        if hostregex.match(host) then

          username = vals.shift
          uid = vals.shift
          gid = vals.shift
          homedir = nil
          if ( vals.length == 2 ) then
            homedir = vals.shift
          end
          pass = "#{vals.shift.chomp}"
          vals.each {|x| pass = pass + "," + x.chomp}

          # See if we already have a hashed pass.
          if ( pass =~ /\$[156]\$/ )
            hash = pass
          # If not, then create one.
          else
            chars = ("a".."z").to_a + ("0".."9").to_a + %w{. /}
            salt = "$6$rounds=10000$" + Array.new(8, '').collect{chars[rand(chars.size)]}.join

            hash = pass.crypt(salt)

            # Check to be sure that we got a hashed password.
            # We really should never get here on a modern system.
            if not hash.include?(salt) then
              # Fall back to MD5
              salt = "$1$" + Array.new(8, '').collect{chars[rand(chars.size)]}.join
              hash = pass.crypt(salt)
            end

            oldfile[index] = "# " + oldfile[index]
            if ( homedir ) then
              oldfile.insert(index+1, "#{extattr}#{orighost},#{username},#{uid},#{gid},#{homedir},#{hash}\n")
            else
              oldfile.insert(index+1, "#{extattr}#{orighost},#{username},#{uid},#{gid},#{hash}\n")
            end
          end
          if ( homedir ) then
            retval << "#{extattr}#{username},#{uid},#{gid},#{homedir},#{hash}"
          else
            retval << "#{extattr}#{username},#{uid},#{gid},#{hash}"
          end
        end
      end
    end
    file.pos = 0
    file.print oldfile.join("\n")
    file.truncate(file.pos)
    file.close
  end

  retval
end