diff --git a/acceptance/tests/environment/directory_environment_production_created_master.rb b/acceptance/tests/environment/directory_environment_production_created_master.rb index 0ad234ca4..386961ace 100644 --- a/acceptance/tests/environment/directory_environment_production_created_master.rb +++ b/acceptance/tests/environment/directory_environment_production_created_master.rb @@ -1,44 +1,44 @@ test_name 'ensure production environment created by master if missing' testdir = create_tmpdir_for_user master, 'prod-env-created' step 'make environmentpath' master_user = on(master, puppet("master --configprint user")).stdout.strip cert_path = on(master, puppet('config', 'print', 'hostcert')).stdout.strip key_path = on(master, puppet('config', 'print', 'hostprivkey')).stdout.strip cacert_path = on(master, puppet('config', 'print', 'localcacert')).stdout.strip apply_manifest_on(master, <<-MANIFEST, :catch_failures => true) File { ensure => directory, owner => #{master_user}, group => #{master['group']}, mode => '0640', } file { "#{testdir}":; "#{testdir}/environments":; } MANIFEST master_opts = { 'main' => { 'environmentpath' => "#{testdir}/environments", } } step 'run master; ensure production environment created' with_puppet_running_on(master, master_opts, testdir) do if master.is_using_passenger? - on(master, "curl -k --cert #{cert_path} --key #{key_path} --cacert #{cacert_path} https://localhost:8140/puppet/v2.0/environments") + on(master, "curl -k --cert #{cert_path} --key #{key_path} --cacert #{cacert_path} https://localhost:8140/puppet/v3/environments") end on(master, "test -d '#{testdir}/environments/production'") step 'ensure catalog returned from production env with no changes' agents.each do |agent| on(agent, puppet("agent -t --server #{master} --environment production --detailed-exitcodes")) do # detailed-exitcodes produces a 0 when no changes are made. assert_equal(0, exit_code) end end end diff --git a/conf/auth.conf b/conf/auth.conf index 2e9e916f5..059e87048 100644 --- a/conf/auth.conf +++ b/conf/auth.conf @@ -1,128 +1,124 @@ # This is the default auth.conf file, which implements the default rules # used by the puppet master. (That is, the rules below will still apply # even if this file is deleted.) # # The ACLs are evaluated in top-down order. More specific stanzas should # be towards the top of the file and more general ones at the bottom; # otherwise, the general rules may "steal" requests that should be # governed by the specific rules. # # See http://docs.puppetlabs.com/guides/rest_auth_conf.html for a more complete # description of auth.conf's behavior. # # Supported syntax: # Each stanza in auth.conf starts with a path to match, followed # by optional modifiers, and finally, a series of allow or deny # directives. # # Example Stanza # --------------------------------- # path /path/to/resource # simple prefix match # # path ~ regex # alternately, regex match # [environment envlist] # [method methodlist] # [auth[enthicated] {yes|no|on|off|any}] # allow [host|backreference|*|regex] # deny [host|backreference|*|regex] # allow_ip [ip|cidr|ip_wildcard|*] # deny_ip [ip|cidr|ip_wildcard|*] # # The path match can either be a simple prefix match or a regular # expression. `path /file` would match both `/file_metadata` and # `/file_content`. Regex matches allow the use of backreferences # in the allow/deny directives. # # The regex syntax is the same as for Ruby regex, and captures backreferences # for use in the `allow` and `deny` lines of that stanza # # Examples: # # path ~ ^/puppet/v3/path/to/resource # Equivalent to `path /puppet/v3/path/to/resource`. # allow * # Allow all authenticated nodes (since auth # # defaults to `yes`). # # path ~ ^/puppet/v3/catalog/([^/]+)$ # Permit nodes to access their own catalog (by # allow $1 # certname), but not any other node's catalog. # # path ~ ^/puppet/v3/file_(metadata|content)/extra_files/ # Only allow certain nodes to # auth yes # access the "extra_files" # allow /^(.+)\.example\.com$/ # mount point; note this must # allow_ip 192.168.100.0/24 # go ABOVE the "/file" rule, # # since it is more specific. # # environment:: restrict an ACL to a comma-separated list of environments # method:: restrict an ACL to a comma-separated list of HTTP methods # auth:: restrict an ACL to an authenticated or unauthenticated request # the default when unspecified is to restrict the ACL to authenticated requests # (ie exactly as if auth yes was present). # ### Authenticated ACLs - these rules apply only when the client ### has a valid certificate and is thus authenticated -path /puppet/v2.0/environments -method find -allow * - path /puppet/v3/environments method find allow * # allow nodes to retrieve their own catalog path ~ ^/puppet/v3/catalog/([^/]+)$ method find allow $1 # allow nodes to retrieve their own node definition path ~ ^/puppet/v3/node/([^/]+)$ method find allow $1 # allow all nodes to store their own reports path ~ ^/puppet/v3/report/([^/]+)$ method save allow $1 # Allow all nodes to access all file services; this is necessary for # pluginsync, file serving from modules, and file serving from custom # mount points (see fileserver.conf). Note that the `/file` prefix matches # requests to both the file_metadata and file_content paths. See "Examples" # above if you need more granular access control for custom mount points. path /puppet/v3/file allow * path /puppet/v3/status method find allow * # allow all nodes to access the certificates services path /puppet-ca/v1/certificate_revocation_list/ca method find allow * ### Unauthenticated ACLs, for clients without valid certificates; authenticated ### clients can also access these paths, though they rarely need to. # allow access to the CA certificate; unauthenticated nodes need this # in order to validate the puppet master's certificate path /puppet-ca/v1/certificate/ca auth any method find allow * # allow nodes to retrieve the certificate they requested earlier path /puppet-ca/v1/certificate/ auth any method find allow * # allow nodes to request a new certificate path /puppet-ca/v1/certificate_request auth any method find, save allow * # deny everything else; this ACL is not strictly necessary, but # illustrates the default policy. path / auth any diff --git a/lib/puppet/network/authconfig.rb b/lib/puppet/network/authconfig.rb index d54ba498a..9f30a44ad 100644 --- a/lib/puppet/network/authconfig.rb +++ b/lib/puppet/network/authconfig.rb @@ -1,95 +1,92 @@ require 'puppet/network/rights' require 'puppet/network/http' module Puppet class ConfigurationError < Puppet::Error; end class Network::AuthConfig attr_accessor :rights def self.master_url_prefix Puppet::Network::HTTP::MASTER_URL_PREFIX end def self.ca_url_prefix Puppet::Network::HTTP::CA_URL_PREFIX end def self.default_acl [ - # Master API V2.0 - { :acl => "#{master_url_prefix}/v2.0/environments", :method => :find, :allow => '*', :authenticated => true }, - # Master API V3 { :acl => "#{master_url_prefix}/v3/environments", :method => :find, :allow => '*', :authenticated => true }, { :acl => "~ ^#{master_url_prefix}\/v3\/catalog\/([^\/]+)$", :method => :find, :allow => '$1', :authenticated => true }, { :acl => "~ ^#{master_url_prefix}\/v3\/node\/([^\/]+)$", :method => :find, :allow => '$1', :authenticated => true }, { :acl => "~ ^#{master_url_prefix}\/v3\/report\/([^\/]+)$", :method => :save, :allow => '$1', :authenticated => true }, # this one will allow all file access, and thus delegate # to fileserver.conf { :acl => "#{master_url_prefix}/v3/file" }, { :acl => "#{master_url_prefix}/v3/status", :method => [:find], :authenticated => true }, # CA API V1 { :acl => "#{ca_url_prefix}/v1/certificate_revocation_list/ca", :method => :find, :authenticated => true }, # These allow `auth any`, because if you can do them anonymously you # should probably also be able to do them when trusted. { :acl => "#{ca_url_prefix}/v1/certificate/ca", :method => :find, :authenticated => :any }, { :acl => "#{ca_url_prefix}/v1/certificate/", :method => :find, :authenticated => :any }, { :acl => "#{ca_url_prefix}/v1/certificate_request", :method => [:find, :save], :authenticated => :any }, ] end # Just proxy the setting methods to our rights stuff [:allow, :deny].each do |method| define_method(method) do |*args| @rights.send(method, *args) end end # force regular ACLs to be present def insert_default_acl self.class.default_acl.each do |acl| unless rights[acl[:acl]] Puppet.info "Inserting default '#{acl[:acl]}' (auth #{acl[:authenticated]}) ACL" mk_acl(acl) end end # queue an empty (ie deny all) right for every other path # actually this is not strictly necessary as the rights system # denies not explicitly allowed paths unless rights["/"] rights.newright("/").restrict_authenticated(:any) end end def mk_acl(acl) right = @rights.newright(acl[:acl]) right.allow(acl[:allow] || "*") if method = acl[:method] method = [method] unless method.is_a?(Array) method.each { |m| right.restrict_method(m) } end right.restrict_authenticated(acl[:authenticated]) unless acl[:authenticated].nil? end # check whether this request is allowed in our ACL # raise an Puppet::Network::AuthorizedError if the request # is denied. def check_authorization(method, path, params) if authorization_failure_exception = @rights.is_request_forbidden_and_why?(method, path, params) Puppet.warning("Denying access: #{authorization_failure_exception}") raise authorization_failure_exception end end def initialize(rights=nil) @rights = rights || Puppet::Network::Rights.new insert_default_acl end end end diff --git a/lib/puppet/network/http.rb b/lib/puppet/network/http.rb index 0c700d630..87eb67e8f 100644 --- a/lib/puppet/network/http.rb +++ b/lib/puppet/network/http.rb @@ -1,28 +1,27 @@ module Puppet::Network::HTTP HEADER_ENABLE_PROFILING = "X-Puppet-Profiling" HEADER_PUPPET_VERSION = "X-Puppet-Version" MASTER_URL_PREFIX = "/puppet" CA_URL_PREFIX = "/puppet-ca" require 'puppet/network/authorization' require 'puppet/network/http/issues' require 'puppet/network/http/error' require 'puppet/network/http/route' require 'puppet/network/http/api' require 'puppet/network/http/api/ca' require 'puppet/network/http/api/ca/v1' require 'puppet/network/http/api/master' - require 'puppet/network/http/api/master/v2' require 'puppet/network/http/api/master/v3' require 'puppet/network/http/handler' require 'puppet/network/http/response' require 'puppet/network/http/request' require 'puppet/network/http/site' require 'puppet/network/http/session' require 'puppet/network/http/factory' require 'puppet/network/http/nocache_pool' require 'puppet/network/http/pool' require 'puppet/network/http/memory_response' require 'puppet/network/http/compression' end diff --git a/lib/puppet/network/http/api.rb b/lib/puppet/network/http/api.rb index ff74963e6..73bc1263b 100644 --- a/lib/puppet/network/http/api.rb +++ b/lib/puppet/network/http/api.rb @@ -1,26 +1,25 @@ class Puppet::Network::HTTP::API def self.not_found Puppet::Network::HTTP::Route. path(/.*/). any(lambda do |req, res| raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new("No route for #{req.method} #{req.path}", Puppet::Network::HTTP::Issues::HANDLER_NOT_FOUND) end) end def self.master_routes master_prefix = Regexp.new("^#{Puppet::Network::HTTP::MASTER_URL_PREFIX}/") Puppet::Network::HTTP::Route.path(master_prefix). any. chain(Puppet::Network::HTTP::API::Master::V3.routes, - Puppet::Network::HTTP::API::Master::V2.routes, Puppet::Network::HTTP::API.not_found) end def self.ca_routes ca_prefix = Regexp.new("^#{Puppet::Network::HTTP::CA_URL_PREFIX}/") Puppet::Network::HTTP::Route.path(ca_prefix). any. chain(Puppet::Network::HTTP::API::CA::V1.routes, Puppet::Network::HTTP::API.not_found) end end diff --git a/lib/puppet/network/http/api/master/v2.rb b/lib/puppet/network/http/api/master/v2.rb deleted file mode 100644 index c0f86285f..000000000 --- a/lib/puppet/network/http/api/master/v2.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Puppet::Network::HTTP::API::Master::V2 - require 'puppet/network/http/api/master/v2/environments' - require 'puppet/network/http/api/master/v2/authorization' - - def self.routes - path(%r{^v2\.0}). - get(Authorization.new). - chain(ENVIRONMENTS, Puppet::Network::HTTP::API.not_found) - end - - private - - def self.path(path) - Puppet::Network::HTTP::Route.path(path) - end - - def self.provide(&block) - lambda do |request, response| - block.call.call(request, response) - end - end - - ENVIRONMENTS = path(%r{^/environments$}).get(provide do - Environments.new(Puppet.lookup(:environments)) - end) -end diff --git a/lib/puppet/network/http/api/master/v2/authorization.rb b/lib/puppet/network/http/api/master/v2/authorization.rb deleted file mode 100644 index 4c9c41159..000000000 --- a/lib/puppet/network/http/api/master/v2/authorization.rb +++ /dev/null @@ -1,15 +0,0 @@ -class Puppet::Network::HTTP::API::Master::V2::Authorization - include Puppet::Network::Authorization - - def call(request, response) - if request.method != "GET" - raise Puppet::Network::HTTP::Error::HTTPNotAuthorizedError.new("Only GET requests are authorized for V2 endpoints", Puppet::Network::HTTP::Issues::UNSUPPORTED_METHOD) - end - - begin - check_authorization(:find, request.path, request.params) - rescue Puppet::Network::AuthorizationError => e - raise Puppet::Network::HTTP::Error::HTTPNotAuthorizedError.new(e.message, Puppet::Network::HTTP::Issues::FAILED_AUTHORIZATION) - end - end -end diff --git a/lib/puppet/network/http/api/master/v2/environments.rb b/lib/puppet/network/http/api/master/v2/environments.rb deleted file mode 100644 index 92181ef4f..000000000 --- a/lib/puppet/network/http/api/master/v2/environments.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'json' - -class Puppet::Network::HTTP::API::Master::V2::Environments - def initialize(env_loader) - @env_loader = env_loader - end - - def call(request, response) - response.respond_with(200, "application/json", JSON.dump({ - "search_paths" => @env_loader.search_paths, - "environments" => Hash[@env_loader.list.collect do |env| - [env.name, { - "settings" => { - "modulepath" => env.full_modulepath, - "manifest" => env.manifest, - "environment_timeout" => timeout(env), - "config_version" => env.config_version || '', - } - }] - end] - })) - end - - private - - def timeout(env) - ttl = @env_loader.get_conf(env.name).environment_timeout - if ttl == Float::INFINITY - "unlimited" - else - ttl - end - end - -end diff --git a/spec/unit/network/http/api/master/v2/authorization_spec.rb b/spec/unit/network/http/api/master/v2/authorization_spec.rb deleted file mode 100644 index f4bff9c4e..000000000 --- a/spec/unit/network/http/api/master/v2/authorization_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -require 'spec_helper' - -require 'puppet/network/http' - -describe Puppet::Network::HTTP::API::Master::V2::Authorization do - HTTP = Puppet::Network::HTTP - - let(:response) { HTTP::MemoryResponse.new } - let(:authz) { HTTP::API::Master::V2::Authorization.new } - - it "only authorizes GET requests" do - request = HTTP::Request.from_hash({ - :method => "POST" - }) - - expect do - authz.call(request, response) - end.to raise_error(HTTP::Error::HTTPNotAuthorizedError) - end - - it "accepts v2 api requests that match allowed authconfig entries" do - request = HTTP::Request.from_hash({ - :path => "/v2.0/environments", - :method => "GET", - :params => { :authenticated => true, :node => "testing", :ip => "127.0.0.1" } - }) - - authz.stubs(:authconfig).returns(Puppet::Network::AuthConfigParser.new(<<-AUTH).parse) -path /v2.0/environments -method find -allow * - AUTH - - expect do - authz.call(request, response) - end.to_not raise_error - end - - it "rejects v2 api requests that are disallowed by authconfig entries" do - request = HTTP::Request.from_hash({ - :path => "/v2.0/environments", - :method => "GET", - :params => { :node => "testing", :ip => "127.0.0.1" } - }) - - authz.stubs(:authconfig).returns(Puppet::Network::AuthConfigParser.new(<<-AUTH).parse) -path /v2.0/environments -method find -auth any -deny testing - AUTH - - expect do - authz.call(request, response) - end.to raise_error(HTTP::Error::HTTPNotAuthorizedError, /Forbidden request/) - end -end diff --git a/spec/unit/network/http/api/master/v2/environments_spec.rb b/spec/unit/network/http/api/master/v2/environments_spec.rb deleted file mode 100644 index 3283b801f..000000000 --- a/spec/unit/network/http/api/master/v2/environments_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -require 'spec_helper' - -require 'puppet/node/environment' -require 'puppet/network/http' -require 'matchers/json' - -describe Puppet::Network::HTTP::API::Master::V2::Environments do - include JSONMatchers - - it "responds with all of the available environments" do - environment = Puppet::Node::Environment.create(:production, ["/first", "/second"], '/manifests') - loader = Puppet::Environments::Static.new(environment) - handler = Puppet::Network::HTTP::API::Master::V2::Environments.new(loader) - response = Puppet::Network::HTTP::MemoryResponse.new - - handler.call(Puppet::Network::HTTP::Request.from_hash(:headers => { 'accept' => 'application/json' }), response) - - expect(response.code).to eq(200) - expect(response.type).to eq("application/json") - expect(JSON.parse(response.body)).to eq({ - "search_paths" => loader.search_paths, - "environments" => { - "production" => { - "settings" => { - "modulepath" => [File.expand_path("/first"), File.expand_path("/second")], - "manifest" => File.expand_path("/manifests"), - "environment_timeout" => 0, - "config_version" => "" - } - } - } - }) - end - - it "the response conforms to the environments schema for unlimited timeout" do - conf_stub = stub 'conf_stub' - conf_stub.expects(:environment_timeout).returns(Float::INFINITY) - environment = Puppet::Node::Environment.create(:production, []) - env_loader = Puppet::Environments::Static.new(environment) - env_loader.expects(:get_conf).with(:production).returns(conf_stub) - handler = Puppet::Network::HTTP::API::Master::V2::Environments.new(env_loader) - response = Puppet::Network::HTTP::MemoryResponse.new - - handler.call(Puppet::Network::HTTP::Request.from_hash(:headers => { 'accept' => 'application/json' }), response) - - expect(response.body).to validate_against('api/schemas/environments.json') - end - - it "the response conforms to the environments schema for integer timeout" do - conf_stub = stub 'conf_stub' - conf_stub.expects(:environment_timeout).returns(1) - environment = Puppet::Node::Environment.create(:production, []) - env_loader = Puppet::Environments::Static.new(environment) - env_loader.expects(:get_conf).with(:production).returns(conf_stub) - handler = Puppet::Network::HTTP::API::Master::V2::Environments.new(env_loader) - response = Puppet::Network::HTTP::MemoryResponse.new - - handler.call(Puppet::Network::HTTP::Request.from_hash(:headers => { 'accept' => 'application/json' }), response) - - expect(response.body).to validate_against('api/schemas/environments.json') - end - -end diff --git a/spec/unit/network/http/api/master/v2_spec.rb b/spec/unit/network/http/api/master/v2_spec.rb deleted file mode 100644 index 219276163..000000000 --- a/spec/unit/network/http/api/master/v2_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'spec_helper' - -require 'puppet/network/http' - -describe Puppet::Network::HTTP::API::Master::V2 do - let(:response) { Puppet::Network::HTTP::MemoryResponse.new } - let(:routes) { Puppet::Network::HTTP::Route.path(Regexp.new("/puppet/")). - any. - chain(Puppet::Network::HTTP::API::Master::V2.routes) } - - it "mounts the environments endpoint" do - request = Puppet::Network::HTTP::Request.from_hash(:path => "/puppet/v2.0/environments") - routes.process(request, response) - - expect(response.code).to eq(200) - end - - it "responds to unknown paths with a 404" do - request = Puppet::Network::HTTP::Request.from_hash(:path => "/puppet/v2.0/unknown") - - expect do - routes.process(request, response) - end.to raise_error(Puppet::Network::HTTP::Error::HTTPNotFoundError) - end -end diff --git a/spec/unit/network/http/api_spec.rb b/spec/unit/network/http/api_spec.rb index d9e6e458c..a4cbeb1fc 100644 --- a/spec/unit/network/http/api_spec.rb +++ b/spec/unit/network/http/api_spec.rb @@ -1,105 +1,98 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet_spec/handler' require 'puppet/network/http' describe Puppet::Network::HTTP::API do def respond(text) lambda { |req, res| res.respond_with(200, "text/plain", text) } end describe "#not_found" do let(:response) { Puppet::Network::HTTP::MemoryResponse.new } let(:routes) { Puppet::Network::HTTP::Route.path(Regexp.new("foo")). any. chain(Puppet::Network::HTTP::Route.path(%r{^/bar$}).get(respond("bar")), Puppet::Network::HTTP::API.not_found) } it "mounts the chained routes" do request = Puppet::Network::HTTP::Request.from_hash(:path => "foo/bar") routes.process(request, response) expect(response.code).to eq(200) expect(response.body).to eq("bar") end it "responds to unknown paths with a 404" do request = Puppet::Network::HTTP::Request.from_hash(:path => "foo/unknown") expect do routes.process(request, response) end.to raise_error(Puppet::Network::HTTP::Error::HTTPNotFoundError) end end describe "Puppet API" do let(:handler) { PuppetSpec::Handler.new(Puppet::Network::HTTP::API.master_routes, Puppet::Network::HTTP::API.ca_routes) } let(:master_prefix) { Puppet::Network::HTTP::MASTER_URL_PREFIX } let(:ca_prefix) { Puppet::Network::HTTP::CA_URL_PREFIX } it "raises a not-found error for non-CA or master routes" do req = Puppet::Network::HTTP::Request.from_hash(:path => "/unknown") res = {} handler.process(req, res) expect(res[:status]).to eq(404) end describe "when processing master routes" do it "responds to v3 indirector requests" do req = Puppet::Network::HTTP::Request.from_hash(:path => "#{master_prefix}/v3/node/foo", :params => {:environment => "production"}, :headers => {'accept' => "pson"}) res = {} handler.process(req, res) expect(res[:status]).to eq(200) end it "responds to v3 environments requests" do req = Puppet::Network::HTTP::Request.from_hash(:path => "#{master_prefix}/v3/environments") res = {} handler.process(req, res) expect(res[:status]).to eq(200) end - it "responds to v2.0 environments requests" do - req = Puppet::Network::HTTP::Request.from_hash(:path => "#{master_prefix}/v2.0/environments") - res = {} - handler.process(req, res) - expect(res[:status]).to eq(200) - end - - it "responds with a not found error to non-v3/2.0 requests" do + it "responds with a not found error to non-v3 requests" do req = Puppet::Network::HTTP::Request.from_hash(:path => "#{master_prefix}/unknown") res = {} handler.process(req, res) expect(res[:status]).to eq(404) end end describe "when processing CA routes" do it "responds to v1 indirector requests" do Puppet::SSL::Certificate.indirection.stubs(:find).returns "foo" req = Puppet::Network::HTTP::Request.from_hash(:path => "#{ca_prefix}/v1/certificate/foo", :params => {:environment => "production"}, :headers => {'accept' => "s"}) res = {} handler.process(req, res) expect(res[:body]).to eq("foo") expect(res[:status]).to eq(200) end it "responds with a not found error to non-v1 requests" do req = Puppet::Network::HTTP::Request.from_hash(:path => "#{ca_prefix}/unknown") res = {} handler.process(req, res) expect(res[:status]).to eq(404) end end end end