chef-zero-4.5.0/0000755000004100000410000000000012654006514013435 5ustar www-datawww-datachef-zero-4.5.0/Rakefile0000644000004100000410000000242612654006514015106 0ustar www-datawww-datarequire 'bundler' require 'bundler/gem_tasks' require 'chef_zero/version' task :default => :pedant desc "run specs" task :spec do system('rspec spec/*_spec.rb') end desc "run oc pedant" task :pedant do require File.expand_path('spec/run_oc_pedant') end desc "run pedant with CHEF_FS set" task :cheffs do ENV['CHEF_FS'] = "yes" require File.expand_path('spec/run_oc_pedant') end desc "run pedant with FILE_STORE set" task :filestore do ENV['FILE_STORE'] = "yes" require File.expand_path('spec/run_oc_pedant') end desc "run oc pedant" task :oc_pedant do require File.expand_path('spec/run_oc_pedant') end task :chef_spec do gem_path = Bundler.environment.specs['chef'].first.full_gem_path system("cd #{gem_path} && rspec spec/integration") end task :berkshelf_spec do gem_path = Bundler.environment.specs['berkshelf'].first.full_gem_path system("cd #{gem_path} && thor spec:ci") end require 'github_changelog_generator/task' GitHubChangelogGenerator::RakeTask.new :changelog do |config| # config.future_release = ChefZero::VERSION config.enhancement_labels = "enhancement,Enhancement,New Feature".split(',') config.bug_labels = "bug,Bug,Improvement,Upstream Bug".split(',') config.exclude_labels = "duplicate,question,invalid,wontfix,no_changelog".split(',') end chef-zero-4.5.0/bin/0000755000004100000410000000000012654006514014205 5ustar www-datawww-datachef-zero-4.5.0/bin/chef-zero0000755000004100000410000000507412654006514016023 0ustar www-datawww-data#!/usr/bin/env ruby # Trap interrupts to quit cleanly. Signal.trap('INT') { exit 1 } require 'rubygems' $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))) require 'chef_zero/log' require 'chef_zero/version' require 'chef_zero/server' require 'chef_zero/data_store/raw_file_store' require 'optparse' def parse_port(port) array = [] port.split(',').each do |part| a,b = part.split('-',2) if b array = array.concat(a.to_i.upto(b.to_i).to_a) else array = array.concat([a.to_i]) end end array end options = {} OptionParser.new do |opts| opts.banner = "Usage: chef-zero [ARGS]" opts.on("-H", "--host HOST", "Host to bind to (default: 127.0.0.1)") do |value| options[:host] = value end opts.on("-p", "--port PORT", "Port to listen on (e.g. 8889, or 8500-8600 or 8885,8888)") do |value| options[:port] ||= [] options[:port] += parse_port(value) end opts.on("--[no-]generate-keys", "Whether to generate actual keys or fake it (faster). Default: false.") do |value| options[:generate_real_keys] = value end opts.on("-d", "--daemon", "Run as a daemon process") do |value| options[:daemon] = value end opts.on("-l", "--log-level LEVEL", "Set the output log level") do |value| options[:log_level] = value end opts.on("--log-file FILE", "Log to a file") do |value| options[:log_file] = value end opts.on("--enterprise", "Whether to run in enterprise mode") do |value| options[:single_org] = nil options[:osc_compat] = false end opts.on("--multi-org", "Whether to run in multi-org mode") do |value| options[:single_org] = nil end opts.on("--file-store PATH", "Persist data to files at the given path") do |value| options[:data_store] = ChefZero::DataStore::RawFileStore.new(value) end opts.on("--[no-]ssl", "Use SSL with self-signed certificate(Auto generate before every run). Default: false.") do |value| options[:ssl] = value end opts.on_tail("-h", "--help", "Show this message") do puts opts exit end opts.on_tail("--version", "Show version") do puts ChefZero::VERSION exit end end.parse! if options[:data_store] options[:data_store] = ChefZero::DataStore::DefaultFacade.new(options[:data_store], options[:single_org], false) end if options[:log_file] ChefZero::Log.init(options[:log_file]) end server = ChefZero::Server.new(options) if options[:daemon] if Process.respond_to?(:daemon) Process.daemon(true) server.start(true) else abort 'Process.daemon requires Ruby >= 1.9' end else server.start(true) end chef-zero-4.5.0/Gemfile0000644000004100000410000000075412654006514014736 0ustar www-datawww-datasource 'https://rubygems.org' gemspec # gem 'rest-client', :github => 'chef/rest-client' gem 'oc-chef-pedant', :github => 'chef/chef-server', :branch => "jk/authorization-tags" # bundler resolve failure on "rspec_junit_formatter" # gem 'chef-pedant', :github => 'opscode/chef-pedant', :ref => "server-cli-option" # gem 'chef', :github => 'chef/chef', :branch => 'jk/policies-acls' if ENV['GEMFILE_MOD'] puts "GEMFILE_MOD: #{ENV['GEMFILE_MOD']}" instance_eval(ENV['GEMFILE_MOD']) end chef-zero-4.5.0/spec/0000755000004100000410000000000012654006514014367 5ustar www-datawww-datachef-zero-4.5.0/spec/server_spec.rb0000644000004100000410000000554512654006514017245 0ustar www-datawww-datarequire 'chef_zero/server' require 'net/http' require 'uri' describe ChefZero::Server do context 'with a server bound to port 8889' do before :each do @server = ChefZero::Server.new(:port => 8889) @server.start_background end after :each do @server.stop end it 'a second server bound to port 8889 throws EADDRINUSE' do expect { ChefZero::Server.new(:port => 8889).start }.to raise_error Errno::EADDRINUSE end it 'a server bound to range 8889-9999 binds to a port > 8889' do server = ChefZero::Server.new(:port => 8889.upto(9999)) server.start_background expect(server.port).to be > 8889 expect(URI(server.url).port).to be > 8889 end it 'a server bound to range 8889-8889 throws an exception' do expect { ChefZero::Server.new(:port => 8889.upto(8889)).start_background }.to raise_error Errno::EADDRINUSE end context 'accept headers' do def get_nodes(accepts) uri = URI(@server.url) httpcall = Net::HTTP.new(uri.host, uri.port) httpcall.get('/nodes', 'Accept' => accepts) end def get_version uri = URI(@server.url) httpcall = Net::HTTP.new(uri.host, uri.port) httpcall.get('/version', 'Accept' => 'text/plain, application/json') end it 'accepts requests with no accept header' do request = Net::HTTP::Get.new('/nodes') request.delete('Accept') uri = URI(@server.url) response = Net::HTTP.new(uri.host, uri.port).request(request) expect(response.code).to eq '200' end it 'accepts requests with accept: application/json' do expect(get_nodes('application/json').code).to eq '200' end it 'accepts requests with accept: application/*' do expect(get_nodes('application/*').code).to eq '200' end it 'accepts requests with accept: application/*' do expect(get_nodes('*/*').code).to eq '200' end it 'denies requests with accept: application/blah' do expect(get_nodes('application/blah').code).to eq '406' end it 'denies requests with accept: blah/json' do expect(get_nodes('blah/json').code).to eq '406' end it 'denies requests with accept: blah/*' do expect(get_nodes('blah/*').code).to eq '406' end it 'denies requests with accept: blah/*' do expect(get_nodes('blah/*').code).to eq '406' end it 'denies requests with accept: ' do expect(get_nodes('').code).to eq '406' end it 'accepts requests with accept: a/b;a=b;c=d, application/json;a=b, application/xml;a=b' do expect(get_nodes('a/b;a=b;c=d, application/json;a=b, application/xml;a=b').code).to eq '200' end it 'accepts /version' do expect(get_version.body.start_with?('chef-zero')).to be true end end end end chef-zero-4.5.0/spec/run_oc_pedant.rb0000644000004100000410000001211412654006514017533 0ustar www-datawww-data#!/usr/bin/env ruby require 'bundler' require 'bundler/setup' require 'chef_zero/server' require 'rspec/core' def start_cheffs_server(chef_repo_path) require 'chef/version' require 'chef/config' require 'chef/chef_fs/config' require 'chef/chef_fs/chef_fs_data_store' require 'chef_zero/server' Dir.mkdir(chef_repo_path) if !File.exists?(chef_repo_path) # 11.6 and below had a bug where it couldn't create the repo children automatically if Chef::VERSION.to_f < 11.8 %w(clients cookbooks data_bags environments nodes roles users).each do |child| Dir.mkdir("#{chef_repo_path}/#{child}") if !File.exists?("#{chef_repo_path}/#{child}") end end # Start the new server Chef::Config.repo_mode = 'hosted_everything' Chef::Config.chef_repo_path = chef_repo_path Chef::Config.versioned_cookbooks = true chef_fs_config = Chef::ChefFS::Config.new data_store = Chef::ChefFS::ChefFSDataStore.new(chef_fs_config.local_fs, chef_fs_config.chef_config) data_store = ChefZero::DataStore::V1ToV2Adapter.new(data_store, 'pedant-testorg') data_store = ChefZero::DataStore::DefaultFacade.new(data_store, 'pedant-testorg', false) data_store.create(%w(organizations pedant-testorg users), 'pivotal', '{}') data_store.set(%w(organizations pedant-testorg groups admins), '{ "users": [ "pivotal" ] }') data_store.set(%w(organizations pedant-testorg groups users), '{ "users": [ "pivotal" ] }') server = ChefZero::Server.new( port: 8889, data_store: data_store, single_org: false, #log_level: :debug ) server.start_background server end tmpdir = nil begin if ENV['FILE_STORE'] require 'tmpdir' require 'chef_zero/data_store/raw_file_store' tmpdir = Dir.mktmpdir data_store = ChefZero::DataStore::RawFileStore.new(tmpdir, true) data_store = ChefZero::DataStore::DefaultFacade.new(data_store, false, false) server = ChefZero::Server.new(:port => 8889, :single_org => false, :data_store => data_store) server.start_background elsif ENV['CHEF_FS'] require 'tmpdir' tmpdir = Dir.mktmpdir server = start_cheffs_server(tmpdir) else server = ChefZero::Server.new(:port => 8889, :single_org => false)#, :log_level => :debug) server.start_background end require 'rspec/core' require 'pedant' require 'pedant/organization' # Pedant::Config.rerun = true Pedant.config.suite = 'api' Pedant.config[:config_file] = 'spec/support/oc_pedant.rb' # Because ChefFS can only ever have one user (pivotal), we can't do most of the # tests that involve multiple chef_fs_skips = if ENV['CHEF_FS'] [ '--skip-association', '--skip-users', '--skip-organizations', '--skip-multiuser', # chef-zero has some non-removable quirks, such as the fact that files # with 255-character names cannot be stored in local mode. This is # reserved only for quirks that are *irrevocable* and by design; and # should barely be used at all. '--skip-chef-zero-quirks', ] else [] end # The latest released Chef doesn't do ACLs, Cookbook Artifacts or Policies yet chef_fs_skips << '--skip-acl' chef_fs_skips << '--skip-cookbook-artifacts' chef_fs_skips << '--skip-policies' # These things aren't supported by Chef Zero in any mode of operation: default_skips = [ # "the goal is that only authorization, authentication and validation tests # are turned off" - @jkeiser # # ...but we're not there yet '--skip-keys', # Chef Zero does not intend to support validation the way erchef does. '--skip-validation', # Chef Zero does not intend to support authentication the way erchef does. '--skip-authentication', # Chef Zero does not intend to support authorization the way erchef does. '--skip-authorization', # Omnibus tests depend on erchef features that are specific to erchef and # bundled in the omnibus package. Currently the only test in this category # is for the search reindexing script. '--skip-omnibus', # USAGs (user-specific association groups) are Authz groups that contain # only one user and represent that user's association with an org. Though # there are good reasons for them, they don't work well in practice and # only the manage console really uses them. Since Chef Zero + Manage is a # quite unusual configuration, we're ignoring them. '--skip-usags', # Chef 12 features not yet 100% supported by Chef Zero '--skip-api-v1', # The universe endpoint is unlikely to ever make sense for Chef Zero '--skip-universe', ] # The knife tests are very slow and don't give us a lot of extra coverage, # so we run them in a different entry in the travis test matrix. pedant_args = if ENV["PEDANT_KNIFE_TESTS"] default_skips + %w{ --focus-knife } else default_skips + chef_fs_skips + %w{ --skip-knife } end Pedant.setup(pedant_args) fail_fast = %w()#--fail-fast) #fail_fast = ["--fail-fast"] result = RSpec::Core::Runner.run(Pedant.config.rspec_args + fail_fast) server.stop if server.running? ensure FileUtils.remove_entry_secure(tmpdir) if tmpdir end exit(result) chef-zero-4.5.0/spec/search_spec.rb0000644000004100000410000000137512654006514017201 0ustar www-datawww-datarequire 'chef_zero/solr/solr_parser' require 'chef_zero/solr/solr_doc' describe ChefZero::Solr::SolrParser do let (:all_docs) do docs = [] [{'foo' => 'a'}, {'foo' => 'd'}].each_with_index do |h, i| docs.push ChefZero::Solr::SolrDoc.new(h, i) end docs end def search_for(query) q = ChefZero::Solr::SolrParser.new(query).parse all_docs.select {|doc| q.matches_doc?(doc) } end it "handles terms" do search_for('foo:d').size.should eq(1) end it "handles ranges" do search_for('foo:[a TO c]').size.should eq(1) end it "handles wildcard ranges" do search_for('foo:[* TO c]').size.should eq(1) search_for('foo:[c TO *]').size.should eq(1) search_for('foo:[* TO *]').size.should eq(2) end end chef-zero-4.5.0/spec/support/0000755000004100000410000000000012654006514016103 5ustar www-datawww-datachef-zero-4.5.0/spec/support/stickywicket.pem0000644000004100000410000000321712654006514021326 0ustar www-datawww-data-----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEApNCkX2k+lFGDWRVhX4uClaVQrumG9XXvk6X7M2izrIg7RzMP Dk4thhZkpx5gr22By7PZQdMEjWC/Zo8MBjtoJ0GV0jw8npefbU1MGKs2dtpYgo0N Fq8fX8MdFPu4h2W3g0dMEdhT8icc2H4EjhZmdeUhUn3RIEt2duCgp3YDYnUUZx3j N7MHcTIdzD58ikr6zQrZzHOv+OOI86Xk9EpyEEQizOLoQxkICNrhqN7ElQDuvXaX BSBrYDRKH2umBMMcXzvsR/SvkmqxoEESSpIlW8zeKAWQ+znNjDC0tmTg7jZmgSP7 siKrwo4t4ebjcmjpIoi/JKww/nGN3Uhz1ZOZuwIDAQABAoIBAQCaJQD2s0nyEeKU uKhfYe155Cl3zbWJcQnmv4AXbr9MiAVY6+oS6Q8ur1bn7kNjDzoruENjiuZhC7E3 TGZklb8tp+tluyy+7vQOmBKpp8fClSfewekR5CultqhGbb8B8yIVR+NfdUHd4rLZ z9KWyWB+txPZQQ8L80gSmrfmpzs3IuT7oPvmtBU1Wq9QapC4n/rUohHUpUV1du4G 0wCIF4zQTg6cbYW2YXozwVQvw+P7P3RVEqZt+aZlbVcy0fNr6jNao0hi1KFC9OH2 VjjU+PioreoA/NU3aZPIUzmJpWtsu31yuOZxXmytAkYooCZgiEQNEHnJlNPv0RmC 6BPMzVoBAoGBAM7yZoSNJpzdP/q1/4+H3zyy7o4I0VTW9u/GqUzhnbjm5poK30X9 YXh/7WOVV0OoVqdO6ljRKygP3Oggf41ZEbi1C6bbsO57pksBWgx9bD9V35XscZ0J F1ERe//kMHwVQy74R8/cIuRwm75haLSBj5/fwGbLeeVDglJkCVqPjtuBAoGBAMvh qsAGG5k9u6voTcXlFwS+B5YjULhK4NSxdJ2BnOxzYzxQ3IYQZMlb2xt8yZYx/ZZK wjkr9rcAPEQIQZ2A6NUbGq6qCD7sSmg6UAi0CgiqTokQ/Wtag0UDvFMzwerdg/On 37uxffpxpte8z1jYi/MxRaoTYueuc1UVnqofVIM7AoGBALZJzwPzUY/bVAADUJmd lYZiFsAGBF42/E05MOgH1GaK/ZWy/fkouDLsfK67XaK7JZk6ajLSDLG9R1kxRym6 y2FoGFtiKPfo8xIenrNhx3gCrG/jVjB9UYyXWiKNXifukr9M8/SkdBfFGWsZYqGd fmXVMiVaFoVcce8hLxwWWEABAoGBAKcyhKX/HEj6YFqlIoqkydDAylXs1jicZ27l rF2yum8KXZpMMdzbutuKsdAD8Ql0K6NB4a+jByuiTMn5/11cJxUEqkgM9sArZQW+ tH2+r+/VQpyTS0/rpXVGj/2nl2K1kI2T4R36e/aTl6CanWweAf9JK/lC9rxKyxg+ p6SaFuObAoGACP6TKCkp2oymXlKgdUUgPrnsaz2VAw8jD5QHtx10U4wty0C8gxsk MLe00h09iLPyFmvJpD+MgbxV/r6RrZeVdsKdU/5LG52YgiVSTaizyy+ciEfW7xoQ CL5EtZd8Cn5OKinBEzzFpELqunlqepIKCIDOcLKz/cjR+3a+E6Zx5Wo= -----END RSA PRIVATE KEY----- chef-zero-4.5.0/spec/support/oc_pedant.rb0000644000004100000410000001337212654006514020372 0ustar www-datawww-data# Copyright: Copyright (c) 2012 Opscode, Inc. # License: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # This annotated Pedant configuration file details the various # configuration settings available to you. It is separate from the # actual Pedant::Config class because not all settings have sane # defaults, and not all settings are appropriate in all settings. ################################################################################ # You MUST specify the address of the server the API requests will be # sent to. Only specify protocol, hostname, and port. chef_server 'http://127.0.0.1:8889' # If you are doing development testing, you can specify the address of # the Solr server. The presence of this parameter will enable tests # to force commits to Solr, greatly decreasing the amout of time # needed for testing the search endpoint. This is only an # optimization for development! If you are testing a "live" Chef # Server, or otherwise do not have access to the Solr server from your # testing location, you should not specify a value for this parameter. # The tests will still run, albeit slower, as they will now need to # poll for a period to ensure they are querying committed results. #search_server "http://localhost:8983" # Related to the 'search_server' parameter, this specifies the maximum # amout of time (in seconds) that search endpoint requests should be # retried before giving up. If not explicitly set, it will default to # 65 seconds; only set it if you know that your Solr commit interval # differs significantly from this. maximum_search_time 0 # OSC sends erchef a host header with a port, so this option needs # # to be enabled for Pedant tests to work correctly explicit_port_url true server_api_version 0 internal_server chef_server # see dummy_endpoint.rb for details. search_server chef_server search_commit_url "/dummy" search_url_fmt "/dummy?fq=+X_CHEF_type_CHEF_X:%{type}&q=%{query}&wt=json" # We're starting to break tests up into groups based on different # criteria. The proper API tests (the results of which are viewable # to OPC customers) should be the only ones run by Pedant embedded in # OPC installs. There are other specs that help us keep track of API # cruft that we want to come back and fix later; these shouldn't be # viewable to customers, but we should be able to run them in # development and CI environments. If this parameter is missing or # explicitly `false` only the customer-friendly tests will be run. # # This is mainly here for documentation purposes, since the # command-line `opscode-pedant` utility ultimately determines this # value. include_internal false key = 'spec/support/stickywicket.pem' org(name: "pedant-testorg", create_me: !ENV['CHEF_FS'], validator_key: key) internal_account_url chef_server delete_org true # Test users. The five users specified below are required; their # names (:user, :non_org_user, etc.) are indicative of their role # within the tests. All users must have a ':name' key. If they have # a ':create_me' key, Pedant will create these users for you. If you # are using pre-existing users, you must supply a ':key_file' key, # which should be the fully-qualified path /on the machine Pedant is # running on/ to a private key for that user. superuser_name 'pivotal' superuser_key key webui_key key def cheffs_or_else_user(value) ENV['CHEF_FS'] ? "pivotal" : value end keyfile_maybe = ENV['CHEF_FS'] ? { key_file: key } : { key_file: nil } requestors({ :clients => { # The the admin user, for the purposes of getting things rolling :admin => { :name => "pedant_admin_client", :create_me => true, :create_knife => true, :admin => true }, :non_admin => { :name => 'pedant_client', :create_me => true, :create_knife => true, }, :bad => { :name => 'bad_client', :create_me => true, :create_knife => true, :bogus => true } }, :users => { # An administrator in the testing organization :admin => { :name => cheffs_or_else_user("pedant_admin_user"), :create_me => !ENV['CHEF_FS'], :associate => !ENV['CHEF_FS'], :create_knife => true }.merge(keyfile_maybe), :non_admin => { :name => cheffs_or_else_user("pedant_user"), :create_me => !ENV['CHEF_FS'], :associate => !ENV['CHEF_FS'], :create_knife => true }.merge(keyfile_maybe), # A user that is not a member of the testing organization :bad => { :name => cheffs_or_else_user("pedant-nobody"), :create_me => !ENV['CHEF_FS'], :create_knife => true, :associate => false }.merge(keyfile_maybe), } }) self[:tags] = [:validation, :authentication, :authorization] verify_error_messages false ruby_users_endpoint? false ruby_acls_endpoint? false ruby_org_assoc? false chef_12? true chef-zero-4.5.0/spec/socketless_server_map_spec.rb0000644000004100000410000000442612654006514022336 0ustar www-datawww-datarequire 'chef_zero/socketless_server_map' describe "Socketless Mode" do let(:server_map) { ChefZero::SocketlessServerMap.instance.tap { |i| i.reset! } } let(:server) { instance_double("ChefZero::Server") } let(:second_server) { instance_double("ChefZero::Server") } it "registers a socketful server" do server_map.register_port(8889, server) expect(server_map).to have_server_on_port(8889) end it "retrieves a server by port" do server_map.register_port(8889, server) expect(ChefZero::SocketlessServerMap.server_on_port(8889)).to eq(server) end context "when a no-listen server is registered" do let!(:port) { server_map.register_no_listen_server(server) } it "assigns the server a low port number" do expect(port).to eq(1) end context "and another server is registered" do let!(:next_port) { server_map.register_no_listen_server(second_server) } it "assigns another port when another server is registered" do expect(next_port).to eq(2) end it "raises NoSocketlessPortAvailable when too many servers are registered" do expect { 1000.times { server_map.register_no_listen_server(server) } }.to raise_error(ChefZero::NoSocketlessPortAvailable) end it "deregisters a server" do expect(server_map).to have_server_on_port(1) server_map.deregister(1) expect(server_map).to_not have_server_on_port(1) end describe "routing requests to a server" do let(:rack_req) do r = {} r["REQUEST_METHOD"] = "GET" r["SCRIPT_NAME"] = "" r["PATH_INFO"] = "/clients" r["QUERY_STRING"] = "" r["rack.input"] = StringIO.new("") r end let(:rack_response) { [200, {}, ["this is the response body"] ] } it "routes a request to the registered port" do expect(server).to receive(:handle_socketless_request).with(rack_req).and_return(rack_response) response = server_map.request(1, rack_req) expect(response).to eq(rack_response) end it "raises ServerNotFound when a request is sent to an unregistered port" do expect { server_map.request(99, rack_req) }.to raise_error(ChefZero::ServerNotFound) end end end end end chef-zero-4.5.0/lib/0000755000004100000410000000000012654006514014203 5ustar www-datawww-datachef-zero-4.5.0/lib/chef_zero.rb0000644000004100000410000000666212654006514016506 0ustar www-datawww-datamodule ChefZero require 'chef_zero/log' MIN_API_VERSION = 0 MAX_API_VERSION = 1 CERTIFICATE = "-----BEGIN CERTIFICATE-----\nMIIDMzCCApygAwIBAgIBATANBgkqhkiG9w0BAQUFADCBnjELMAkGA1UEBhMCVVMx\nEzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUxFjAUBgNVBAoM\nDU9wc2NvZGUsIEluYy4xHDAaBgNVBAsME0NlcnRpZmljYXRlIFNlcnZpY2UxMjAw\nBgNVBAMMKW9wc2NvZGUuY29tL2VtYWlsQWRkcmVzcz1hdXRoQG9wc2NvZGUuY29t\nMB4XDTEyMTEyMTAwMzQyMVoXDTIyMTExOTAwMzQyMVowgZsxEDAOBgNVBAcTB1Nl\nYXR0bGUxEzARBgNVBAgTCldhc2hpbmd0b24xCzAJBgNVBAYTAlVTMRwwGgYDVQQL\nExNDZXJ0aWZpY2F0ZSBTZXJ2aWNlMRYwFAYDVQQKEw1PcHNjb2RlLCBJbmMuMS8w\nLQYDVQQDFCZVUkk6aHR0cDovL29wc2NvZGUuY29tL0dVSURTL3VzZXJfZ3VpZDCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANLDmPbR71bS2esZlZh/HfC6\n0azXFjl2677wq2ovk9xrUb0Ui4ZLC66TqQ9C/RBzOjXU4TRf3hgPTqvlCgHusl0d\nIcLCrsSl6kPEhJpYWWfRoroIAwf82A9yLQekhqXZEXu5EKkwoUMqyF6m0ZCasaE1\ny8niQxdLAsk3ady/CGQlFqHTPKFfU5UASR2LRtYC1MCIvJHDFRKAp9kPJbQo9P37\nZ8IU7cDudkZFgNLmDixlWsh7C0ghX8fgAlj1P6FgsFufygam973k79GhIP54dELB\nc0S6E8ekkRSOXU9jX/IoiXuFglBvFihAdhvED58bMXzj2AwXUyeAlxItnvs+NVUC\nAwEAATANBgkqhkiG9w0BAQUFAAOBgQBkFZRbMoywK3hb0/X7MXmPYa7nlfnd5UXq\nr2n32ettzZNmEPaI2d1j+//nL5qqhOlrWPS88eKEPnBOX/jZpUWOuAAddnrvFzgw\nrp/C2H7oMT+29F+5ezeViLKbzoFYb4yECHBoi66IFXNae13yj7taMboBeUmE664G\nTB/MZpRr8g==\n-----END CERTIFICATE-----\n" PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0sOY9tHvVtLZ6xmVmH8d\n8LrRrNcWOXbrvvCrai+T3GtRvRSLhksLrpOpD0L9EHM6NdThNF/eGA9Oq+UKAe6y\nXR0hwsKuxKXqQ8SEmlhZZ9GiuggDB/zYD3ItB6SGpdkRe7kQqTChQyrIXqbRkJqx\noTXLyeJDF0sCyTdp3L8IZCUWodM8oV9TlQBJHYtG1gLUwIi8kcMVEoCn2Q8ltCj0\n/ftnwhTtwO52RkWA0uYOLGVayHsLSCFfx+ACWPU/oWCwW5/KBqb3veTv0aEg/nh0\nQsFzRLoTx6SRFI5dT2Nf8iiJe4WCUG8WKEB2G8QPnxsxfOPYDBdTJ4CXEi2e+z41\nVQIDAQAB\n-----END PUBLIC KEY-----\n" PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA0sOY9tHvVtLZ6xmVmH8d8LrRrNcWOXbrvvCrai+T3GtRvRSL\nhksLrpOpD0L9EHM6NdThNF/eGA9Oq+UKAe6yXR0hwsKuxKXqQ8SEmlhZZ9GiuggD\nB/zYD3ItB6SGpdkRe7kQqTChQyrIXqbRkJqxoTXLyeJDF0sCyTdp3L8IZCUWodM8\noV9TlQBJHYtG1gLUwIi8kcMVEoCn2Q8ltCj0/ftnwhTtwO52RkWA0uYOLGVayHsL\nSCFfx+ACWPU/oWCwW5/KBqb3veTv0aEg/nh0QsFzRLoTx6SRFI5dT2Nf8iiJe4WC\nUG8WKEB2G8QPnxsxfOPYDBdTJ4CXEi2e+z41VQIDAQABAoIBAALhqbW2KQ+G0nPk\nZacwFbi01SkHx8YBWjfCEpXhEKRy0ytCnKW5YO+CFU2gHNWcva7+uhV9OgwaKXkw\nKHLeUJH1VADVqI4Htqw2g5mYm6BPvWnNsjzpuAp+BR+VoEGkNhj67r9hatMAQr0I\nitTvSH5rvd2EumYXIHKfz1K1SegUk1u1EL1RcMzRmZe4gDb6eNBs9Sg4im4ybTG6\npPIytA8vBQVWhjuAR2Tm+wZHiy0Az6Vu7c2mS07FSX6FO4E8SxWf8idaK9ijMGSq\nFvIS04mrY6XCPUPUC4qm1qNnhDPpOr7CpI2OO98SqGanStS5NFlSFXeXPpM280/u\nfZUA0AECgYEA+x7QUnffDrt7LK2cX6wbvn4mRnFxet7bJjrfWIHf+Rm0URikaNma\nh0/wNKpKBwIH+eHK/LslgzcplrqPytGGHLOG97Gyo5tGAzyLHUWBmsNkRksY2sPL\nuHq6pYWJNkqhnWGnIbmqCr0EWih82x/y4qxbJYpYqXMrit0wVf7yAgkCgYEA1twI\ngFaXqesetTPoEHSQSgC8S4D5/NkdriUXCYb06REcvo9IpFMuiOkVUYNN5d3MDNTP\nIdBicfmvfNELvBtXDomEUD8ls1UuoTIXRNGZ0VsZXu7OErXCK0JKNNyqRmOwcvYL\nJRqLfnlei5Ndo1lu286yL74c5rdTLs/nI2p4e+0CgYB079ZmcLeILrmfBoFI8+Y/\ngJLmPrFvXBOE6+lRV7kqUFPtZ6I3yQzyccETZTDvrnx0WjaiFavUPH27WMjY01S2\nTMtO0Iq1MPsbSrglO1as8MvjB9ldFcvp7gy4Q0Sv6XT0yqJ/S+vo8Df0m+H4UBpU\nf5o6EwBSd/UQxwtZIE0lsQKBgQCswfjX8Eg8KL/lJNpIOOE3j4XXE9ptksmJl2sB\njxDnQYoiMqVO808saHVquC/vTrpd6tKtNpehWwjeTFuqITWLi8jmmQ+gNTKsC9Gn\n1Pxf2Gb67PqnEpwQGln+TRtgQ5HBrdHiQIi+5am+gnw89pDrjjO5rZwhanAo6KPJ\n1zcPNQKBgQDxFu8v4frDmRNCVaZS4f1B6wTrcMrnibIDlnzrK9GG6Hz1U7dDv8s8\nNf4UmeMzDXjlPWZVOvS5+9HKJPdPj7/onv8B2m18+lcgTTDJBkza7R1mjL1Cje/Z\nKcVGsryKN6cjE7yCDasnA7R2rVBV/7NWeJV77bmzT5O//rW4yIfUIg==\n-----END RSA PRIVATE KEY-----\n" end chef-zero-4.5.0/lib/chef_zero/0000755000004100000410000000000012654006514016147 5ustar www-datawww-datachef-zero-4.5.0/lib/chef_zero/rest_router.rb0000644000004100000410000000232312654006514021051 0ustar www-datawww-datamodule ChefZero class RestRouter def initialize(routes) @routes = routes.map do |route, endpoint| if route =~ /\*\*$/ pattern = Regexp.new("^#{route[0..-3].gsub('*', '[^/]*')}") else pattern = Regexp.new("^#{route.gsub('*', '[^/]*')}$") end [ pattern, endpoint ] end end attr_reader :routes attr_accessor :not_found def call(request) begin ChefZero::Log.debug(request) ChefZero::Log.debug(request.body) if request.body clean_path = "/" + request.rest_path.join("/") response = find_endpoint(clean_path).call(request) ChefZero::Log.debug([ "", "--- RESPONSE (#{response[0]}) ---", response[2], "--- END RESPONSE ---", ].join("\n")) return response rescue ChefZero::Log.error("#{$!.inspect}\n#{$!.backtrace.join("\n")}") [500, {"Content-Type" => "text/plain"}, "Exception raised! #{$!.inspect}\n#{$!.backtrace.join("\n")}"] end end private def find_endpoint(clean_path) _, endpoint = routes.find { |route, endpoint| route.match(clean_path) } endpoint || not_found end end end chef-zero-4.5.0/lib/chef_zero/chef_data/0000755000004100000410000000000012654006514020045 5ustar www-datawww-datachef-zero-4.5.0/lib/chef_zero/chef_data/default_creator.rb0000644000004100000410000003357312654006514023550 0ustar www-datawww-datarequire 'chef_zero/chef_data/acl_path' module ChefZero module ChefData # # The DefaultCreator creates default values when you ask for them. # - It relies on created and deleted being called when things get # created and deleted, so that it knows the owners of said objects # and knows to eliminate default values on delete. # - get, list and exists? get data. # class DefaultCreator def initialize(data, single_org, osc_compat, superusers = nil) @data = data @single_org = single_org @osc_compat = osc_compat @superusers = superusers || DEFAULT_SUPERUSERS clear end attr_reader :data attr_reader :single_org attr_reader :osc_compat attr_reader :creators attr_reader :deleted PERMISSIONS = %w(create read update delete grant) DEFAULT_SUPERUSERS = %w(pivotal) def clear @creators = { [] => @superusers } @deleted = {} end def deleted(path) # acl deletes mean nothing, they are entirely subservient to their # parent object if path[0] == 'acls' || (path[0] == 'organizations' && path[2] == 'acls') return false end result = exists?(path) @deleted[path] = true result end def deleted?(path) 1.upto(path.size) do |index| return true if @deleted[path[0..-index]] end false end def created(path, creator, create_parents) # If a parent has been deleted, we will need to clear that. deleted_index = nil 0.upto(path.size-1) do |index| deleted_index = index if @deleted[path[0..index]] end # Walk up the tree, setting the creator on anything that doesn't exist # (anything that is either deleted or was never created) while (deleted_index && path.size > deleted_index) || !@creators[path] @creators[path] = creator ? [ creator ] : [] @deleted.delete(path) # Only do this once if create_parents is false break if !create_parents || path.size == 0 path = path[0..-2] end end def superusers @creators[[]] end def get(path) return nil if deleted?(path) result = case path[0] when 'acls' # /acls/* object_path = AclPath.get_object_path(path) if data_exists?(object_path) default_acl(path) end when 'containers' if path.size == 2 && exists?(path) {} end when 'users' if path.size == 2 && data.exists?(path) # User is empty user {} end when 'organizations' if path.size >= 2 # /organizations/*/** if data.exists_dir?(path[0..1]) get_org_default(path) end end end result end def list(path) return nil if deleted?(path) if path.size == 0 return %w(containers users organizations acls) end case path[0] when 'acls' if path.size == 1 [ 'root' ] + (data.list(path + [ 'containers' ]) - [ 'organizations' ]) else data.list(AclPath.get_object_path(path)) end when 'containers' [ 'containers', 'users', 'organizations' ] when 'users' superusers when 'organizations' if path.size == 1 single_org ? [ single_org ] : [] elsif path.size >= 2 && data.exists_dir?(path[0..1]) list_org_default(path) end end end def exists?(path) return true if path.size == 0 parent_list = list(path[0..-2]) parent_list && parent_list.include?(path[-1]) end protected DEFAULT_ORG_SPINE = { 'clients' => {}, 'cookbook_artifacts' => {}, 'cookbooks' => {}, 'data' => {}, 'environments' => %w(_default), 'file_store' => { 'checksums' => {} }, 'nodes' => {}, 'policies' => {}, 'policy_groups' => {}, 'roles' => {}, 'sandboxes' => {}, 'users' => {}, 'org' => {}, 'containers' => %w(clients containers cookbook_artifacts cookbooks data environments groups nodes policies policy_groups roles sandboxes), 'groups' => %w(admins billing-admins clients users), 'association_requests' => {} } def list_org_default(path) if path.size >= 3 && path[2] == 'acls' if path.size == 3 # /organizations/ORG/acls return [ 'root' ] + data.list(path[0..1] + [ 'containers' ]) elsif path.size == 4 # /organizations/ORG/acls/TYPE return data.list(path[0..1] + [ path[3] ]) else return nil end end value = DEFAULT_ORG_SPINE 2.upto(path.size-1) do |index| value = nil if @deleted[path[0..index]] break if !value value = value[path[index]] end result = if value.is_a?(Hash) value.keys elsif value value end if path.size == 3 if path[2] == 'clients' result << "#{path[1]}-validator" if osc_compat result << "#{path[1]}-webui" end elsif path[2] == 'users' if osc_compat result << 'admin' end end end result end def get_org_default(path) if path[2] == 'acls' get_org_acl_default(path) elsif path.size >= 4 if path[2] == 'containers' && path.size == 4 if exists?(path) return {} else return nil end end # /organizations/(*)/clients/\1-validator # /organizations/*/environments/_default # /organizations/*/groups/{admins,billing-admins,clients,users} case path[2..-1].join('/') when "clients/#{path[1]}-validator" { 'validator' => 'true' } when "clients/#{path[1]}-webui", "users/admin" if osc_compat { 'admin' => 'true' } end when "environments/_default" { "description" => "The default Chef environment" } when "groups/admins" admins = data.list(path[0..1] + [ 'users' ]).select do |name| user = FFI_Yajl::Parser.parse(data.get(path[0..1] + [ 'users', name ]), :create_additions => false) user['admin'] end admins += data.list(path[0..1] + [ 'clients' ]).select do |name| client = FFI_Yajl::Parser.parse(data.get(path[0..1] + [ 'clients', name ]), :create_additions => false) client['admin'] end admins += @creators[path[0..1]] if @creators[path[0..1]] { 'actors' => admins.uniq } when "groups/billing-admins" {} when "groups/clients" { 'clients' => data.list(path[0..1] + [ 'clients' ]) } when "groups/users" users = data.list(path[0..1] + [ 'users' ]) users |= @creators[path[0..1]] if @creators[path[0..1]] { 'users' => users } when "org" {} end end end def get_org_acl_default(path) object_path = AclPath.get_object_path(path) # The actual things containers correspond to don't have to exist, as long as the container does return nil if !data_exists?(object_path) basic_acl = case path[3..-1].join('/') when 'root', 'containers/containers', 'containers/groups' { 'create' => { 'groups' => %w(admins) }, 'read' => { 'groups' => %w(admins users) }, 'update' => { 'groups' => %w(admins) }, 'delete' => { 'groups' => %w(admins) }, 'grant' => { 'groups' => %w(admins) }, } when 'containers/environments', 'containers/roles', 'containers/policy_groups', 'containers/policies' { 'create' => { 'groups' => %w(admins users) }, 'read' => { 'groups' => %w(admins users clients) }, 'update' => { 'groups' => %w(admins users) }, 'delete' => { 'groups' => %w(admins users) }, 'grant' => { 'groups' => %w(admins) }, } when 'containers/cookbooks', 'containers/cookbook_artifacts', 'containers/data' { 'create' => { 'groups' => %w(admins users clients) }, 'read' => { 'groups' => %w(admins users clients) }, 'update' => { 'groups' => %w(admins users clients) }, 'delete' => { 'groups' => %w(admins users clients) }, 'grant' => { 'groups' => %w(admins) }, } when 'containers/nodes' { 'create' => { 'groups' => %w(admins users clients) }, 'read' => { 'groups' => %w(admins users clients) }, 'update' => { 'groups' => %w(admins users) }, 'delete' => { 'groups' => %w(admins users) }, 'grant' => { 'groups' => %w(admins) }, } when 'containers/clients' { 'create' => { 'groups' => %w(admins) }, 'read' => { 'groups' => %w(admins users) }, 'update' => { 'groups' => %w(admins) }, 'delete' => { 'groups' => %w(admins users) }, 'grant' => { 'groups' => %w(admins) }, } when 'containers/sandboxes' { 'create' => { 'groups' => %w(admins users) }, 'read' => { 'groups' => %w(admins) }, 'update' => { 'groups' => %w(admins) }, 'delete' => { 'groups' => %w(admins) }, 'grant' => { 'groups' => %w(admins) }, } when 'groups/admins', 'groups/clients', 'groups/users' { 'create' => { 'groups' => %w(admins) }, 'read' => { 'groups' => %w(admins) }, 'update' => { 'groups' => %w(admins) }, 'delete' => { 'groups' => %w(admins) }, 'grant' => { 'groups' => %w(admins) }, } when 'groups/billing-admins' { 'create' => { 'groups' => %w() }, 'read' => { 'groups' => %w(billing-admins) }, 'update' => { 'groups' => %w(billing-admins) }, 'delete' => { 'groups' => %w() }, 'grant' => { 'groups' => %w() }, } else {} end default_acl(path, basic_acl) end def get_owners(acl_path) owners = [] path = AclPath.get_object_path(acl_path) if path # Non-validator clients own themselves. if path.size == 4 && path[0] == 'organizations' && path[2] == 'clients' begin client = FFI_Yajl::Parser.parse(data.get(path), :create_additions => false) if !client['validator'] owners |= [ path[3] ] end rescue owners |= [ path[3] ] end # Add creators as owners (except any validator clients). if @creators[path] @creators[path].each do |creator| begin client = FFI_Yajl::Parser.parse(data.get(path[0..2] + [ creator ]), :create_additions => false) next if client['validator'] rescue end owners |= [ creator ] end end else owners |= @creators[path] if @creators[path] end #ANGRY # Non-default containers do not get superusers added to them, # because reasons. unless path.size == 4 && path[0] == 'organizations' && path[2] == 'containers' && !exists?(path) owners += superusers end end # we don't de-dup this list, because pedant expects to see ["pivotal", "pivotal"] in some cases. owners end def default_acl(acl_path, acl={}) owners = nil container_acl = nil PERMISSIONS.each do |perm| acl[perm] ||= {} acl[perm]['actors'] ||= begin owners ||= get_owners(acl_path) end acl[perm]['groups'] ||= begin # When we create containers, we don't merge groups (not sure why). if acl_path[0] == 'organizations' && acl_path[3] == 'containers' [] else container_acl ||= get_container_acl(acl_path) || {} (container_acl[perm] ? container_acl[perm]['groups'] : []) || [] end end end acl end def get_container_acl(acl_path) parent_path = AclPath.parent_acl_data_path(acl_path) if parent_path FFI_Yajl::Parser.parse(data.get(parent_path), :create_additions => false) else nil end end def data_exists?(path) if is_dir?(path) data.exists_dir?(path) else data.exists?(path) end end def is_dir?(path) case path.size when 0, 1 return true when 2 return path[0] == 'organizations' || (path[0] == 'acls' && path[1] != 'root') when 3 # If it has a container, it is a directory. return path[0] == 'organizations' && (path[2] == 'acls' || data.exists?(path[0..1] + [ 'containers', path[2] ])) when 4 return path[0] == 'organizations' && ( (path[2] == 'acls' && path[1] != 'root') || %w(cookbooks cookbook_artifacts data policies policy_groups).include?(path[2])) else return false end end end end end chef-zero-4.5.0/lib/chef_zero/chef_data/cookbook_data.rb0000644000004100000410000001676512654006514023210 0ustar www-datawww-datarequire 'digest/md5' require 'hashie/mash' module ChefZero module ChefData module CookbookData def self.to_hash(cookbook, name, version=nil) frozen = false if cookbook.has_key?(:frozen) frozen = cookbook[:frozen] cookbook = cookbook.dup cookbook.delete(:frozen) end result = files_from(cookbook) recipe_names = result[:recipes].map do |recipe| recipe_name = recipe[:name][0..-2] recipe_name == 'default' ? name : "#{name}::#{recipe_name}" end result[:metadata] = metadata_from(cookbook, name, version, recipe_names) result[:name] = "#{name}-#{result[:metadata][:version]}" result[:json_class] = 'Chef::CookbookVersion' result[:cookbook_name] = name result[:version] = result[:metadata][:version] result[:chef_type] = 'cookbook_version' result[:frozen?] = true if frozen result end def self.metadata_from(directory, name, version, recipe_names) metadata = PretendCookbookMetadata.new(PretendCookbook.new(name, recipe_names)) # If both .rb and .json exist, read .rb # TODO if recipes has 3 recipes in it, and the Ruby/JSON has only one, should # the resulting recipe list have 1, or 3-4 recipes in it? if has_child(directory, 'metadata.rb') begin file = filename(directory, 'metadata.rb') || "(#{name}/metadata.rb)" metadata.instance_eval(read_file(directory, 'metadata.rb'), file) rescue ChefZero::Log.error("Error loading cookbook #{name}: #{$!}\n #{$!.backtrace.join("\n ")}") end elsif has_child(directory, 'metadata.json') metadata.from_json(read_file(directory, 'metadata.json')) end result = {} metadata.to_hash.each_pair do |key,value| result[key.to_sym] = value end result[:version] = version if version result end private # Just enough cookbook to make a Metadata object class PretendCookbook def initialize(name, fully_qualified_recipe_names) @name = name @fully_qualified_recipe_names = fully_qualified_recipe_names end attr_reader :name, :fully_qualified_recipe_names end # Handles loading configuration values from a Chef config file # # @author Justin Campbell class PretendCookbookMetadata < Hash # @param [String] path def initialize(cookbook) self.name(cookbook.name) self.recipes(cookbook.fully_qualified_recipe_names) %w(attributes grouping dependencies supports recommendations suggestions conflicting providing replacing recipes).each do |hash_arg| self[hash_arg.to_sym] = Hashie::Mash.new end end def from_json(json) self.merge!(FFI_Yajl::Parser.parse(json)) end private def depends(cookbook, *version_constraints) cookbook_arg(:dependencies, cookbook, version_constraints) end def supports(cookbook, *version_constraints) cookbook_arg(:supports, cookbook, version_constraints) end def recommends(cookbook, *version_constraints) cookbook_arg(:recommendations, cookbook, version_constraints) end def suggests(cookbook, *version_constraints) cookbook_arg(:suggestions, cookbook, version_constraints) end def conflicts(cookbook, *version_constraints) cookbook_arg(:conflicting, cookbook, version_constraints) end def provides(cookbook, *version_constraints) cookbook_arg(:providing, cookbook, version_constraints) end def replaces(cookbook, *version_constraints) cookbook_arg(:replacing, cookbook, version_constraints) end def recipe(recipe, description) self[:recipes][recipe] = description end def attribute(name, options) self[:attributes][name] = options end def grouping(name, options) self[:grouping][name] = options end def cookbook_arg(key, cookbook, version_constraints) self[key][cookbook] = version_constraints.first || ">= 0.0.0" end def method_missing(key, value = nil) if value.nil? self[key.to_sym] else store key.to_sym, value end end end def self.files_from(directory) # TODO some support .rb only result = { :attributes => load_child_files(directory, 'attributes', false), :definitions => load_child_files(directory, 'definitions', false), :recipes => load_child_files(directory, 'recipes', false), :libraries => load_child_files(directory, 'libraries', false), :templates => load_child_files(directory, 'templates', true), :files => load_child_files(directory, 'files', true), :resources => load_child_files(directory, 'resources', true), :providers => load_child_files(directory, 'providers', true), :root_files => load_files(directory, false) } set_specificity(result[:templates]) set_specificity(result[:files]) result end def self.has_child(directory, name) if directory.is_a?(Hash) directory.has_key?(name) else directory.child(name).exists? end end def self.read_file(directory, name) if directory.is_a?(Hash) directory[name] else directory.child(name).read end end def self.filename(directory, name) if directory.respond_to?(:file_path) File.join(directory.file_path, name) else nil end end def self.get_directory(directory, name) if directory.is_a?(Hash) directory[name].is_a?(Hash) ? directory[name] : nil else result = directory.child(name) result.dir? ? result : nil end end def self.list(directory) if directory.is_a?(Hash) directory.keys else directory.children.map { |c| c.name } end end def self.load_child_files(parent, key, recursive) result = load_files(get_directory(parent, key), recursive) result.each do |file| file[:path] = "#{key}/#{file[:path]}" end result end def self.load_files(directory, recursive) result = [] if directory list(directory).each do |child_name| dir = get_directory(directory, child_name) if dir if recursive result += load_child_files(directory, child_name, recursive) end else result += load_file(read_file(directory, child_name), child_name) end end end result end def self.load_file(value, name) [{ :name => name, :path => name, :checksum => Digest::MD5.hexdigest(value), :specificity => 'default' }] end def self.set_specificity(files) files.each do |file| parts = file[:path].split('/') raise "Only directories are allowed directly under templates or files: #{file[:path]}" if parts.size == 2 file[:specificity] = parts[1] end end end end CookbookData = ChefData::CookbookData end chef-zero-4.5.0/lib/chef_zero/chef_data/acl_path.rb0000644000004100000410000001207112654006514022146 0ustar www-datawww-datamodule ChefZero module ChefData # Manages translations between REST and ACL data paths # and parent paths. # # Suggestions # - make /organizations/ORG/_acl and deprecate organization/_acl and organizations/_acl # - add endpoints for /containers/(users|organizations|containers)(/_acl) # - add PUT for */_acl # - add endpoints for /organizations/ORG/data/containers and /organizations/ORG/cookbooks/containers # - sane, fully documented ACL model # - sane inheritance / override model: if actors or groups are explicitly # specified on X, they are not inherited from X's parent # - stop adding pivotal to acls (he already has access to what he needs) module AclPath ORG_DATA_TYPES = %w(clients cookbook_artifacts cookbooks containers data environments groups nodes policies policy_groups roles sandboxes) TOP_DATA_TYPES = %w(containers organizations users) # ACL data paths for a partition are: # / -> /acls/root # /TYPE -> /acls/containers/TYPE # /TYPE/NAME -> /acls/TYPE/NAME # # The root partition "/" has its own acls, so it looks like this: # # / -> /acls/root # /users -> /acls/containers/users # /organizations -> /acls/containers/organizations # /users/schlansky -> /acls/users/schlansky # # Each organization is its own partition, so it looks like this: # # /organizations/blah -> /organizations/blah/acls/root # /organizations/blah/roles -> /organizations/blah/acls/containers/roles # /organizations/blah/roles/web -> /organizations/blah/acls/roles/web # /organizations/ORG is its own partition. ACLs for anything under it follow # This method takes a Chef REST path and returns the chef-zero path # used to look up the ACL. If an object does not have an ACL directly, # it will return nil. Paths like /organizations/ORG/data/bag/item will # return nil, because it is the parent path (data/bag) that has an ACL. def self.get_acl_data_path(path) # Things under organizations have their own acls hierarchy if path[0] == 'organizations' && path.size >= 2 under_org = partition_acl_data_path(path[2..-1], ORG_DATA_TYPES) if under_org path[0..1] + under_org end else partition_acl_data_path(path, TOP_DATA_TYPES) end end # # Reverse transform from acl_data_path to path. # /acls/root -> / # /acls/** -> /** # /organizations/ORG/acls/root -> /organizations/ORG # /organizations/ORG/acls/** -> /organizations/ORG/** # # This means that /acls/containers/nodes maps to # /containers/nodes, not /nodes. # def self.get_object_path(acl_data_path) if acl_data_path[0] == 'acls' if acl_data_path[1] == 'root' [] else acl_data_path[1..-1] end elsif acl_data_path[0] == 'organizations' && acl_data_path[2] == 'acls' if acl_data_path[3] == 'root' acl_data_path[0..1] else acl_data_path[0..1] + acl_data_path[3..-1] end end end # Method *assumes* acl_data_path is valid. # /organizations/BLAH's parent is /organizations # # An example traversal up the whole tree: # /organizations/foo/acls/nodes/mario -> # /organizations/foo/acls/containers/nodes -> # /organizations/foo/acls/containers/containers -> # /organizations/foo/acls/root -> # /acls/containers/organizations -> # /acls/containers/containers -> # /acls/root -> # nil def self.parent_acl_data_path(acl_data_path) if acl_data_path[0] == 'organizations' under_org = partition_parent_acl_data_path(acl_data_path[2..-1]) if under_org acl_data_path[0..1] + under_org else # ACL data path is /organizations/X/acls/root; therefore parent is "/organizations" [ 'acls', 'containers', 'organizations' ] end else partition_parent_acl_data_path(acl_data_path) end end private # /acls/root -> nil # /acls/containers/containers -> /acls/root # /acls/TYPE/X -> /acls/containers/TYPE # # Method *assumes* acl_data_path is valid. # Returns nil if the path is /acls/root def self.partition_parent_acl_data_path(acl_data_path) if acl_data_path.size == 3 if acl_data_path == %w(acls containers containers) [ 'acls', 'root' ] else [ 'acls', 'containers', acl_data_path[1]] end else nil end end def self.partition_acl_data_path(path, data_types) if path.size == 0 [ 'acls', 'root'] elsif data_types.include?(path[0]) if path.size == 0 [ 'acls', 'containers', path[0] ] elsif path.size == 2 [ 'acls', path[0], path[1] ] end end end end end end chef-zero-4.5.0/lib/chef_zero/chef_data/data_normalizer.rb0000644000004100000410000001716012654006514023552 0ustar www-datawww-datarequire 'chef_zero' require 'chef_zero/rest_base' require 'chef_zero/chef_data/default_creator' module ChefZero module ChefData class DataNormalizer def self.normalize_acls(acls) ChefData::DefaultCreator::PERMISSIONS.each do |perm| acls[perm] ||= {} (acls[perm]['actors'] ||= []).uniq! # this gets doubled sometimes, for reasons. acls[perm]['groups'] ||= [] end acls end def self.normalize_client(client, name, orgname = nil) client['name'] ||= name client['clientname'] ||= name client['admin'] = !!client['admin'] if client.has_key?('admin') client['public_key'] ||= PUBLIC_KEY client['orgname'] ||= orgname client['validator'] ||= false client['validator'] = !!client['validator'] client['json_class'] ||= "Chef::ApiClient" client['chef_type'] ||= "client" client end def self.normalize_container(container, name) container.delete('id') container['containername'] = name container['containerpath'] = name container end def self.normalize_user(user, name, identity_keys, osc_compat, method=nil) user[identity_keys.first] ||= name user['public_key'] ||= PUBLIC_KEY user['admin'] ||= false user['admin'] = !!user['admin'] user['openid'] ||= nil if !osc_compat if method == 'GET' user.delete('admin') user.delete('password') user.delete('openid') end user['email'] ||= nil user['first_name'] ||= nil user['last_name'] ||= nil end user end def self.normalize_data_bag_item(data_bag_item, data_bag_name, id, method) if method == 'DELETE' # TODO SERIOUSLY, WHO DOES THIS MANY EXCEPTIONS IN THEIR INTERFACE if !(data_bag_item['json_class'] == 'Chef::DataBagItem' && data_bag_item['raw_data']) data_bag_item['id'] ||= id data_bag_item = { 'raw_data' => data_bag_item } data_bag_item['chef_type'] ||= 'data_bag_item' data_bag_item['json_class'] ||= 'Chef::DataBagItem' data_bag_item['data_bag'] ||= data_bag_name data_bag_item['name'] ||= "data_bag_item_#{data_bag_name}_#{id}" end else # If it's not already wrapped with raw_data, wrap it. if data_bag_item['json_class'] == 'Chef::DataBagItem' && data_bag_item['raw_data'] data_bag_item = data_bag_item['raw_data'] end # Argh. We don't do this on GET, but we do on PUT and POST???? if %w(PUT POST).include?(method) data_bag_item['chef_type'] ||= 'data_bag_item' data_bag_item['data_bag'] ||= data_bag_name end data_bag_item['id'] ||= id end data_bag_item end def self.normalize_cookbook(endpoint, org_prefix, cookbook, name, version, base_uri, method, is_cookbook_artifact=false) # TODO I feel dirty if method != 'PUT' cookbook.each_pair do |key, value| if value.is_a?(Array) value.each do |file| if file.is_a?(Hash) && file.has_key?('checksum') file['url'] ||= endpoint.build_uri(base_uri, org_prefix + ['file_store', 'checksums', file['checksum']]) end end end end cookbook['name'] ||= "#{name}-#{version}" # TODO it feels wrong, but the real chef server doesn't expand 'version', so we don't either. cookbook['frozen?'] ||= false cookbook['metadata'] ||= {} cookbook['metadata']['version'] ||= version # defaults set by the client and not the Server: # metadata[name, description, maintainer, maintainer_email, license] cookbook['metadata']['long_description'] ||= "" cookbook['metadata']['dependencies'] ||= {} cookbook['metadata']['attributes'] ||= {} cookbook['metadata']['recipes'] ||= {} end if is_cookbook_artifact cookbook.delete('json_class') else cookbook['cookbook_name'] ||= name cookbook['json_class'] ||= 'Chef::CookbookVersion' end cookbook['chef_type'] ||= 'cookbook_version' if method == 'MIN' cookbook['metadata'].delete('attributes') cookbook['metadata'].delete('long_description') end cookbook end def self.normalize_environment(environment, name) environment['name'] ||= name environment['description'] ||= '' environment['cookbook_versions'] ||= {} environment['json_class'] ||= "Chef::Environment" environment['chef_type'] ||= "environment" environment['default_attributes'] ||= {} environment['override_attributes'] ||= {} environment end def self.normalize_group(group, name, orgname) group.delete('id') if group['actors'].is_a?(Hash) group['users'] ||= group['actors']['users'] group['clients'] ||= group['actors']['clients'] group['groups'] ||= group['actors']['groups'] group['actors'] = nil end group['users'] ||= [] group['clients'] ||= [] group['actors'] ||= (group['clients'] + group['users']) group['groups'] ||= [] group['orgname'] ||= orgname if orgname group['name'] ||= name group['groupname'] ||= name group['users'].uniq! group['clients'].uniq! group['actors'].uniq! group['groups'].uniq! group end def self.normalize_node(node, name) node['name'] ||= name node['json_class'] ||= 'Chef::Node' node['chef_type'] ||= 'node' node['chef_environment'] ||= '_default' node['override'] ||= {} node['normal'] ||= {} node['default'] ||= {} node['automatic'] ||= {} node['run_list'] ||= [] node['run_list'] = normalize_run_list(node['run_list']) node end def self.normalize_policy(policy, name, revision) policy['name'] ||= name policy['revision_id'] ||= revision policy['run_list'] ||= [] policy['cookbook_locks'] ||= {} policy end def self.normalize_policy_group(policy_group, name) policy_group[name] ||= 'name' policy_group['policies'] ||= {} policy_group end def self.normalize_organization(org, name) org['name'] ||= name org['full_name'] ||= name org['org_type'] ||= 'Business' org['clientname'] ||= "#{name}-validator" org['billing_plan'] ||= 'platform-free' org end def self.normalize_role(role, name) role['name'] ||= name role['description'] ||= '' role['json_class'] ||= 'Chef::Role' role['chef_type'] ||= 'role' role['default_attributes'] ||= {} role['override_attributes'] ||= {} role['run_list'] ||= [] role['run_list'] = normalize_run_list(role['run_list']) role['env_run_lists'] ||= {} role['env_run_lists'].each_pair do |env, run_list| role['env_run_lists'][env] = normalize_run_list(run_list) end role end def self.normalize_run_list(run_list) run_list.map{|item| case item when /^recipe\[.*\]$/ item # explicit recipe when /^role\[.*\]$/ item # explicit role else "recipe[#{item}]" end }.uniq end end end end chef-zero-4.5.0/lib/chef_zero/solr/0000755000004100000410000000000012654006514017126 5ustar www-datawww-datachef-zero-4.5.0/lib/chef_zero/solr/solr_parser.rb0000644000004100000410000001371012654006514022010 0ustar www-datawww-datarequire 'chef_zero/solr/query/binary_operator' require 'chef_zero/solr/query/unary_operator' require 'chef_zero/solr/query/term' require 'chef_zero/solr/query/phrase' require 'chef_zero/solr/query/range_query' require 'chef_zero/solr/query/subquery' module ChefZero module Solr class SolrParser def initialize(query_string) @query_string = query_string @index = 0 end def parse read_expression end # # Tokenization # def peek_token @next_token ||= parse_token end def next_token result = peek_token @next_token = nil result end def parse_token # Skip whitespace skip_whitespace return nil if eof? # Operators operator = peek_operator_token if operator @index+=operator.length operator else # Everything that isn't whitespace or an operator, is part of a term # (characters plus backslashed escaped characters) start_index = @index begin if @query_string[@index] == '\\' @index+=1 end @index+=1 if !eof? end while !eof? && peek_term_token @query_string[start_index..@index-1] end end def skip_whitespace if @query_string[@index] =~ /\s/ whitespace = /\s+/.match(@query_string, @index) || peek @index += whitespace[0].length end end def peek_term_token return nil if @query_string[@index] =~ /\s/ op = peek_operator_token return !op || op == '-' end def peek_operator_token if ['"', '+', '-', '!', '(', ')', '{', '}', '[', ']', '^', ':'].include?(@query_string[@index]) return @query_string[@index] else result = @query_string[@index..@index+1] if ['&&', '||'].include?(result) return result end end nil end def eof? !@next_token && @index >= @query_string.length end # Parse tree creation def read_expression result = read_single_expression # Expression is over when we hit a close paren or eof # (peek_token has the side effect of skipping whitespace for us, so we # really know if we're at eof or not) until peek_token == ')' || eof? operator = peek_token if binary_operator?(operator) next_token else # If 2 terms are next to each other, the default operator is OR operator = 'OR' end next_expression = read_single_expression # Build the operator, taking precedence into account if result.is_a?(Query::BinaryOperator) && binary_operator_precedence(operator) > binary_operator_precedence(result.operator) # a+b*c -> a+(b*c) new_right = Query::BinaryOperator.new(result.right, operator, next_expression) result = Query::BinaryOperator.new(result.left, result.operator, new_right) else # a*b+c -> (a*b)+c result = Query::BinaryOperator.new(result, operator, next_expression) end end result end def parse_error(token, str) raise "Error on token '#{token}' at #{@index} of '#{@query_string}': #{str}" end def read_single_expression token = next_token # If EOF, we have a problem Houston if !token parse_error(nil, "Expected expression!") # If it's an unary operand, build that elsif unary_operator?(token) operand = read_single_expression # TODO We rely on all unary operators having higher precedence than all # binary operators. Check if this is the case. Query::UnaryOperator.new(token, operand) # If it's the start of a phrase, read the terms in the phrase elsif token == '"' # Read terms until close " phrase_terms = [] until (term = next_token) == '"' phrase_terms << Query::Term.new(term) end Query::Phrase.new(phrase_terms) # If it's the start of a range query, build that elsif token == '{' || token == '[' left = next_token parse_error(left, "Expected left term in range query") if !left to = next_token parse_error(left, "Expected TO in range query") if to != "TO" right = next_token parse_error(right, "Expected left term in range query") if !right end_range = next_token parse_error(right, "Expected end range '#{end_range}") if !['}', ']'].include?(end_range) Query::RangeQuery.new(left, right, token == '[', end_range == ']') elsif token == '(' subquery = read_expression close_paren = next_token parse_error(close_paren, "Expected ')'") if close_paren != ')' Query::Subquery.new(subquery) # If it's the end of a closure, raise an exception elsif ['}',']',')'].include?(token) parse_error(token, "Unexpected end paren") # If it's a binary operator, raise an exception elsif binary_operator?(token) parse_error(token, "Unexpected binary operator") # Otherwise it's a term. else term = Query::Term.new(token) if peek_token == ':' Query::BinaryOperator.new(term, next_token, read_single_expression) else term end end end def unary_operator?(token) [ 'NOT', '+', '-' ].include?(token) end def binary_operator?(token) [ 'AND', 'OR', '^', ':'].include?(token) end def binary_operator_precedence(token) case token when '^' 4 when ':' 3 when 'AND' 2 when 'OR' 1 end end DEFAULT_FIELD = 'text' end end end chef-zero-4.5.0/lib/chef_zero/solr/solr_doc.rb0000644000004100000410000000260612654006514021263 0ustar www-datawww-datamodule ChefZero module Solr # This does what expander does, flattening the json doc into keys and values # so that solr can search them. class SolrDoc def initialize(json, id) @json = json @id = id end def [](key) matching_values { |match_key| match_key == key } end def matching_values(&block) result = [] key_values(nil, @json) do |key, value| if block.call(key) result << value.to_s end end # Handle manufactured value(s) if block.call('X_CHEF_id_CHEF_X') result << @id.to_s end result.uniq end private def key_values(key_so_far, value, &block) if value.is_a?(Hash) value.each_pair do |child_key, child_value| block.call(child_key, child_value.to_s) if key_so_far new_key = "#{key_so_far}_#{child_key}" key_values(new_key, child_value, &block) else key_values(child_key, child_value, &block) if child_value.is_a?(Hash) || child_value.is_a?(Array) end end elsif value.is_a?(Array) value.each do |child_value| key_values(key_so_far, child_value, &block) end else block.call(key_so_far || 'text', value.to_s) end end end end end chef-zero-4.5.0/lib/chef_zero/solr/query/0000755000004100000410000000000012654006514020273 5ustar www-datawww-datachef-zero-4.5.0/lib/chef_zero/solr/query/range_query.rb0000644000004100000410000000210612654006514023140 0ustar www-datawww-datamodule ChefZero module Solr module Query class RangeQuery def initialize(from, to, from_inclusive, to_inclusive) @from = from @to = to @from_inclusive = from_inclusive @to_inclusive = to_inclusive end def to_s "#{@from_inclusive ? '[' : '{'}#{@from} TO #{@to}#{@to_inclusive ? ']' : '}'}" end def matches_values?(values) values.any? do |value| unless @from == '*' case @from <=> value when -1 return false when 0 return false if !@from_inclusive end end unless @to == '*' case value <=> @to when 1 return false when 0 return false if !@to_inclusive end end return true end end def matches_doc?(doc) matches_values?(doc[DEFAULT_FIELD]) end DEFAULT_FIELD = "text" end end end end chef-zero-4.5.0/lib/chef_zero/solr/query/unary_operator.rb0000644000004100000410000000220412654006514023667 0ustar www-datawww-datamodule ChefZero module Solr module Query class UnaryOperator def initialize(operator, operand) @operator = operator @operand = operand end def to_s "#{operator} #{operand}" end attr_reader :operator attr_reader :operand def matches_doc?(doc) case @operator when '-' when 'NOT' !operand.matches_doc?(doc) when '+' # TODO This operator uses relevance to eliminate other, unrelated # expressions. +a OR b means "if it has b but not a, don't return it" raise "+ not supported yet, because it is hard." end end def matches_values?(values) case @operator when '-' when 'NOT' !operand.matches_values?(values) when '+' # TODO This operator uses relevance to eliminate other, unrelated # expressions. +a OR b means "if it has b but not a, don't return it" raise "+ not supported yet, because it is hard." end end end end end end chef-zero-4.5.0/lib/chef_zero/solr/query/binary_operator.rb0000644000004100000410000000257312654006514024026 0ustar www-datawww-datamodule ChefZero module Solr module Query class BinaryOperator def initialize(left, operator, right) @left = left @operator = operator @right = right end def to_s "(#{left} #{operator} #{right})" end attr_reader :left attr_reader :operator attr_reader :right def matches_doc?(doc) case @operator when 'AND' left.matches_doc?(doc) && right.matches_doc?(doc) when 'OR' left.matches_doc?(doc) || right.matches_doc?(doc) when '^' left.matches_doc?(doc) when ':' if left.respond_to?(:literal_string) && left.literal_string values = doc[left.literal_string] else values = doc.matching_values { |key| left.matches_values?([key]) } end right.matches_values?(values) end end def matches_values?(values) case @operator when 'AND' left.matches_values?(values) && right.matches_values?(values) when 'OR' left.matches_values?(values) || right.matches_values?(values) when '^' left.matches_values?(values) when ':' raise ": does not work inside a : or term" end end end end end end chef-zero-4.5.0/lib/chef_zero/solr/query/subquery.rb0000644000004100000410000000117312654006514022501 0ustar www-datawww-datamodule ChefZero module Solr module Query class Subquery def initialize(subquery) @subquery = subquery end attr_reader :subquery def to_s "(#{subquery})" end def literal_string subquery.literal_string end def regexp subquery.regexp end def regexp_string subquery.regexp_string end def matches_doc?(doc) subquery.matches_doc?(doc) end def matches_values?(values) subquery.matches_values?(values) end end end end end chef-zero-4.5.0/lib/chef_zero/solr/query/phrase.rb0000644000004100000410000000113212654006514022077 0ustar www-datawww-datarequire 'chef_zero/solr/query/regexpable_query' module ChefZero module Solr module Query class Phrase < RegexpableQuery def initialize(terms) # Phrase is terms separated by whitespace if terms.size == 0 && terms[0].literal_string literal_string = terms[0].literal_string else literal_string = nil end super(terms.map { |term| term.regexp_string }.join("#{NON_WORD_CHARACTER}+"), literal_string) end def to_s "Phrase(\"#{@regexp_string}\")" end end end end end chef-zero-4.5.0/lib/chef_zero/solr/query/term.rb0000644000004100000410000000245512654006514021575 0ustar www-datawww-datarequire 'chef_zero/solr/query/regexpable_query' module ChefZero module Solr module Query class Term < RegexpableQuery def initialize(term) # Get rid of escape characters, turn * and ? into .* and . for regex, and # escape everything that needs escaping literal_string = "" regexp_string = "" index = 0 while index < term.length if term[index] == '*' regexp_string << "#{WORD_CHARACTER}*" literal_string = nil index += 1 elsif term[index] == '?' regexp_string << WORD_CHARACTER literal_string = nil index += 1 elsif term[index] == '~' raise "~ unsupported" else if term[index] == '\\' index = index+1 if index >= term.length raise "Backslash at end of string '#{term}'" end end literal_string << term[index] if literal_string regexp_string << Regexp.escape(term[index]) index += 1 end end super(regexp_string, literal_string) end def to_s "Term(#{regexp_string})" end end end end end chef-zero-4.5.0/lib/chef_zero/solr/query/regexpable_query.rb0000644000004100000410000000150412654006514024163 0ustar www-datawww-datamodule ChefZero module Solr module Query class RegexpableQuery def initialize(regexp_string, literal_string) @regexp_string = regexp_string # Surround the regexp with word boundaries @regexp = Regexp.new("(^|#{NON_WORD_CHARACTER})#{regexp_string}($|#{NON_WORD_CHARACTER})", true) @literal_string = literal_string end attr_reader :literal_string attr_reader :regexp_string attr_reader :regexp def matches_doc?(doc) matches_values?(doc[DEFAULT_FIELD]) end def matches_values?(values) values.any? { |value| !@regexp.match(value).nil? } end DEFAULT_FIELD = "text" WORD_CHARACTER = "[A-Za-z0-9@._':]" NON_WORD_CHARACTER = "[^A-Za-z0-9@._':]" end end end end chef-zero-4.5.0/lib/chef_zero/data_store/0000755000004100000410000000000012654006514020274 5ustar www-datawww-datachef-zero-4.5.0/lib/chef_zero/data_store/memory_store_v2.rb0000644000004100000410000001021212654006514023750 0ustar www-datawww-data# # Author:: John Keiser () # Copyright:: Copyright (c) 2013 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'chef_zero/data_store/data_already_exists_error' require 'chef_zero/data_store/data_not_found_error' require 'chef_zero/data_store/interface_v2' module ChefZero module DataStore class MemoryStoreV2 < ChefZero::DataStore::InterfaceV2 def initialize clear end attr_reader :data def clear @data = {} end def create_dir(path, name, *options) parent = _get(path, options.include?(:recursive)) if parent.has_key?(name) if !options.include?(:recursive) raise DataAlreadyExistsError.new(path + [name]) end else parent[name] = {} end end def create(path, name, data, *options) if !data.is_a?(String) raise "set only works with strings (given data: #{data.inspect})" end parent = _get(path, options.include?(:create_dir)) if parent.has_key?(name) raise DataAlreadyExistsError.new(path + [name]) end parent[name] = data end def get(path, request=nil) value = _get(path) if value.is_a?(Hash) raise "get() called on directory #{path.join('/')}" end value end def set(path, data, *options) if !data.is_a?(String) raise "set only works with strings: #{path} = #{data.inspect}" end # Get the parent parent = _get(path[0..-2], options.include?(:create_dir)) if !options.include?(:create) && !parent[path[-1]] raise DataNotFoundError.new(path) end parent[path[-1]] = data end def delete(path) parent = _get(path[0,path.length-1]) if !parent.has_key?(path[-1]) raise DataNotFoundError.new(path) end if !parent[path[-1]].is_a?(String) raise "delete only works with strings: #{path}" end parent.delete(path[-1]) end def delete_dir(path, *options) parent = _get(path[0,path.length-1]) if !parent.has_key?(path[-1]) raise DataNotFoundError.new(path) end if !parent[path[-1]].is_a?(Hash) raise "delete_dir only works with directories: #{path}" end parent.delete(path[-1]) end def list(path) dir = _get(path) if !dir.is_a? Hash raise "list only works with directories (#{path} = #{dir.class})" end dir.keys.sort end def exists?(path, options = {}) begin value = _get(path) if value.is_a?(Hash) && !options[:allow_dirs] raise "exists? does not work with directories (#{path} = #{value.class})" end return true rescue DataNotFoundError return false end end def exists_dir?(path) begin dir = _get(path) if !dir.is_a? Hash raise "exists_dir? only works with directories (#{path} = #{dir.class})" end return true rescue DataNotFoundError return false end end private def _get(path, create_dir=false) value = @data path.each_with_index do |path_part, index| if !value.has_key?(path_part) if create_dir value[path_part] = {} else raise DataNotFoundError.new(path[0,index+1]) end end value = value[path_part] end value end end end end chef-zero-4.5.0/lib/chef_zero/data_store/memory_store.rb0000644000004100000410000000213112654006514023342 0ustar www-datawww-data# # Author:: John Keiser () # Copyright:: Copyright (c) 2013 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'chef_zero/data_store/v2_to_v1_adapter' require 'chef_zero/data_store/memory_store_v2' require 'chef_zero/data_store/default_facade' module ChefZero module DataStore class MemoryStore < ChefZero::DataStore::V2ToV1Adapter def initialize super @real_store = ChefZero::DataStore::DefaultFacade.new(ChefZero::DataStore::MemoryStoreV2.new, 'chef', true) clear end end end end chef-zero-4.5.0/lib/chef_zero/data_store/interface_v1.rb0000644000004100000410000000352512654006514023174 0ustar www-datawww-datamodule ChefZero module DataStore class InterfaceV1 def interface_version 1 end def clear raise "clear not implemented by class #{self.class}" end # Create a directory. # options is a list of symbols, including: # :recursive - create any parents needed def create_dir(path, name, *options) raise "create_dir not implemented by class #{self.class}" end # Create a file. # options is a list of symbols, including: # :create_dir - create any parents needed def create(path, name, data, *options) raise "create not implemented by class #{self.class}" end # Get a file. def get(path, request=nil) raise "get not implemented by class #{self.class}" end # Set a file's value. # options is a list of symbols, including: # :create - create the file if it does not exist # :create_dir - create the directory if it does not exist def set(path, data, *options) raise "set not implemented by class #{self.class}" end # Delete a file. def delete(path) raise "delete not implemented by class #{self.class}" end # Delete a directory. # options is a list of symbols, including: # :recursive - delete even if empty def delete_dir(path, *options) raise "delete_dir not implemented by class #{self.class}" end # List a directory. def list(path) raise "list not implemented by class #{self.class}" end # Check a file's existence. def exists?(path) raise "exists? not implemented by class #{self.class}" end # Check a directory's existence. def exists_dir?(path) raise "exists_dir? not implemented by class #{self.class}" end end end end chef-zero-4.5.0/lib/chef_zero/data_store/data_already_exists_error.rb0000644000004100000410000000157212654006514026050 0ustar www-datawww-data# # Author:: John Keiser () # Copyright:: Copyright (c) 2013 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'chef_zero/data_store/data_error' module ChefZero module DataStore class DataAlreadyExistsError < DataError def initialize(path, cause = nil) super end end end end chef-zero-4.5.0/lib/chef_zero/data_store/data_not_found_error.rb0000644000004100000410000000156412654006514025024 0ustar www-datawww-data# # Author:: John Keiser () # Copyright:: Copyright (c) 2013 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'chef_zero/data_store/data_error' module ChefZero module DataStore class DataNotFoundError < DataError def initialize(path, cause = nil) super end end end endchef-zero-4.5.0/lib/chef_zero/data_store/data_error.rb0000644000004100000410000000162612654006514022750 0ustar www-datawww-data# # Author:: John Keiser () # Copyright:: Copyright (c) 2013 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # module ChefZero module DataStore class DataError < StandardError def initialize(path, cause = nil) @path = path @cause = cause end attr_reader :path attr_reader :cause end end end chef-zero-4.5.0/lib/chef_zero/data_store/default_facade.rb0000644000004100000410000001063012654006514023530 0ustar www-datawww-datarequire 'chef_zero/data_store/interface_v2' require 'chef_zero/chef_data/default_creator' module ChefZero module DataStore # # The DefaultFacade exists to layer defaults on top of an existing data # store. When you create an org, you just create the directory itself: # the rest of the org (such as environments/_default) will not actually # exist anywhere, but when you get(/organizations/org/environments/_default), # the DefaultFacade will create one for you on the fly. # # acls in particular are instantiated on the fly using this method. # class DefaultFacade < ChefZero::DataStore::InterfaceV2 def initialize(real_store, single_org, osc_compat, superusers = nil) @real_store = real_store @default_creator = ChefData::DefaultCreator.new(self, single_org, osc_compat, superusers) clear end attr_reader :real_store attr_reader :default_creator def clear real_store.clear if real_store.respond_to?(:clear) default_creator.clear end def create_dir(path, name, *options) if default_creator.exists?(path + [ name ]) && !options.include?(:recursive) raise DataAlreadyExistsError.new(path + [name]) end begin real_store.create_dir(path, name, *options) rescue DataNotFoundError if default_creator.exists?(path) real_store.create_dir(path, name, :recursive, *options) else raise end end options_hash = options.last.is_a?(Hash) ? options.last : {} default_creator.created(path + [ name ], options_hash[:requestor], options.include?(:recursive)) end def create(path, name, data, *options) if default_creator.exists?(path + [ name ]) && !options.include?(:create_dir) raise DataAlreadyExistsError.new(path + [name]) end begin real_store.create(path, name, data, *options) rescue DataNotFoundError if default_creator.exists?(path) real_store.create(path, name, data, :create_dir, *options) else raise end end options_hash = options.last.is_a?(Hash) ? options.last : {} default_creator.created(path + [ name ], options_hash[:requestor], options.include?(:create_dir)) end def get(path, request=nil) begin real_store.get(path, request) rescue DataNotFoundError result = default_creator.get(path) if result FFI_Yajl::Encoder.encode(result, :pretty => true) else raise end end end def set(path, data, *options) begin real_store.set(path, data, *options) rescue DataNotFoundError if options.include?(:create_dir) || options.include?(:create) && default_creator.exists?(path[0..-2]) || default_creator.exists?(path) real_store.set(path, data, :create, :create_dir, *options) else raise end end if options.include?(:create) options_hash = options.last.is_a?(Hash) ? options.last : {} default_creator.created(path, options_hash[:requestor], options.include?(:create_dir)) end end def delete(path, *options) deleted = default_creator.deleted(path) begin real_store.delete(path) rescue DataNotFoundError if !deleted raise end end end def delete_dir(path, *options) deleted = default_creator.deleted(path) begin real_store.delete_dir(path, *options) rescue DataNotFoundError if !deleted raise end end end def list(path) default_results = default_creator.list(path) begin real_results = real_store.list(path) if default_results (real_results + default_results).uniq else real_results end rescue DataNotFoundError if default_results default_results else raise end end end def exists?(path) real_store.exists?(path) || default_creator.exists?(path) end def exists_dir?(path) real_store.exists_dir?(path) || default_creator.exists?(path) end end end end chef-zero-4.5.0/lib/chef_zero/data_store/v2_to_v1_adapter.rb0000644000004100000410000000516012654006514023762 0ustar www-datawww-data# # Author:: John Keiser () # Copyright:: Copyright (c) 2014 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'chef_zero/data_store/interface_v1' module ChefZero module DataStore class V2ToV1Adapter < ChefZero::DataStore::InterfaceV1 def initialize @single_org = 'chef' end attr_reader :real_store attr_reader :single_org def clear real_store.clear real_store.create_dir([ 'organizations' ], single_org, :recursive) end def create_dir(path, name, *options) fix_exceptions do real_store.create_dir(fix_path(path), name, *options) end end def create(path, name, data, *options) fix_exceptions do real_store.create(fix_path(path), name, data, *options) end end def get(path, request=nil) fix_exceptions do real_store.get(fix_path(path), request) end end def set(path, data, *options) fix_exceptions do real_store.set(fix_path(path), data, *options) end end def delete(path) fix_exceptions do real_store.delete(fix_path(path)) end end def delete_dir(path, *options) fix_exceptions do real_store.delete_dir(fix_path(path), *options) end end def list(path) fix_exceptions do real_store.list(fix_path(path)) end end def exists?(path) fix_exceptions do real_store.exists?(fix_path(path)) end end def exists_dir?(path) fix_exceptions do real_store.exists_dir?(fix_path(path)) end end protected def fix_exceptions begin yield rescue DataAlreadyExistsError => e raise DataAlreadyExistsError.new(e.path[2..-1], e) rescue DataNotFoundError => e raise DataNotFoundError.new(e.path[2..-1], e) end end def fix_path(path) [ 'organizations', single_org ] + path end end end end chef-zero-4.5.0/lib/chef_zero/data_store/interface_v2.rb0000644000004100000410000000105012654006514023164 0ustar www-datawww-datarequire 'chef_zero/data_store/interface_v1' module ChefZero module DataStore # V2 assumes paths starting with /organizations/ORGNAME. It also REQUIRES that # new organizations have these defaults: # chef-validator client: '{ "validator": true }', # chef-webui client: '{ "admin": true }' # _default environment: '{ "description": "The default Chef environment" }' # admin user: '{ "admin": "true" }' class InterfaceV2 < ChefZero::DataStore::InterfaceV1 def interface_version 2 end end end end chef-zero-4.5.0/lib/chef_zero/data_store/v1_to_v2_adapter.rb0000644000004100000410000001043412654006514023762 0ustar www-datawww-datarequire 'chef_zero/data_store/interface_v2' module ChefZero module DataStore class V1ToV2Adapter < ChefZero::DataStore::InterfaceV2 def initialize(real_store, single_org, options = {}) @real_store = real_store @single_org = single_org @options = options clear end attr_reader :real_store attr_reader :single_org def clear real_store.clear if real_store.respond_to?(:clear) end def create_dir(path, name, *options) raise DataNotFoundError.new(path) if skip_organizations?(path) raise "Cannot create #{name} at #{path} with V1ToV2Adapter: only handles a single org named #{single_org}." if skip_organizations?(path, name) raise DataAlreadyExistsError.new(path + [ name ]) if path.size < 2 fix_exceptions do real_store.create_dir(path[2..-1], name, *options) end end def create(path, name, data, *options) raise DataNotFoundError.new(path) if skip_organizations?(path) raise "Cannot create #{name} at #{path} with V1ToV2Adapter: only handles a single org named #{single_org}." if skip_organizations?(path, name) raise DataAlreadyExistsError.new(path + [ name ]) if path.size < 2 fix_exceptions do real_store.create(path[2..-1], name, data, *options) end end def get(path, request=nil) raise DataNotFoundError.new(path) if skip_organizations?(path) fix_exceptions do # Make it so build_uri will include /organizations/ORG inside the V1 data store if request && request.rest_base_prefix.size == 0 old_base_uri = request.base_uri request.base_uri = File.join(request.base_uri, 'organizations', single_org) end begin real_store.get(path[2..-1], request) ensure request.base_uri = old_base_uri if request && request.rest_base_prefix.size == 0 end end end def set(path, data, *options) raise DataNotFoundError.new(path) if skip_organizations?(path) fix_exceptions do real_store.set(path[2..-1], data, *options) end end def delete(path, *options) raise DataNotFoundError.new(path) if skip_organizations?(path) && !options.include?(:recursive) fix_exceptions do real_store.delete(path[2..-1]) end end def delete_dir(path, *options) raise DataNotFoundError.new(path) if skip_organizations?(path) && !options.include?(:recursive) fix_exceptions do real_store.delete_dir(path[2..-1], *options) end end def list(path) raise DataNotFoundError.new(path) if skip_organizations?(path) if path == [] [ 'organizations' ] elsif path == [ 'organizations' ] [ single_org ] else fix_exceptions do real_store.list(path[2..-1]) end end end def exists?(path) return false if skip_organizations?(path) fix_exceptions do real_store.exists?(path[2..-1]) end end def exists_dir?(path) return false if skip_organizations?(path) if path == [] true elsif path == [ 'organizations' ] || path == [ 'users' ] true else return false if skip_organizations?(path) fix_exceptions do real_store.exists_dir?(path[2..-1]) end end end private def fix_exceptions begin yield rescue DataAlreadyExistsError => e err = DataAlreadyExistsError.new([ 'organizations', single_org ] + e.path, e) err.set_backtrace(e.backtrace) raise err rescue DataNotFoundError => e err = DataNotFoundError.new([ 'organizations', single_org ] + e.path, e) err.set_backtrace(e.backtrace) raise e end end def skip_organizations?(path, name = nil) if path == [] false elsif path[0] == 'organizations' if path.size == 1 false elsif path.size >= 2 && path[1] != single_org true else false end else true end end end end end chef-zero-4.5.0/lib/chef_zero/data_store/raw_file_store.rb0000644000004100000410000001002512654006514023623 0ustar www-datawww-data# # Author:: John Keiser () # Copyright:: Copyright (c) 2013 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'chef_zero/data_store/data_already_exists_error' require 'chef_zero/data_store/data_not_found_error' require 'chef_zero/data_store/interface_v2' require 'fileutils' module ChefZero module DataStore class RawFileStore < ChefZero::DataStore::InterfaceV2 def initialize(root, destructible = false) @root = root @destructible = destructible end attr_reader :root attr_reader :destructible def path_to(path, name=nil) if name File.join(root, *path, name) else File.join(root, *path) end end def clear if destructible Dir.entries(root).each do |entry| next if entry == '.' || entry == '..' FileUtils.rm_rf(Path.join(root, entry)) end end end def create_dir(path, name, *options) real_path = path_to(path, name) if options.include?(:recursive) FileUtils.mkdir_p(real_path) else begin Dir.mkdir(File.join(path, name)) rescue Errno::ENOENT raise DataNotFoundError.new(path) rescue Errno::EEXIST raise DataAlreadyExistsError.new(path + [name]) end end end def create(path, name, data, *options) if options.include?(:create_dir) FileUtils.mkdir_p(path_to(path)) end begin File.open(path_to(path, name), File::WRONLY|File::CREAT|File::EXCL|File::BINARY, :internal_encoding => nil) do |file| file.write data end rescue Errno::ENOENT raise DataNotFoundError.new(path) rescue Errno::EEXIST raise DataAlreadyExistsError.new(path + [name]) end end def get(path, request=nil) begin return IO.read(path_to(path)) rescue Errno::ENOENT raise DataNotFoundError.new(path) end end def set(path, data, *options) if options.include?(:create_dir) FileUtils.mkdir_p(path_to(path[0..-2])) end begin mode = File::WRONLY|File::TRUNC|File::BINARY if options.include?(:create) mode |= File::CREAT end File.open(path_to(path), mode, :internal_encoding => nil) do |file| file.write data end rescue Errno::ENOENT raise DataNotFoundError.new(path) end end def delete(path) begin File.delete(path_to(path)) rescue Errno::ENOENT raise DataNotFoundError.new(path) end end def delete_dir(path, *options) if options.include?(:recursive) if !File.exist?(path_to(path)) raise DataNotFoundError.new(path) end FileUtils.rm_rf(path_to(path)) else begin Dir.rmdir(path_to(path)) rescue Errno::ENOENT raise DataNotFoundError.new(path) end end end def list(path) begin Dir.entries(path_to(path)).select { |entry| entry != '.' && entry != '..' }.to_a rescue Errno::ENOENT raise DataNotFoundError.new(path) end end def exists?(path, options = {}) File.exists?(path_to(path)) end def exists_dir?(path) File.exists?(path_to(path)) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/0000755000004100000410000000000012654006514020152 5ustar www-datawww-datachef-zero-4.5.0/lib/chef_zero/endpoints/organization_association_request_endpoint.rb0000644000004100000410000000134612654006514031253 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' module ChefZero module Endpoints # /organizations/ORG/association_requests/ID class OrganizationAssociationRequestEndpoint < RestBase def delete(request) orgname = request.rest_path[1] id = request.rest_path[3] if id !~ /(.+)-#{orgname}$/ raise HttpErrorResponse.new(404, "Invalid ID #{id}. Must be of the form username-#{orgname}") end username = $1 path = request.rest_path[0..-2] + [username] data = FFI_Yajl::Parser.parse(get_data(request, path), :create_additions => false) delete_data(request, path) json_response(200, { "id" => id, "username" => username }) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/policies_endpoint.rb0000644000004100000410000000134512654006514024211 0ustar www-datawww-datarequire 'chef_zero/chef_data/data_normalizer' module ChefZero module Endpoints # /organizations/ORG/policies class PoliciesEndpoint < RestBase # GET /organizations/ORG/policies def get(request) response_data = {} policy_names = list_data(request) policy_names.each do |policy_name| policy_path = request.rest_path + [policy_name] policy_uri = build_uri(request.base_uri, policy_path) revisions = list_data(request, policy_path + ["revisions"]) response_data[policy_name] = { uri: policy_uri, revisions: hashify_list(revisions) } end return json_response(200, response_data) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/user_association_request_endpoint.rb0000644000004100000410000000324112654006514027521 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' module ChefZero module Endpoints # /users/USER/association_requests/ID class UserAssociationRequestEndpoint < RestBase def put(request) username = request.rest_path[1] id = request.rest_path[3] if id !~ /^#{username}-(.+)/ raise RestErrorResponse.new(400, "Association request #{id} is invalid. Must be #{username}-orgname.") end orgname = $1 json = FFI_Yajl::Parser.parse(request.body, :create_additions => false) association_request_path = [ 'organizations', orgname, 'association_requests', username ] if json['response'] == 'accept' users = get_data(request, [ 'organizations', orgname, 'groups', 'users' ]) users = FFI_Yajl::Parser.parse(users, :create_additions => false) delete_data(request, association_request_path) create_data(request, [ 'organizations', orgname, 'users' ], username, '{}') # Add the user to the users group if it isn't already there if !users['users'] || !users['users'].include?(username) users['users'] ||= [] users['users'] |= [ username ] set_data(request, [ 'organizations', orgname, 'groups', 'users' ], FFI_Yajl::Encoder.encode(users, :pretty => true)) end elsif json['response'] == 'reject' delete_data(request, association_request_path) else raise RestErrorResponse.new(400, "response parameter was missing or set to the wrong value (must be accept or reject)") end json_response(200, { 'organization' => { 'name' => orgname } }) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/environment_role_endpoint.rb0000644000004100000410000000233112654006514025763 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/endpoints/cookbooks_base' module ChefZero module Endpoints # /environments/NAME/roles/NAME # /roles/NAME/environments/NAME class EnvironmentRoleEndpoint < CookbooksBase def get(request) # 404 if environment does not exist if request.rest_path[2] == 'environments' environment_path = request.rest_path[0..1] + request.rest_path[2..3] role_path = request.rest_path[0..1] + request.rest_path[4..5] else environment_path = request.rest_path[0..1] + request.rest_path[4..5] role_path = request.rest_path[0..1] + request.rest_path[2..3] end # Verify that the environment exists get_data(request, environment_path) role = FFI_Yajl::Parser.parse(get_data(request, role_path), :create_additions => false) environment_name = environment_path[3] if environment_name == '_default' run_list = role['run_list'] else if role['env_run_lists'] run_list = role['env_run_lists'][environment_name] else run_list = nil end end json_response(200, { 'run_list' => run_list }) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/node_identifiers_endpoint.rb0000644000004100000410000000133112654006514025707 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' require 'uuidtools' module ChefZero module Endpoints # /organizations/NAME/nodes/NAME/_identifiers class NodeIdentifiersEndpoint < RestBase def get(request) if get_data(request, request.rest_path[0..3]) result = { :id => UUIDTools::UUID.parse_raw(request.rest_path[0..4].to_s).to_s.gsub('-',''), :authz_id => '0'*32, :org_id => UUIDTools::UUID.parse_raw(request.rest_path[0..1].to_s).to_s.gsub('-','') } json_response(200, result) else raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") end end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/cookbooks_base.rb0000644000004100000410000000441712654006514023470 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' require 'chef_zero/chef_data/data_normalizer' module ChefZero module Endpoints # Common code for endpoints that return cookbook lists class CookbooksBase < RestBase def format_cookbooks_list(request, cookbooks_list, constraints = {}, num_versions = nil) results = {} filter_cookbooks(cookbooks_list, constraints, num_versions) do |name, versions| versions_list = versions.map do |version| { 'url' => build_uri(request.base_uri, request.rest_path[0..1] + ['cookbooks', name, version]), 'version' => version } end results[name] = { 'url' => build_uri(request.base_uri, request.rest_path[0..1] + ['cookbooks', name]), 'versions' => versions_list } end results end def all_cookbooks_list(request) result = {} # Race conditions exist here (if someone deletes while listing). I don't care. data_store.list(request.rest_path[0..1] + ['cookbooks']).each do |name| result[name] = data_store.list(request.rest_path[0..1] + ['cookbooks', name]) end result end def filter_cookbooks(cookbooks_list, constraints = {}, num_versions = nil) cookbooks_list.keys.sort.each do |name| constraint = Gem::Requirement.new(constraints[name]) versions = [] cookbooks_list[name].sort_by { |version| Gem::Version.new(version.dup) }.reverse.each do |version| break if num_versions && versions.size >= num_versions if constraint.satisfied_by?(Gem::Version.new(version.dup)) versions << version end end yield [name, versions] end end def recipe_names(cookbook_name, cookbook) result = [] if cookbook['recipes'] cookbook['recipes'].each do |recipe| if recipe['path'] == "recipes/#{recipe['name']}" && recipe['name'][-3..-1] == '.rb' if recipe['name'] == 'default.rb' result << cookbook_name end result << "#{cookbook_name}::#{recipe['name'][0..-4]}" end end end result end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/search_endpoint.rb0000644000004100000410000001560712654006514023655 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/endpoints/rest_object_endpoint' require 'chef_zero/chef_data/data_normalizer' require 'chef_zero/rest_error_response' require 'chef_zero/solr/solr_parser' require 'chef_zero/solr/solr_doc' module ChefZero module Endpoints # /search/INDEX class SearchEndpoint < RestBase def get(request) orgname = request.rest_path[1] results = search(request, orgname) results['rows'] = results['rows'].map { |name,uri,value,search_value| value } json_response(200, results) end def post(request) orgname = request.rest_path[1] full_results = search(request, orgname) keys = FFI_Yajl::Parser.parse(request.body, :create_additions => false) partial_results = full_results['rows'].map do |name, uri, doc, search_value| data = {} keys.each_pair do |key, path| if path.size > 0 value = search_value path.each do |path_part| value = value[path_part] if !value.nil? end data[key] = value else data[key] = nil end end { 'url' => uri, 'data' => data } end json_response(200, { 'rows' => partial_results, 'start' => full_results['start'], 'total' => full_results['total'] }) end private def search_container(request, index, orgname) relative_parts, normalize_proc = case index when 'client' [ ['clients'], Proc.new { |client, name| ChefData::DataNormalizer.normalize_client(client, name, orgname) } ] when 'node' [ ['nodes'], Proc.new { |node, name| ChefData::DataNormalizer.normalize_node(node, name) } ] when 'environment' [ ['environments'], Proc.new { |environment, name| ChefData::DataNormalizer.normalize_environment(environment, name) } ] when 'role' [ ['roles'], Proc.new { |role, name| ChefData::DataNormalizer.normalize_role(role, name) } ] else [ ['data', index], Proc.new { |data_bag_item, id| ChefData::DataNormalizer.normalize_data_bag_item(data_bag_item, index, id, 'DELETE') } ] end [ request.rest_path[0..1] + relative_parts, normalize_proc ] end def expand_for_indexing(value, index, id) if index == 'node' result = {} deep_merge!(value['default'] || {}, result) deep_merge!(value['normal'] || {}, result) deep_merge!(value['override'] || {}, result) deep_merge!(value['automatic'] || {}, result) result['recipe'] = [] result['role'] = [] if value['run_list'] value['run_list'].each do |run_list_entry| if run_list_entry =~ /^(recipe|role)\[(.*)\]/ result[$1] << $2 end end end value.each_pair do |key, value| result[key] = value unless %w(default normal override automatic).include?(key) end result elsif !%w(client environment role).include?(index) ChefData::DataNormalizer.normalize_data_bag_item(value, index, id, 'GET') else value end end def search(request, orgname = nil) # Extract parameters index = request.rest_path[3] query_string = request.query_params['q'] || '*:*' solr_query = ChefZero::Solr::SolrParser.new(query_string).parse sort_string = request.query_params['sort'] start = request.query_params['start'] start = start.to_i if start rows = request.query_params['rows'] rows = rows.to_i if rows # Get the search container container, expander = search_container(request, index, orgname) # Search! result = [] list_data(request, container).each do |name| value = get_data(request, container + [name]) expanded = expander.call(FFI_Yajl::Parser.parse(value, :create_additions => false), name) result << [ name, build_uri(request.base_uri, container + [name]), expanded, expand_for_indexing(expanded, index, name) ] end result = result.select do |name, uri, value, search_value| solr_query.matches_doc?(ChefZero::Solr::SolrDoc.new(search_value, name)) end total = result.size # Sort if sort_string sort_key, sort_order = sort_string.split(/\s+/, 2) result = result.sort_by { |name,uri,value,search_value| ChefZero::Solr::SolrDoc.new(search_value, name)[sort_key] } result = result.reverse if sort_order == "DESC" end # Paginate if start result = result[start..start+(rows||-1)] end { 'rows' => result, 'start' => start || 0, 'total' => total } end private # Deep Merge core documentation. # deep_merge! method permits merging of arbitrary child elements. The two top level # elements must be hashes. These hashes can contain unlimited (to stack limit) levels # of child elements. These child elements to not have to be of the same types. # Where child elements are of the same type, deep_merge will attempt to merge them together. # Where child elements are not of the same type, deep_merge will skip or optionally overwrite # the destination element with the contents of the source element at that level. # So if you have two hashes like this: # source = {:x => [1,2,3], :y => 2} # dest = {:x => [4,5,'6'], :y => [7,8,9]} # dest.deep_merge!(source) # Results: {:x => [1,2,3,4,5,'6'], :y => 2} # By default, "deep_merge!" will overwrite any unmergeables and merge everything else. # To avoid this, use "deep_merge" (no bang/exclamation mark) def deep_merge!(source, dest) # if dest doesn't exist, then simply copy source to it if dest.nil? dest = source; return dest end case source when nil dest when Hash source.each do |src_key, src_value| if dest.kind_of?(Hash) if dest[src_key] dest[src_key] = deep_merge!(src_value, dest[src_key]) else # dest[src_key] doesn't exist so we take whatever source has dest[src_key] = src_value end else # dest isn't a hash, so we overwrite it completely dest = source end end when Array if dest.kind_of?(Array) dest = dest | source else dest = source end when String dest = source else # src_hash is not an array or hash, so we'll have to overwrite dest dest = source end dest end # deep_merge! end end end chef-zero-4.5.0/lib/chef_zero/endpoints/actors_endpoint.rb0000644000004100000410000000461512654006514023700 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/endpoints/rest_list_endpoint' module ChefZero module Endpoints # /users, /organizations/ORG/clients or /organizations/ORG/users class ActorsEndpoint < RestListEndpoint def get(request) response = super(request) if request.query_params['email'] results = FFI_Yajl::Parser.parse(response[2], :create_additions => false) new_results = {} results.each do |name, url| record = get_data(request, request.rest_path + [ name ], :nil) if record record = FFI_Yajl::Parser.parse(record, :create_additions => false) new_results[name] = url if record['email'] == request.query_params['email'] end end response[2] = FFI_Yajl::Encoder.encode(new_results, :pretty => true) end if request.query_params['verbose'] results = FFI_Yajl::Parser.parse(response[2], :create_additions => false) results.each do |name, url| record = get_data(request, request.rest_path + [ name ], :nil) if record record = FFI_Yajl::Parser.parse(record, :create_additions => false) record = ChefData::DataNormalizer.normalize_user(record, name, identity_keys, server.options[:osc_compat]) results[name] = record end end response[2] = FFI_Yajl::Encoder.encode(results, :pretty => true) end response end def post(request) # First, find out if the user actually posted a public key. If not, make # one. request_body = FFI_Yajl::Parser.parse(request.body, :create_additions => false) public_key = request_body['public_key'] if !public_key private_key, public_key = server.gen_key_pair request_body['public_key'] = public_key request.body = FFI_Yajl::Encoder.encode(request_body, :pretty => true) end result = super(request) if result[0] == 201 # If we generated a key, stuff it in the response. response = FFI_Yajl::Parser.parse(result[2], :create_additions => false) response['private_key'] = private_key if private_key response['public_key'] = public_key unless request.rest_path[0] == 'users' json_response(201, response) else result end end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/organizations_endpoint.rb0000644000004100000410000000411312654006514025265 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' require 'uuidtools' module ChefZero module Endpoints # /organizations class OrganizationsEndpoint < RestBase def get(request) result = {} data_store.list(request.rest_path).each do |name| result[name] = build_uri(request.base_uri, request.rest_path + [name]) end json_response(200, result) end def post(request) contents = FFI_Yajl::Parser.parse(request.body, :create_additions => false) name = contents['name'] full_name = contents['full_name'] if name.nil? error(400, "Must specify 'name' in JSON") elsif full_name.nil? error(400, "Must specify 'full_name' in JSON") elsif exists_data_dir?(request, request.rest_path + [ name ]) error(409, "Organization already exists") else create_data_dir(request, request.rest_path, name, :requestor => request.requestor) org = { "guid" => UUIDTools::UUID.random_create.to_s.gsub('-', ''), "assigned_at" => Time.now.to_s }.merge(contents) org_path = request.rest_path + [ name ] set_data(request, org_path + [ 'org' ], FFI_Yajl::Encoder.encode(org, :pretty => true)) if server.generate_real_keys? # Create the validator client validator_name = "#{name}-validator" validator_path = org_path + [ 'clients', validator_name ] private_key, public_key = server.gen_key_pair validator = FFI_Yajl::Encoder.encode({ 'validator' => true, 'public_key' => public_key }, :pretty => true) set_data(request, validator_path, validator) end json_response(201, { "uri" => "#{build_uri(request.base_uri, org_path)}", "name" => name, "org_type" => org["org_type"], "full_name" => full_name, "clientname" => validator_name, "private_key" => private_key }) end end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/cookbook_artifacts_endpoint.rb0000644000004100000410000000200512654006514026242 0ustar www-datawww-datarequire 'chef_zero/chef_data/data_normalizer' module ChefZero module Endpoints class CookbookArtifactsEndpoint < RestBase # GET /organizations/ORG/cookbook_artifacts def get(request) data = {} artifacts = begin list_data(request) rescue Exception => e if e.response_code == 404 return already_json_response(200, "{}") end end artifacts.each do |cookbook_artifact| cookbook_url = build_uri(request.base_uri, request.rest_path + [cookbook_artifact]) versions = [] list_data(request, request.rest_path + [cookbook_artifact]).each do |identifier| artifact_url = build_uri(request.base_uri, request.rest_path + [cookbook_artifact, identifier]) versions << { url: artifact_url, identifier: identifier } end data[cookbook_artifact] = { url: cookbook_url, versions: versions } end return json_response(200, data) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/policy_group_policy_endpoint.rb0000644000004100000410000000761212654006514026477 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' require 'chef_zero/chef_data/data_normalizer' module ChefZero module Endpoints # /organizations/ORG/policy_groups/GROUP/policies/NAME # # in the data store, this REST path actually stores the revision ID of ${policy_name} that's currently # associated with ${policy_group}. class PolicyGroupPolicyEndpoint < RestBase # GET /organizations/ORG/policy_groups/GROUP/policies/NAME def get(request) policy_name = request.rest_path[5] # fetch /organizations/{organization}/policies/{policy_name}/revisions/{revision_id} revision_id = parse_json(get_data(request)) result = get_data(request, request.rest_path[0..1] + ["policies", policy_name, "revisions", revision_id]) result = ChefData::DataNormalizer.normalize_policy(parse_json(result), policy_name, revision_id) json_response(200, result) end # Create or update the policy document for the given policy group and policy name. If no policy group # with the given name exists, it will be created. If no policy with the given revision_id exists, it # will be created from the document in the request body. If a policy with that revision_id exists, the # Chef Server simply associates that revision id with the given policy group. When successful, the # document that was created or updated is returned. ## MANDATORY FIELDS AND FORMATS # * `revision_id`: String; Must be < 255 chars, matches /^[\-[:alnum:]_\.\:]+$/ # * `name`: String; Must match name in URI; Must be <= 255 chars, matches /^[\-[:alnum:]_\.\:]+$/ # * `run_list`: Array # * `run_list[i]`: Fully Qualified Recipe Run List Item # * `cookbook_locks`: JSON Object # * `cookbook_locks(key)`: CookbookName # * `cookbook_locks[item]`: JSON Object, mandatory keys: "identifier", "dotted_decimal_identifier" # * `cookbook_locks[item]["identifier"]`: varchar(255) ? # * `cookbook_locks[item]["dotted_decimal_identifier"]` ChefCompatibleVersionNumber # PUT /organizations/ORG/policy_groups/GROUP/policies/NAME def put(request) policyfile_data = parse_json(request.body) policy_name = request.rest_path[5] revision_id = policyfile_data["revision_id"] # If the policy revision being submitted does not exist, create it. # Storage: /organizations/ORG/policies/POLICY/revisions/REVISION policyfile_path = request.rest_path[0..1] + ["policies", policy_name, "revisions", revision_id] if !exists_data?(request, policyfile_path) create_data(request, policyfile_path[0..-2], revision_id, request.body, :create_dir) end # if named policy exists and the given revision ID exists, associate the revision ID with the policy # group. # Storage: /organizations/ORG/policies/POLICY/revisions/REVISION response_code = exists_data?(request) ? 200 : 201 set_data(request, nil, to_json(revision_id), :create, :create_dir) already_json_response(response_code, request.body) end # DELETE /organizations/ORG/policy_groups/GROUP/policies/NAME def delete(request) # Save the existing association. current_revision_id = parse_json(get_data(request)) # delete the association. delete_data(request) # return the full policy document at the no-longer-associated revision. policy_name = request.rest_path[5] policy_path = request.rest_path[0..1] + ["policies", policy_name, "revisions", current_revision_id] full_policy_doc = parse_json(get_data(request, policy_path)) full_policy_doc = ChefData::DataNormalizer.normalize_policy(full_policy_doc, policy_name, current_revision_id) return json_response(200, full_policy_doc) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/group_endpoint.rb0000644000004100000410000000120112654006514023525 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/endpoints/rest_object_endpoint' require 'chef_zero/chef_data/data_normalizer' module ChefZero module Endpoints # /organizations/ORG/groups/NAME class GroupEndpoint < RestObjectEndpoint def initialize(server) super(server, %w(id groupname)) end def populate_defaults(request, response_json) group = FFI_Yajl::Parser.parse(response_json, :create_additions => false) group = ChefData::DataNormalizer.normalize_group(group, request.rest_path[3], request.rest_path[1]) FFI_Yajl::Encoder.encode(group, :pretty => true) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/acls_endpoint.rb0000644000004100000410000000205412654006514023322 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' require 'chef_zero/chef_data/data_normalizer' require 'chef_zero/chef_data/acl_path' module ChefZero module Endpoints # /organizations/ORG/THING/NAME/_acl # Where THING is: # - clients, data, containers, cookbooks, environments # groups, roles, nodes, users # or # /organizations/ORG/organization/_acl # /users/NAME/_acl class AclsEndpoint < RestBase def get(request) path = request.rest_path[0..-2] # Strip off _acl path = path[0..1] if path.size == 3 && path[0] == 'organizations' && %w(organization organizations).include?(path[2]) acl_path = ChefData::AclPath.get_acl_data_path(path) if !acl_path raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") end acls = FFI_Yajl::Parser.parse(get_data(request, acl_path), :create_additions => false) acls = ChefData::DataNormalizer.normalize_acls(acls) json_response(200, acls) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/file_store_file_endpoint.rb0000644000004100000410000000110212654006514025523 0ustar www-datawww-datarequire 'chef_zero/rest_base' module ChefZero module Endpoints # The minimum amount of S3 necessary to support cookbook upload/download # /organizations/NAME/file_store/FILE class FileStoreFileEndpoint < RestBase def json_only false end def get(request) [200, {"Content-Type" => 'application/x-binary'}, get_data(request) ] end def put(request) data_store.set(request.rest_path, request.body, :create, :create_dir, :requestor => request.requestor) json_response(200, {}) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/user_organizations_endpoint.rb0000644000004100000410000000135612654006514026331 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' module ChefZero module Endpoints # /users/USER/organizations class UserOrganizationsEndpoint < RestBase def get(request) username = request.rest_path[1] result = list_data(request, [ 'organizations' ]).select do |orgname| exists_data?(request, [ 'organizations', orgname, 'users', username ]) end result = result.map do |orgname| org = get_data(request, [ 'organizations', orgname, 'org' ]) org = FFI_Yajl::Parser.parse(org, :create_additions => false) { "organization" => ChefData::DataNormalizer.normalize_organization(org, orgname) } end json_response(200, result) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/controls_endpoint.rb0000644000004100000410000000060512654006514024243 0ustar www-datawww-datamodule ChefZero module Endpoints # /organizations/ORG/controls class ControlsEndpoint < RestBase # ours is not to wonder why; ours is but to make the pedant specs pass. def get(request) error(410, "Server says 410, chef-zero says 410.") end def post(request) error(410, "Server says 410, chef-zero says 410.") end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/environment_cookbooks_endpoint.rb0000644000004100000410000000142612654006514027017 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/endpoints/cookbooks_base' module ChefZero module Endpoints # /environments/NAME/cookbooks class EnvironmentCookbooksEndpoint < CookbooksBase def get(request) environment = FFI_Yajl::Parser.parse(get_data(request, request.rest_path[0..3]), :create_additions => false) constraints = environment['cookbook_versions'] || {} if request.query_params['num_versions'] == 'all' num_versions = nil elsif request.query_params['num_versions'] num_versions = request.query_params['num_versions'].to_i else num_versions = 1 end json_response(200, format_cookbooks_list(request, all_cookbooks_list(request), constraints, num_versions)) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/license_endpoint.rb0000644000004100000410000000120712654006514024021 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' module ChefZero module Endpoints # /license class LicenseEndpoint < RestBase MAX_NODE_COUNT = 25 def get(request) node_count = 0 list_data(request, [ 'organizations' ]).each do |orgname| node_count += list_data(request, [ 'organizations', orgname, 'nodes' ]).size end json_response(200, { "limit_exceeded" => (node_count > MAX_NODE_COUNT) ? true : false, "node_license" => MAX_NODE_COUNT, "node_count" => node_count, "upgrade_url" => 'http://blah.com' }) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/sandbox_endpoint.rb0000644000004100000410000000157612654006514024046 0ustar www-datawww-datarequire 'chef_zero/rest_base' require 'chef_zero/rest_error_response' require 'ffi_yajl' module ChefZero module Endpoints # /sandboxes/ID class SandboxEndpoint < RestBase def put(request) existing_sandbox = FFI_Yajl::Parser.parse(get_data(request), :create_additions => false) existing_sandbox['checksums'].each do |checksum| if !exists_data?(request, request.rest_path[0..1] + ['file_store', 'checksums', checksum]) raise RestErrorResponse.new(503, "Checksum not uploaded: #{checksum}") end end delete_data(request) json_response(200, { :guid => request.rest_path[3], :name => request.rest_path[3], :checksums => existing_sandbox['checksums'], :create_time => existing_sandbox['create_time'], :is_completed => true }) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/authenticate_user_endpoint.rb0000644000004100000410000000217412654006514026117 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' module ChefZero module Endpoints # /authenticate_user class AuthenticateUserEndpoint < RestBase def post(request) request_json = FFI_Yajl::Parser.parse(request.body, :create_additions => false) name = request_json['username'] password = request_json['password'] begin user = data_store.get(['users', name]) rescue ChefZero::DataStore::DataNotFoundError raise RestErrorResponse.new(401, "Bad username or password") end user = FFI_Yajl::Parser.parse(user, :create_additions => false) user = ChefData::DataNormalizer.normalize_user(user, name, [ 'username' ], server.options[:osc_compat]) if user['password'] != password raise RestErrorResponse.new(401, "Bad username or password") end # Include only particular user data in the response user.keep_if { |key,value| %w(first_name last_name display_name email username).include?(key) } json_response(200, { 'status' => 'linked', 'user' => user }) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/acl_endpoint.rb0000644000004100000410000000271312654006514023141 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' require 'chef_zero/chef_data/acl_path' module ChefZero module Endpoints # /organizations/ORG//NAME/_acl/PERM # Where thing is: # clients, data, containers, cookbooks, environments # groups, roles, nodes, users # or # /organizations/ORG/organization/_acl/PERM # or # /users/NAME/_acl/PERM # # Where PERM is create,read,update,delete,grant class AclEndpoint < RestBase def validate_request(request) path = request.rest_path[0..-3] # Strip off _acl/PERM path = path[0..1] if path.size == 3 && path[0] == 'organizations' && %w(organization organizations).include?(path[2]) acl_path = ChefData::AclPath.get_acl_data_path(path) perm = request.rest_path[-1] if !acl_path || !%w(read create update delete grant).include?(perm) raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") end [acl_path, perm] end def put(request) path, perm = validate_request(request) acls = FFI_Yajl::Parser.parse(get_data(request, path), :create_additions => false) acls[perm] = FFI_Yajl::Parser.parse(request.body, :create_additions => false)[perm] set_data(request, path, FFI_Yajl::Encoder.encode(acls, :pretty => true)) json_response(200, {'uri' => "#{build_uri(request.base_uri, request.rest_path)}"}) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/cookbooks_endpoint.rb0000644000004100000410000000105312654006514024367 0ustar www-datawww-datarequire 'chef_zero/endpoints/cookbooks_base' module ChefZero module Endpoints # /cookbooks class CookbooksEndpoint < CookbooksBase def get(request) if request.query_params['num_versions'] == 'all' num_versions = nil elsif request.query_params['num_versions'] num_versions = request.query_params['num_versions'].to_i else num_versions = 1 end json_response(200, format_cookbooks_list(request, all_cookbooks_list(request), {}, num_versions)) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/rest_list_endpoint.rb0000644000004100000410000000241412654006514024410 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' module ChefZero module Endpoints # Typical REST list endpoint (/roles or /data/BAG) class RestListEndpoint < RestBase def initialize(server, identity_keys = [ 'name' ]) super(server) identity_keys = [ identity_keys ] if identity_keys.is_a?(String) @identity_keys = identity_keys end attr_reader :identity_keys def get(request) # Get the result result_hash = {} list_data(request).sort.each do |name| result_hash[name] = "#{build_uri(request.base_uri, request.rest_path + [name])}" end json_response(200, result_hash) end def post(request) contents = request.body key = get_key(contents) if key.nil? error(400, "Must specify #{identity_keys.map { |k| k.inspect }.join(' or ')} in JSON") else create_data(request, request.rest_path, key, contents) json_response(201, {'uri' => "#{build_uri(request.base_uri, request.rest_path + [key])}"}) end end def get_key(contents) json = FFI_Yajl::Parser.parse(contents, :create_additions => false) identity_keys.map { |k| json[k] }.select { |v| v }.first end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/organization_users_endpoint.rb0000644000004100000410000000324612654006514026331 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' require 'chef_zero/endpoints/organization_user_base' module ChefZero module Endpoints # /organizations/ORG/users class OrganizationUsersEndpoint < RestBase def post(request) orgname = request.rest_path[1] json = FFI_Yajl::Parser.parse(request.body, :create_additions => false) username = json['username'] if exists_data?(request, [ 'organizations', orgname, 'users', username ]) raise RestErrorResponse.new(409, "User #{username} is already in organization #{orgname}") end users = get_data(request, [ 'organizations', orgname, 'groups', 'users' ]) users = FFI_Yajl::Parser.parse(users, :create_additions => false) create_data(request, request.rest_path, username, '{}') # /organizations/ORG/association_requests/USERNAME-ORG begin delete_data(request, [ 'organizations', orgname, 'association_requests', username], :data_store_exceptions) rescue DataStore::DataNotFoundError end # Add the user to the users group if it isn't already there if !users['users'] || !users['users'].include?(username) users['users'] ||= [] users['users'] |= [ username ] set_data(request, [ 'organizations', orgname, 'groups', 'users' ], FFI_Yajl::Encoder.encode(users, :pretty => true)) end json_response(201, { "uri" => build_uri(request.base_uri, request.rest_path + [ username ]) }) end def get(request) ChefZero::Endpoints::OrganizationUserBase.get(self, request) { |username| { "user" => { "username" => username } } } end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/cookbook_version_endpoint.rb0000644000004100000410000001214412654006514025754 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/endpoints/rest_object_endpoint' require 'chef_zero/rest_error_response' require 'chef_zero/chef_data/data_normalizer' require 'chef_zero/data_store/data_not_found_error' module ChefZero module Endpoints # /organizations/ORG/cookbooks/NAME/VERSION class CookbookVersionEndpoint < RestObjectEndpoint def get(request) if request.rest_path[4] == "_latest" || request.rest_path[4] == "latest" request.rest_path[4] = latest_version(list_data(request, request.rest_path[0..3])) end super(request) end def put(request) name = request.rest_path[3] version = request.rest_path[4] existing_cookbook = get_data(request, request.rest_path, :nil) # Honor frozen if existing_cookbook existing_cookbook_json = FFI_Yajl::Parser.parse(existing_cookbook, :create_additions => false) if existing_cookbook_json['frozen?'] if request.query_params['force'] != "true" raise RestErrorResponse.new(409, "The cookbook #{name} at version #{version} is frozen. Use the 'force' option to override.") end # For some reason, you are forever unable to modify "frozen?" on a frozen cookbook. request_body = FFI_Yajl::Parser.parse(request.body, :create_additions => false) if !request_body['frozen?'] request_body['frozen?'] = true request.body = FFI_Yajl::Encoder.encode(request_body, :pretty => true) end end end # Set the cookbook set_data(request, request.rest_path, request.body, :create_dir, :create) # If the cookbook was updated, check for deleted files and clean them up if existing_cookbook missing_checksums = get_checksums(existing_cookbook) - get_checksums(request.body) if missing_checksums.size > 0 hoover_unused_checksums(missing_checksums, request) end end already_json_response(existing_cookbook ? 200 : 201, populate_defaults(request, request.body)) end def delete(request) if request.rest_path[4] == "_latest" || request.rest_path[4] == "latest" request.rest_path[4] = latest_version(list_data(request, request.rest_path[0..3])) end deleted_cookbook = get_data(request) response = super(request) # Last one out turns out the lights: delete /organizations/ORG/cookbooks/NAME if it no longer has versions cookbook_path = request.rest_path[0..3] if exists_data_dir?(request, cookbook_path) && list_data(request, cookbook_path).size == 0 delete_data_dir(request, cookbook_path) end # Hoover deleted files, if they exist hoover_unused_checksums(get_checksums(deleted_cookbook), request) response end def get_checksums(cookbook) result = [] FFI_Yajl::Parser.parse(cookbook, :create_additions => false).each_pair do |key, value| if value.is_a?(Array) value.each do |file| if file.is_a?(Hash) && file.has_key?('checksum') result << file['checksum'] end end end end result.uniq end private def hoover_unused_checksums(deleted_checksums, request) %w(cookbooks cookbook_artifacts).each do |cookbook_type| begin cookbooks = data_store.list(request.rest_path[0..1] + [cookbook_type]) rescue ChefZero::DataStore::DataNotFoundError # Not all chef versions support cookbook_artifacts raise unless cookbook_type == "cookbook_artifacts" cookbooks = [] end cookbooks.each do |cookbook_name| data_store.list(request.rest_path[0..1] + [cookbook_type, cookbook_name]).each do |version| cookbook = data_store.get(request.rest_path[0..1] + [cookbook_type, cookbook_name, version], request) deleted_checksums = deleted_checksums - get_checksums(cookbook) end end end deleted_checksums.each do |checksum| # There can be a race here if multiple cookbooks are uploading. # This deals with an exception on delete, but things can still get deleted # that shouldn't be. begin delete_data(request, request.rest_path[0..1] + ['file_store', 'checksums', checksum], :data_store_exceptions) rescue ChefZero::DataStore::DataNotFoundError end end end def populate_defaults(request, response_json) # Inject URIs into each cookbook file cookbook = FFI_Yajl::Parser.parse(response_json, :create_additions => false) cookbook = ChefData::DataNormalizer.normalize_cookbook(self, request.rest_path[0..1], cookbook, request.rest_path[3], request.rest_path[4], request.base_uri, request.method) FFI_Yajl::Encoder.encode(cookbook, :pretty => true) end def latest_version(versions) sorted = versions.sort_by { |version| Gem::Version.new(version.dup) } sorted[-1] end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/user_association_requests_count_endpoint.rb0000644000004100000410000000106412654006514031115 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' module ChefZero module Endpoints # /users/NAME/association_requests/count class UserAssociationRequestsCountEndpoint < RestBase def get(request) get_data(request, request.rest_path[0..-3]) username = request.rest_path[1] result = list_data(request, [ 'organizations' ]).select do |org| exists_data?(request, [ 'organizations', org, 'association_requests', username ]) end json_response(200, { "value" => result.size }) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/actor_endpoint.rb0000644000004100000410000000640712654006514023516 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/endpoints/rest_object_endpoint' require 'chef_zero/chef_data/data_normalizer' module ChefZero module Endpoints # /organizations/ORG/clients/NAME # /organizations/ORG/users/NAME # /users/NAME class ActorEndpoint < RestObjectEndpoint def delete(request) result = super if request.rest_path[0] == 'users' list_data(request, [ 'organizations' ]).each do |org| begin delete_data(request, [ 'organizations', org, 'users', request.rest_path[1] ], :data_store_exceptions) rescue DataStore::DataNotFoundError end end end result end def put(request) # Find out if we're updating the public key. request_body = FFI_Yajl::Parser.parse(request.body, :create_additions => false) if request_body['public_key'].nil? # If public_key is null, then don't overwrite it. Weird patchiness. body_modified = true request_body.delete('public_key') else updating_public_key = true end # Generate private_key if requested. if request_body.has_key?('private_key') body_modified = true if request_body['private_key'] private_key, public_key = server.gen_key_pair updating_public_key = true request_body['public_key'] = public_key end request_body.delete('private_key') end # Save request request.body = FFI_Yajl::Encoder.encode(request_body, :pretty => true) if body_modified # PUT /clients is patchy request.body = patch_request_body(request) result = super(request) # Inject private_key into response, delete public_key/password if applicable if result[0] == 200 || result[0] == 201 if request.rest_path[0] == 'users' key = nil identity_keys.each do |identity_key| key ||= request_body[identity_key] end key ||= request.rest_path[-1] response = { 'uri' => build_uri(request.base_uri, [ 'users', key ]) } else response = FFI_Yajl::Parser.parse(result[2], :create_additions => false) end if request.rest_path[2] == 'clients' response['private_key'] = private_key ? private_key : false else response['private_key'] = private_key if private_key end response.delete('public_key') if !updating_public_key && request.rest_path[2] == 'users' response.delete('password') json_response(result[0], response) else result end end def populate_defaults(request, response_json) response = FFI_Yajl::Parser.parse(response_json, :create_additions => false) if request.rest_path[2] == 'clients' response = ChefData::DataNormalizer.normalize_client(response,request.rest_path[3], request.rest_path[1]) else response = ChefData::DataNormalizer.normalize_user(response, request.rest_path[3], identity_keys, server.options[:osc_compat], request.method) end FFI_Yajl::Encoder.encode(response, :pretty => true) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/searches_endpoint.rb0000644000004100000410000000076712654006514024206 0ustar www-datawww-datarequire 'chef_zero/rest_base' module ChefZero module Endpoints # /search class SearchesEndpoint < RestBase def get(request) # Get the result result_hash = {} indices = (%w(client environment node role) + data_store.list(request.rest_path[0..1] + ['data'])).sort indices.each do |index| result_hash[index] = build_uri(request.base_uri, request.rest_path + [index]) end json_response(200, result_hash) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/organization_endpoint.rb0000644000004100000410000000331712654006514025107 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' module ChefZero module Endpoints # /organizations/NAME class OrganizationEndpoint < RestBase def get(request) org = get_data(request, request.rest_path + [ 'org' ]) already_json_response(200, populate_defaults(request, org)) end def put(request) org = FFI_Yajl::Parser.parse(get_data(request, request.rest_path + [ 'org' ]), :create_additions => false) new_org = FFI_Yajl::Parser.parse(request.body, :create_additions => false) new_org.each do |key, value| org[key] = value end save_org = FFI_Yajl::Encoder.encode(org, :pretty => true) if new_org['name'] != request.rest_path[-1] # This is a rename return error(400, "Cannot rename org #{request.rest_path[-1]} to #{new_org['name']}: rename not supported for orgs") end set_data(request, request.rest_path + [ 'org' ], save_org) json_response(200, { "uri" => "#{build_uri(request.base_uri, request.rest_path)}", "name" => org['name'], "org_type" => org['org_type'], "full_name" => org['full_name'] }) end def delete(request) org = get_data(request, request.rest_path + [ 'org' ]) delete_data_dir(request, request.rest_path, :recursive) already_json_response(200, populate_defaults(request, org)) end def populate_defaults(request, response_json) org = FFI_Yajl::Parser.parse(response_json, :create_additions => false) org = ChefData::DataNormalizer.normalize_organization(org, request.rest_path[1]) FFI_Yajl::Encoder.encode(org, :pretty => true) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/organization_user_base.rb0000644000004100000410000000042712654006514025236 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' module ChefZero module Endpoints module OrganizationUserBase def self.get(obj, request, &block) result = obj.list_data(request).map(&block) obj.json_response(200, result) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/nodes_endpoint.rb0000644000004100000410000000204512654006514023510 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/endpoints/rest_object_endpoint' require 'chef_zero/chef_data/data_normalizer' module ChefZero module Endpoints # /nodes class NodesEndpoint < RestListEndpoint def post(request) # /nodes validation if request.rest_path.last == "nodes" data = parse_json(request.body) if data.has_key?("policy_name") && policy_name_invalid?(data["policy_name"]) return error(400, "Field 'policy_name' invalid", :pretty => false) end if data.has_key?("policy_group") && policy_name_invalid?(data["policy_group"]) return error(400, "Field 'policy_group' invalid", :pretty => false) end end super(request) end def populate_defaults(request, response_json) node = FFI_Yajl::Parser.parse(response_json, :create_additions => false) node = ChefData::DataNormalizer.normalize_node(node, request.rest_path[3]) FFI_Yajl::Encoder.encode(node, :pretty => true) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/groups_endpoint.rb0000644000004100000410000000043212654006514023715 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/endpoints/rest_list_endpoint' module ChefZero module Endpoints # /organizations/ORG/groups/NAME class GroupsEndpoint < RestListEndpoint def initialize(server) super(server, %w(id groupname)) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/environment_cookbook_endpoint.rb0000644000004100000410000000166612654006514026642 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/endpoints/cookbooks_base' module ChefZero module Endpoints # /environments/NAME/cookbooks/NAME class EnvironmentCookbookEndpoint < CookbooksBase def get(request) cookbook_name = request.rest_path[5] environment = FFI_Yajl::Parser.parse(get_data(request, request.rest_path[0..3]), :create_additions => false) constraints = environment['cookbook_versions'] || {} cookbook_versions = list_data(request, request.rest_path[0..1] + request.rest_path[4..5]) if request.query_params['num_versions'] == 'all' num_versions = nil elsif request.query_params['num_versions'] num_versions = request.query_params['num_versions'].to_i else num_versions = nil end json_response(200, format_cookbooks_list(request, { cookbook_name => cookbook_versions }, constraints, num_versions)) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/data_bags_endpoint.rb0000644000004100000410000000153012654006514024303 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/endpoints/rest_list_endpoint' module ChefZero module Endpoints # /data class DataBagsEndpoint < RestListEndpoint def post(request) contents = request.body json = FFI_Yajl::Parser.parse(contents, :create_additions => false) name = identity_keys.map { |k| json[k] }.select { |v| v }.first if name.nil? error(400, "Must specify #{identity_keys.map { |k| k.inspect }.join(' or ')} in JSON") elsif exists_data_dir?(request, request.rest_path[0..1] + ['data', name]) error(409, "Object already exists") else create_data_dir(request, request.rest_path[0..1] + ['data'], name, :recursive) json_response(201, {"uri" => "#{build_uri(request.base_uri, request.rest_path + [name])}"}) end end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/cookbook_artifact_identifier_endpoint.rb0000644000004100000410000000527512654006514030275 0ustar www-datawww-datarequire 'chef_zero/chef_data/data_normalizer' module ChefZero module Endpoints class CookbookArtifactIdentifierEndpoint < ChefZero::Endpoints::CookbookVersionEndpoint # these endpoints are almost, but not quite, not entirely unlike the corresponding /cookbooks endpoints. # it could all be refactored for maximum reuse, but they're short REST methods with well-defined # behavioral specs (pedant), so there's not a huge benefit. # GET /organizations/ORG/cookbook_artifacts/NAME/IDENTIFIER def get(request) cookbook_data = normalize(request, parse_json(get_data(request))) return json_response(200, cookbook_data) end # PUT /organizations/ORG/cookbook_artifacts/COOKBOOK/IDENTIFIER def put(request) if exists_data?(request) return error(409, "Cookbooks cannot be modified, and a cookbook with this identifier already exists.") end set_data(request, nil, request.body, :create_dir) return already_json_response(201, request.body) end # DELETE /organizations/ORG/cookbook_artifacts/COOKBOOK/IDENTIFIER def delete(request) begin doomed_cookbook_json = get_data(request) identified_cookbook_data = normalize(request, parse_json(doomed_cookbook_json)) delete_data(request) # go through the recipes and delete stuff in the file store. hoover_unused_checksums(get_checksums(doomed_cookbook_json), request) # if this was the last revision, delete the directory so future requests will 404, instead of # returning 200 with an empty list. # Last one out turns out the lights: delete /organizations/ORG/cookbooks/COOKBOOK if it no longer has versions cookbook_path = request.rest_path[0..3] if exists_data_dir?(request, cookbook_path) && list_data(request, cookbook_path).size == 0 delete_data_dir(request, cookbook_path) end json_response(200, identified_cookbook_data) rescue RestErrorResponse => ex if ex.response_code == 404 error(404, "not_found") else raise end end end private def make_file_store_path(rest_path, recipe) rest_path.first(2) + ["file_store", "checksums", recipe["checksum"]] end def normalize(request, cookbook_artifact_data) ChefData::DataNormalizer.normalize_cookbook(self, request.rest_path[0..1], cookbook_artifact_data, request.rest_path[3], request.rest_path[4], request.base_uri, request.method, true) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/role_endpoint.rb0000644000004100000410000000100212654006514023331 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/endpoints/rest_object_endpoint' require 'chef_zero/chef_data/data_normalizer' module ChefZero module Endpoints # /roles/NAME class RoleEndpoint < RestObjectEndpoint def populate_defaults(request, response_json) role = FFI_Yajl::Parser.parse(response_json, :create_additions => false) role = ChefData::DataNormalizer.normalize_role(role, request.rest_path[3]) FFI_Yajl::Encoder.encode(role, :pretty => true) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/cookbook_endpoint.rb0000644000004100000410000000252412654006514024210 0ustar www-datawww-datarequire 'chef_zero/endpoints/cookbooks_base' module ChefZero module Endpoints # /cookbooks/NAME class CookbookEndpoint < CookbooksBase def get(request) filter = request.rest_path[3] case filter when '_latest' result = {} filter_cookbooks(all_cookbooks_list(request), {}, 1) do |name, versions| if versions.size > 0 result[name] = build_uri(request.base_uri, request.rest_path[0..1] + ['cookbooks', name, versions[0]]) end end json_response(200, result) when '_recipes' result = [] filter_cookbooks(all_cookbooks_list(request), {}, 1) do |name, versions| if versions.size > 0 cookbook = FFI_Yajl::Parser.parse(get_data(request, request.rest_path[0..1] + ['cookbooks', name, versions[0]]), :create_additions => false) result += recipe_names(name, cookbook) end end json_response(200, result.sort) else cookbook_list = { filter => list_data(request, request.rest_path) } json_response(200, format_cookbooks_list(request, cookbook_list)) end end def latest_version(versions) sorted = versions.sort_by { |version| Gem::Version.new(version.dup) } sorted[-1] end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/policy_groups_endpoint.rb0000644000004100000410000000254612654006514025304 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' require 'chef_zero/chef_data/data_normalizer' module ChefZero module Endpoints # /organizations/ORG/policy_groups # # in the data store, this REST path actually stores the revision ID of ${policy_name} that's currently # associated with ${policy_group}. class PolicyGroupsEndpoint < RestBase # GET /organizations/ORG/policy_groups def get(request) # each policy group has policies and associated revisions under # /policy_groups/{group name}/policies/{policy name}. response_data = {} list_data(request).each do |group_name| group_path = request.rest_path + [group_name] policy_list = list_data(request, group_path + ["policies"]) # build the list of policies with their revision ID associated with this policy group. policies = {} policy_list.each do |policy_name| revision_id = parse_json(get_data(request, group_path + ["policies", policy_name])) policies[policy_name] = { revision_id: revision_id } end response_data[group_name] = { uri: build_uri(request.base_uri, group_path) } response_data[group_name][:policies] = policies unless policies.empty? end json_response(200, response_data) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/role_environments_endpoint.rb0000644000004100000410000000062312654006514026150 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' module ChefZero module Endpoints # /roles/NAME/environments class RoleEnvironmentsEndpoint < RestBase def get(request) role = FFI_Yajl::Parser.parse(get_data(request, request.rest_path[0..3]), :create_additions => false) json_response(200, [ '_default' ] + (role['env_run_lists'].keys || [])) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/node_endpoint.rb0000644000004100000410000000170412654006514023326 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/endpoints/rest_object_endpoint' require 'chef_zero/chef_data/data_normalizer' module ChefZero module Endpoints # /nodes/ID class NodeEndpoint < RestObjectEndpoint def put(request) data = parse_json(request.body) if data.has_key?("policy_name") && policy_name_invalid?(data["policy_name"]) return error(400, "Field 'policy_name' invalid", :pretty => false) end if data.has_key?("policy_group") && policy_name_invalid?(data["policy_group"]) return error(400, "Field 'policy_group' invalid", :pretty => false) end super(request) end def populate_defaults(request, response_json) node = FFI_Yajl::Parser.parse(response_json, :create_additions => false) node = ChefData::DataNormalizer.normalize_node(node, request.rest_path[3]) FFI_Yajl::Encoder.encode(node, :pretty => true) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/cookbook_artifact_endpoint.rb0000644000004100000410000000137712654006514026072 0ustar www-datawww-datarequire 'chef_zero/chef_data/data_normalizer' module ChefZero module Endpoints class CookbookArtifactEndpoint < RestBase # GET /organizations/ORG/cookbook_artifacts/COOKBOOK def get(request) cookbook_name = request.rest_path.last cookbook_url = build_uri(request.base_uri, request.rest_path) response_data = {} versions = [] list_data(request).each do |identifier| artifact_url = build_uri(request.base_uri, request.rest_path + [cookbook_name, identifier]) versions << { url: artifact_url, identifier: identifier } end response_data[cookbook_name] = { url: cookbook_url, versions: versions } return json_response(200, response_data) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/policy_revisions_endpoint.rb0000644000004100000410000000076212654006514026004 0ustar www-datawww-datarequire 'chef_zero/chef_data/data_normalizer' module ChefZero module Endpoints # /organizations/ORG/policies/NAME/revisions class PolicyRevisionsEndpoint < RestBase # POST /organizations/ORG/policies/NAME/revisions def post(request) policyfile_data = parse_json(request.body) create_data(request, request.rest_path, policyfile_data["revision_id"], request.body, :create_dir) return already_json_response(201, request.body) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/server_api_version_endpoint.rb0000644000004100000410000000054412654006514026306 0ustar www-datawww-datarequire 'chef_zero/rest_base' module ChefZero module Endpoints # /server_api_version class ServerAPIVersionEndpoint < RestBase API_VERSION = 1 def get(request) json_response(200, {"min_api_version"=>MIN_API_VERSION, "max_api_version"=>MAX_API_VERSION}, request.api_version, API_VERSION) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/sandboxes_endpoint.rb0000644000004100000410000000307312654006514024370 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' module ChefZero module Endpoints # /sandboxes class SandboxesEndpoint < RestBase def initialize(server) super(server) @next_id = 1 end def post(request) sandbox_checksums = [] needed_checksums = FFI_Yajl::Parser.parse(request.body, :create_additions => false)['checksums'] result_checksums = {} needed_checksums.keys.each do |needed_checksum| if list_data(request, request.rest_path[0..1] + ['file_store', 'checksums']).include?(needed_checksum) result_checksums[needed_checksum] = { :needs_upload => false } else result_checksums[needed_checksum] = { :needs_upload => true, :url => build_uri(request.base_uri, request.rest_path[0..1] + ['file_store', 'checksums', needed_checksum]) } sandbox_checksums << needed_checksum end end # There is an obvious race condition here. id = @next_id.to_s @next_id+=1 time_str = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S%z') time_str = "#{time_str[0..21]}:#{time_str[22..23]}" create_data(request, request.rest_path, id, FFI_Yajl::Encoder.encode({ :create_time => time_str, :checksums => sandbox_checksums }, :pretty => true)) json_response(201, { :uri => build_uri(request.base_uri, request.rest_path + [id]), :checksums => result_checksums, :sandbox_id => id }) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/version_endpoint.rb0000644000004100000410000000035012654006514024062 0ustar www-datawww-datarequire 'chef_zero/rest_base' module ChefZero module Endpoints # /version class VersionEndpoint < RestBase def get(request) text_response(200, "chef-zero #{ChefZero::VERSION}\n") end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/data_bag_endpoint.rb0000644000004100000410000000251512654006514024124 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/endpoints/rest_list_endpoint' require 'chef_zero/endpoints/data_bag_item_endpoint' require 'chef_zero/rest_error_response' module ChefZero module Endpoints # /data/NAME class DataBagEndpoint < RestListEndpoint def initialize(server) super(server, 'id') end def post(request) json = FFI_Yajl::Parser.parse(request.body, :create_additions => false) key = identity_keys.map { |k| json[k] }.select { |v| v }.first response = super(request) if response[0] == 201 already_json_response(201, DataBagItemEndpoint::populate_defaults(request, request.body, request.rest_path[3], key)) else response end end def get_key(contents) data_bag_item = FFI_Yajl::Parser.parse(contents, :create_additions => false) if data_bag_item['json_class'] == 'Chef::DataBagItem' && data_bag_item['raw_data'] data_bag_item['raw_data']['id'] else data_bag_item['id'] end end def delete(request) key = request.rest_path[3] delete_data_dir(request, request.rest_path, :recursive) json_response(200, { 'chef_type' => 'data_bag', 'json_class' => 'Chef::DataBag', 'name' => key }) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/rest_object_endpoint.rb0000644000004100000410000000447112654006514024710 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' require 'chef_zero/rest_error_response' module ChefZero module Endpoints # Typical REST leaf endpoint (/roles/NAME or /data/BAG/NAME) class RestObjectEndpoint < RestBase def initialize(server, identity_keys = [ 'name' ]) super(server) identity_keys = [ identity_keys ] if identity_keys.is_a?(String) @identity_keys = identity_keys end attr_reader :identity_keys def get(request) already_json_response(200, populate_defaults(request, get_data(request))) end def put(request) # We grab the old body to trigger a 404 if it doesn't exist old_body = get_data(request) request_json = FFI_Yajl::Parser.parse(request.body, :create_additions => false) key = identity_keys.map { |k| request_json[k] }.select { |v| v }.first key ||= request.rest_path[-1] # If it's a rename, check for conflict and delete the old value rename = key != request.rest_path[-1] if rename begin create_data(request, request.rest_path[0..-2], key, request.body, :data_store_exceptions) rescue DataStore::DataAlreadyExistsError return error(409, "Cannot rename '#{request.rest_path[-1]}' to '#{key}': '#{key}' already exists") end delete_data(request) already_json_response(201, populate_defaults(request, request.body)) else set_data(request, request.rest_path, request.body) already_json_response(200, populate_defaults(request, request.body)) end end def delete(request) result = get_data(request) delete_data(request) already_json_response(200, populate_defaults(request, result)) end def patch_request_body(request) existing_value = get_data(request, nil, :nil) if existing_value request_json = FFI_Yajl::Parser.parse(request.body, :create_additions => false) existing_json = FFI_Yajl::Parser.parse(existing_value, :create_additions => false) merged_json = existing_json.merge(request_json) if merged_json.size > request_json.size return FFI_Yajl::Encoder.encode(merged_json, :pretty => true) end end request.body end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/system_recovery_endpoint.rb0000644000004100000410000000207512654006514025645 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' module ChefZero module Endpoints # /system_recovery class SystemRecoveryEndpoint < RestBase def post(request) request_json = FFI_Yajl::Parser.parse(request.body, :create_additions => false) name = request_json['username'] password = request_json['password'] user = get_data(request, request.rest_path[0..-2] + ['users', name], :nil) if !user raise RestErrorResponse.new(403, "Nonexistent user") end user = FFI_Yajl::Parser.parse(user, :create_additions => false) user = ChefData::DataNormalizer.normalize_user(user, name, [ 'username' ], server.options[:osc_compat]) if !user['recovery_authentication_enabled'] raise RestErrorResponse.new(403, "Only users with recovery_authentication_enabled=true may use /system_recovery to log in") end if user['password'] != password raise RestErrorResponse.new(401, "Incorrect password") end json_response(200, user) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/containers_endpoint.rb0000644000004100000410000000155012654006514024545 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/endpoints/rest_list_endpoint' module ChefZero module Endpoints # /organizations/ORG/containers class ContainersEndpoint < RestListEndpoint def initialize(server) super(server, %w(id containername)) end # create a container. # input: {"containername"=>"new-container", "containerpath"=>"/"} def post(request) data = parse_json(request.body) # if they don't match, id wins. container_name = data["id"] || data["containername"] container_path_suffix = data["containerpath"].split("/").reject { |o| o.empty? } create_data(request, request.rest_path, container_name, to_json({}), :create_dir) json_response(201, { uri: build_uri(request.base_uri, request.rest_path + container_path_suffix + [container_name]) }) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/policy_endpoint.rb0000644000004100000410000000132512654006514023677 0ustar www-datawww-datarequire 'chef_zero/chef_data/data_normalizer' module ChefZero module Endpoints # /organizations/ORG/policies/NAME class PolicyEndpoint < RestBase # GET /organizations/ORG/policies/NAME def get(request) revisions = list_data(request, request.rest_path + ["revisions"]) data = { revisions: hashify_list(revisions) } return json_response(200, data) end # DELETE /organizations/ORG/policies/NAME def delete(request) revisions = list_data(request, request.rest_path + ["revisions"]) data = { revisions: hashify_list(revisions) } delete_data_dir(request, nil, :recursive) return json_response(200, data) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/organization_authenticate_user_endpoint.rb0000644000004100000410000000144512654006514030703 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' module ChefZero module Endpoints # /organizations/NAME/authenticate_user class OrganizationAuthenticateUserEndpoint < RestBase def post(request) request_json = FFI_Yajl::Parser.parse(request.body, :create_additions => false) name = request_json['name'] password = request_json['password'] begin user = data_store.get(request.rest_path[0..-2] + ['users', name]) user = FFI_Yajl::Parser.parse(user, :create_additions => false) verified = user['password'] == password rescue DataStore::DataNotFoundError verified = false end json_response(200, { 'name' => name, 'verified' => !!verified }) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/principal_endpoint.rb0000644000004100000410000000271112654006514024361 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero' require 'chef_zero/rest_base' module ChefZero module Endpoints # /principals/NAME class PrincipalEndpoint < RestBase def get(request) name = request.rest_path[-1] # If /organizations/ORG/users/NAME exists, use this user (only org members have precedence over clients). hey are an org member. json = get_data(request, request.rest_path[0..1] + [ 'users', name ], :nil) if json type = 'user' org_member = true else # If /organizations/ORG/clients/NAME exists, use the client. json = get_data(request, request.rest_path[0..1] + [ 'clients', name ], :nil) if json type = 'client' org_member = true else # If there is no client with that name, check for a user (/users/NAME) and return that with # org_member = false. json = get_data(request, [ 'users', name ], :nil) if json type = 'user' org_member = false end end end if json json_response(200, { 'name' => name, 'type' => type, 'public_key' => FFI_Yajl::Parser.parse(json)['public_key'] || PUBLIC_KEY, 'authz_id' => '0'*32, 'org_member' => org_member }) else error(404, 'Principal not found') end end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/environment_nodes_endpoint.rb0000644000004100000410000000136612654006514026141 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' module ChefZero module Endpoints # /environment/NAME/nodes class EnvironmentNodesEndpoint < RestBase def get(request) # 404 if environment does not exist get_data(request, request.rest_path[0..3]) result = {} list_data(request, request.rest_path[0..1] + ['nodes']).each do |name| node = FFI_Yajl::Parser.parse(get_data(request, request.rest_path[0..1] + ['nodes', name]), :create_additions => false) if node['chef_environment'] == request.rest_path[3] result[name] = build_uri(request.base_uri, request.rest_path[0..1] + ['nodes', name]) end end json_response(200, result) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/environment_cookbook_versions_endpoint.rb0000644000004100000410000001330712654006514030565 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' require 'chef_zero/rest_error_response' module ChefZero module Endpoints # /environments/NAME/cookbook_versions class EnvironmentCookbookVersionsEndpoint < RestBase def post(request) cookbook_names = list_data(request, request.rest_path[0..1] + ['cookbooks']) # Get the list of cookbooks and versions desired by the runlist desired_versions = {} run_list = FFI_Yajl::Parser.parse(request.body, :create_additions => false)['run_list'] run_list.each do |run_list_entry| if run_list_entry =~ /(.+)::.+\@(.+)/ || run_list_entry =~ /(.+)\@(.+)/ raise RestErrorResponse.new(412, "No such cookbook: #{$1}") if !cookbook_names.include?($1) raise RestErrorResponse.new(412, "No such cookbook version for cookbook #{$1}: #{$2}") if !list_data(request, request.rest_path[0..1] + ['cookbooks', $1]).include?($2) desired_versions[$1] = [ $2 ] else desired_cookbook = run_list_entry.split('::')[0] raise RestErrorResponse.new(412, "No such cookbook: #{desired_cookbook}") if !cookbook_names.include?(desired_cookbook) desired_versions[desired_cookbook] = list_data(request, request.rest_path[0..1] + ['cookbooks', desired_cookbook]) end end # Filter by environment constraints environment = FFI_Yajl::Parser.parse(get_data(request, request.rest_path[0..3]), :create_additions => false) environment_constraints = environment['cookbook_versions'] || {} desired_versions.each_key do |name| desired_versions = filter_by_constraint(desired_versions, name, environment_constraints[name]) end # Depsolve! solved = depsolve(request, desired_versions.keys, desired_versions, environment_constraints) if !solved if @last_missing_dep && !cookbook_names.include?(@last_missing_dep) return raise RestErrorResponse.new(412, "No such cookbook: #{@last_missing_dep}") elsif @last_constraint_failure return raise RestErrorResponse.new(412, "Could not satisfy version constraints for: #{@last_constraint_failure}") else return raise RestErrorResponse.new(412, "Unsolvable versions!") end end result = {} solved.each_pair do |name, versions| cookbook = FFI_Yajl::Parser.parse(get_data(request, request.rest_path[0..1] + ['cookbooks', name, versions[0]]), :create_additions => false) result[name] = ChefData::DataNormalizer.normalize_cookbook(self, request.rest_path[0..1], cookbook, name, versions[0], request.base_uri, 'MIN') end json_response(200, result) end def depsolve(request, unsolved, desired_versions, environment_constraints) desired_versions.each do |cb, ver| if ver.empty? @last_constraint_failure = cb return nil end end # If everything is already solve_for = unsolved[0] return desired_versions if !solve_for # Go through each desired version of this cookbook, starting with the latest, # until we find one we can solve successfully with sort_versions(desired_versions[solve_for]).each do |desired_version| new_desired_versions = desired_versions.clone new_desired_versions[solve_for] = [ desired_version ] new_unsolved = unsolved[1..-1] # Pick this cookbook, and add dependencies cookbook_obj = FFI_Yajl::Parser.parse(get_data(request, request.rest_path[0..1] + ['cookbooks', solve_for, desired_version]), :create_additions => false) cookbook_metadata = cookbook_obj['metadata'] || {} cookbook_dependencies = cookbook_metadata['dependencies'] || {} dep_not_found = false cookbook_dependencies.each_pair do |dep_name, dep_constraint| # If the dep is not already in the list, add it to the list to solve # and bring in all environment-allowed cookbook versions to desired_versions if !new_desired_versions.has_key?(dep_name) new_unsolved = new_unsolved + [dep_name] # If the dep is missing, we will try other versions of the cookbook that might not have the bad dep. if !exists_data_dir?(request, request.rest_path[0..1] + ['cookbooks', dep_name]) @last_missing_dep = dep_name.to_s dep_not_found = true break end new_desired_versions[dep_name] = list_data(request, request.rest_path[0..1] + ['cookbooks', dep_name]) new_desired_versions = filter_by_constraint(new_desired_versions, dep_name, environment_constraints[dep_name]) end new_desired_versions = filter_by_constraint(new_desired_versions, dep_name, dep_constraint) end next if dep_not_found # Depsolve children with this desired version! First solution wins. result = depsolve(request, new_unsolved, new_desired_versions, environment_constraints) return result if result end return nil end def sort_versions(versions) result = versions.sort_by { |version| Gem::Version.new(version.dup) } result.reverse end def filter_by_constraint(versions, cookbook_name, constraint) return versions if !constraint constraint = Gem::Requirement.new(constraint) new_versions = versions[cookbook_name] new_versions = new_versions.select { |version| constraint.satisfied_by?(Gem::Version.new(version.dup)) } result = versions.clone result[cookbook_name] = new_versions result end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/environment_recipes_endpoint.rb0000644000004100000410000000147112654006514026460 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/endpoints/cookbooks_base' module ChefZero module Endpoints # /environment/NAME/recipes class EnvironmentRecipesEndpoint < CookbooksBase def get(request) environment = FFI_Yajl::Parser.parse(get_data(request, request.rest_path[0..3]), :create_additions => false) constraints = environment['cookbook_versions'] || {} result = [] filter_cookbooks(all_cookbooks_list(request), constraints, 1) do |name, versions| if versions.size > 0 cookbook = FFI_Yajl::Parser.parse(get_data(request, request.rest_path[0..1] + ['cookbooks', name, versions[0]]), :create_additions => false) result += recipe_names(name, cookbook) end end json_response(200, result.sort) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/organization_validator_key_endpoint.rb0000644000004100000410000000135112654006514030020 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' require 'uuidtools' module ChefZero module Endpoints # /organizations/NAME/_validator_key class OrganizationValidatorKeyEndpoint < RestBase def post(request) org_name = request.rest_path[-2] validator_path = [ 'organizations', org_name, 'clients', "#{org_name}-validator"] validator = FFI_Yajl::Parser.parse(get_data(request, validator_path), :create_additions => false) private_key, public_key = server.gen_key_pair validator['public_key'] = public_key set_data(request, validator_path, FFI_Yajl::Encoder.encode(validator, :pretty => true)) json_response(200, { 'private_key' => private_key }) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/container_endpoint.rb0000644000004100000410000000124512654006514024363 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/endpoints/rest_object_endpoint' require 'chef_zero/chef_data/data_normalizer' module ChefZero module Endpoints # /organizations/ORG/containers/NAME class ContainerEndpoint < RestObjectEndpoint def initialize(server) super(server, %w(id containername)) end undef_method(:put) def populate_defaults(request, response_json) container = FFI_Yajl::Parser.parse(response_json, :create_additions => false) container = ChefData::DataNormalizer.normalize_container(container, request.rest_path[3]) FFI_Yajl::Encoder.encode(container, :pretty => true) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/user_association_requests_endpoint.rb0000644000004100000410000000115512654006514027706 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' module ChefZero module Endpoints # /users/USER/association_requests class UserAssociationRequestsEndpoint < RestBase def get(request) get_data(request, request.rest_path[0..-2]) username = request.rest_path[1] result = list_data(request, [ 'organizations' ]).select do |org| exists_data?(request, [ 'organizations', org, 'association_requests', username ]) end result = result.map { |org| { "id" => "#{username}-#{org}", "orgname" => org } } json_response(200, result) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/policy_group_endpoint.rb0000644000004100000410000000260412654006514025114 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' require 'chef_zero/chef_data/data_normalizer' module ChefZero module Endpoints # /organizations/ORG/policy_groups/NAME class PolicyGroupEndpoint < RestBase # GET /organizations/ORG/policy_groups/NAME def get(request) data = { uri: build_uri(request.base_uri, request.rest_path), policies: get_policy_group_policies(request) } json_response(200, data) end # build a hash of {"some_policy_name"=>{"revision_id"=>"909c26701e291510eacdc6c06d626b9fa5350d25"}} def get_policy_group_policies(request) policies_revisions = {} policies_path = request.rest_path + ["policies"] policy_names = list_data(request, policies_path) policy_names.each do |policy_name| revision = parse_json(get_data(request, policies_path + [policy_name])) policies_revisions[policy_name] = { revision_id: revision} end policies_revisions end # DELETE /organizations/ORG/policy_groups/NAME def delete(request) policy_group_policies = get_policy_group_policies(request) delete_data_dir(request, nil, :recursive) data = { uri: build_uri(request.base_uri, request.rest_path), policies: policy_group_policies } json_response(200, data) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/data_bag_item_endpoint.rb0000644000004100000410000000160212654006514025136 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/endpoints/rest_object_endpoint' require 'chef_zero/endpoints/data_bag_item_endpoint' require 'chef_zero/chef_data/data_normalizer' module ChefZero module Endpoints # /data/NAME/NAME class DataBagItemEndpoint < RestObjectEndpoint def initialize(server) super(server, 'id') end def populate_defaults(request, response_json) DataBagItemEndpoint::populate_defaults(request, response_json, request.rest_path[3], request.rest_path[4]) end def self.populate_defaults(request, response_json, data_bag, data_bag_item) response = FFI_Yajl::Parser.parse(response_json, :create_additions => false) response = ChefData::DataNormalizer.normalize_data_bag_item(response, data_bag, data_bag_item, request.method) FFI_Yajl::Encoder.encode(response, :pretty => true) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/organization_association_requests_endpoint.rb0000644000004100000410000000200612654006514031430 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' module ChefZero module Endpoints # /organizations/ORG/association_requests class OrganizationAssociationRequestsEndpoint < RestBase def post(request) json = FFI_Yajl::Parser.parse(request.body, :create_additions => false) username = json['user'] orgname = request.rest_path[1] id = "#{username}-#{orgname}" if exists_data?(request, [ 'organizations', orgname, 'users', username ]) raise RestErrorResponse.new(409, "User #{username} is already in organization #{orgname}") end create_data(request, request.rest_path, username, '{}') json_response(201, { "uri" => build_uri(request.base_uri, request.rest_path + [ id ]) }) end def get(request) orgname = request.rest_path[1] ChefZero::Endpoints::OrganizationUserBase.get(self, request) do |username| { "id" => "#{username}-#{orgname}", 'username' => username } end end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/dummy_endpoint.rb0000644000004100000410000000216112654006514023532 0ustar www-datawww-data # pedant makes a couple of Solr-related calls from its search_utils.rb file that we can't work around (e.g. # with monkeypatching). the necessary Pedant::Config values are set in run_oc_pedant.rb. --cdoherty module ChefZero module Endpoints class DummyEndpoint < RestBase # called by #direct_solr_query, once each for roles, nodes, and data bag items. each RSpec example makes # 3 calls, with the expected sequence of return values [0, 1, 0]. def get(request) # this could be made less brittle, but if things change to have more than 3 cycles, we should really # be notified by a spec failure. @mock_values ||= ([0, 1, 0] * 3).map { |val| make_response(val) } retval = @mock_values.shift json_response(200, retval) end # called by #force_solr_commit in pedant's , which doesn't check the return value. def post(request) # sure thing! json_response(200, { message: "This dummy POST endpoint didn't do anything." }) end def make_response(value) { "response" => { "numFound" => value } } end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/not_found_endpoint.rb0000644000004100000410000000046112654006514024373 0ustar www-datawww-datarequire 'ffi_yajl' module ChefZero module Endpoints class NotFoundEndpoint def call(request) return [404, {"Content-Type" => "application/json"}, FFI_Yajl::Encoder.encode({"error" => ["Object not found: #{request.env['REQUEST_PATH']}"]}, :pretty => true)] end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/organization_user_endpoint.rb0000644000004100000410000000173412654006514026146 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/rest_base' module ChefZero module Endpoints # /organizations/ORG/users/NAME class OrganizationUserEndpoint < RestBase def get(request) username = request.rest_path[3] get_data(request) # 404 if user is not in org user = get_data(request, [ 'users', username ]) user = FFI_Yajl::Parser.parse(user, :create_additions => false) json_response(200, ChefData::DataNormalizer.normalize_user(user, username, ['username'], server.options[:osc_compat], request.method)) end def delete(request) user = get_data(request) delete_data(request) user = FFI_Yajl::Parser.parse(user, :create_additions => false) json_response(200, ChefData::DataNormalizer.normalize_user(user, request.rest_path[3], ['username'], server.options[:osc_compat])) end # Note: post to a named org user is not permitted, alllow invalid method handling (405) end end end chef-zero-4.5.0/lib/chef_zero/endpoints/policy_revision_endpoint.rb0000644000004100000410000000153412654006514025617 0ustar www-datawww-datarequire 'chef_zero/chef_data/data_normalizer' module ChefZero module Endpoints # /organizations/ORG/policies/NAME/revisions/REVISION class PolicyRevisionEndpoint < RestBase # GET /organizations/ORG/policies/NAME/revisions/REVISION def get(request) data = parse_json(get_data(request)) data = ChefData::DataNormalizer.normalize_policy(data, request.rest_path[3], request.rest_path[5]) return json_response(200, data) end # DELETE /organizations/ORG/policies/NAME/revisions/REVISION def delete(request) policyfile_data = parse_json(get_data(request)) policyfile_data = ChefData::DataNormalizer.normalize_policy(policyfile_data, request.rest_path[3], request.rest_path[5]) delete_data(request) return json_response(200, policyfile_data) end end end end chef-zero-4.5.0/lib/chef_zero/endpoints/environment_endpoint.rb0000644000004100000410000000172512654006514024750 0ustar www-datawww-datarequire 'ffi_yajl' require 'chef_zero/endpoints/rest_object_endpoint' require 'chef_zero/chef_data/data_normalizer' module ChefZero module Endpoints # /environments/NAME class EnvironmentEndpoint < RestObjectEndpoint def delete(request) if request.rest_path[3] == "_default" # 405, really? error(405, "The '_default' environment cannot be modified.") else super(request) end end def put(request) if request.rest_path[3] == "_default" error(405, "The '_default' environment cannot be modified.") else super(request) end end def populate_defaults(request, response_json) response = FFI_Yajl::Parser.parse(response_json, :create_additions => false) response = ChefData::DataNormalizer.normalize_environment(response, request.rest_path[3]) FFI_Yajl::Encoder.encode(response, :pretty => true) end end end end chef-zero-4.5.0/lib/chef_zero/version.rb0000644000004100000410000000005012654006514020154 0ustar www-datawww-datamodule ChefZero VERSION = '4.5.0' end chef-zero-4.5.0/lib/chef_zero/log.rb0000644000004100000410000000012312654006514017251 0ustar www-datawww-datarequire 'mixlib/log' module ChefZero class Log extend Mixlib::Log end end chef-zero-4.5.0/lib/chef_zero/rest_request.rb0000644000004100000410000000267312654006514021231 0ustar www-datawww-datarequire 'rack/request' module ChefZero class RestRequest def initialize(env, rest_base_prefix = []) @env = env @rest_base_prefix = rest_base_prefix end attr_reader :env attr_accessor :rest_base_prefix def base_uri @base_uri ||= "#{env['rack.url_scheme']}://#{env['HTTP_HOST']}#{env['SCRIPT_NAME']}" end def base_uri=(value) @base_uri = value end def api_version @env['HTTP_X_OPS_SERVER_API_VERSION'] || 0 end def requestor @env['HTTP_X_OPS_USERID'] end def method @env['REQUEST_METHOD'] end def rest_path @rest_path ||= rest_base_prefix + env['PATH_INFO'].split('/').select { |part| part != "" } end def body=(body) @body = body end def body @body ||= env['rack.input'].read end def query_params @query_params ||= begin params = Rack::Request.new(env).GET params.keys.each do |key| params[key] = URI.unescape(params[key]) end params end end def to_s result = "#{method} #{rest_path.join('/')}" if query_params.size > 0 result << "?#{query_params.map { |k,v| "#{k}=#{v}" }.join('&') }" end if body.chomp != '' result << "\n--- #{method} BODY ---\n" result << body result << "\n" if !body.end_with?("\n") result << "--- END #{method} BODY ---" end result end end end chef-zero-4.5.0/lib/chef_zero/rest_error_response.rb0000644000004100000410000000034612654006514022603 0ustar www-datawww-datamodule ChefZero class RestErrorResponse < StandardError def initialize(response_code, error) @response_code = response_code @error = error end attr_reader :response_code attr_reader :error end end chef-zero-4.5.0/lib/chef_zero/socketless_server_map.rb0000644000004100000410000000413312654006514023077 0ustar www-datawww-data# # Author:: Daniel DeLeo () # Copyright:: Copyright (c) 2012 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'thread' require 'singleton' module ChefZero class ServerNotFound < StandardError end class NoSocketlessPortAvailable < StandardError end class SocketlessServerMap def self.request(port, request_env) instance.request(port, request_env) end def self.server_on_port(port) instance.server_on_port(port) end MUTEX = Mutex.new include Singleton def initialize() reset! end def reset! @servers_by_port = {} end def register_port(port, server) MUTEX.synchronize do @servers_by_port[port] = server end end def register_no_listen_server(server) MUTEX.synchronize do 1.upto(1000) do |port| unless @servers_by_port.key?(port) @servers_by_port[port] = server return port end end raise NoSocketlessPortAvailable, "No socketless ports left to register" end end def has_server_on_port?(port) @servers_by_port.key?(port) end def server_on_port(port) @servers_by_port[port] end def deregister(port) MUTEX.synchronize do @servers_by_port.delete(port) end end def request(port, request_env) server = @servers_by_port[port] raise ServerNotFound, "No socketless chef-zero server on given port #{port.inspect}" unless server server.handle_socketless_request(request_env) end end end chef-zero-4.5.0/lib/chef_zero/rest_base.rb0000644000004100000410000002206612654006514020451 0ustar www-datawww-datarequire 'chef_zero/rest_request' require 'chef_zero/rest_error_response' require 'chef_zero/data_store/data_not_found_error' require 'chef_zero/chef_data/acl_path' module ChefZero class RestBase def initialize(server) @server = server end attr_reader :server def data_store server.data_store end def check_api_version(request) version = request.api_version return nil if version.nil? # Not present in headers if version.to_i.to_s != version.to_s # Version is not an Integer return json_response(406, { "username" => request.requestor }, -1, -1) elsif version.to_i > MAX_API_VERSION or version.to_i < MIN_API_VERSION response = { "error" => "invalid-x-ops-server-api-version", "message" => "Specified version #{version} not supported", "min_api_version" => MIN_API_VERSION, "max_api_version" => MAX_API_VERSION } return json_response(406, response, version, -1) else return nil end end def call(request) response = check_api_version(request) return response unless response.nil? method = request.method.downcase.to_sym if !self.respond_to?(method) accept_methods = [:get, :put, :post, :delete].select { |m| self.respond_to?(m) } accept_methods_str = accept_methods.map { |m| m.to_s.upcase }.join(', ') return [405, {"Content-Type" => "text/plain", "Allow" => accept_methods_str}, "Bad request method for '#{request.env['REQUEST_PATH']}': #{request.env['REQUEST_METHOD']}"] end if json_only && !accepts?(request, 'application', 'json') return [406, {"Content-Type" => "text/plain"}, "Must accept application/json"] end # Dispatch to get()/post()/put()/delete() begin self.send(method, request) rescue RestErrorResponse => e ChefZero::Log.debug("#{e.inspect}\n#{e.backtrace.join("\n")}") error(e.response_code, e.error) end end def json_only true end def accepts?(request, category, type) # If HTTP_ACCEPT is not sent at all, assume it accepts anything # This parses as per http://tools.ietf.org/html/rfc7231#section-5.3 return true if !request.env['HTTP_ACCEPT'] accepts = request.env['HTTP_ACCEPT'].split(/,\s*/).map { |x| x.split(';',2)[0].strip } return accepts.include?("#{category}/#{type}") || accepts.include?("#{category}/*") || accepts.include?('*/*') end def get_data(request, rest_path=nil, *options) rest_path ||= request.rest_path begin data_store.get(rest_path, request) rescue DataStore::DataNotFoundError if options.include?(:nil) nil elsif options.include?(:data_store_exceptions) raise else raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}") end end end def list_data(request, rest_path=nil, *options) rest_path ||= request.rest_path begin data_store.list(rest_path) rescue DataStore::DataNotFoundError if options.include?(:data_store_exceptions) raise else raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}") end end end def delete_data(request, rest_path=nil, *options) rest_path ||= request.rest_path begin data_store.delete(rest_path, *options) rescue DataStore::DataNotFoundError if options.include?(:data_store_exceptions) raise else raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") end end begin acl_path = ChefData::AclPath.get_acl_data_path(rest_path) data_store.delete(acl_path) if acl_path rescue DataStore::DataNotFoundError end end def delete_data_dir(request, rest_path, *options) rest_path ||= request.rest_path begin data_store.delete_dir(rest_path, *options) rescue DataStore::DataNotFoundError if options.include?(:data_store_exceptions) raise else raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") end end begin acl_path = ChefData::AclPath.get_acl_data_path(rest_path) data_store.delete(acl_path) if acl_path rescue DataStore::DataNotFoundError end end def set_data(request, rest_path, data, *options) rest_path ||= request.rest_path begin data_store.set(rest_path, data, *options, :requestor => request.requestor) rescue DataStore::DataNotFoundError if options.include?(:data_store_exceptions) raise else raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") end end end def create_data_dir(request, rest_path, name, *options) rest_path ||= request.rest_path begin data_store.create_dir(rest_path, name, *options, :requestor => request.requestor) rescue DataStore::DataNotFoundError if options.include?(:data_store_exceptions) raise else raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, request.rest_path)}") end rescue DataStore::DataAlreadyExistsError if options.include?(:data_store_exceptions) raise else raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, request.rest_path + [name])}") end end end def create_data(request, rest_path, name, data, *options) rest_path ||= request.rest_path begin data_store.create(rest_path, name, data, *options, :requestor => request.requestor) rescue DataStore::DataNotFoundError if options.include?(:data_store_exceptions) raise else raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, request.rest_path)}") end rescue DataStore::DataAlreadyExistsError if options.include?(:data_store_exceptions) raise else raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, request.rest_path + [name])}") end end end def exists_data?(request, rest_path=nil) rest_path ||= request.rest_path data_store.exists?(rest_path) end def exists_data_dir?(request, rest_path=nil) rest_path ||= request.rest_path data_store.exists_dir?(rest_path) end def error(response_code, error, opts={}) json_response(response_code, {"error" => [error]}, 0, 0, opts) end def json_response(response_code, json, request_version=0, response_version=0, opts={pretty: true}) do_pretty_json = !!opts[:pretty] # make sure we have a proper Boolean. already_json_response(response_code, FFI_Yajl::Encoder.encode(json, :pretty => do_pretty_json), request_version, response_version) end def text_response(response_code, text) [response_code, {"Content-Type" => "text/plain"}, text] end def already_json_response(response_code, json_text, request_version=0, response_version=0) header = { "min_version" => MIN_API_VERSION.to_s, "max_version" => MAX_API_VERSION.to_s, "request_version" => request_version.to_s, "response_version" => response_version.to_s } [ response_code, { "Content-Type" => "application/json", "X-Ops-Server-API-Version" => FFI_Yajl::Encoder.encode(header) }, json_text ] end # To be called from inside rest endpoints def build_uri(base_uri, rest_path) if server.options[:single_org] # Strip off /organizations/chef if we are in single org mode if rest_path[0..1] != [ 'organizations', server.options[:single_org] ] raise "Unexpected URL #{rest_path[0..1]} passed to build_uri in single org mode" else "#{base_uri}/#{rest_path[2..-1].join('/')}" end else "#{base_uri}/#{rest_path.join('/')}" end end def self.build_uri(base_uri, rest_path) "#{base_uri}/#{rest_path.join('/')}" end def populate_defaults(request, response) response end def parse_json(json) FFI_Yajl::Parser.parse(json, create_additions: false) end def to_json(data) FFI_Yajl::Encoder.encode(data, :pretty => true) end def get_data_or_else(request, path, or_else_value) if exists_data?(request, path) parse_json(get_data(request, path)) else or_else_value end end def list_data_or_else(request, path, or_else_value) if exists_data_dir?(request, path) list_data(request, path) else or_else_value end end def hashify_list(list) list.reduce({}) { |acc, obj| acc.merge( obj => {} ) } end def policy_name_invalid?(name) !name.is_a?(String) || name.size > 255 || name =~ /[+ !]/ end end end chef-zero-4.5.0/lib/chef_zero/rspec.rb0000644000004100000410000002775112654006514017624 0ustar www-datawww-datarequire 'tempfile' require 'chef_zero/server' require 'chef_zero/rest_request' module ChefZero module RSpec module RSpecClassMethods attr_accessor :server attr_accessor :client_key attr_reader :request_log def clear_request_log @request_log = [] end def set_server_options(chef_server_options) if server && chef_server_options != server.options server.stop self.server = nil end unless server # TODO: can this be logged easily? # pp :zero_opts => chef_server_options # Set up configuration so that clients will point to the server self.server = ChefZero::Server.new(chef_server_options) self.client_key = Tempfile.new(['chef_zero_client_key', '.pem']) client_key.write(ChefZero::PRIVATE_KEY) client_key.close # Start the server server.start_background server.on_response do |request, response| request_log << [ request, response ] end else server.clear_data end clear_request_log end end extend RSpecClassMethods def when_the_chef_server(description, *tags, &block) context "When the Chef server #{description}", *tags do extend WhenTheChefServerClassMethods include WhenTheChefServerInstanceMethods # Take the passed-in options define_singleton_method(:chef_server_options) { @chef_server_options ||= begin _chef_server_options = { port: 8900, signals: false, log_requests: true } _chef_server_options = _chef_server_options.merge(tags.last) if tags.last.is_a?(Hash) _chef_server_options = _chef_server_options.freeze end } # Merge in chef_server_options from let(:chef_server_options) def chef_server_options chef_server_options = self.class.chef_server_options.dup chef_server_options = chef_server_options.merge(chef_zero_opts) if self.respond_to?(:chef_zero_opts) chef_server_options end before chef_server_options[:server_scope] do if chef_server_options[:server_scope] != self.class.chef_server_options[:server_scope] raise "server_scope: #{chef_server_options[:server_scope]} will not be honored: it can only be set on when_the_chef_server!" end Log.debug("Starting Chef server with options #{chef_server_options}") ChefZero::RSpec.set_server_options(chef_server_options) if chef_server_options[:organization] organization chef_server_options[:organization] end if defined?(Chef::Config) @old_chef_server_url = Chef::Config.chef_server_url @old_node_name = Chef::Config.node_name @old_client_key = Chef::Config.client_key if chef_server_options[:organization] Chef::Config.chef_server_url = "#{ChefZero::RSpec.server.url}/organizations/#{chef_server_options[:organization]}" else Chef::Config.chef_server_url = ChefZero::RSpec.server.url end Chef::Config.node_name = 'admin' Chef::Config.client_key = ChefZero::RSpec.client_key.path Chef::Config.http_retry_count = 0 end end if defined?(Chef::Config) after chef_server_options[:server_scope] do Chef::Config.chef_server_url = @old_chef_server_url Chef::Config.node_name = @old_node_name Chef::Config.client_key = @old_client_key end end instance_eval(&block) end end module WhenTheChefServerClassMethods def organization(name, org = '{}', &block) before(chef_server_options[:server_scope]) { organization(name, org, &block) } end def acl_for(path, data) before(chef_server_options[:server_scope]) { acl_for(path, data) } end def client(name, data, &block) before(chef_server_options[:server_scope]) { client(name, data, &block) } end def container(name, data, &block) before(chef_server_options[:server_scope]) { container(name, data, &block) } end def cookbook(name, version, data = {}, options = {}, &block) before(chef_server_options[:server_scope]) do cookbook(name, version, data, &block) end end def cookbook_artifact(name, identifier, data = {}, &block) before(chef_server_options[:server_scope]) { cookbook_artifact(name, identifier, data, &block) } end def data_bag(name, data, &block) before(chef_server_options[:server_scope]) { data_bag(name, data, &block) } end def environment(name, data, &block) before(chef_server_options[:server_scope]) { environment(name, data, &block) } end def group(name, data, &block) before(chef_server_options[:server_scope]) { group(name, data, &block) } end def node(name, data, &block) before(chef_server_options[:server_scope]) { node(name, data, &block) } end def org_invite(*usernames) before(chef_server_options[:server_scope]) { org_invite(*usernames) } end def org_member(*usernames) before(chef_server_options[:server_scope]) { org_member(*usernames) } end def policy(name, data, &block) before(chef_server_options[:server_scope]) { policy(name, data, &block) } end def policy_group(name, data, &block) before(chef_server_options[:server_scope]) { policy_group(name, data, &block) } end def role(name, data, &block) before(chef_server_options[:server_scope]) { role(name, data, &block) } end def sandbox(name, data, &block) before(chef_server_options[:server_scope]) { sandbox(name, data, &block) } end def user(name, data, &block) before(chef_server_options[:server_scope]) { user(name, data, &block) } end end module WhenTheChefServerInstanceMethods def organization(name, org = '{}', &block) ChefZero::RSpec.server.data_store.set([ 'organizations', name, 'org' ], dejsonize(org), :create_dir, :create) prev_org_name = @current_org @current_org = name prev_object_path = @current_object_path @current_object_path = "organizations/#{name}" if block_given? begin instance_eval(&block) ensure @current_org = prev_org_name @current_object_path = prev_object_path end end end def acl_for(path, data) ChefZero::RSpec.server.load_data({ 'acls' => { path => data } }, current_org) end def acl(data) acl_for(@current_object_path, data) end def client(name, data, &block) with_object_path("clients/#{name}") do ChefZero::RSpec.server.load_data({ 'clients' => { name => data } }, current_org) instance_eval(&block) if block_given? end end def container(name, data, &block) with_object_path("containers/#{name}") do ChefZero::RSpec.server.load_data({ 'containers' => { name => data } }, current_org) instance_eval(&block) if block_given? end end def cookbook(name, version, data = {}, options = {}, &block) with_object_path("cookbooks/#{name}") do # If you didn't specify metadata.rb, we generate it for you. If you # explicitly set it to nil, that means you don't want it at all. if data.has_key?('metadata.rb') if data['metadata.rb'].nil? data.delete('metadata.rb') end else data['metadata.rb'] = "name #{name.inspect}; version #{version.inspect}" end ChefZero::RSpec.server.load_data({ 'cookbooks' => { "#{name}-#{version}" => data.merge(options) }}, current_org) instance_eval(&block) if block_given? end end def cookbook_artifact(name, identifier, data = {}, &block) with_object_path("cookbook_artifacts/#{name}") do # If you didn't specify metadata.rb, we generate it for you. If you # explicitly set it to nil, that means you don't want it at all. if data.has_key?('metadata.rb') if data['metadata.rb'].nil? data.delete('metadata.rb') end else data['metadata.rb'] = "name #{name.inspect}" end ChefZero::RSpec.server.load_data({ 'cookbook_artifacts' => { "#{name}-#{identifier}" => data } }, current_org) instance_eval(&block) if block_given? end end def data_bag(name, data, &block) with_object_path("data/#{name}") do ChefZero::RSpec.server.load_data({ 'data' => { name => data }}, current_org) instance_eval(&block) if block_given? end end def environment(name, data, &block) with_object_path("environments/#{name}") do ChefZero::RSpec.server.load_data({ 'environments' => { name => data } }, current_org) instance_eval(&block) if block_given? end end def group(name, data, &block) with_object_path("groups/#{name}") do ChefZero::RSpec.server.load_data({ 'groups' => { name => data } }, current_org) instance_eval(&block) if block_given? end end def node(name, data, &block) with_object_path("nodes/#{name}") do ChefZero::RSpec.server.load_data({ 'nodes' => { name => data } }, current_org) instance_eval(&block) if block_given? end end def org_invite(*usernames) ChefZero::RSpec.server.load_data({ 'invites' => usernames }, current_org) end def org_member(*usernames) ChefZero::RSpec.server.load_data({ 'members' => usernames }, current_org) end def policy(name, version, data, &block) with_object_path("policies/#{name}") do ChefZero::RSpec.server.load_data({ 'policies' => { name => { version => data } } }, current_org) instance_eval(&block) if block_given? end end def policy_group(name, data, &block) with_object_path("policy_groups/#{name}") do ChefZero::RSpec.server.load_data({ 'policy_groups' => { name => data } }, current_org) instance_eval(&block) if block_given? end end def role(name, data, &block) with_object_path("roles/#{name}") do ChefZero::RSpec.server.load_data({ 'roles' => { name => data } }, current_org) instance_eval(&block) if block_given? end end def sandbox(name, data, &block) with_object_path("sandboxes/#{name}") do ChefZero::RSpec.server.load_data({ 'sandboxes' => { name => data } }, current_org) instance_eval(&block) if block_given? end end def user(name, data, &block) if ChefZero::RSpec.server.options[:osc_compat] with_object_path("users/#{name}") do ChefZero::RSpec.server.load_data({ 'users' => { name => data }}, current_org) instance_eval(&block) if block_given? end else old_object_path = @current_object_path @current_object_path = "users/#{name}" begin ChefZero::RSpec.server.load_data({ 'users' => { name => data }}, current_org) instance_eval(&block) if block_given? ensure @current_object_path = old_object_path end end end def dejsonize(data) if data.is_a?(String) data else FFI_Yajl::Encoder.encode(data, :pretty => true) end end def current_org @current_org || ChefZero::RSpec.server.options[:single_org] || nil end def with_object_path(object_path) old_object_path = @current_object_path @current_object_path = object_path begin yield if block_given? end @current_object_path = old_object_path end end end end chef-zero-4.5.0/lib/chef_zero/server.rb0000644000004100000410000006174412654006514020016 0ustar www-datawww-data# # Author:: John Keiser () # Copyright:: Copyright (c) 2012 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'openssl' require 'open-uri' require 'rubygems' require 'timeout' require 'stringio' require 'rack' require 'webrick' require 'webrick/https' require 'chef_zero' require 'chef_zero/socketless_server_map' require 'chef_zero/chef_data/cookbook_data' require 'chef_zero/chef_data/acl_path' require 'chef_zero/rest_router' require 'chef_zero/data_store/memory_store_v2' require 'chef_zero/data_store/v1_to_v2_adapter' require 'chef_zero/data_store/default_facade' require 'chef_zero/version' require 'chef_zero/endpoints/rest_list_endpoint' require 'chef_zero/endpoints/authenticate_user_endpoint' require 'chef_zero/endpoints/acls_endpoint' require 'chef_zero/endpoints/acl_endpoint' require 'chef_zero/endpoints/actors_endpoint' require 'chef_zero/endpoints/actor_endpoint' require 'chef_zero/endpoints/cookbooks_endpoint' require 'chef_zero/endpoints/cookbook_endpoint' require 'chef_zero/endpoints/cookbook_version_endpoint' require 'chef_zero/endpoints/cookbook_artifacts_endpoint' require 'chef_zero/endpoints/cookbook_artifact_endpoint' require 'chef_zero/endpoints/cookbook_artifact_identifier_endpoint' require 'chef_zero/endpoints/containers_endpoint' require 'chef_zero/endpoints/container_endpoint' require 'chef_zero/endpoints/controls_endpoint' require 'chef_zero/endpoints/dummy_endpoint' require 'chef_zero/endpoints/data_bags_endpoint' require 'chef_zero/endpoints/data_bag_endpoint' require 'chef_zero/endpoints/data_bag_item_endpoint' require 'chef_zero/endpoints/groups_endpoint' require 'chef_zero/endpoints/group_endpoint' require 'chef_zero/endpoints/environment_endpoint' require 'chef_zero/endpoints/environment_cookbooks_endpoint' require 'chef_zero/endpoints/environment_cookbook_endpoint' require 'chef_zero/endpoints/environment_cookbook_versions_endpoint' require 'chef_zero/endpoints/environment_nodes_endpoint' require 'chef_zero/endpoints/environment_recipes_endpoint' require 'chef_zero/endpoints/environment_role_endpoint' require 'chef_zero/endpoints/license_endpoint' require 'chef_zero/endpoints/node_endpoint' require 'chef_zero/endpoints/nodes_endpoint' require 'chef_zero/endpoints/node_identifiers_endpoint' require 'chef_zero/endpoints/organizations_endpoint' require 'chef_zero/endpoints/organization_endpoint' require 'chef_zero/endpoints/organization_association_requests_endpoint' require 'chef_zero/endpoints/organization_association_request_endpoint' require 'chef_zero/endpoints/organization_authenticate_user_endpoint' require 'chef_zero/endpoints/organization_users_endpoint' require 'chef_zero/endpoints/organization_user_endpoint' require 'chef_zero/endpoints/organization_validator_key_endpoint' require 'chef_zero/endpoints/policies_endpoint' require 'chef_zero/endpoints/policy_endpoint' require 'chef_zero/endpoints/policy_revisions_endpoint' require 'chef_zero/endpoints/policy_revision_endpoint' require 'chef_zero/endpoints/policy_groups_endpoint' require 'chef_zero/endpoints/policy_group_endpoint' require 'chef_zero/endpoints/policy_group_policy_endpoint' require 'chef_zero/endpoints/principal_endpoint' require 'chef_zero/endpoints/role_endpoint' require 'chef_zero/endpoints/role_environments_endpoint' require 'chef_zero/endpoints/sandboxes_endpoint' require 'chef_zero/endpoints/sandbox_endpoint' require 'chef_zero/endpoints/searches_endpoint' require 'chef_zero/endpoints/search_endpoint' require 'chef_zero/endpoints/system_recovery_endpoint' require 'chef_zero/endpoints/user_association_requests_endpoint' require 'chef_zero/endpoints/user_association_requests_count_endpoint' require 'chef_zero/endpoints/user_association_request_endpoint' require 'chef_zero/endpoints/user_organizations_endpoint' require 'chef_zero/endpoints/file_store_file_endpoint' require 'chef_zero/endpoints/not_found_endpoint' require 'chef_zero/endpoints/version_endpoint' require 'chef_zero/endpoints/server_api_version_endpoint' module ChefZero class Server DEFAULT_OPTIONS = { :host => '127.0.0.1', :port => 8889, :log_level => :info, :generate_real_keys => true, :single_org => 'chef', :ssl => false }.freeze GLOBAL_ENDPOINTS = [ '/license', '/version', '/server_api_version' ] def initialize(options = {}) @options = DEFAULT_OPTIONS.merge(options) if @options[:single_org] && !@options.has_key?(:osc_compat) @options[:osc_compat] = true end @options.freeze ChefZero::Log.level = @options[:log_level].to_sym @app = nil end # @return [Hash] attr_reader :options # @return [Integer] def port if @port @port elsif !options[:port].respond_to?(:each) options[:port] else raise "port cannot be determined until server is started" end end # @return [WEBrick::HTTPServer] attr_reader :server include ChefZero::Endpoints # # The URL for this Chef Zero server. If the given host is an IPV6 address, # it is escaped in brackets according to RFC-2732. # # @see http://www.ietf.org/rfc/rfc2732.txt RFC-2732 # # @return [String] # def url sch = @options[:ssl] ? 'https' : 'http' @url ||= if @options[:host].include?(':') URI("#{sch}://[#{@options[:host]}]:#{port}").to_s else URI("#{sch}://#{@options[:host]}:#{port}").to_s end end def local_mode_url raise "Port not yet set, cannot generate URL" unless port.kind_of?(Integer) "chefzero://localhost:#{port}" end # # The data store for this server (default is in-memory). # # @return [ChefZero::DataStore] # def data_store @data_store ||= begin result = @options[:data_store] || DataStore::DefaultFacade.new(DataStore::MemoryStoreV2.new, options[:single_org], options[:osc_compat]) if options[:single_org] if !result.respond_to?(:interface_version) || result.interface_version == 1 result = ChefZero::DataStore::V1ToV2Adapter.new(result, options[:single_org]) result = ChefZero::DataStore::DefaultFacade.new(result, options[:single_org], options[:osc_compat]) end else if !result.respond_to?(:interface_version) || result.interface_version == 1 raise "Multi-org not supported by data store #{result}!" end end result end end # # Boolean method to determine if real Public/Private keys should be # generated. # # @return [Boolean] # true if real keys should be created, false otherwise # def generate_real_keys? !!@options[:generate_real_keys] end # # Start a Chef Zero server in the current thread. You can stop this server # by canceling the current thread. # # @param [Boolean|IO] publish # publish the server information to the publish parameter or to STDOUT if it's "true" # # @return [nil] # this method will block the main thread until interrupted # def start(publish = true) publish = publish[:publish] if publish.is_a?(Hash) # Legacy API if publish output = publish.respond_to?(:puts) ? publish : STDOUT output.puts <<-EOH.gsub(/^ {10}/, '') >> Starting Chef Zero (v#{ChefZero::VERSION})... EOH end thread = start_background if publish output = publish.respond_to?(:puts) ? publish : STDOUT output.puts <<-EOH.gsub(/^ {10}/, '') >> WEBrick (v#{WEBrick::VERSION}) on Rack (v#{Rack.release}) is listening at #{url} >> Press CTRL+C to stop EOH end %w[INT TERM].each do |signal| Signal.trap(signal) do puts "\n>> Stopping Chef Zero..." @server.shutdown end end # Move the background process to the main thread thread.join end # # Start a Chef Zero server in a forked process. This method returns the PID # to the forked process. # # @param [Fixnum] wait # the number of seconds to wait for the server to start # # @return [Thread] # the thread the background process is running in # def start_background(wait = 5) @server = WEBrick::HTTPServer.new( :DoNotListen => true, :AccessLog => [], :Logger => WEBrick::Log.new(StringIO.new, 7), :SSLEnable => options[:ssl], :SSLCertName => [ [ 'CN', WEBrick::Utils::getservername ] ], :StartCallback => proc { @running = true } ) ENV['HTTPS'] = 'on' if options[:ssl] @server.mount('/', Rack::Handler::WEBrick, app) # Pick a port if options[:port].respond_to?(:each) options[:port].each do |port| begin @server.listen(options[:host], port) @port = port break rescue Errno::EADDRINUSE ChefZero::Log.info("Port #{port} in use: #{$!}") end end if !@port raise Errno::EADDRINUSE, "No port in :port range #{options[:port]} is available" end else @server.listen(options[:host], options[:port]) @port = options[:port] end # Start the server in the background @thread = Thread.new do begin Thread.current.abort_on_exception = true @server.start ensure @port = nil @running = false end end # Do not return until the web server is genuinely started. while !@running && @thread.alive? sleep(0.01) end SocketlessServerMap.instance.register_port(@port, self) @thread end def start_socketless @port = SocketlessServerMap.instance.register_no_listen_server(self) end def handle_socketless_request(request_env) app.call(request_env) end # # Boolean method to determine if the server is currently ready to accept # requests. This method will attempt to make an HTTP request against the # server. If this method returns true, you are safe to make a request. # # @return [Boolean] # true if the server is accepting requests, false otherwise # def running? !@server.nil? && @running && @server.status == :Running end # # Gracefully stop the Chef Zero server. # # @param [Fixnum] wait # the number of seconds to wait before raising force-terminating the # server # def stop(wait = 5) if @running @server.shutdown if @server @thread.join(wait) if @thread end rescue Timeout::Error if @thread ChefZero::Log.error("Chef Zero did not stop within #{wait} seconds! Killing...") @thread.kill SocketlessServerMap.deregister(port) end ensure @server = nil @thread = nil end def gen_key_pair if generate_real_keys? private_key = OpenSSL::PKey::RSA.new(2048) public_key = private_key.public_key.to_s public_key.sub!(/^-----BEGIN RSA PUBLIC KEY-----/, '-----BEGIN PUBLIC KEY-----') public_key.sub!(/-----END RSA PUBLIC KEY-----(\s+)$/, '-----END PUBLIC KEY-----\1') [private_key.to_s, public_key] else [PRIVATE_KEY, PUBLIC_KEY] end end def on_request(&block) @on_request_proc = block end def on_response(&block) @on_response_proc = block end # Load data in a nice, friendly form: # { # 'roles' => { # 'desert' => '{ "description": "Hot and dry"' }, # 'rainforest' => { "description" => 'Wet and humid' } # }, # 'cookbooks' => { # 'apache2-1.0.1' => { # 'templates' => { 'default' => { 'blah.txt' => 'hi' }} # 'recipes' => { 'default.rb' => 'template "blah.txt"' } # 'metadata.rb' => 'depends "mysql"' # }, # 'apache2-1.2.0' => { # 'templates' => { 'default' => { 'blah.txt' => 'lo' }} # 'recipes' => { 'default.rb' => 'template "blah.txt"' } # 'metadata.rb' => 'depends "mysql"' # }, # 'mysql' => { # 'recipes' => { 'default.rb' => 'file { contents "hi" }' }, # 'metadata.rb' => 'version "1.0.0"' # } # } # } def load_data(contents, org_name = nil) org_name ||= options[:single_org] if org_name.nil? && contents.keys != [ 'users' ] raise "Must pass an org name to load_data or run in single_org mode" end %w(clients containers environments groups nodes roles sandboxes).each do |data_type| if contents[data_type] dejsonize_children(contents[data_type]).each_pair do |name, data| data_store.set(['organizations', org_name, data_type, name], data, :create) end end end if contents['users'] dejsonize_children(contents['users']).each_pair do |name, data| if options[:osc_compat] data_store.set(['organizations', org_name, 'users', name], data, :create) else # Create the user and put them in the org data_store.set(['users', name], data, :create) if org_name data_store.set(['organizations', org_name, 'users', name], '{}', :create) end end end end if contents['members'] contents['members'].each do |name| data_store.set(['organizations', org_name, 'users', name], '{}', :create) end end if contents['invites'] contents['invites'].each do |name| data_store.set(['organizations', org_name, 'association_requests', name], '{}', :create) end end if contents['acls'] dejsonize_children(contents['acls']).each do |path, acl| path = [ 'organizations', org_name ] + path.split('/') path = ChefData::AclPath.get_acl_data_path(path) ChefZero::RSpec.server.data_store.set(path, acl) end end if contents['data'] contents['data'].each_pair do |key, data_bag| data_store.create_dir(['organizations', org_name, 'data'], key, :recursive) dejsonize_children(data_bag).each do |item_name, item| data_store.set(['organizations', org_name, 'data', key, item_name], item, :create) end end end if contents['policies'] contents['policies'].each_pair do |policy_name, policy_struct| # data_store.create_dir(['organizations', org_name, 'policies', policy_name], "revisions", :recursive) dejsonize_children(policy_struct).each do |revision, policy_data| data_store.set(['organizations', org_name, 'policies', policy_name, "revisions", revision], policy_data, :create, :create_dir) end end end if contents['policy_groups'] contents['policy_groups'].each_pair do |group_name, group| group['policies'].each do |policy_name, policy_revision| data_store.set(['organizations', org_name, 'policy_groups', group_name, 'policies', policy_name], FFI_Yajl::Encoder.encode(policy_revision['revision_id'], :pretty => true), :create, :create_dir) end end end %w(cookbooks cookbook_artifacts).each do |cookbook_type| if contents[cookbook_type] contents[cookbook_type].each_pair do |name_version, cookbook| if cookbook_type == 'cookbook_artifacts' name, dash, identifier = name_version.rpartition('-') cookbook_data = ChefData::CookbookData.to_hash(cookbook, name, identifier) elsif name_version =~ /(.+)-(\d+\.\d+\.\d+)$/ cookbook_data = ChefData::CookbookData.to_hash(cookbook, $1, $2) else cookbook_data = ChefData::CookbookData.to_hash(cookbook, name_version) end raise "No version specified" if !cookbook_data[:version] data_store.create_dir(['organizations', org_name, cookbook_type], cookbook_data[:cookbook_name], :recursive) data_store.set(['organizations', org_name, cookbook_type, cookbook_data[:cookbook_name], cookbook_data[:version]], FFI_Yajl::Encoder.encode(cookbook_data, :pretty => true), :create) cookbook_data.values.each do |files| next unless files.is_a? Array files.each do |file| data_store.set(['organizations', org_name, 'file_store', 'checksums', file[:checksum]], get_file(cookbook, file[:path]), :create) end end end end end end def clear_data data_store.clear end def request_handler(&block) @request_handler = block end def to_s "#<#{self.class} #{url}>" end def inspect "#<#{self.class} @url=#{url.inspect}>" end private def endpoints result = if options[:osc_compat] # OSC-only [ [ "/organizations/*/users", ActorsEndpoint.new(self) ], [ "/organizations/*/users/*", ActorEndpoint.new(self) ], [ "/organizations/*/authenticate_user", OrganizationAuthenticateUserEndpoint.new(self) ], ] else # EC-only [ [ "/organizations/*/users", OrganizationUsersEndpoint.new(self) ], [ "/organizations/*/users/*", OrganizationUserEndpoint.new(self) ], [ "/users", ActorsEndpoint.new(self, 'username') ], [ "/users/*", ActorEndpoint.new(self, 'username') ], [ "/users/*/_acl", AclsEndpoint.new(self) ], [ "/users/*/_acl/*", AclEndpoint.new(self) ], [ "/users/*/association_requests", UserAssociationRequestsEndpoint.new(self) ], [ "/users/*/association_requests/count", UserAssociationRequestsCountEndpoint.new(self) ], [ "/users/*/association_requests/*", UserAssociationRequestEndpoint.new(self) ], [ "/users/*/organizations", UserOrganizationsEndpoint.new(self) ], [ "/authenticate_user", AuthenticateUserEndpoint.new(self) ], [ "/system_recovery", SystemRecoveryEndpoint.new(self) ], [ "/license", LicenseEndpoint.new(self) ], [ "/organizations", OrganizationsEndpoint.new(self) ], [ "/organizations/*", OrganizationEndpoint.new(self) ], [ "/organizations/*/_validator_key", OrganizationValidatorKeyEndpoint.new(self) ], [ "/organizations/*/association_requests", OrganizationAssociationRequestsEndpoint.new(self) ], [ "/organizations/*/association_requests/*", OrganizationAssociationRequestEndpoint.new(self) ], [ "/organizations/*/containers", ContainersEndpoint.new(self) ], [ "/organizations/*/containers/*", ContainerEndpoint.new(self) ], [ "/organizations/*/groups", GroupsEndpoint.new(self) ], [ "/organizations/*/groups/*", GroupEndpoint.new(self) ], [ "/organizations/*/organization/_acl", AclsEndpoint.new(self) ], [ "/organizations/*/organizations/_acl", AclsEndpoint.new(self) ], [ "/organizations/*/*/*/_acl", AclsEndpoint.new(self) ], [ "/organizations/*/organization/_acl/*", AclEndpoint.new(self) ], [ "/organizations/*/organizations/_acl/*", AclEndpoint.new(self) ], [ "/organizations/*/*/*/_acl/*", AclEndpoint.new(self) ] ] end result + [ # Both [ "/dummy", DummyEndpoint.new(self) ], [ "/organizations/*/clients", ActorsEndpoint.new(self) ], [ "/organizations/*/clients/*", ActorEndpoint.new(self) ], [ "/organizations/*/controls", ControlsEndpoint.new(self) ], [ "/organizations/*/cookbooks", CookbooksEndpoint.new(self) ], [ "/organizations/*/cookbooks/*", CookbookEndpoint.new(self) ], [ "/organizations/*/cookbooks/*/*", CookbookVersionEndpoint.new(self) ], [ "/organizations/*/cookbook_artifacts", CookbookArtifactsEndpoint.new(self) ], [ "/organizations/*/cookbook_artifacts/*", CookbookArtifactEndpoint.new(self) ], [ "/organizations/*/cookbook_artifacts/*/*", CookbookArtifactIdentifierEndpoint.new(self) ], [ "/organizations/*/data", DataBagsEndpoint.new(self) ], [ "/organizations/*/data/*", DataBagEndpoint.new(self) ], [ "/organizations/*/data/*/*", DataBagItemEndpoint.new(self) ], [ "/organizations/*/environments", RestListEndpoint.new(self) ], [ "/organizations/*/environments/*", EnvironmentEndpoint.new(self) ], [ "/organizations/*/environments/*/cookbooks", EnvironmentCookbooksEndpoint.new(self) ], [ "/organizations/*/environments/*/cookbooks/*", EnvironmentCookbookEndpoint.new(self) ], [ "/organizations/*/environments/*/cookbook_versions", EnvironmentCookbookVersionsEndpoint.new(self) ], [ "/organizations/*/environments/*/nodes", EnvironmentNodesEndpoint.new(self) ], [ "/organizations/*/environments/*/recipes", EnvironmentRecipesEndpoint.new(self) ], [ "/organizations/*/environments/*/roles/*", EnvironmentRoleEndpoint.new(self) ], [ "/organizations/*/nodes", NodesEndpoint.new(self) ], [ "/organizations/*/nodes/*", NodeEndpoint.new(self) ], [ "/organizations/*/nodes/*/_identifiers", NodeIdentifiersEndpoint.new(self) ], [ "/organizations/*/policies", PoliciesEndpoint.new(self) ], [ "/organizations/*/policies/*", PolicyEndpoint.new(self) ], [ "/organizations/*/policies/*/revisions", PolicyRevisionsEndpoint.new(self) ], [ "/organizations/*/policies/*/revisions/*", PolicyRevisionEndpoint.new(self) ], [ "/organizations/*/policy_groups", PolicyGroupsEndpoint.new(self) ], [ "/organizations/*/policy_groups/*", PolicyGroupEndpoint.new(self) ], [ "/organizations/*/policy_groups/*/policies/*", PolicyGroupPolicyEndpoint.new(self) ], [ "/organizations/*/principals/*", PrincipalEndpoint.new(self) ], [ "/organizations/*/roles", RestListEndpoint.new(self) ], [ "/organizations/*/roles/*", RoleEndpoint.new(self) ], [ "/organizations/*/roles/*/environments", RoleEnvironmentsEndpoint.new(self) ], [ "/organizations/*/roles/*/environments/*", EnvironmentRoleEndpoint.new(self) ], [ "/organizations/*/sandboxes", SandboxesEndpoint.new(self) ], [ "/organizations/*/sandboxes/*", SandboxEndpoint.new(self) ], [ "/organizations/*/search", SearchesEndpoint.new(self) ], [ "/organizations/*/search/*", SearchEndpoint.new(self) ], [ "/version", VersionEndpoint.new(self) ], [ "/server_api_version", ServerAPIVersionEndpoint.new(self) ], # Internal [ "/organizations/*/file_store/**", FileStoreFileEndpoint.new(self) ] ] end def global_endpoint?(ep) GLOBAL_ENDPOINTS.any? do |g_ep| ep.start_with?(g_ep) end end def app return @app if @app router = RestRouter.new(endpoints) router.not_found = NotFoundEndpoint.new if options[:single_org] rest_base_prefix = [ 'organizations', options[:single_org] ] else rest_base_prefix = [] end @app = proc do |env| begin prefix = global_endpoint?(env['PATH_INFO']) ? [] : rest_base_prefix request = RestRequest.new(env, prefix) if @on_request_proc @on_request_proc.call(request) end response = nil if @request_handler response = @request_handler.call(request) end unless response response = router.call(request) end if @on_response_proc @on_response_proc.call(request, response) end # Insert Server header response[1]['Server'] = 'chef-zero' # Add CORS header response[1]['Access-Control-Allow-Origin'] = '*' # Puma expects the response to be an array (chunked responses). Since # we are statically generating data, we won't ever have said chunked # response, so fake it. response[-1] = Array(response[-1]) response rescue if options[:log_level] == :debug STDERR.puts "Request Error: #{$!}" STDERR.puts $!.backtrace.join("\n") end end end @app end def dejsonize_children(hash) result = {} hash.each_pair do |key, value| result[key] = dejsonize(value) end result end def dejsonize(value) value.is_a?(Hash) ? FFI_Yajl::Encoder.encode(value, :pretty => true) : value end def get_file(directory, path) value = directory path.split('/').each do |part| value = value[part] end value end end end chef-zero-4.5.0/chef-zero.gemspec0000644000004100000410000000231612654006514016666 0ustar www-datawww-data$:.unshift(File.dirname(__FILE__) + '/lib') require 'chef_zero/version' Gem::Specification.new do |s| s.name = 'chef-zero' s.version = ChefZero::VERSION s.platform = Gem::Platform::RUBY s.summary = 'Self-contained, easy-setup, fast-start in-memory Chef server for testing and solo setup purposes' s.description = s.summary s.author = 'John Keiser' s.email = 'jkeiser@opscode.com' s.homepage = 'http://www.opscode.com' s.license = 'Apache 2.0' s.add_dependency 'mixlib-log', '~> 1.3' s.add_dependency 'hashie', '>= 2.0', '< 4.0' s.add_dependency 'uuidtools', '~> 2.1' s.add_dependency 'ffi-yajl', '~> 2.2' s.add_dependency 'rack' s.add_development_dependency 'pry' s.add_development_dependency 'pry-byebug' s.add_development_dependency 'pry-stack_explorer' s.add_development_dependency 'rake' s.add_development_dependency 'rspec' s.add_development_dependency 'github_changelog_generator' s.add_development_dependency 'chef' s.bindir = 'bin' s.executables = ['chef-zero'] s.require_path = 'lib' s.files = %w(LICENSE README.md Gemfile Rakefile) + Dir.glob('*.gemspec') + Dir.glob('{lib,spec}/**/*', File::FNM_DOTMATCH).reject {|f| File.directory?(f) } end chef-zero-4.5.0/metadata.yml0000644000004100000410000002451712654006514015751 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: chef-zero version: !ruby/object:Gem::Version version: 4.5.0 platform: ruby authors: - John Keiser autorequire: bindir: bin cert_chain: [] date: 2016-01-29 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: mixlib-log requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '1.3' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '1.3' - !ruby/object:Gem::Dependency name: hashie requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '2.0' - - "<" - !ruby/object:Gem::Version version: '4.0' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '2.0' - - "<" - !ruby/object:Gem::Version version: '4.0' - !ruby/object:Gem::Dependency name: uuidtools requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '2.1' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '2.1' - !ruby/object:Gem::Dependency name: ffi-yajl requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '2.2' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '2.2' - !ruby/object:Gem::Dependency name: rack requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: pry requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: pry-byebug requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: pry-stack_explorer requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: rake requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: rspec requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: github_changelog_generator requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: chef requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' description: Self-contained, easy-setup, fast-start in-memory Chef server for testing and solo setup purposes email: jkeiser@opscode.com executables: - chef-zero extensions: [] extra_rdoc_files: [] files: - Gemfile - LICENSE - README.md - Rakefile - bin/chef-zero - chef-zero.gemspec - lib/chef_zero.rb - lib/chef_zero/chef_data/acl_path.rb - lib/chef_zero/chef_data/cookbook_data.rb - lib/chef_zero/chef_data/data_normalizer.rb - lib/chef_zero/chef_data/default_creator.rb - lib/chef_zero/data_store/data_already_exists_error.rb - lib/chef_zero/data_store/data_error.rb - lib/chef_zero/data_store/data_not_found_error.rb - lib/chef_zero/data_store/default_facade.rb - lib/chef_zero/data_store/interface_v1.rb - lib/chef_zero/data_store/interface_v2.rb - lib/chef_zero/data_store/memory_store.rb - lib/chef_zero/data_store/memory_store_v2.rb - lib/chef_zero/data_store/raw_file_store.rb - lib/chef_zero/data_store/v1_to_v2_adapter.rb - lib/chef_zero/data_store/v2_to_v1_adapter.rb - lib/chef_zero/endpoints/acl_endpoint.rb - lib/chef_zero/endpoints/acls_endpoint.rb - lib/chef_zero/endpoints/actor_endpoint.rb - lib/chef_zero/endpoints/actors_endpoint.rb - lib/chef_zero/endpoints/authenticate_user_endpoint.rb - lib/chef_zero/endpoints/container_endpoint.rb - lib/chef_zero/endpoints/containers_endpoint.rb - lib/chef_zero/endpoints/controls_endpoint.rb - lib/chef_zero/endpoints/cookbook_artifact_endpoint.rb - lib/chef_zero/endpoints/cookbook_artifact_identifier_endpoint.rb - lib/chef_zero/endpoints/cookbook_artifacts_endpoint.rb - lib/chef_zero/endpoints/cookbook_endpoint.rb - lib/chef_zero/endpoints/cookbook_version_endpoint.rb - lib/chef_zero/endpoints/cookbooks_base.rb - lib/chef_zero/endpoints/cookbooks_endpoint.rb - lib/chef_zero/endpoints/data_bag_endpoint.rb - lib/chef_zero/endpoints/data_bag_item_endpoint.rb - lib/chef_zero/endpoints/data_bags_endpoint.rb - lib/chef_zero/endpoints/dummy_endpoint.rb - lib/chef_zero/endpoints/environment_cookbook_endpoint.rb - lib/chef_zero/endpoints/environment_cookbook_versions_endpoint.rb - lib/chef_zero/endpoints/environment_cookbooks_endpoint.rb - lib/chef_zero/endpoints/environment_endpoint.rb - lib/chef_zero/endpoints/environment_nodes_endpoint.rb - lib/chef_zero/endpoints/environment_recipes_endpoint.rb - lib/chef_zero/endpoints/environment_role_endpoint.rb - lib/chef_zero/endpoints/file_store_file_endpoint.rb - lib/chef_zero/endpoints/group_endpoint.rb - lib/chef_zero/endpoints/groups_endpoint.rb - lib/chef_zero/endpoints/license_endpoint.rb - lib/chef_zero/endpoints/node_endpoint.rb - lib/chef_zero/endpoints/node_identifiers_endpoint.rb - lib/chef_zero/endpoints/nodes_endpoint.rb - lib/chef_zero/endpoints/not_found_endpoint.rb - lib/chef_zero/endpoints/organization_association_request_endpoint.rb - lib/chef_zero/endpoints/organization_association_requests_endpoint.rb - lib/chef_zero/endpoints/organization_authenticate_user_endpoint.rb - lib/chef_zero/endpoints/organization_endpoint.rb - lib/chef_zero/endpoints/organization_user_base.rb - lib/chef_zero/endpoints/organization_user_endpoint.rb - lib/chef_zero/endpoints/organization_users_endpoint.rb - lib/chef_zero/endpoints/organization_validator_key_endpoint.rb - lib/chef_zero/endpoints/organizations_endpoint.rb - lib/chef_zero/endpoints/policies_endpoint.rb - lib/chef_zero/endpoints/policy_endpoint.rb - lib/chef_zero/endpoints/policy_group_endpoint.rb - lib/chef_zero/endpoints/policy_group_policy_endpoint.rb - lib/chef_zero/endpoints/policy_groups_endpoint.rb - lib/chef_zero/endpoints/policy_revision_endpoint.rb - lib/chef_zero/endpoints/policy_revisions_endpoint.rb - lib/chef_zero/endpoints/principal_endpoint.rb - lib/chef_zero/endpoints/rest_list_endpoint.rb - lib/chef_zero/endpoints/rest_object_endpoint.rb - lib/chef_zero/endpoints/role_endpoint.rb - lib/chef_zero/endpoints/role_environments_endpoint.rb - lib/chef_zero/endpoints/sandbox_endpoint.rb - lib/chef_zero/endpoints/sandboxes_endpoint.rb - lib/chef_zero/endpoints/search_endpoint.rb - lib/chef_zero/endpoints/searches_endpoint.rb - lib/chef_zero/endpoints/server_api_version_endpoint.rb - lib/chef_zero/endpoints/system_recovery_endpoint.rb - lib/chef_zero/endpoints/user_association_request_endpoint.rb - lib/chef_zero/endpoints/user_association_requests_count_endpoint.rb - lib/chef_zero/endpoints/user_association_requests_endpoint.rb - lib/chef_zero/endpoints/user_organizations_endpoint.rb - lib/chef_zero/endpoints/version_endpoint.rb - lib/chef_zero/log.rb - lib/chef_zero/rest_base.rb - lib/chef_zero/rest_error_response.rb - lib/chef_zero/rest_request.rb - lib/chef_zero/rest_router.rb - lib/chef_zero/rspec.rb - lib/chef_zero/server.rb - lib/chef_zero/socketless_server_map.rb - lib/chef_zero/solr/query/binary_operator.rb - lib/chef_zero/solr/query/phrase.rb - lib/chef_zero/solr/query/range_query.rb - lib/chef_zero/solr/query/regexpable_query.rb - lib/chef_zero/solr/query/subquery.rb - lib/chef_zero/solr/query/term.rb - lib/chef_zero/solr/query/unary_operator.rb - lib/chef_zero/solr/solr_doc.rb - lib/chef_zero/solr/solr_parser.rb - lib/chef_zero/version.rb - spec/run_oc_pedant.rb - spec/search_spec.rb - spec/server_spec.rb - spec/socketless_server_map_spec.rb - spec/support/oc_pedant.rb - spec/support/stickywicket.pem homepage: http://www.opscode.com licenses: - Apache 2.0 metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: rubygems_version: 2.4.5.1 signing_key: specification_version: 4 summary: Self-contained, easy-setup, fast-start in-memory Chef server for testing and solo setup purposes test_files: [] chef-zero-4.5.0/LICENSE0000644000004100000410000002514212654006514014446 0ustar www-datawww-data Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. chef-zero-4.5.0/README.md0000644000004100000410000001150412654006514014715 0ustar www-datawww-data Chef Zero ========= [![Status](https://travis-ci.org/chef/chef-zero.svg?branch=master)](https://travis-ci.org/chef/chef-zero) [![Gem Version](https://badge.fury.io/rb/chef-zero.svg)](http://badge.fury.io/rb/chef-zero) [![Stories in Ready](https://badge.waffle.io/chef/chef-zero.png?label=ready&title=Ready)](https://waffle.io/chef/chef-zero) [![Stories in Progress](https://badge.waffle.io/chef/chef-zero.png?label=in+progress&title=In+Progress)](https://waffle.io/chef/chef-zero) Description ----------- Chef Zero is a simple, easy-install, in-memory Chef server that can be useful for Chef Client testing and chef-solo-like tasks that require a full Chef Server. It IS intended to be simple, Chef 11 compliant, easy to run and fast to start. It is NOT intended to be secure, scalable, performant or persistent. It does NO input validation, authentication or authorization (it will not throw a 400, 401 or 403). It does not save data, and will start up empty each time you start it. Because Chef Zero runs in memory, it's super fast and lightweight. This makes it perfect for testing against a "real" Chef Server without mocking the entire Internet. Installation ------------ This server can be installed as a Ruby Gem. $ gem install chef-zero If you're using bundler, add `chef-zero` as a development dependency: ```ruby group :development do gem 'chef-zero' end ``` Or in a `.gemspec` ```ruby s.add_development_dependency 'chef-zero' ``` You can also clone the source repository and install it using `rake install`. Usage ----- One of chef-zero's primary uses is as a small test server for people writing and testing clients. Here's a simple example of starting it up: ```ruby require 'chef_zero/server' server = ChefZero::Server.new(port: 4000) server.start ``` This will create a server instance in the foreground. To stop the server: ```ruby server.stop ``` This is great for debugging and logging requests, but you'll probably want to run this in the background so you have full control of your thread. To run Chef Zero in the background, simply issue the `start_background` command: ```ruby require 'chef_zero/server' server = ChefZero::Server.new(port: 4000) server.start_background ``` You can stop the server the same way: ```ruby server.stop ``` ### Valid Options You may currently pass the following options to the initializer: - `host` - the host to run on (Default: '127.0.0.1') - `port` - the port to run on (Default: 8889) - `debug` - run in debug mode to see all requests and responses (Default: false) CLI (Command Line) ----------------- If you don't want to use Chef Zero as a library, you can simply start an instance with the included `chef-zero` executable: $ chef-zero Note, this will run in the foreground. You now have a fully functional (empty) Chef Server running. To try it out, go into the `chef-zero/playground` directory and run `knife`. It will behave the same as a normal Chef Server, and all normal knife commands will work (show, list, delete, from file, upload, download, diff ...). For example, with +knife-essentials+ (or Chef 11) you can upload everything in the repo: chef-zero/playground> knife upload . Created nodes/desktop.json Created data_bags/dns Created environments/production.json Created nodes/lb.json Created nodes/dns.json Created nodes/ldap.json Created nodes/www.json Created data_bags/dns/services.json Created environments/staging.json Created data_bags/passwords Created data_bags/users Created data_bags/users/jkeiser.json Created data_bags/passwords/github.json Created data_bags/passwords/twitter.json Created data_bags/users/schisamo.json Created data_bags/users/sethvargo.json Created cookbooks/apache2 Created cookbooks/php chef-zero/playground> knife environment list _default production staging To use it in your own repository, create a `knife.rb` like so: chef_server_url 'http://127.0.0.1:8889' node_name 'stickywicket' client_key 'path_to_any_pem_file.pem' And use knife like you normally would. Since Chef Zero does no authentication, any `.pem` file will do. The client just needs something to sign requests with (which will be ignored on the server). Even though it's ignored, the `.pem` must still be a valid format. Now, stop the Chef Zero server and all the data is gone! Run `chef-zero --help` to see a list of the supported flags and options: ```text Usage: chef-zero [ARGS] -H, --host HOST Host to bind to (default: 127.0.0.1) -p, --port PORT Port to listen on --[no-]generate-keys Whether to generate actual keys or fake it (faster). Default: false. -l, --log-level LEVEL Set the output log level -h, --help Show this message --version Show version ```