diff --git a/lib/puppet/network/authstore.rb b/lib/puppet/network/authstore.rb index 51fd34138..bd19aeb9c 100755 --- a/lib/puppet/network/authstore.rb +++ b/lib/puppet/network/authstore.rb @@ -1,259 +1,267 @@ # standard module for determining whether a given hostname or IP has access to # the requested resource require 'ipaddr' require 'puppet/util/logging' module Puppet class AuthStoreError < Puppet::Error; end class AuthorizationError < Puppet::Error; end class Network::AuthStore include Puppet::Util::Logging # Mark a given pattern as allowed. def allow(pattern) # a simple way to allow anyone at all to connect if pattern == "*" @globalallow = true else store(:allow, pattern) end nil end # Is a given combination of name and ip address allowed? If either input # is non-nil, then both inputs must be provided. If neither input # is provided, then the authstore is considered local and defaults to "true". def allowed?(name, ip) if name or ip # This is probably unnecessary, and can cause some weirdnesses in # cases where we're operating over localhost but don't have a real # IP defined. raise Puppet::DevError, "Name and IP must be passed to 'allowed?'" unless name and ip # else, we're networked and such else # we're local return true end # yay insecure overrides return true if globalallow? if decl = declarations.find { |d| d.match?(name, ip) } return decl.result end info "defaulting to no access for #{name}" false end # Deny a given pattern. def deny(pattern) store(:deny, pattern) end # Is global allow enabled? def globalallow? @globalallow end # does this auth store has any rules? def empty? @globalallow.nil? && @declarations.size == 0 end def initialize @globalallow = nil @declarations = [] end def to_s "authstore" end def interpolate(match) Thread.current[:declarations] = @declarations.collect { |ace| ace.interpolate(match) }.sort end def reset_interpolation Thread.current[:declarations] = nil end private # returns our ACEs list, but if we have a modification of it # in our current thread, let's return it # this is used if we want to override the this purely immutable list # by a modified version in a multithread safe way. def declarations Thread.current[:declarations] || @declarations end # Store the results of a pattern into our hash. Basically just # converts the pattern and sticks it into the hash. def store(type, pattern) @declarations << Declaration.new(type, pattern) @declarations.sort! nil end # A single declaration. Stores the info for a given declaration, # provides the methods for determining whether a declaration matches, # and handles sorting the declarations appropriately. class Declaration include Puppet::Util include Comparable # The type of declaration: either :allow or :deny attr_reader :type # The name: :ip or :domain attr_accessor :name # The pattern we're matching against. Can be an IPAddr instance, # or an array of strings, resulting from reversing a hostname # or domain name. attr_reader :pattern # The length. Only used for iprange and domain. attr_accessor :length # Sort the declarations most specific first. def <=>(other) compare(exact?, other.exact?) || compare(ip?, other.ip?) || ((length != other.length) && (other.length <=> length)) || compare(deny?, other.deny?) || ( ip? ? pattern.to_s <=> other.pattern.to_s : pattern <=> other.pattern) end def deny? type == :deny end def exact? @exact == :exact end def initialize(type, pattern) self.type = type self.pattern = pattern end # Are we an IP type? def ip? name == :ip end # Does this declaration match the name/ip combo? def match?(name, ip) - ip? ? pattern.include?(IPAddr.new(ip)) : matchname?(name) + if ip? + if pattern.include?(IPAddr.new(ip)) + Puppet.deprecation_warning "Authentication based on IP address is deprecated; please use certname-based rules instead" + true + else + false + end + else + matchname?(name) + end end # Set the pattern appropriately. Also sets the name and length. def pattern=(pattern) parse(pattern) @orig = pattern end # Mapping a type of statement into a return value. def result type == :allow end def to_s "#{type}: #{pattern}" end # Set the declaration type. Either :allow or :deny. def type=(type) type = symbolize(type) raise ArgumentError, "Invalid declaration type #{type}" unless [:allow, :deny].include?(type) @type = type end # interpolate a pattern to replace any # backreferences by the given match # for instance if our pattern is $1.reductivelabs.com # and we're called with a MatchData whose capture 1 is puppet # we'll return a pattern of puppet.reductivelabs.com def interpolate(match) clone = dup if @name == :dynamic clone.pattern = clone.pattern.reverse.collect do |p| p.gsub(/\$(\d)/) { |m| match[$1.to_i] } end.join(".") end clone end private # Returns nil if both values are true or both are false, returns # -1 if the first is true, and 1 if the second is true. Used # in the <=> operator. def compare(me, them) (me and them) ? nil : me ? -1 : them ? 1 : nil end # Does the name match our pattern? def matchname?(name) case @name when :domain, :dynamic, :opaque name = munge_name(name) (pattern == name) or (not exact? and pattern.zip(name).all? { |p,n| p == n }) when :regex Regexp.new(pattern.slice(1..-2)).match(name) end end # Convert the name to a common pattern. def munge_name(name) - # LAK:NOTE http://snurl.com/21zf8 [groups_google_com] # Change to name.downcase.split(".",-1).reverse for FQDN support name.downcase.split(".").reverse end # Parse our input pattern and figure out what kind of allowal # statement it is. The output of this is used for later matching. Octet = '(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])' IPv4 = "#{Octet}\.#{Octet}\.#{Octet}\.#{Octet}" IPv6_full = "_:_:_:_:_:_:_:_|_:_:_:_:_:_::_?|_:_:_:_:_::((_:)?_)?|_:_:_:_::((_:){0,2}_)?|_:_:_::((_:){0,3}_)?|_:_::((_:){0,4}_)?|_::((_:){0,5}_)?|::((_:){0,6}_)?" IPv6_partial = "_:_:_:_:_:_:|_:_:_:_::(_:)?|_:_::(_:){0,2}|_::(_:){0,3}" # It should be: # IP = "#{IPv4}|#{IPv6_full}|(#{IPv6_partial}#{IPv4})".gsub(/_/,'([0-9a-fA-F]{1,4})').gsub(/\(/,'(?:') # but ruby's ipaddr lib doesn't support the hybrid format IP = "#{IPv4}|#{IPv6_full}".gsub(/_/,'([0-9a-fA-F]{1,4})').gsub(/\(/,'(?:') def parse(value) @name,@exact,@length,@pattern = *case value when /^(?:#{IP})\/(\d+)$/ # 12.34.56.78/24, a001:b002::efff/120, c444:1000:2000::9:192.168.0.1/112 [:ip,:inexact,$1.to_i,IPAddr.new(value)] when /^(#{IP})$/ # 10.20.30.40, [:ip,:exact,nil,IPAddr.new(value)] when /^(#{Octet}\.){1,3}\*$/ # an ip address with a '*' at the end segments = value.split(".")[0..-2] bits = 8*segments.length [:ip,:inexact,bits,IPAddr.new((segments+[0,0,0])[0,4].join(".") + "/#{bits}")] when /^(\w[-\w]*\.)+[-\w]+$/ # a full hostname # Change to /^(\w[-\w]*\.)+[-\w]+\.?$/ for FQDN support [:domain,:exact,nil,munge_name(value)] when /^\*(\.(\w[-\w]*)){1,}$/ # *.domain.com host_sans_star = munge_name(value)[0..-2] [:domain,:inexact,host_sans_star.length,host_sans_star] when /\$\d+/ # a backreference pattern ala $1.reductivelabs.com or 192.168.0.$1 or $1.$2 [:dynamic,:exact,nil,munge_name(value)] when /^\w[-.@\w]*$/ # ? Just like a host name but allow '@'s and ending '.'s [:opaque,:exact,nil,[value]] when /^\/.*\/$/ # a regular expression [:regex,:inexact,nil,value] else raise AuthStoreError, "Invalid pattern #{value}" end end end end end diff --git a/spec/integration/network/rest_authconfig_spec.rb b/spec/integration/network/rest_authconfig_spec.rb index fb21abddd..129a9550c 100644 --- a/spec/integration/network/rest_authconfig_spec.rb +++ b/spec/integration/network/rest_authconfig_spec.rb @@ -1,145 +1,164 @@ require 'spec_helper' require 'puppet/network/rest_authconfig' RSpec::Matchers.define :allow do |params| match do |auth| begin auth.check_authorization(params[0], params[1], params[2], params[3]) true rescue Puppet::Network::AuthorizationError false end end failure_message_for_should do |instance| "expected #{params[3][:node]}/#{params[3][:ip]} to be allowed" end failure_message_for_should_not do |instance| "expected #{params[3][:node]}/#{params[3][:ip]} to be forbidden" end end describe Puppet::Network::RestAuthConfig do include PuppetSpec::Files before(:each) do Puppet[:rest_authconfig] = tmpfile('auth.conf') end def add_rule(rule) File.open(Puppet[:rest_authconfig],"w+") do |f| f.print "path /test\n#{rule}\n" end @auth = Puppet::Network::RestAuthConfig.new(Puppet[:rest_authconfig], true) end def add_regex_rule(regex, rule) File.open(Puppet[:rest_authconfig],"w+") do |f| f.print "path ~ #{regex}\n#{rule}\n" end @auth = Puppet::Network::RestAuthConfig.new(Puppet[:rest_authconfig], true) end def request(args = {}) - { :ip => '10.1.1.1', :node => 'host.domain.com', :key => 'key', :authenticated => true }.each do |k,v| - args[k] ||= v - end + args = { + :key => 'key', + :node => 'host.domain.com', + :ip => '10.1.1.1', + :authenticated => true + }.merge(args) ['test', :find, args[:key], args] end + it "should warn when matching against IP addresses" do + add_rule("allow 10.1.1.1") + + @auth.should allow(request) + + @logs.should be_any {|log| log.level == :warning and log.message =~ /Authentication based on IP address is deprecated/} + end + + it "should not warn when matches against IP addresses fail" do + add_rule("allow 10.1.1.2") + + @auth.should_not allow(request) + + @logs.should_not be_any {|log| log.level == :warning and log.message =~ /Authentication based on IP address is deprecated/} + end + it "should support IPv4 address" do add_rule("allow 10.1.1.1") @auth.should allow(request) end it "should support CIDR IPv4 address" do add_rule("allow 10.0.0.0/8") @auth.should allow(request) end it "should support wildcard IPv4 address" do add_rule("allow 10.1.1.*") @auth.should allow(request) end it "should support IPv6 address" do add_rule("allow 2001:DB8::8:800:200C:417A") @auth.should allow(request(:ip => '2001:DB8::8:800:200C:417A')) end it "should support hostname" do add_rule("allow host.domain.com") @auth.should allow(request) end it "should support wildcard host" do add_rule("allow *.domain.com") @auth.should allow(request) end it "should support hostname backreferences" do add_regex_rule('^/test/([^/]+)$', "allow $1.domain.com") @auth.should allow(request(:key => 'host')) end it "should support opaque strings" do add_rule("allow this-is-opaque@or-not") @auth.should allow(request(:node => 'this-is-opaque@or-not')) end it "should support opaque strings and backreferences" do add_regex_rule('^/test/([^/]+)$', "allow $1") @auth.should allow(request(:key => 'this-is-opaque@or-not', :node => 'this-is-opaque@or-not')) end it "should support hostname ending with '.'" do pending('bug #7589') add_rule("allow host.domain.com.") @auth.should allow(request(:node => 'host.domain.com.')) end it "should support hostname ending with '.' and backreferences" do pending('bug #7589') add_regex_rule('^/test/([^/]+)$',"allow $1") @auth.should allow(request(:node => 'host.domain.com.')) end it "should support trailing whitespace" do add_rule('allow host.domain.com ') @auth.should allow(request) end it "should support inlined comments" do add_rule('allow host.domain.com # will it work?') @auth.should allow(request) end it "should deny non-matching host" do add_rule("allow inexistant") @auth.should_not allow(request) end it "should deny denied hosts" do add_rule("deny host.domain.com") @auth.should_not allow(request) end end