chef-zero-5.1.1/0000755000004100000410000000000013030257613013430 5ustar www-datawww-datachef-zero-5.1.1/Rakefile0000644000004100000410000000407613030257613015104 0ustar www-datawww-datarequire "bundler" require "bundler/gem_tasks" require "chef_zero/version" def run_oc_pedant(env = {}) ENV.update(env) require File.expand_path("spec/run_oc_pedant") end ENV_DOCS = < :pedant desc "Run specs" task :spec do system("rspec spec/*_spec.rb") end desc "Run oc-chef-pedant\n\n#{ENV_DOCS}" task :pedant => :oc_pedant desc "Run oc-chef-pedant with CHEF_FS set\n\n#{ENV_DOCS}" task :cheffs do run_oc_pedant("CHEF_FS" => "yes") end desc "Run oc-chef-pedant with FILE_STORE set\n\n#{ENV_DOCS}" task :filestore do run_oc_pedant("FILE_STORE" => "yes") end task :oc_pedant do 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 begin require "chefstyle" require "rubocop/rake_task" RuboCop::RakeTask.new(:style) do |task| task.options += ["--display-cop-names", "--no-color"] end rescue LoadError puts "chefstyle/rubocop is not available. gem install chefstyle to do style checking." end begin require "github_changelog_generator/task" GitHubChangelogGenerator::RakeTask.new :changelog do |config| config.future_release = ChefZero::VERSION config.enhancement_labels = "enhancement,Enhancement,New Feature,Feature".split(",") config.bug_labels = "bug,Bug,Improvement,Upstream Bug".split(",") config.exclude_labels = "duplicate,question,invalid,wontfix,no_changelog,Exclude From Changelog,Question,Discussion".split(",") end rescue LoadError puts "github_changelog_generator is not available. gem install github_changelog_generator to generate changelogs" end chef-zero-5.1.1/bin/0000755000004100000410000000000013030257613014200 5ustar www-datawww-datachef-zero-5.1.1/bin/chef-zero0000755000004100000410000000536713030257613016023 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] ||= [] 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 if ENV["OS"] == "Windows_NT" abort "Daemonization is not supported on Windows. Running 'start chef-zero' will fork the process." else abort "Process.daemon requires Ruby >= 1.9" end end else server.start(true) end chef-zero-5.1.1/Gemfile0000644000004100000410000000065113030257613014725 0ustar www-datawww-datasource "https://rubygems.org" gemspec # gem 'rest-client', :git => 'https://github.com/chef/rest-client.git' gem "oc-chef-pedant", :git => "https://github.com/chef/chef-server.git" group :changelog do gem "github_changelog_generator" end group :development, :test do gem "chefstyle", "= 0.3.1" end gem "chef" if ENV["GEMFILE_MOD"] puts "GEMFILE_MOD: #{ENV['GEMFILE_MOD']}" instance_eval(ENV["GEMFILE_MOD"]) end chef-zero-5.1.1/spec/0000755000004100000410000000000013030257613014362 5ustar www-datawww-datachef-zero-5.1.1/spec/server_spec.rb0000644000004100000410000000573413030257613017240 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 it "has a very patient request timeout" do expect(@server.server.config[:RequestTimeout]).to eq 300 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-5.1.1/spec/run_oc_pedant.rb0000644000004100000410000001461613030257613017537 0ustar www-datawww-data#!/usr/bin/env ruby require "bundler" require "bundler/setup" require "chef_zero/server" require "rspec/core" # This file runs oc-chef-pedant specs and is invoked by `rake pedant` # and other Rake tasks. Run `rake -T` to list tasks. # # Options for oc-chef-pedant and rspec can be specified via # ENV['PEDANT_OPTS'] and ENV['RSPEC_OPTS'], respectively. # # The log level can be specified via ENV['LOG_LEVEL']. # # Example: # # $ PEDANT_OPTS="--focus-users --skip-keys" \ # > RSPEC_OPTS="--fail-fast --profile 5" \ # > LOG_LEVEL=debug \ # > rake pedant # DEFAULT_SERVER_OPTIONS = { port: 8889, single_org: false, }.freeze DEFAULT_LOG_LEVEL = :warn def log_level return ENV["LOG_LEVEL"].downcase.to_sym if ENV["LOG_LEVEL"] return :debug if ENV["DEBUG"] DEFAULT_LOG_LEVEL end def start_chef_server(opts = {}) opts = DEFAULT_SERVER_OPTIONS.merge(opts) opts[:log_level] = log_level ChefZero::Server.new(opts).tap { |server| server.start_background } end 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" ] }') start_chef_server(data_store: data_store) end def pedant_args_from_env args_from_env("PEDANT_OPTS") end def rspec_args_from_env args_from_env("RSPEC_OPTS") end def args_from_env(key) return [] unless ENV[key] ENV[key].split end begin tmpdir = nil server = 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) start_chef_server(data_store: data_store) elsif ENV["CHEF_FS"] require "tmpdir" tmpdir = Dir.mktmpdir start_cheffs_server(tmpdir) else start_chef_server 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", "--skip-user-keys", # 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 unless Gem::Requirement.new(">= 12.8.0").satisfied_by?(Gem::Version.new(Chef::VERSION)) chef_fs_skips << "--skip-keys" end unless Gem::Requirement.new(">= 12.13.19").satisfied_by?(Gem::Version.new(Chef::VERSION)) chef_fs_skips << "--skip-acl" chef_fs_skips << "--skip-cookbook-artifacts" chef_fs_skips << "--skip-policies" end # 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 # 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 # 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 + pedant_args_from_env) rspec_args = Pedant.config.rspec_args + rspec_args_from_env if defined? Chef::ChefFS::FileSystemCache RSpec.configure do |c| c.before(:each) do Chef::ChefFS::FileSystemCache.instance.reset! end end end result = RSpec::Core::Runner.run(rspec_args) server.stop if server.running? ensure FileUtils.remove_entry_secure(tmpdir) if tmpdir end exit(result) chef-zero-5.1.1/spec/search_spec.rb0000644000004100000410000000151013030257613017163 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 -" do search_for("-foo:a").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-5.1.1/spec/support/0000755000004100000410000000000013030257613016076 5ustar www-datawww-datachef-zero-5.1.1/spec/support/stickywicket.pem0000644000004100000410000000321713030257613021321 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-5.1.1/spec/support/oc_pedant.rb0000644000004100000410000001340013030257613020355 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-5.1.1/spec/socketless_server_map_spec.rb0000644000004100000410000000442413030257613022327 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-5.1.1/lib/0000755000004100000410000000000013030257613014176 5ustar www-datawww-datachef-zero-5.1.1/lib/chef_zero.rb0000644000004100000410000000666213030257613016501 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-5.1.1/lib/chef_zero/0000755000004100000410000000000013030257613016142 5ustar www-datawww-datachef-zero-5.1.1/lib/chef_zero/rest_router.rb0000644000004100000410000000342513030257613021050 0ustar www-datawww-datarequire "pp" module 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) log_request(request) clean_path = "/" + request.rest_path.join("/") find_endpoint(clean_path).call(request).tap do |response| log_response(response) end rescue => ex exception = "#{ex.inspect}\n#{ex.backtrace.join("\n")}" ChefZero::Log.error(exception) [ 500, { "Content-Type" => "text/plain" }, "Exception raised! #{exception}" ] end private def find_endpoint(clean_path) _, endpoint = routes.find { |route, endpoint| route.match(clean_path) } endpoint || not_found end def log_request(request) ChefZero::Log.debug do "#{request.method} /#{request.rest_path.join("/")}".tap do |msg| next unless request.method =~ /^(POST|PUT)$/ if request.body.nil? || request.body.empty? msg << " (no body)" else msg << [ "", "--- #{request.method} BODY ---", request.body.chomp, "--- END #{request.method} BODY ---", ].join("\n") end end end ChefZero::Log.debug { request.pretty_inspect } end def log_response(response) ChefZero::Log.debug do [ "", "--- RESPONSE (#{response[0]}) ---", response[2].chomp, "--- END RESPONSE ---", ].join("\n") end end end end chef-zero-5.1.1/lib/chef_zero/chef_data/0000755000004100000410000000000013030257613020040 5ustar www-datawww-datachef-zero-5.1.1/lib/chef_zero/chef_data/default_creator.rb0000644000004100000410000003600713030257613023536 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" %w{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 ])) 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 ])) 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) unknown_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)) if !client["validator"] unknown_owners |= [ path[3] ] end rescue unknown_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 ])) next if client["validator"] rescue end unknown_owners |= [ creator ] end end else unknown_owners |= @creators[path] if @creators[path] end owners = filter_owners(path, unknown_owners) #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[:users] += superusers end else owners = { clients: [], users: [] } end owners[:users].uniq! owners[:clients].uniq! owners end # Figures out if an object was created by a user or client. # If the object does not exist in the context # of an organization, it can only be a user # # This isn't perfect, because we are never explicitly told # if a requestor creating an object is a user or client - # but it gets us reasonably close def filter_owners(path, unknown_owners) owners = { clients: [], users: [] } unknown_owners.each do |entity| if path[0] == "organizations" && path.length > 2 begin data.get(["organizations", path[1], "clients", entity]) owners[:clients] |= [ entity ] rescue owners[:users] |= [ entity ] end else owners[:users] |= [ entity ] end end owners end def default_acl(acl_path, acl = {}) owners = get_owners(acl_path) container_acl = nil PERMISSIONS.each do |perm| acl[perm] ||= {} acl[perm]["users"] = owners[:users] acl[perm]["clients"] = owners[:clients] 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 acl[perm]["actors"] = acl[perm]["clients"] + acl[perm]["users"] 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)) 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-5.1.1/lib/chef_zero/chef_data/cookbook_data.rb0000644000004100000410000001730713030257613023174 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 gem(*opts) self[:gems] ||= [] self[:gems] << opts 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, *values) if values.nil? self[key.to_sym] else if values.length > 1 store key.to_sym, values else store key.to_sym, values.first end 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", true), :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-5.1.1/lib/chef_zero/chef_data/acl_path.rb0000644000004100000410000001205013030257613022136 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" %w{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} %w{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 %w{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-5.1.1/lib/chef_zero/chef_data/data_normalizer.rb0000644000004100000410000002015113030257613023537 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]["groups"] ||= [] if acls[perm].has_key? "users" # When clients and users are split, their combined list # is the final list of actors that a subsequent GET will # provide. Each list is guaranteed to be unique, but the # combined list is not. acls[perm]["actors"] = acls[perm]["clients"].uniq + acls[perm]["users"].uniq else # this gets doubled sometimes, for reasons. (acls[perm]["actors"] ||= []).uniq! end end acls end def self.normalize_client(client, name, orgname = nil) client["name"] ||= name client["clientname"] ||= name client["admin"] = !!client["admin"] if client.key?("admin") client["public_key"] = PUBLIC_KEY unless client.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 unless user.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"] ||= { "tags" => [] } 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 do |item| case item when /^recipe\[.*\]$/ item # explicit recipe when /^role\[.*\]$/ item # explicit role else "recipe[#{item}]" end end.uniq end end end end chef-zero-5.1.1/lib/chef_zero/solr/0000755000004100000410000000000013030257613017121 5ustar www-datawww-datachef-zero-5.1.1/lib/chef_zero/solr/solr_parser.rb0000644000004100000410000001405413030257613022005 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 ParseError < RuntimeError; end 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 loop do if @query_string[@index] == '\\' @index += 1 end @index += 1 if !eof? break if eof? || !peek_term_token end @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 ChefZero::Solr::ParseError, "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-5.1.1/lib/chef_zero/solr/solr_doc.rb0000644000004100000410000000256213030257613021257 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 yield(key) result << value.to_s end end # Handle manufactured value(s) if yield("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| yield(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 yield(key_so_far || "text", value.to_s) end end end end end chef-zero-5.1.1/lib/chef_zero/solr/query/0000755000004100000410000000000013030257613020266 5ustar www-datawww-datachef-zero-5.1.1/lib/chef_zero/solr/query/range_query.rb0000644000004100000410000000210613030257613023133 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-5.1.1/lib/chef_zero/solr/query/unary_operator.rb0000644000004100000410000000215013030257613023662 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 "-", "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 "-", "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-5.1.1/lib/chef_zero/solr/query/binary_operator.rb0000644000004100000410000000257313030257613024021 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-5.1.1/lib/chef_zero/solr/query/subquery.rb0000644000004100000410000000117313030257613022474 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-5.1.1/lib/chef_zero/solr/query/phrase.rb0000644000004100000410000000113213030257613022072 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-5.1.1/lib/chef_zero/solr/query/term.rb0000644000004100000410000000245713030257613021572 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-5.1.1/lib/chef_zero/solr/query/regexpable_query.rb0000644000004100000410000000151113030257613024154 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-5.1.1/lib/chef_zero/data_store/0000755000004100000410000000000013030257613020267 5ustar www-datawww-datachef-zero-5.1.1/lib/chef_zero/data_store/memory_store_v2.rb0000644000004100000410000001022713030257613023751 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-5.1.1/lib/chef_zero/data_store/memory_store.rb0000644000004100000410000000213113030257613023335 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-5.1.1/lib/chef_zero/data_store/interface_v1.rb0000644000004100000410000000352713030257613023171 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-5.1.1/lib/chef_zero/data_store/data_already_exists_error.rb0000644000004100000410000000157213030257613026043 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-5.1.1/lib/chef_zero/data_store/data_not_found_error.rb0000644000004100000410000000156513030257613025020 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 end chef-zero-5.1.1/lib/chef_zero/data_store/data_error.rb0000644000004100000410000000176013030257613022742 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 attr_reader :path, :cause def initialize(path, cause = nil) @path = path @cause = cause path_for_msg = path.nil? ? "nil" : "/#{path.join('/')}" super "Data path: #{path_for_msg}" end end end end chef-zero-5.1.1/lib/chef_zero/data_store/default_facade.rb0000644000004100000410000001063413030257613023527 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-5.1.1/lib/chef_zero/data_store/v2_to_v1_adapter.rb0000644000004100000410000000516213030257613023757 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-5.1.1/lib/chef_zero/data_store/interface_v2.rb0000644000004100000410000000105013030257613023157 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-5.1.1/lib/chef_zero/data_store/v1_to_v2_adapter.rb0000644000004100000410000001043613030257613023757 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-5.1.1/lib/chef_zero/data_store/raw_file_store.rb0000644000004100000410000001004313030257613023616 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-5.1.1/lib/chef_zero/endpoints/0000755000004100000410000000000013030257613020145 5ustar www-datawww-datachef-zero-5.1.1/lib/chef_zero/endpoints/organization_association_request_endpoint.rb0000644000004100000410000000131213030257613031237 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)) delete_data(request, path) json_response(200, { "id" => id, "username" => username }) end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/policies_endpoint.rb0000644000004100000410000000134613030257613024205 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-5.1.1/lib/chef_zero/endpoints/user_association_request_endpoint.rb0000644000004100000410000000315113030257613027514 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) 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) 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-5.1.1/lib/chef_zero/endpoints/environment_role_endpoint.rb0000644000004100000410000000227513030257613025765 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)) 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-5.1.1/lib/chef_zero/endpoints/node_identifiers_endpoint.rb0000644000004100000410000000133013030257613025701 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.delete("-"), :authz_id => "0" * 32, :org_id => UUIDTools::UUID.parse_raw(request.rest_path[0..1].to_s).to_s.delete("-") } 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-5.1.1/lib/chef_zero/endpoints/cookbooks_base.rb0000644000004100000410000000442113030257613023456 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-5.1.1/lib/chef_zero/endpoints/search_endpoint.rb0000644000004100000410000001700213030257613023637 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) rescue ChefZero::Solr::ParseError bad_search_request(request) end def post(request) orgname = request.rest_path[1] full_results = search(request, orgname) keys = FFI_Yajl::Parser.parse(request.body) 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"], }) rescue ChefZero::Solr::ParseError bad_search_request(request) end private def bad_search_request(request) query_string = request.query_params["q"] resp = { "error" => ["invalid search query: '#{query_string}'"] } json_response(400, resp) end 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"].to_i rows = request.query_params["rows"].to_i # 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), 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 # # Slice the array based on the value of the "rows" query parameter. If # this is a positive integer, we'll get the number of rows asked for. # If it's nil, we'll get -1, which gives us all of the elements. # # Do the same for "start", which will start at 0 if that value is not # given. "rows" => result[start..(rows - 1)], # Also return start and total in the object "start" => start, "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-5.1.1/lib/chef_zero/endpoints/actors_endpoint.rb0000644000004100000410000000667213030257613023700 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) # apply query filters: if one applies, stop processing rest # (precendence matches chef-server: https://github.com/chef/chef-server/blob/268a0c9/src/oc_erchef/apps/chef_objects/src/chef_user.erl#L554-L559) if value = request.query_params["external_authentication_uid"] response[2] = filter("external_authentication_uid", value, request, response[2]) elsif value = request.query_params["email"] response[2] = filter("email", value, request, response[2]) end if request.query_params["verbose"] results = parse_json(response[2]) results.each do |name, url| record = get_data(request, request.rest_path + [ name ], :nil) if record record = parse_json(record) record = ChefData::DataNormalizer.normalize_user(record, name, identity_keys, server.options[:osc_compat]) results[name] = record end end response[2] = to_json(results) end response end def post(request) # First, find out if the user actually posted a public key. If not, make # one. request_body = parse_json(request.body) public_key = request_body["public_key"] skip_key_create = !request.api_v0? && !request_body["create_key"] if !public_key && !skip_key_create private_key, public_key = server.gen_key_pair request_body["public_key"] = public_key request.body = to_json(request_body) elsif skip_key_create request_body["public_key"] = nil request.body = to_json(request_body) end result = super(request) if result[0] == 201 # If we generated a key, stuff it in the response. user_data = parse_json(result[2]) key_data = {} key_data["private_key"] = private_key if private_key key_data["public_key"] = public_key unless request.rest_path[0] == "users" response = if request.api_v0? user_data.merge!(key_data) elsif skip_key_create && !public_key user_data else actor_name = request_body["name"] || request_body["username"] || request_body["clientname"] relpath_to_default_key = [ actor_name, "keys", "default" ] key_data["uri"] = build_uri(request.base_uri, request.rest_path + relpath_to_default_key) key_data["public_key"] = public_key key_data["name"] = "default" key_data["expiration_date"] = "infinity" user_data["chef_key"] = key_data user_data end json_response(201, response) else result end end private def filter(key, value, request, resp) results = parse_json(resp) new_results = {} results.each do |name, url| record = get_data(request, request.rest_path + [ name ], :nil) if record record = parse_json(record) new_results[name] = url if record[key] == value end end to_json(new_results) end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/organizations_endpoint.rb0000644000004100000410000000405713030257613025267 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) 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.delete("-"), "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-5.1.1/lib/chef_zero/endpoints/cookbook_artifacts_endpoint.rb0000644000004100000410000000200513030257613026235 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-5.1.1/lib/chef_zero/endpoints/policy_group_policy_endpoint.rb0000644000004100000410000000761113030257613026471 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-5.1.1/lib/chef_zero/endpoints/group_endpoint.rb0000644000004100000410000000114513030257613023527 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) 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-5.1.1/lib/chef_zero/endpoints/acls_endpoint.rb0000644000004100000410000000247213030257613023321 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)) acls = ChefData::DataNormalizer.normalize_acls(acls) if request.query_params["detail"] == "granular" acls.each do |perm, ace| acls[perm]["actors"] = [] end else acls.each do |perm, ace| acls[perm].delete("clients") acls[perm].delete("users") end end json_response(200, acls) end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/organization_user_default_key_endpoint.rb0000644000004100000410000000073613030257613030516 0ustar www-datawww-datarequire "chef_zero/rest_base" module ChefZero module Endpoints # GET /organizations/ORG/users/USER/keys/default class OrganizationUserDefaultKeyEndpoint < RestBase def get(request) # 404 if it doesn't exist get_data(request, request.rest_path[0..3]) # Just use the /users/USER/keys/default endpoint request.rest_path = request.rest_path[2..-1] ActorDefaultKeyEndpoint.new(server).get(request) end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/file_store_file_endpoint.rb0000644000004100000410000000110413030257613025520 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-5.1.1/lib/chef_zero/endpoints/user_organizations_endpoint.rb0000644000004100000410000000132213030257613026315 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) { "organization" => ChefData::DataNormalizer.normalize_organization(org, orgname) } end json_response(200, result) end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/controls_endpoint.rb0000644000004100000410000000060513030257613024236 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-5.1.1/lib/chef_zero/endpoints/environment_cookbooks_endpoint.rb0000644000004100000410000000137213030257613027012 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])) 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-5.1.1/lib/chef_zero/endpoints/license_endpoint.rb0000644000004100000410000000121013030257613024006 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-5.1.1/lib/chef_zero/endpoints/sandbox_endpoint.rb0000644000004100000410000000154313030257613024033 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)) 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-5.1.1/lib/chef_zero/endpoints/authenticate_user_endpoint.rb0000644000004100000410000000210613030257613026105 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) 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) 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-5.1.1/lib/chef_zero/endpoints/acl_endpoint.rb0000644000004100000410000000262513030257613023136 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)) acls[perm] = FFI_Yajl::Parser.parse(request.body)[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-5.1.1/lib/chef_zero/endpoints/cookbooks_endpoint.rb0000644000004100000410000000105313030257613024362 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-5.1.1/lib/chef_zero/endpoints/rest_list_endpoint.rb0000644000004100000410000000236213030257613024405 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) identity_keys.map { |k| json[k] }.select { |v| v }.first end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/organization_users_endpoint.rb0000644000004100000410000000315613030257613026324 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) 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_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-5.1.1/lib/chef_zero/endpoints/cookbook_version_endpoint.rb0000644000004100000410000001220213030257613025742 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) 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) 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).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| # as below, this can be racy. begin 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 rescue ChefZero::DataStore::DataNotFoundError 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) 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-5.1.1/lib/chef_zero/endpoints/user_association_requests_count_endpoint.rb0000644000004100000410000000106413030257613031110 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-5.1.1/lib/chef_zero/endpoints/actor_endpoint.rb0000644000004100000410000001270413030257613023506 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 get(request) result = super user_data = parse_json(result[2]) user_data.delete("public_key") unless request.api_v0? json_response(200, user_data) end 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 delete_actor_keys!(request) result end def put(request) # Find out if we're updating the public key. request_body = FFI_Yajl::Parser.parse(request.body) 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.key?("private_key") body_modified = true if request_body.delete("private_key") private_key, public_key = server.gen_key_pair updating_public_key = true request_body["public_key"] = public_key end end # Put modified body back in `request.body` request.body = to_json(request_body) 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 client_or_user_name = identity_key_value(request) || request.rest_path[-1] if is_rename?(request) rename_keys!(request, client_or_user_name) end if request.rest_path[0] == "users" response = { "uri" => build_uri(request.base_uri, [ "users", client_or_user_name ]), } else response = parse_json(result[2]) end if client?(request) response["private_key"] = private_key ? private_key : false else response["private_key"] = private_key if private_key response.delete("public_key") unless updating_public_key end response.delete("password") json_response(result[0], response) else result end end def populate_defaults(request, response_json) response = parse_json(response_json) populated_response = if client?(request) ChefData::DataNormalizer.normalize_client( response, response["name"] || request.rest_path[-1], request.rest_path[1] ) else ChefData::DataNormalizer.normalize_user( response, response["username"] || request.rest_path[-1], identity_keys, server.options[:osc_compat], request.method ) end to_json(populated_response) end private # Move key data to new path def rename_keys!(request, new_client_or_user_name) orig_keys_path = keys_path_base(request) new_keys_path = orig_keys_path.dup .tap { |path| path[-2] = new_client_or_user_name } key_names = list_data_or_else(request, orig_keys_path, nil) return unless key_names # No keys to move key_names.each do |key_name| # Get old data orig_path = [ *orig_keys_path, key_name ] data = get_data(request, orig_path, :data_store_exceptions) # Copy data to new path create_data( request, new_keys_path, key_name, data, :create_dir ) end # Delete original data delete_data_dir(request, orig_keys_path, :recursive, :data_store_exceptions) end def delete_actor_keys!(request) path = keys_path_base(request)[0..-2] delete_data_dir(request, path, :recursive, :data_store_exceptions) rescue DataStore::DataNotFoundError end def client?(request, rest_path = nil) rest_path ||= request.rest_path request.rest_path[2] == "clients" end # Return the data store keys path for the request client or user, e.g. # # /organizations/ORG/clients/CLIENT -> /organizations/ORG/client_keys/CLIENT/keys # /organizations/ORG/users/USER -> /organizations/ORG/user_keys/USER/keys # /users/USER -> /user_keys/USER # def keys_path_base(request, client_or_user_name = nil) rest_path = (rest_path || request.rest_path).dup rest_path = rest_path.dup case rest_path[-2] when "users" rest_path[-2] = "user_keys" when "clients" rest_path[-2] = "client_keys" else raise "Unexpected URL #{rest_path.join("/")}: cannot determine key path" end rest_path << "keys" rest_path end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/searches_endpoint.rb0000644000004100000410000000076713030257613024201 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-5.1.1/lib/chef_zero/endpoints/organization_endpoint.rb0000644000004100000410000000317413030257613025103 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" ])) new_org = FFI_Yajl::Parser.parse(request.body) 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) org = ChefData::DataNormalizer.normalize_organization(org, request.rest_path[1]) FFI_Yajl::Encoder.encode(org, :pretty => true) end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/organization_user_base.rb0000644000004100000410000000042713030257613025231 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-5.1.1/lib/chef_zero/endpoints/actor_key_endpoint.rb0000644000004100000410000000337713030257613024364 0ustar www-datawww-datarequire "chef_zero/rest_base" module ChefZero module Endpoints # ActorKeyEndpoint # # This class handles DELETE/GET/PUT requests for all client/user keys # **except** default public keys, i.e. requests with identity key # "default". Those are handled by ActorDefaultKeyEndpoint. See that class # for more information. # # /users/USER/keys/NAME # /organizations/ORG/clients/CLIENT/keys/NAME class ActorKeyEndpoint < RestBase def get(request) validate_actor!(request) key_path = data_path(request) already_json_response(200, get_data(request, key_path)) end def delete(request) validate_actor!(request) # 404 if actor doesn't exist key_path = data_path(request) data = get_data(request, key_path) delete_data(request, key_path) already_json_response(200, data) end def put(request) validate_actor!(request) # 404 if actor doesn't exist set_data(request, data_path(request), request.body) end private # Returns the keys data store path, which is the same as # `request.rest_path` except with "client_keys" instead of "clients" or # "user_keys" instead of "users." def data_path(request) request.rest_path.dup.tap do |path| if client?(request) path[2] = "client_keys" else path[0] = "user_keys" end end end # Raises RestErrorResponse (404) if actor doesn't exist def validate_actor!(request) actor_path = request.rest_path[ client?(request) ? 0..3 : 0..1 ] get_data(request, actor_path) end def client?(request) request.rest_path[2] == "clients" end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/nodes_endpoint.rb0000644000004100000410000000201013030257613023473 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) node = ChefData::DataNormalizer.normalize_node(node, request.rest_path[3]) FFI_Yajl::Encoder.encode(node, :pretty => true) end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/groups_endpoint.rb0000644000004100000410000000043213030257613023710 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-5.1.1/lib/chef_zero/endpoints/environment_cookbook_endpoint.rb0000644000004100000410000000163213030257613026626 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])) 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-5.1.1/lib/chef_zero/endpoints/data_bags_endpoint.rb0000644000004100000410000000147613030257613024307 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) 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-5.1.1/lib/chef_zero/endpoints/cookbook_artifact_identifier_endpoint.rb0000644000004100000410000000527513030257613030270 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-5.1.1/lib/chef_zero/endpoints/role_endpoint.rb0000644000004100000410000000074613030257613023342 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) role = ChefData::DataNormalizer.normalize_role(role, request.rest_path[3]) FFI_Yajl::Encoder.encode(role, :pretty => true) end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/cookbook_endpoint.rb0000644000004100000410000000247013030257613024203 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]])) 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-5.1.1/lib/chef_zero/endpoints/policy_groups_endpoint.rb0000644000004100000410000000254713030257613025300 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-5.1.1/lib/chef_zero/endpoints/role_environments_endpoint.rb0000644000004100000410000000056713030257613026152 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])) json_response(200, [ "_default" ] + (role["env_run_lists"].keys || [])) end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/node_endpoint.rb0000644000004100000410000000164713030257613023327 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) node = ChefData::DataNormalizer.normalize_node(node, request.rest_path[3]) FFI_Yajl::Encoder.encode(node, :pretty => true) end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/cookbook_artifact_endpoint.rb0000644000004100000410000000137713030257613026065 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-5.1.1/lib/chef_zero/endpoints/policy_revisions_endpoint.rb0000644000004100000410000000076213030257613025777 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-5.1.1/lib/chef_zero/endpoints/server_api_version_endpoint.rb0000644000004100000410000000061513030257613026300 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_version: request.api_version, response_version: API_VERSION) end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/organization_user_key_endpoint.rb0000644000004100000410000000077613030257613027016 0ustar www-datawww-datarequire "chef_zero/rest_base" require "chef_zero/endpoints/actor_keys_endpoint" module ChefZero module Endpoints # GET /organizations/ORG/users/USER/keys/NAME class OrganizationUserKeyEndpoint < RestBase def get(request) # 404 if not a member of the org get_data(request, request.rest_path[0..3]) # Just use the /users/USER/keys endpoint request.rest_path = request.rest_path[2..-1] ActorKeyEndpoint.new(server).get(request) end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/sandboxes_endpoint.rb0000644000004100000410000000304113030257613024356 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)["checksums"] result_checksums = {} needed_checksums.keys.each do |needed_checksum| if list_data(request, request.rest_path[0..1] + %w{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-5.1.1/lib/chef_zero/endpoints/actor_keys_endpoint.rb0000644000004100000410000000767413030257613024553 0ustar www-datawww-datarequire "chef_zero/rest_base" module ChefZero module Endpoints # /users/USER/keys # /organizations/ORG/clients/CLIENT/keys class ActorKeysEndpoint < RestBase DEFAULT_PUBLIC_KEY_NAME = "default" DATE_FORMAT = "%FT%TZ" # e.g. 2015-12-24T21:00:00Z def get(request, alt_uri_root = nil) path = data_path(request) # Get actor or 404 if it doesn't exist actor_json = get_data(request, actor_path(request)) key_names = list_data_or_else(request, path, []) key_names.unshift(DEFAULT_PUBLIC_KEY_NAME) if actor_has_default_public_key?(actor_json) result = key_names.map do |key_name| list_key(request, [ *path, key_name ], alt_uri_root) end json_response(200, result) end def post(request) request_body = parse_json(request.body) # Try loading the client or user so a 404 is returned if it doesn't exist actor_json = get_data(request, actor_path(request)) generate_keys = request_body["public_key"].nil? if generate_keys private_key, public_key = server.gen_key_pair else public_key = request_body["public_key"] end key_name = request_body["name"] if key_name == DEFAULT_PUBLIC_KEY_NAME store_actor_default_public_key!(request, actor_json, public_key) else store_actor_public_key!(request, key_name, public_key, request_body["expiration_date"]) end response_body = { "uri" => key_uri(request, key_name) } response_body["private_key"] = private_key if generate_keys json_response(201, response_body, headers: { "Location" => response_body["uri"] }) end private def store_actor_public_key!(request, name, public_key, expiration_date) data = to_json( "name" => name, "public_key" => public_key, "expiration_date" => expiration_date ) create_data(request, data_path(request), name, data, :create_dir) end def store_actor_default_public_key!(request, actor_json, public_key) actor_data = parse_json(actor_json) if actor_data["public_key"] raise RestErrorResponse.new(409, "Object already exists: #{key_uri(request, DEFAULT_PUBLIC_KEY_NAME)}") end actor_data["public_key"] = public_key set_data(request, actor_path(request), to_json(actor_data)) end # Returns the keys data store path, which is the same as # `request.rest_path` except with "user_keys" instead of "users" or # "client_keys" instead of "clients." def data_path(request) request.rest_path.dup.tap do |path| if client?(request) path[2] = "client_keys" else path[0] = "user_keys" end end end def list_key(request, data_path, alt_uri_root = nil) key_name, expiration_date = if data_path[-1] == DEFAULT_PUBLIC_KEY_NAME [ DEFAULT_PUBLIC_KEY_NAME, "infinity" ] else parse_json(get_data(request, data_path)) .values_at("name", "expiration_date") end expired = expiration_date != "infinity" && DateTime.now > DateTime.strptime(expiration_date, DATE_FORMAT) { "name" => key_name, "uri" => key_uri(request, key_name, alt_uri_root), "expired" => expired } end def client?(request) request.rest_path[2] == "clients" end def key_uri(request, key_name, alt_uri_root = nil) uri_root = alt_uri_root.nil? ? request.rest_path : alt_uri_root build_uri(request.base_uri, [ *uri_root, key_name ]) end def actor_path(request) return request.rest_path[0..3] if client?(request) request.rest_path[0..1] end def actor_has_default_public_key?(actor_json) !!parse_json(actor_json)["public_key"] end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/version_endpoint.rb0000644000004100000410000000035013030257613024055 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-5.1.1/lib/chef_zero/endpoints/data_bag_endpoint.rb0000644000004100000410000000242513030257613024117 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) 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) 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-5.1.1/lib/chef_zero/endpoints/rest_object_endpoint.rb0000644000004100000410000000502613030257613024700 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) # If it's a rename, check for conflict and delete the old value if is_rename?(request) key = identity_key_value(request) 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) existing_json = FFI_Yajl::Parser.parse(existing_value) 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 private # Get the value of the (first existing) identity key from the request body or nil def identity_key_value(request) request_json = parse_json(request.body) identity_keys.map { |k| request_json[k] }.compact.first end # Does this request change the value of the identity key? def is_rename?(request) return false unless key = identity_key_value(request) key != request.rest_path[-1] end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/system_recovery_endpoint.rb0000644000004100000410000000200513030257613025631 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) 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) 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-5.1.1/lib/chef_zero/endpoints/containers_endpoint.rb0000644000004100000410000000155013030257613024540 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-5.1.1/lib/chef_zero/endpoints/policy_endpoint.rb0000644000004100000410000000132513030257613023672 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-5.1.1/lib/chef_zero/endpoints/organization_authenticate_user_endpoint.rb0000644000004100000410000000135613030257613030677 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) 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) verified = user["password"] == password rescue DataStore::DataNotFoundError verified = false end json_response(200, { "name" => name, "verified" => !!verified, }) end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/principal_endpoint.rb0000644000004100000410000000324313030257613024355 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 principal_data = { "name" => name, "type" => type, "public_key" => FFI_Yajl::Parser.parse(json)["public_key"] || PUBLIC_KEY, "authz_id" => "0" * 32, "org_member" => org_member, } response_data = if request.api_v0? principal_data else { "principals" => [ principal_data ] } end json_response(200, response_data) else error(404, "Principal not found") end end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/environment_nodes_endpoint.rb0000644000004100000410000000133213030257613026125 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])) 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-5.1.1/lib/chef_zero/endpoints/environment_cookbook_versions_endpoint.rb0000644000004100000410000001312713030257613030560 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)["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])) 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]])) 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])) 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-5.1.1/lib/chef_zero/endpoints/environment_recipes_endpoint.rb0000644000004100000410000000140113030257613026444 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])) 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]])) result += recipe_names(name, cookbook) end end json_response(200, result.sort) end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/organization_validator_key_endpoint.rb0000644000004100000410000000131513030257613030013 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)) 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-5.1.1/lib/chef_zero/endpoints/container_endpoint.rb0000644000004100000410000000121113030257613024347 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) container = ChefData::DataNormalizer.normalize_container(container, request.rest_path[3]) FFI_Yajl::Encoder.encode(container, :pretty => true) end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/user_association_requests_endpoint.rb0000644000004100000410000000115513030257613027701 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-5.1.1/lib/chef_zero/endpoints/policy_group_endpoint.rb0000644000004100000410000000260713030257613025112 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-5.1.1/lib/chef_zero/endpoints/data_bag_item_endpoint.rb0000644000004100000410000000154513030257613025137 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) 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-5.1.1/lib/chef_zero/endpoints/actor_default_key_endpoint.rb0000644000004100000410000000503413030257613026060 0ustar www-datawww-datarequire "chef_zero/rest_base" module ChefZero module Endpoints # ActorDefaultKeyEndpoint # # This class handles DELETE/GET/PUT requests for client/user default public # keys, i.e. requests with identity key "default". All others are handled # by ActorKeyEndpoint. # # Default public keys are stored with the actor (client or user) instead of # under user/client_keys. Handling those in a separate endpoint offloads # the branching logic onto the router rather than branching in every # endpoint method (`if request.rest_path[-1] == "default" ...`). # # /users/USER/keys/default # /organizations/ORG/clients/CLIENT/keys/default class ActorDefaultKeyEndpoint < RestBase DEFAULT_PUBLIC_KEY_NAME = "default".freeze def get(request) # 404 if actor doesn't exist actor_data = get_actor_data(request) key_data = default_public_key_from_actor(actor_data) # 404 if the actor doesn't have a default key if key_data["public_key"].nil? raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") end json_response(200, default_public_key_from_actor(actor_data)) end def delete(request) path = actor_path(request) actor_data = get_actor_data(request) # 404 if actor doesn't exist default_public_key = delete_actor_default_public_key!(request, path, actor_data) json_response(200, default_public_key) end def put(request) # 404 if actor doesn't exist actor_data = get_actor_data(request) new_public_key = parse_json(request.body)["public_key"] actor_data["public_key"] = new_public_key set_data(request, actor_path(request), to_json(actor_data)) end private def actor_path(request) return request.rest_path[0..3] if request.rest_path[2] == "clients" request.rest_path[0..1] end def get_actor_data(request) path = actor_path(request) parse_json(get_data(request, path)) end def default_public_key_from_actor(actor_data) { "name" => DEFAULT_PUBLIC_KEY_NAME, "public_key" => actor_data["public_key"], "expiration_date" => "infinity" } end def delete_actor_default_public_key!(request, path, actor_data) new_actor_data = actor_data.merge("public_key" => nil) set_data(request, path, to_json(new_actor_data)) default_public_key_from_actor(actor_data) end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/organization_association_requests_endpoint.rb0000644000004100000410000000175213030257613031432 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) 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-5.1.1/lib/chef_zero/endpoints/dummy_endpoint.rb0000644000004100000410000000215713030257613023532 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-5.1.1/lib/chef_zero/endpoints/organization_user_keys_endpoint.rb0000644000004100000410000000077713030257613027202 0ustar www-datawww-datarequire "chef_zero/rest_base" module ChefZero module Endpoints # GET /organizations/ORG/users/USER/keys class OrganizationUserKeysEndpoint < RestBase def get(request) # 404 if it doesn't exist get_data(request, request.rest_path[0..3]) # Just use the /users/USER/keys/key endpoint original_path = request.rest_path request.rest_path = request.rest_path[2..-1] ActorKeysEndpoint.new(server).get(request, original_path) end end end end chef-zero-5.1.1/lib/chef_zero/endpoints/not_found_endpoint.rb0000644000004100000410000000046513030257613024372 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-5.1.1/lib/chef_zero/endpoints/organization_user_endpoint.rb0000644000004100000410000000164413030257613026141 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) 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) 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-5.1.1/lib/chef_zero/endpoints/policy_revision_endpoint.rb0000644000004100000410000000153413030257613025612 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-5.1.1/lib/chef_zero/endpoints/environment_endpoint.rb0000644000004100000410000000167113030257613024743 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) response = ChefData::DataNormalizer.normalize_environment(response, request.rest_path[3]) FFI_Yajl::Encoder.encode(response, :pretty => true) end end end end chef-zero-5.1.1/lib/chef_zero/version.rb0000644000004100000410000000005013030257613020147 0ustar www-datawww-datamodule ChefZero VERSION = "5.1.1" end chef-zero-5.1.1/lib/chef_zero/log.rb0000644000004100000410000000012313030257613017244 0ustar www-datawww-datarequire "mixlib/log" module ChefZero class Log extend Mixlib::Log end end chef-zero-5.1.1/lib/chef_zero/rest_request.rb0000644000004100000410000000336113030257613021217 0ustar www-datawww-datarequire "rack/request" module ChefZero class RestRequest ZERO = "0".freeze 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 # Load balancer awareness if env["HTTP_X_FORWARDED_PROTO"] scheme = env["HTTP_X_FORWARDED_PROTO"] else scheme = env["rack.url_scheme"] end @base_uri ||= "#{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"] || ZERO end def api_v0? api_version == ZERO 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 rest_path=(rest_path) @rest_path = rest_path 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-5.1.1/lib/chef_zero/rest_error_response.rb0000644000004100000410000000040013030257613022565 0ustar www-datawww-datamodule ChefZero class RestErrorResponse < StandardError attr_reader :response_code, :error def initialize(response_code, error) @response_code = response_code @error = error super "#{response_code}: #{error}" end end end chef-zero-5.1.1/lib/chef_zero/socketless_server_map.rb0000644000004100000410000000413213030257613023071 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-5.1.1/lib/chef_zero/rest_base.rb0000644000004100000410000002502113030257613020436 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 DEFAULT_REQUEST_VERSION = 0 DEFAULT_RESPONSE_VERSION = 0 def initialize(server) @server = server end attr_reader :server def data_store server.data_store end def check_api_version(request) return if request.api_version.nil? # Not present in headers version = request.api_version.to_i unless version.to_s == request.api_version.to_s # Version is not an Integer return json_response(406, { "username" => request.requestor }, request_version: -1, response_version: -1 ) end if version > MAX_API_VERSION || version < 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, request_version: version, response_version: -1 ) 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 rest_path = rest_path.map { |v| URI.decode(v) } 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, 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, 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, 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, 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, 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, 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, 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 ] }, opts) end # Serializes `data` to JSON and returns an Array with the # response code, HTTP headers and JSON body. # # @param [Fixnum] response_code HTTP response code # @param [Hash] data The data for the response body as a Hash # @param [Hash] options # @option options [Hash] :headers (see #already_json_response) # @option options [Boolean] :pretty (true) Pretty-format the JSON # @option options [Fixnum] :request_version (see #already_json_response) # @option options [Fixnum] :response_version (see #already_json_response) # # @return (see #already_json_response) # def json_response(response_code, data, options = {}) options = { pretty: true }.merge(options) do_pretty_json = !!options.delete(:pretty) # make sure we have a proper Boolean. json = FFI_Yajl::Encoder.encode(data, pretty: do_pretty_json) already_json_response(response_code, json, options) end def text_response(response_code, text) [response_code, { "Content-Type" => "text/plain" }, text] end # Returns an Array with the response code, HTTP headers, and JSON body. # # @param [Fixnum] response_code The HTTP response code # @param [String] json_text The JSON body for the response # @param [Hash] options # @option options [Hash] :headers ({}) HTTP headers (may override default headers) # @option options [Fixnum] :request_version (0) Request API version # @option options [Fixnum] :response_version (0) Response API version # # @return [Array(Fixnum, Hash{String => String}, String)] # def already_json_response(response_code, json_text, options = {}) version_header = FFI_Yajl::Encoder.encode( "min_version" => MIN_API_VERSION.to_s, "max_version" => MAX_API_VERSION.to_s, "request_version" => options[:request_version] || DEFAULT_REQUEST_VERSION.to_s, "response_version" => options[:response_version] || DEFAULT_RESPONSE_VERSION.to_s ) headers = { "Content-Type" => "application/json", "X-Ops-Server-API-Version" => version_header, } headers.merge!(options[:headers]) if options[:headers] [ response_code, headers, 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" end return self.class.build_uri(base_uri, rest_path[2..-1]) end self.class.build_uri(base_uri, rest_path) end def self.build_uri(base_uri, rest_path) "#{base_uri}/#{rest_path.map { |v| URI.escape(v) }.join('/')}" end def populate_defaults(request, response) response end def parse_json(json) FFI_Yajl::Parser.parse(json) 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-5.1.1/lib/chef_zero/rspec.rb0000644000004100000410000002775713030257613017625 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) do @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 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.info("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-5.1.1/lib/chef_zero/server.rb0000644000004100000410000006644113030257613020010 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/actor_endpoint" require "chef_zero/endpoints/actors_endpoint" require "chef_zero/endpoints/actor_key_endpoint" require "chef_zero/endpoints/organization_user_key_endpoint" require "chef_zero/endpoints/organization_user_default_key_endpoint" require "chef_zero/endpoints/organization_user_keys_endpoint" require "chef_zero/endpoints/actor_default_key_endpoint" require "chef_zero/endpoints/actor_keys_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 => :warn, :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 # If options[:port] is not an Array or an Enumerable, it is just an Integer. 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" hosts = Array(@options[:host]) @url ||= if hosts.first.include?(":") URI("#{sch}://[#{hosts.first}]:#{port}").to_s else URI("#{sch}://#{hosts.first}:#{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 listen(hosts, port) hosts.each do |host| @server.listen(host, port) end true rescue Errno::EADDRINUSE ChefZero::Log.warn("Port #{port} not available") @server.listeners.each { |l| l.close } @server.listeners.clear false end def start_background(wait = 5) @server = WEBrick::HTTPServer.new( :DoNotListen => true, :AccessLog => [], :Logger => WEBrick::Log.new(StringIO.new, 7), :RequestTimeout => 300, :SSLEnable => options[:ssl], :SSLOptions => ssl_opts, :SSLCertName => [ [ "CN", WEBrick::Utils.getservername ] ], :StartCallback => proc do @running = true end ) ENV["HTTPS"] = "on" if options[:ssl] @server.mount("/", Rack::Handler::WEBrick, app) # Pick a port # If options[:port] can be an Enumerator, an Array, or an Integer, # we need something that can respond to .each (Enum and Array can already). Array(options[:port]).each do |port| if listen(Array(options[:host]), port) @port = port break end end if !@port raise Errno::EADDRINUSE, "No port in :port range #{options[:port]} is available" 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. sleep(0.01) while !@running && @thread.alive? 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/*/keys", ActorKeysEndpoint.new(self) ], [ "/users/*/keys/default", ActorDefaultKeyEndpoint.new(self) ], [ "/users/*/keys/*", ActorKeyEndpoint.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/*/clients/*/keys", ActorKeysEndpoint.new(self) ], [ "/organizations/*/clients/*/keys/default", ActorDefaultKeyEndpoint.new(self) ], [ "/organizations/*/clients/*/keys/*", ActorKeyEndpoint.new(self) ], [ "/organizations/*/users/*/keys", OrganizationUserKeysEndpoint.new(self) ], [ "/organizations/*/users/*/keys/default", OrganizationUserDefaultKeyEndpoint.new(self) ], [ "/organizations/*/users/*/keys/*", OrganizationUserKeyEndpoint.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 ## Disable unsecure ssl ## Ref: https://www.ruby-lang.org/en/news/2014/10/27/changing-default-settings-of-ext-openssl/ def ssl_opts ssl_opts = OpenSSL::SSL::OP_ALL ssl_opts &= ~OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS if defined?(OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS) ssl_opts |= OpenSSL::SSL::OP_NO_COMPRESSION if defined?(OpenSSL::SSL::OP_NO_COMPRESSION) ssl_opts |= OpenSSL::SSL::OP_NO_SSLv2 if defined?(OpenSSL::SSL::OP_NO_SSLv2) ssl_opts |= OpenSSL::SSL::OP_NO_SSLv3 if defined?(OpenSSL::SSL::OP_NO_SSLv3) ssl_opts end end end chef-zero-5.1.1/chef-zero.gemspec0000644000004100000410000000221313030257613016655 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@chef.io" s.homepage = "http://www.chef.io" s.license = "Apache 2.0" s.required_ruby_version = ">= 2.2.2" 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", "~> 2.0" 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.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-5.1.1/LICENSE0000644000004100000410000002514213030257613014441 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-5.1.1/README.md0000644000004100000410000001321313030257613014707 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) ## 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. ```bash $ 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: ```bash $ 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: ```bash 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: ```ruby 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 (e.g. 8889, or 8500-8600 or 8885,8888) --[no-]generate-keys Whether to generate actual keys or fake it (faster). Default: false. -d, --daemon Run as a daemon process -l, --log-level LEVEL Set the output log level --log-file FILE Log to a file --enterprise Whether to run in enterprise mode --multi-org Whether to run in multi-org mode --file-store PATH Persist data to files at the given path --[no-]ssl Use SSL with self-signed certificate(Auto generate before every run). Default: false. -h, --help Show this message --version Show version ``` ## Contributing For information on contributing to this project see ## License Copyright 2012-2016, Chef Software, Inc. ``` 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. ```