diff options
-rw-r--r-- | documentation/documentation/fsconfigref.page | 16 | ||||
-rwxr-xr-x | lib/puppet/server/fileserver.rb | 50 | ||||
-rwxr-xr-x | test/server/fileserver.rb | 115 |
3 files changed, 176 insertions, 5 deletions
diff --git a/documentation/documentation/fsconfigref.page b/documentation/documentation/fsconfigref.page index c7f962448..598676184 100644 --- a/documentation/documentation/fsconfigref.page +++ b/documentation/documentation/fsconfigref.page @@ -45,6 +45,22 @@ While the path is the only required option, the default security configuration is to deny all access, so if no ``allow`` lines are specified, the module will be configured but available to no one. +The path can contain the patterns ``%h`` and ``%H``, which are dynamically +replaced by the client's short name and its fully qualified domain name, +both taken from the client's SSL certificate. This is useful in creating +modules where files for each client are kept completely separately, +e.g. for private ssh host keys. For example, with the configuration + + [private] + path /data/private/%h + allow * + +the request for file ``/private/file.txt`` from client +``client1.example.com`` will look for a file +``/data/private/client1/file.txt``, while the same request from +``client2.example.com`` will try to retrieve the file +``/data/private/client1/file.txt`` on the fileserver. + # Security There are two aspects to securing the Puppet file server: Allowing specific diff --git a/lib/puppet/server/fileserver.rb b/lib/puppet/server/fileserver.rb index 7e0e49105..983e39271 100755 --- a/lib/puppet/server/fileserver.rb +++ b/lib/puppet/server/fileserver.rb @@ -1,6 +1,7 @@ require 'puppet' require 'webrick/httpstatus' require 'cgi' +require 'delegate' module Puppet class FileServerError < Puppet::Error; end @@ -40,7 +41,7 @@ class Server raise Puppet::FileServerError, "Cannot currently copy links" end - mount, path = splitpath(file) + mount, path = splitpath(file, client) authcheck(file, mount, client, clientip) @@ -133,7 +134,7 @@ class Server # List a specific directory's contents. def list(dir, links = :ignore, recurse = false, ignore = false, client = nil, clientip = nil) readconfig - mount, path = splitpath(dir) + mount, path = splitpath(dir, client) authcheck(dir, mount, client, clientip) @@ -297,7 +298,7 @@ class Server def retrieve(file, links = :ignore, client = nil, clientip = nil) readconfig links = links.intern if links.is_a? String - mount, path = splitpath(file) + mount, path = splitpath(file, client) authcheck(file, mount, client, clientip) @@ -390,7 +391,7 @@ class Server end # Split the path into the separate mount point and path. - def splitpath(dir) + def splitpath(dir, client) # the dir is based on one of the mounts # so first retrieve the mount path mount = nil @@ -410,6 +411,7 @@ class Server # And now replace the name with the actual object. mount = @mounts[mount] + mount = SubstMount.new(mount, client) unless client.nil? else raise FileServerError, "Fileserver error: Invalid path '%s'" % dir end @@ -529,7 +531,13 @@ class Server # Set the path. def path=(path) unless FileTest.exists?(path) - raise FileServerError, "%s does not exist" % path + map = { "h" => "\000", "H" => "\000" } + # FIXME: What should we do if there are replacement + # patterns in path ? Replace with '*' and glob ? + # But that could turn out to be _very_ expensive + unless Mount::subst(path, map).index("\000") + raise FileServerError, "%s does not exist" % path + end end @path = path end @@ -553,6 +561,38 @@ class Server raise FileServerError, "No path specified" end end + + # Replace occurences of %C in PATH with entries from MAP and + # return a new string. Literal percent signs can be included as + # '%%', and a replacement is only done when C is a key in MAP + def self.subst(path, map) + path.gsub(/%./) do |v| + if v == "%%" + "%" + elsif ! map.key?(v[1,1]) + v + else + map[v[1,1]] + end + end + end + + end + + # A mount that does substitutions in the path on the fly + class SubstMount < DelegateClass(Mount) + def initialize(mount, client) + @mount = mount + super(@mount) + @map = { + "h" => client.sub(/\..*$/, ""), + "H" => client + } + end + + def path + Mount::subst(@mount.path, @map) + end end end end diff --git a/test/server/fileserver.rb b/test/server/fileserver.rb index c3b9d3bb7..ef191b304 100755 --- a/test/server/fileserver.rb +++ b/test/server/fileserver.rb @@ -749,6 +749,121 @@ class TestFileServer < Test::Unit::TestCase assert(v != "", "%s has no value" % p) } end + + # Test that substitution patterns in the path are exapanded + # properly + def test_host_specific + client1 = "client1.example.com" + client2 = "client2.example.com" + ip = "127.0.0.1" + + # Setup a directory hierarchy for the tests + fsdir = File.join(tmpdir(), "host-specific") + @@tmpfiles << fsdir + hostdir = File.join(fsdir, "host") + fqdndir = File.join(fsdir, "fqdn") + client1_hostdir = File.join(hostdir, "client1") + client2_fqdndir = File.join(fqdndir, client2) + [fsdir, hostdir, fqdndir, + client1_hostdir, client2_fqdndir].each { |d| Dir.mkdir(d) } + + [client1_hostdir, client2_fqdndir].each do |d| + File.open(File.join(d, "file.txt"), "w") { |f| f.puts d } + end + + conffile = tempfile() + File.open(conffile, "w") do |f| + f.print(" +[host] +path #{hostdir}/%h +allow * +[fqdn] +path #{fqdndir}/%H +allow * +") + end + + server = nil + assert_nothing_raised { + server = Puppet::Server::FileServer.new( + :Local => true, + :Config => conffile + ) + } + + # check that list returns the correct thing for the two clients + list = nil + sfile = "/host/file.txt" + assert_nothing_raised { + list = server.list(sfile, :ignore, true, false, client1, ip) + } + assert_equal("/\tfile", list) + assert_nothing_raised { + list = server.list(sfile, :ignore, true, false, client2, ip) + } + assert_equal("", list) + + sfile = "/fqdn/file.txt" + assert_nothing_raised { + list = server.list(sfile, :ignore, true, false, client1, ip) + } + assert_equal("", list) + assert_nothing_raised { + list = server.list(sfile, :ignore, true, false, client2, ip) + } + assert_equal("/\tfile", list) + + # check describe + sfile = "/host/file.txt" + assert_nothing_raised { + list = server.describe(sfile, :ignore, client1, ip).split("\t") + } + assert_equal(5, list.size) + assert_equal("file", list[1]) + assert_equal("{md5}95b0dea1b0c692b7563120afb4056e7f", list[4]) + + assert_nothing_raised { + list = server.describe(sfile, :ignore, client2, ip).split("\t") + } + assert_equal([], list) + + sfile = "/fqdn/file.txt" + assert_nothing_raised { + list = server.describe(sfile, :ignore, client1, ip).split("\t") + } + assert_equal([], list) + + assert_nothing_raised { + list = server.describe(sfile, :ignore, client2, ip).split("\t") + } + assert_equal(5, list.size) + assert_equal("file", list[1]) + assert_equal("{md5}4dcf36004229f400c5821a3faf0f2300", list[4]) + + # Check retrieve + sfile = "/host/file.txt" + assert_nothing_raised { + list = server.retrieve(sfile, :ignore, client1, ip).chomp + } + assert_equal(client1_hostdir, list) + + assert_nothing_raised { + list = server.retrieve(sfile, :ignore, client2, ip).chomp + } + assert_equal("", list) + + sfile = "/fqdn/file.txt" + assert_nothing_raised { + list = server.retrieve(sfile, :ignore, client1, ip).chomp + } + assert_equal("", list) + + assert_nothing_raised { + list = server.retrieve(sfile, :ignore, client2, ip).chomp + } + assert_equal(client2_fqdndir, list) + end + end # $Id$ |