File: parsed.rb

package info (click to toggle)
puppet-module-puppetlabs-sshkeys-core 2.3.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 404 kB
  • sloc: ruby: 2,019; sh: 16; makefile: 4
file content (138 lines) | stat: -rw-r--r-- 4,895 bytes parent folder | download | duplicates (4)
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
require 'puppet/provider/parsedfile'

Puppet::Type.type(:ssh_authorized_key).provide(
  :parsed,
  parent: Puppet::Provider::ParsedFile,
  filetype: :flat,
  default_target: '',
) do
  desc 'Parse and generate authorized_keys files for SSH.'

  text_line :comment, match: %r{^\s*#}
  text_line :blank, match: %r{^\s*$}

  record_line :parsed,
              fields: ['options', 'type', 'key', 'name'],
              optional: ['options'],
              rts: %r{^\s+},
              match: Puppet::Type.type(:ssh_authorized_key).keyline_regex,
              post_parse: proc { |h|
                h[:name] = '' if h[:name] == :absent
                h[:options] ||= [:absent]
                h[:options] = Puppet::Type::Ssh_authorized_key::ProviderParsed.parse_options(h[:options]) if h[:options].is_a? String
              },
              pre_gen: proc { |h|
                # if this name was generated, don't write it back to disk
                h[:name] = '' if h[:unnamed]
                h[:options] = [] if h[:options].include?(:absent)
                h[:options] = h[:options].join(',')
              }

  record_line :key_v1,
              fields: ['options', 'bits', 'exponent', 'modulus', 'name'],
              optional: ['options'],
              rts: %r{^\s+},
              match: %r{^(?:(.+) )?(\d+) (\d+) (\d+)(?: (.+))?$}

  def dir_perm
    0o700
  end

  def file_perm
    0o600
  end

  def group_writable_perm
    0o020
  end

  def group_writable?(path)
    path.stat.mode & group_writable_perm != 0
  end

  def trusted_path
    # return if the parent directory does not exist
    return false unless Puppet::FileSystem.dir_exist?(target)
    path = Puppet::FileSystem.pathname(target).dirname
    until path.dirname.root?
      path = path.realpath if path.symlink?
      # do not trust if path is world or group writable
      if path.stat.uid != Process.euid || path.world_writable? || group_writable?(path)
        Puppet.debug('Path untrusted, will attempt to write as the target user')
        return false
      end
      path = path.dirname
    end
    Puppet.debug('Path trusted, writing the file as the current user')
  end

  def flush
    raise Puppet::Error, 'Cannot write SSH authorized keys without user'    unless @resource.should(:user)
    raise Puppet::Error, "User '#{@resource.should(:user)}' does not exist" unless Puppet::Util.uid(@resource.should(:user))
    # ParsedFile usually calls backup_target much later in the flush process,
    # but our SUID makes that fail to open filebucket files for writing.
    # Fortunately, there's already logic to make sure it only ever happens once,
    # so calling it here suppresses the later attempt by our superclass's flush method.
    self.class.backup_target(target)

    # attempt to create the file as the specified user if we're not dropping privileges
    if @resource[:drop_privileges]
      Puppet::Util::SUIDManager.asuser(@resource.should(:user)) do
        unless Puppet::FileSystem.exist?(dir = File.dirname(target))
          Puppet.debug "Creating #{dir} as #{@resource.should(:user)}"
          Dir.mkdir(dir, dir_perm)
        end
        super

        File.chmod(file_perm, target)
      end
    # to avoid race conditions when handling permissions as a privileged user
    # (CVE-2011-3870) we use the trusted_path method to ensure the entire
    # directory structure is "safe" to write in
    else
      raise Puppet::Error, 'drop_privileges is false but the target path is not trusted' unless trusted_path
      super

      uid = Puppet::Util.uid(@resource.should(:user))
      gid = Puppet::Util.gid(@resource.should(:user))
      File.open(target) do |target|
        target.chown(uid, gid)
        target.chmod(file_perm)
      end
    end
  end

  # Parse sshv2 option strings, which is a comma-separated list of
  # either key="values" elements or bare-word elements
  def self.parse_options(options)
    result = []
    scanner = StringScanner.new(options)
    until scanner.eos?
      scanner.skip(%r{[ \t]*})
      # scan a long option
      out = scanner.scan(%r{[-a-z0-9A-Z_]+=\".*?[^\\]\"}) || scanner.scan(%r{[-a-z0-9A-Z_]+})

      # found an unscannable token, let's abort
      break unless out

      result << out

      # eat a comma
      scanner.skip(%r{[ \t]*,[ \t]*})
    end
    result
  end

  def self.prefetch_hook(records)
    name_index = 0
    records.each do |record|
      next unless record[:record_type] == :parsed && record[:name].empty?
      record[:unnamed] = true
      # Generate a unique ID for unnamed keys, in case they need purging.
      # If you change this, you have to keep
      # Puppet::Type::User#unknown_keys_in_file in sync! (PUP-3357)
      record[:name] = "#{record[:target]}:unnamed-#{name_index += 1}"
      Puppet.debug("generating name for on-disk ssh_authorized_key #{record[:key]}: #{record[:name]}")
    end
  end
end