summaryrefslogtreecommitdiffstats
path: root/lib/puppet/network/rights.rb
blob: 6fde181c9f56e967850707eff734813a737d4068 (plain)
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
require 'puppet/network/authstore'
require 'puppet/error'

module Puppet::Network

# this exception is thrown when a request is not authenticated
class AuthorizationError < Puppet::Error; end

# Define a set of rights and who has access to them.
# There are two types of rights:
#  * named rights (ie a common string)
#  * path based rights (which are matched on a longest prefix basis)
class Rights

  # We basically just proxy directly to our rights.  Each Right stores
  # its own auth abilities.
  [:allow, :deny, :restrict_method, :restrict_environment, :restrict_authenticated].each do |method|
    define_method(method) do |name, *args|
      if obj = self[name]
        obj.send(method, *args)
      else
        raise ArgumentError, "Unknown right '#{name}'"
      end
    end
  end

  # Check that name is allowed or not
  def allowed?(name, *args)
    !is_forbidden_and_why?(name, :node => args[0], :ip => args[1])
  end

  def is_request_forbidden_and_why?(indirection, method, key, params)
    methods_to_check = if method == :head
                         # :head is ok if either :find or :save is ok.
                         [:find, :save]
                       else
                         [method]
                       end
    authorization_failure_exceptions = methods_to_check.map do |method|
      is_forbidden_and_why?("/#{indirection}/#{key}", params.merge({:method => method}))
    end
    if authorization_failure_exceptions.include? nil
      # One of the methods we checked is ok, therefore this request is ok.
      nil
    else
      # Just need to return any of the failure exceptions.
      authorization_failure_exceptions.first
    end
  end

  def is_forbidden_and_why?(name, args = {})
    res = :nomatch
    right = @rights.find do |acl|
      found = false
      # an acl can return :dunno, which means "I'm not qualified to answer your question,
      # please ask someone else". This is used when for instance an acl matches, but not for the
      # current rest method, where we might think some other acl might be more specific.
      if match = acl.match?(name)
        args[:match] = match
        if (res = acl.allowed?(args[:node], args[:ip], args)) != :dunno
          # return early if we're allowed
          return nil if res
          # we matched, select this acl
          found = true
        end
      end
      found
    end

    # if we end here, then that means we either didn't match
    # or failed, in any case will throw an error to the outside world
    if name =~ /^\// or right
      # we're a patch ACL, let's fail
      msg = "#{(args[:node].nil? ? args[:ip] : "#{args[:node]}(#{args[:ip]})")} access to #{name} [#{args[:method]}]"

      msg += " authenticated " if args[:authenticated]

      error = AuthorizationError.new("Forbidden request: #{msg}")
      if right
        error.file = right.file
        error.line = right.line
      end
    else
      # there were no rights allowing/denying name
      # if name is not a path, let's throw
      raise ArgumentError, "Unknown namespace right '#{name}'"
    end
    error
  end

  def initialize
    @rights = []
  end

  def [](name)
    @rights.find { |acl| acl == name }
  end

  def include?(name)
    @rights.include?(name)
  end

  def each
    @rights.each { |r| yield r.name,r }
  end

  # Define a new right to which access can be provided.
  def newright(name, line=nil, file=nil)
    add_right( Right.new(name, line, file) )
  end

  private

  def add_right(right)
    if right.acl_type == :name and include?(right.key)
      raise ArgumentError, "Right '%s' already exists"
    end
    @rights << right
    sort_rights
    right
  end

  def sort_rights
    @rights.sort!
  end

  # Retrieve a right by name.
  def right(name)
    self[name]
  end

  # A right.
  class Right < Puppet::Network::AuthStore
    include Puppet::FileCollection::Lookup

    attr_accessor :name, :key, :acl_type
    attr_accessor :methods, :environment, :authentication

    ALL = [:save, :destroy, :find, :search]

    Puppet::Util.logmethods(self, true)

    def initialize(name, line, file)
      @methods = []
      @environment = []
      @authentication = true # defaults to authenticated
      @name = name
      @line = line || 0
      @file = file

      case name
      when Symbol
        @acl_type = :name
        @key = name
      when /^\[(.+)\]$/
        @acl_type = :name
        @key = $1.intern if name.is_a?(String)
      when /^\//
        @acl_type = :regex
        @key = Regexp.new("^" + Regexp.escape(name))
        @methods = ALL
      when /^~/ # this is a regex
        @acl_type = :regex
        @name = name.gsub(/^~\s+/,'')
        @key = Regexp.new(@name)
        @methods = ALL
      else
        raise ArgumentError, "Unknown right type '#{name}'"
      end
      super()
    end

    def to_s
      "access[#{@name}]"
    end

    # There's no real check to do at this point
    def valid?
      true
    end

    def regex?
      acl_type == :regex
    end

    # does this right is allowed for this triplet?
    # if this right is too restrictive (ie we don't match this access method)
    # then return :dunno so that upper layers have a chance to try another right
    # tailored to the given method
    def allowed?(name, ip, args = {})
      return :dunno if acl_type == :regex and not @methods.include?(args[:method])
      return :dunno if acl_type == :regex and @environment.size > 0 and not @environment.include?(args[:environment])
      return :dunno if acl_type == :regex and not @authentication.nil? and args[:authenticated] != @authentication

      begin
        # make sure any capture are replaced if needed
        interpolate(args[:match]) if acl_type == :regex and args[:match]
        res = super(name,ip)
      ensure
        reset_interpolation if acl_type == :regex
      end
      res
    end

    # restrict this right to some method only
    def restrict_method(m)
      m = m.intern if m.is_a?(String)

      raise ArgumentError, "'#{m}' is not an allowed value for method directive" unless ALL.include?(m)

      # if we were allowing all methods, then starts from scratch
      if @methods === ALL
        @methods = []
      end

      raise ArgumentError, "'#{m}' is already in the '#{name}' ACL" if @methods.include?(m)

      @methods << m
    end

    def restrict_environment(env)
      env = Puppet::Node::Environment.new(env)
      raise ArgumentError, "'#{env}' is already in the '#{name}' ACL" if @environment.include?(env)

      @environment << env
    end

    def restrict_authenticated(authentication)
      case authentication
      when "yes", "on", "true", true
        authentication = true
      when "no", "off", "false", false
        authentication = false
      when "all","any", :all, :any
        authentication = nil
      else
        raise ArgumentError, "'#{name}' incorrect authenticated value: #{authentication}"
      end
      @authentication = authentication
    end

    def match?(key)
      # if we are a namespace compare directly
      return self.key == namespace_to_key(key) if acl_type == :name

      # otherwise match with the regex
      self.key.match(key)
    end

    def namespace_to_key(key)
      key = key.intern if key.is_a?(String)
      key
    end

    # this is where all the magic happens.
    # we're sorting the rights array with this scheme:
    #  * namespace rights are all in front
    #  * regex path rights are then all queued in file order
    def <=>(rhs)
      # move namespace rights at front
      return self.acl_type == :name ? -1 : 1 if self.acl_type != rhs.acl_type

      # sort by creation order (ie first match appearing in the file will win)
      # that is don't sort, in which case the sort algorithm will order in the
      # natural array order (ie the creation order)
      0
    end

    def ==(name)
      return(acl_type == :name ? self.key == namespace_to_key(name) : self.name == name.gsub(/^~\s+/,''))
    end

  end

end
end