summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xlib/puppet/server/authstore.rb224
-rwxr-xr-xtest/server/tc_authstore.rb185
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$
+