summaryrefslogtreecommitdiffstats
path: root/lib/puppet/network/rights.rb
blob: 63ebae892b0869fbe81235b808466e68705cf249 (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
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)
        begin
            fail_on_deny(name, :node => args[0], :ip => args[1])
        rescue AuthorizationError
            return false
        rescue ArgumentError
            # the namespace contract says we should raise this error
            # if we didn't find the right acl
            raise
        end
        true
    end

    def fail_on_deny(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 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
            Puppet.warning("Denying access: #{error}")
        else
            # there were no rights allowing/denying name
            # if name is not a path, let's throw
            error = ArgumentError.new "Unknown namespace right '#{name}'"
        end
        raise 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