diff options
-rwxr-xr-x | lib/puppet/server/authstore.rb | 224 | ||||
-rwxr-xr-x | test/server/tc_authstore.rb | 185 |
2 files changed, 409 insertions, 0 deletions
diff --git a/lib/puppet/server/authstore.rb b/lib/puppet/server/authstore.rb new file mode 100755 index 000000000..966bc8372 --- /dev/null +++ b/lib/puppet/server/authstore.rb @@ -0,0 +1,224 @@ +#!/usr/bin/ruby -w + +#-------------------- +# standard module for determining whether a given hostname or IP has access to +# the requested resource +# +# $Id$ + +require 'ipaddr' + +module Puppet +class Server + class AuthStoreError < Puppet::Error; end + class AuthorizationError < Puppet::Error; end + + class AuthStore + ORDER = { + :ip => [:ip], + :name => [:hostname, :domain] + } + + def allow(pattern) + # a simple way to allow anyone at all to connect + if pattern == "*" + @globalallow = true + else + store(pattern, @allow) + end + end + + def allowed?(name, ip) + if name or ip + unless name and ip + raise Puppet::DevError, "Name and IP must be passed to 'allowed?'" + end + # else, we're networked and such + else + # we're local + return true + end + + # yay insecure overrides + if @globalallow + return true + end + + value = nil + ORDER.each { |nametype, array| + if nametype == :ip + value = IPAddr.new(ip) + else + value = name.split(".").reverse + end + + array.each { |type| + [[@deny, false], [@allow, true]].each { |ary| + hash, retval = ary + if hash.include?(type) + hash[type].each { |pattern| + if match?(nametype, value, pattern) + return retval + end + } + end + } + } + } + + # default to false + return false + end + + def deny(pattern) + store(pattern, @deny) + end + + def initialize + @globalallow = nil + @allow = Hash.new { |hash, key| + hash[key] = [] + } + @deny = Hash.new { |hash, key| + hash[key] = [] + } + end + + private + + def match?(nametype, value, pattern) + if value == pattern # simplest shortcut + return true + end + + case nametype + when :ip: matchip?(value, pattern) + when :name: matchname?(value, pattern) + else + raise Puppet::DevError, "Invalid match type %s" % nametype + end + end + + def matchip?(value, pattern) + # we're just using builtin stuff for this, thankfully + if pattern.include?(value) + return true + else + return false + end + end + + def matchname?(value, pattern) + # yay, horribly inefficient + if pattern[-1] != '*' # the pattern has no metachars and is not equal + # thus, no match + #Puppet.info "%s is not equal with no * in %s" % [value, pattern] + return false + else + # we know the last field of the pattern is '*' + # if everything up to that doesn't match, we're definitely false + if pattern[0..-2] != value[0..pattern.length-2] + #Puppet.notice "subpatterns didn't match; %s vs %s" % + # [pattern[0..-2], value[0..pattern.length-2]] + return false + end + + case value.length <=> pattern.length + when -1: # value is shorter than pattern + if pattern.length - value.length == 1 + # only ever allowed when the value is the domain of a + # splatted pattern + #Puppet.info "allowing splatted domain %s" % [value] + return true + else + return false + end + when 0: # value is the same length as pattern + if pattern[-1] == "*" + #Puppet.notice "same length with *" + return true + else + return false + end + when 1: # value is longer than pattern + # at this point we've already verified that everything up to + # the '*' in the pattern matches, so we are true + return true + end + end + end + + def store(pattern, hash) + type, value = type(pattern) + + if type and value + # this won't work once we get beyond simple stuff... + hash[type] << value + else + raise AuthStoreError, "Invalid pattern %s" % pattern + end + end + + def type(pattern) + type = value = nil + case pattern + when /^(\d+\.){3}\d+$/: + type = :ip + begin + value = IPAddr.new(pattern) + rescue ArgumentError => detail + raise AuthStoreError, "Invalid IP address pattern %s" % pattern + end + when /^(\d+\.){3}\d+\/(\d+)$/: + mask = Integer($2) + if mask < 1 or mask > 32 + raise AuthStoreError, "Invalid IP mask %s" % mask + end + type = :ip + begin + value = IPAddr.new(pattern) + rescue ArgumentError => detail + raise AuthStoreError, "Invalid IP address pattern %s" % pattern + end + when /^(\d+\.){1,3}\*$/: # an ip address with a '*' at the end + type = :ip + match = $1 + match.sub!(".", '') + ary = pattern.split(".") + + mask = case ary.index(match) + when 0: 8 + when 1: 16 + when 2: 24 + else + raise AuthStoreError, "Invalid IP pattern %s" % pattern + end + + ary.pop + while ary.length < 4 + ary.push("0") + end + + begin + value = IPAddr.new(ary.join(".") + "/" + mask.to_s) + rescue ArgumentError => detail + raise AuthStoreError, "Invalid IP address pattern %s" % pattern + end + when /^[\d.]+$/: # necessary so incomplete IP addresses can't look + # like hostnames + raise AuthStoreError, "Invalid IP address pattern %s" % pattern + when /^([a-zA-Z][-\w]*\.)+[-\w]+$/: # a full hostname + type = :hostname + value = pattern.split(".").reverse + when /^\*\.([a-zA-Z][-\w]*\.)+[-\w]+$/: # this doesn't match TLDs + type = :domain + value = pattern.split(".").reverse + else + raise AuthStoreError, "Invalid pattern %s" % pattern + end + + return [type, value] + end + end +end +end diff --git a/test/server/tc_authstore.rb b/test/server/tc_authstore.rb new file mode 100755 index 000000000..89d3c72e1 --- /dev/null +++ b/test/server/tc_authstore.rb @@ -0,0 +1,185 @@ +if __FILE__ == $0 + if Dir.getwd =~ /test\/server$/ + Dir.chdir("..") + end + + $:.unshift '../lib' + $puppetbase = ".." + +end + +require 'puppet' +require 'puppet/server/authstore' +require 'test/unit' +require 'puppettest.rb' + +class TestAuthStore < TestPuppet + def setup + if __FILE__ == $0 + Puppet[:loglevel] = :debug + end + + super + end + + def mkstore + store = nil + assert_nothing_raised { + store = Puppet::Server::AuthStore.new + } + + return store + end + + def test_localallow + store = mkstore + + assert_nothing_raised { + assert(store.allowed?(nil, nil), "Store disallowed local access") + } + + assert_raise(Puppet::DevError) { + store.allowed?("kirby.madstop.com", nil) + } + + assert_raise(Puppet::DevError) { + store.allowed?(nil, "192.168.0.1") + } + end + + def test_hostnames + store = mkstore + + %w{ + kirby.madstop.com + luke.madstop.net + name-other.madstop.net + }.each { |name| + assert_nothing_raised("Failed to store simple name %s" % name) { + store.allow(name) + } + assert(store.allowed?(name, "192.168.0.1"), "Name %s not allowed" % name) + } + + %w{ + invalid + ^invalid! + inval$id + + }.each { |pat| + assert_raise(Puppet::Server::AuthStoreError, + "name '%s' was allowed" % pat) { + store.allow(pat) + } + } + end + + def test_domains + store = mkstore + + assert_nothing_raised("Failed to store domains") { + store.allow("*.a.very.long.domain.name.com") + store.allow("*.madstop.com") + store.allow("*.some-other.net") + store.allow("*.much.longer.more-other.net") + } + + %w{ + madstop.com + culain.madstop.com + kirby.madstop.com + funtest.some-other.net + ya-test.madstop.com + some.much.much.longer.more-other.net + }.each { |name| + assert(store.allowed?(name, "192.168.0.1"), "Host %s not allowed" % name) + } + + assert_raise(Puppet::Server::AuthStoreError) { + store.allow("domain.*.com") + } + + assert(!store.allowed?("very.long.domain.name.com", "1.2.3.4"), + "Long hostname allowed") + + assert_raise(Puppet::Server::AuthStoreError) { + store.allow("domain.*.other.com") + } + end + + def test_simpleips + store = mkstore + + %w{ + 192.168.0.5 + 7.0.48.7 + }.each { |ip| + assert_nothing_raised("Failed to store IP address %s" % ip) { + store.allow(ip) + } + + assert(store.allowed?("hosttest.com", ip), "IP %s not allowed" % ip) + } + + assert_raise(Puppet::Server::AuthStoreError) { + store.allow("192.168.674.0") + } + + assert_raise(Puppet::Server::AuthStoreError) { + store.allow("192.168.0") + } + end + + def test_ipranges + store = mkstore + + %w{ + 192.168.0.* + 192.168.1.0/24 + 192.178.* + 193.179.0.0/8 + }.each { |range| + assert_nothing_raised("Failed to store IP range %s" % range) { + store.allow(range) + } + } + + %w{ + 192.168.0.1 + 192.168.1.5 + 192.178.0.5 + 193.0.0.1 + }.each { |ip| + assert(store.allowed?("fakename.com", ip), "IP %s is not allowed" % ip) + } + end + + def test_ziprangedenials + store = mkstore + + assert_nothing_raised("Failed to store overlapping IP ranges") { + store.allow("192.168.0.0/16") + store.deny("192.168.0.0/24") + } + + assert(store.allowed?("fake.name", "192.168.1.50"), "/16 ip not allowed") + assert(! store.allowed?("fake.name", "192.168.0.50"), "/24 ip allowed") + end + + def test_zsubdomaindenails + store = mkstore + + assert_nothing_raised("Failed to store overlapping IP ranges") { + store.allow("*.madstop.com") + store.deny("*.sub.madstop.com") + } + + assert(store.allowed?("hostname.madstop.com", "192.168.1.50"), + "hostname not allowed") + assert(! store.allowed?("name.sub.madstop.com", "192.168.0.50"), + "subname name allowed") + end +end + +# $Id$ + |