pax_global_header00006660000000000000000000000064136233430210014507gustar00rootroot0000000000000052 comment=34ac42b649c171ddea8ec54ec6f69fbd279d4df0 chef-zero-15.0.0/000077500000000000000000000000001362334302100134545ustar00rootroot00000000000000chef-zero-15.0.0/.expeditor/000077500000000000000000000000001362334302100155355ustar00rootroot00000000000000chef-zero-15.0.0/.expeditor/config.yml000066400000000000000000000026731362334302100175350ustar00rootroot00000000000000# Documentation available at https://expeditor.chef.io/docs/getting-started/ --- # Slack channel in Chef Software slack to send notifications about build failures, etc slack: notify_channel: chef-infra-notify # This publish is triggered by the `built_in:publish_rubygems` artifact_action. rubygems: - chef-zero github: # This deletes the GitHub PR branch after successfully merged into the release branch delete_branch_on_merge: true # The tag format to use (e.g. v1.0.0) version_tag_format: "v{{version}}" # allow bumping the minor release via label minor_bump_labels: - "Expeditor: Bump Version Minor" # allow bumping the major release via label major_bump_labels: - "Expeditor: Bump Version Major" changelog: rollup_header: Changes not yet released to rubygems.org # These actions are taken, in order they are specified, anytime a Pull Request is merged. merge_actions: - built_in:bump_version: ignore_labels: - "Expeditor: Skip Version Bump" - "Expeditor: Skip All" - bash:.expeditor/update_version.sh: only_if: built_in:bump_version - built_in:update_changelog: ignore_labels: - "Expeditor: Skip Changelog" - "Expeditor: Skip All" - built_in:build_gem: only_if: built_in:bump_version promote: actions: - built_in:rollover_changelog - built_in:publish_rubygems pipelines: - verify: description: Pull Request validation tests public: true chef-zero-15.0.0/.expeditor/run_linux_tests.sh000077500000000000000000000023451362334302100213450ustar00rootroot00000000000000#!/bin/bash # # This script runs a passed in command, but first setups up the bundler caching on the repo set -ue export USER="root" echo "--- dependencies" export LANG=C.UTF-8 LANGUAGE=C.UTF-8 S3_URL="s3://public-cd-buildkite-cache/${BUILDKITE_PIPELINE_SLUG}/${BUILDKITE_LABEL}" pull_s3_file() { aws s3 cp "${S3_URL}/$1" "$1" || echo "Could not pull $1 from S3" } push_s3_file() { if [ -f "$1" ]; then aws s3 cp "$1" "${S3_URL}/$1" || echo "Could not push $1 to S3 for caching." fi } apt-get update -y apt-get install awscli -y echo "--- bundle install" pull_s3_file "bundle.tar.gz" pull_s3_file "bundle.sha256" if [ -f bundle.tar.gz ]; then tar -xzf bundle.tar.gz fi if [ -n "${RESET_BUNDLE_CACHE:-}" ]; then rm bundle.sha256 fi bundle config --local path vendor/bundle bundle install --jobs=7 --retry=3 echo "--- bundle cache" if test -f bundle.sha256 && shasum --check bundle.sha256 --status; then echo "Bundled gems have not changed. Skipping upload to s3" else echo "Bundled gems have changed. Uploading to s3" shasum -a 256 Gemfile.lock > bundle.sha256 tar -czf bundle.tar.gz vendor/ push_s3_file bundle.tar.gz push_s3_file bundle.sha256 fi echo "+++ bundle exec task" bundle exec $@ chef-zero-15.0.0/.expeditor/update_version.sh000066400000000000000000000007031362334302100211200ustar00rootroot00000000000000#!/bin/sh # # After a PR merge, Chef Expeditor will bump the PATCH version in the VERSION file. # It then executes this file to update any other files/components with that new version. # set -evx sed -i -r "s/^(\s*)VERSION = \".+\"/\1VERSION = \"$(cat VERSION)\"/" lib/chef_zero/version.rb # Once Expeditor finshes executing this script, it will commit the changes and push # the commit as a new tag corresponding to the value in the VERSION file. chef-zero-15.0.0/.expeditor/verify.pipeline.yml000066400000000000000000000012531362334302100213710ustar00rootroot00000000000000--- expeditor: defaults: buildkite: timeout_in_minutes: 30 steps: - label: run-lint-and-specs-ruby-2.6 command: - apt-get update - .expeditor/run_linux_tests.sh rake spec pedant style expeditor: executor: docker: image: ruby:2.6-buster - label: run-lint-and-specs-ruby-2.7 command: - apt-get update - .expeditor/run_linux_tests.sh rake spec pedant style expeditor: executor: docker: image: ruby:2.7-buster - label: run-specs-windows command: - bundle install --jobs=7 --retry=3 --without docs debug - bundle exec rake pedant style expeditor: executor: docker: host_os: windows chef-zero-15.0.0/.github/000077500000000000000000000000001362334302100150145ustar00rootroot00000000000000chef-zero-15.0.0/.github/CODEOWNERS000066400000000000000000000003741362334302100164130ustar00rootroot00000000000000# Order is important. The last matching pattern has the most precedence. * @chef/chef-foundation-owners @chef/chef-foundation-approvers @chef/chef-foundation-reviewers .expeditor/ @chef/jex-team *.md @chef/docs-team chef-zero-15.0.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001362334302100171775ustar00rootroot00000000000000chef-zero-15.0.0/.github/ISSUE_TEMPLATE/BUG_TEMPLATE.md000066400000000000000000000011651362334302100214340ustar00rootroot00000000000000--- name: � Bug Report about: If something isn't working as expected �. labels: "Status: Untriaged, Type: Bug" --- # Version: [Version of the project installed] # Environment: [Details about the environment such as the Operating System, cookbook details, etc...] # Scenario: [What you are trying to achieve and you can't?] # Steps to Reproduce: [If you are filing an issue what are the things we need to do in order to repro your problem?] # Expected Result: [What are you expecting to happen as the consequence of above reproduction steps?] # Actual Result: [What actually happens after the reproduction steps?] chef-zero-15.0.0/.github/ISSUE_TEMPLATE/DESIGN_PROPOSAL.md000066400000000000000000000023721362334302100220150ustar00rootroot00000000000000--- name: Design Proposal about: I have a significant change I would like to propose and discuss before starting labels: "Status: Untriaged, Type: Design Proposal" --- ### When a Change Needs a Design Proposal A design proposal should be opened any time a change meets one of the following qualifications: - Significantly changes the user experience of a project in a way that impacts users. - Significantly changes the underlying architecture of the project in a way that impacts other developers. - Changes the development or testing process of the project such as a change of CI systems or test frameworks. ### Why We Use This Process - Allows all interested parties (including any community member) to discuss large impact changes to a project. - Serves as a durable paper trail for discussions regarding project architecture. - Forces design discussions to occur before PRs are created. - Reduces PR refactoring and rejected PRs. --- ## Motivation ## Specification ## Downstream Impact chef-zero-15.0.0/.github/ISSUE_TEMPLATE/ENHANCEMENT_REQUEST_TEMPLATE.md000066400000000000000000000014071362334302100237530ustar00rootroot00000000000000--- name: 🚀 Enhancement Request about: I have a suggestion (and may want to implement it 🙂)! labels: "Status: Untriaged" --- ### Describe the Enhancement: ### Describe the Need: ### Current Alternative ### Can We Help You Implement This?: chef-zero-15.0.0/.github/ISSUE_TEMPLATE/SUPPORT_QUESTION.md000066400000000000000000000006601362334302100222260ustar00rootroot00000000000000--- name: 🤗 Support Question about: If you have a question 💬, please check out our Slack! --- We use GitHub issues to track bugs and feature requests. If you need help please post to our Mailing List or join the Chef Community Slack. * Chef Community Slack at http://community-slack.chef.io/. * Chef Mailing List https://discourse.chef.io/ Support issues opened here will be closed and redirected to Slack or Discourse. chef-zero-15.0.0/.gitignore000066400000000000000000000003211362334302100154400ustar00rootroot00000000000000*.gem *.rbc .bundle .config .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc/ binstubs/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp rspec.failures gemfiles/*.lock .ruby-version chef-zero-15.0.0/.rspec000066400000000000000000000000371362334302100145710ustar00rootroot00000000000000--format documentation --color chef-zero-15.0.0/.rubocop.yml000066400000000000000000000002011362334302100157170ustar00rootroot00000000000000# Feel free to correct anything in this file Lint/UselessAccessModifier: Exclude: - 'lib/chef_zero/chef_data/acl_path.rb' chef-zero-15.0.0/CHANGELOG.md000066400000000000000000001063121362334302100152700ustar00rootroot00000000000000 # Change Log ## [v15.0.0](https://github.com/chef/chef-zero/tree/v15.0.0) (2020-02-19) #### Merged Pull Requests - remove deprecation warnings for ruby 2.7 [#300](https://github.com/chef/chef-zero/pull/300) ([lamont-granquist](https://github.com/lamont-granquist)) ### Changes not yet released to rubygems.org #### Merged Pull Requests - remove deprecation warnings for ruby 2.7 [#300](https://github.com/chef/chef-zero/pull/300) ([lamont-granquist](https://github.com/lamont-granquist)) ## [v14.0.17](https://github.com/chef/chef-zero/tree/v14.0.17) (2019-12-30) #### Merged Pull Requests - Move testing to Buildkite [#297](https://github.com/chef/chef-zero/pull/297) ([tas50](https://github.com/tas50)) - Apply Chefstyle [#298](https://github.com/chef/chef-zero/pull/298) ([tas50](https://github.com/tas50)) - Substitute require for require_relative [#296](https://github.com/chef/chef-zero/pull/296) ([tas50](https://github.com/tas50)) - Use Chefstyle gem and move dev deps to the Gemfile [#299](https://github.com/chef/chef-zero/pull/299) ([tas50](https://github.com/tas50)) ## [v14.0.13](https://github.com/chef/chef-zero/tree/v14.0.13) (2019-10-07) #### Merged Pull Requests - Add ChefZero::Dist to abstract branding details to a single location [#293](https://github.com/chef/chef-zero/pull/293) ([Tensibai](https://github.com/Tensibai)) ## [v14.0.12](https://github.com/chef/chef-zero/tree/v14.0.12) (2019-03-19) #### Merged Pull Requests - Loosen the mixlib-log depedency + misc cleanup [#292](https://github.com/chef/chef-zero/pull/292) ([tas50](https://github.com/tas50)) ## [v14.0.11](https://github.com/chef/chef-zero/tree/v14.0.11) (2018-11-15) ## [v14.0.11](https://github.com/chef/chef-zero/tree/v14.0.11) (2018-11-15) #### Merged Pull Requests - remove hashrocket syntax [#283](https://github.com/chef/chef-zero/pull/283) ([lamont-granquist](https://github.com/lamont-granquist)) - fixes for new chefstyle [#284](https://github.com/chef/chef-zero/pull/284) ([lamont-granquist](https://github.com/lamont-granquist)) - Misc cleanup for gemspec, rakefile, gemfile, and expeditor [#287](https://github.com/chef/chef-zero/pull/287) ([tas50](https://github.com/tas50)) - Don't ship the readme in the gem [#289](https://github.com/chef/chef-zero/pull/289) ([tas50](https://github.com/tas50)) - Require Rack 2.0.6 or later to resolve CVEs [#288](https://github.com/chef/chef-zero/pull/288) ([tas50](https://github.com/tas50)) ## [v14.0.6](https://github.com/chef/chef-zero/tree/v14.0.6) (2018-04-23) #### Merged Pull Requests - bump required ruby version [#274](https://github.com/chef/chef-zero/pull/274) ([thommay](https://github.com/thommay)) - Disable Hashie method override warns [#276](https://github.com/chef/chef-zero/pull/276) ([adamdecaf](https://github.com/adamdecaf)) - fix for new rubocop engine [#278](https://github.com/chef/chef-zero/pull/278) ([lamont-granquist](https://github.com/lamont-granquist)) - remove the explicit chef gem [#279](https://github.com/chef/chef-zero/pull/279) ([lamont-granquist](https://github.com/lamont-granquist)) - reinstate the default chef gem pin [#280](https://github.com/chef/chef-zero/pull/280) ([lamont-granquist](https://github.com/lamont-granquist)) - pin chef to 14.x [#281](https://github.com/chef/chef-zero/pull/281) ([lamont-granquist](https://github.com/lamont-granquist)) ## [v13.1.0](https://github.com/chef/chef-zero/tree/v13.1.0) (2017-07-17) [Full Changelog](https://github.com/chef/chef-zero/compare/v13.0.0...v13.1.0) **Merged pull requests:** - add the universe endpoint [\#269](https://github.com/chef/chef-zero/pull/269) ([lamont-granquist](https://github.com/lamont-granquist)) - Update gemfile dependencies [\#267](https://github.com/chef/chef-zero/pull/267) ([thommay](https://github.com/thommay)) - GET /users?email=ME@MINE.COM -- compare emails ignoring case [\#266](https://github.com/chef/chef-zero/pull/266) ([srenatus](https://github.com/srenatus)) - GET /users?email=ME@MINE.COM -- compare emails ignoring case [\#265](https://github.com/chef/chef-zero/pull/265) ([srenatus](https://github.com/srenatus)) - implement rfc090 for named nodes endpoint [\#264](https://github.com/chef/chef-zero/pull/264) ([jeremymv2](https://github.com/jeremymv2)) - Add skip-chef-zero-quirks to the defaults in chef-zero [\#263](https://github.com/chef/chef-zero/pull/263) ([jaymalasinha](https://github.com/jaymalasinha)) - Add skip-chef-zero-quirks to the defaults in chef-zero [\#262](https://github.com/chef/chef-zero/pull/262) ([jaymalasinha](https://github.com/jaymalasinha)) - Ensure that tests that use chef-zero git will work [\#259](https://github.com/chef/chef-zero/pull/259) ([thommay](https://github.com/thommay)) ## [v13.0.0](https://github.com/chef/chef-zero/tree/v13.0.0) (2017-04-03) [Full Changelog](https://github.com/chef/chef-zero/compare/v5.3.2...v13.0.0) **Merged pull requests:** - Remove cookbook segments [\#250](https://github.com/chef/chef-zero/pull/250) ([thommay](https://github.com/thommay)) ## [v5.3.2](https://github.com/chef/chef-zero/tree/v5.3.2) (2017-03-28) [Full Changelog](https://github.com/chef/chef-zero/compare/v5.3.1...v5.3.2) **Merged pull requests:** - fix hardcoded default acls to match chef-server [\#257](https://github.com/chef/chef-zero/pull/257) ([srenatus](https://github.com/srenatus)) ## [v5.3.1](https://github.com/chef/chef-zero/tree/v5.3.1) (2017-03-08) [Full Changelog](https://github.com/chef/chef-zero/compare/v5.3.0...v5.3.1) ## [v5.3.0](https://github.com/chef/chef-zero/tree/v5.3.0) (2017-02-02) [Full Changelog](https://github.com/chef/chef-zero/compare/v5.2.0...v5.3.0) **Merged pull requests:** - change require per hashie author [\#252](https://github.com/chef/chef-zero/pull/252) ([lamont-granquist](https://github.com/lamont-granquist)) ## [v5.2.0](https://github.com/chef/chef-zero/tree/v5.2.0) (2017-01-23) [Full Changelog](https://github.com/chef/chef-zero/compare/v5.1.1...v5.2.0) **Merged pull requests:** - favor metadata.json over metadata.rb [\#251](https://github.com/chef/chef-zero/pull/251) ([lamont-granquist](https://github.com/lamont-granquist)) - support ! in searches [\#233](https://github.com/chef/chef-zero/pull/233) ([thommay](https://github.com/thommay)) ## [v5.1.1](https://github.com/chef/chef-zero/tree/v5.1.1) (2016-12-14) [Full Changelog](https://github.com/chef/chef-zero/compare/v5.1.0...v5.1.1) **Merged pull requests:** - Fix pagination with start parameters [\#248](https://github.com/chef/chef-zero/pull/248) ([smith](https://github.com/smith)) - Handle the start and rows parameters correctly [\#247](https://github.com/chef/chef-zero/pull/247) ([smith](https://github.com/smith)) - Add link to contributing docs and fix chefstyle warnings [\#245](https://github.com/chef/chef-zero/pull/245) ([tas50](https://github.com/tas50)) ## [v5.1.0](https://github.com/chef/chef-zero/tree/v5.1.0) (2016-09-07) [Full Changelog](https://github.com/chef/chef-zero/compare/v5.0.0...v5.1.0) **Merged pull requests:** - Bump version to 5.1.0 [\#243](https://github.com/chef/chef-zero/pull/243) ([jkeiser](https://github.com/jkeiser)) - store ACEs by client/user [\#240](https://github.com/chef/chef-zero/pull/240) ([marcparadise](https://github.com/marcparadise)) ## [v5.0.0](https://github.com/chef/chef-zero/tree/v5.0.0) (2016-08-24) [Full Changelog](https://github.com/chef/chef-zero/compare/v4.9.0...v5.0.0) **Merged pull requests:** - Support clients and users fields in ACL PUT requests [\#239](https://github.com/chef/chef-zero/pull/239) ([marcparadise](https://github.com/marcparadise)) - Remove support for Ruby 2.1 [\#237](https://github.com/chef/chef-zero/pull/237) ([jkeiser](https://github.com/jkeiser)) ## [v4.9.0](https://github.com/chef/chef-zero/tree/v4.9.0) (2016-08-11) [Full Changelog](https://github.com/chef/chef-zero/compare/v4.8.0...v4.9.0) **Merged pull requests:** - Connect to chef zero default port in the playground [\#229](https://github.com/chef/chef-zero/pull/229) ([thommay](https://github.com/thommay)) - Correct ruby syntax error to make script usable [\#228](https://github.com/chef/chef-zero/pull/228) ([AntonOfTheWoods](https://github.com/AntonOfTheWoods)) ## [v4.8.0](https://github.com/chef/chef-zero/tree/v4.8.0) (2016-07-25) [Full Changelog](https://github.com/chef/chef-zero/compare/v4.7.1...v4.8.0) **Merged pull requests:** - Load libraries recursively [\#227](https://github.com/chef/chef-zero/pull/227) ([thommay](https://github.com/thommay)) - Reset the ChefFS cache after each test [\#226](https://github.com/chef/chef-zero/pull/226) ([thommay](https://github.com/thommay)) - Format readme, remove waffle badges, add license [\#225](https://github.com/chef/chef-zero/pull/225) ([tas50](https://github.com/tas50)) - Return HTTP 400 Bad Request on Solr parse error [\#224](https://github.com/chef/chef-zero/pull/224) ([lamont-granquist](https://github.com/lamont-granquist)) - Set log level to debug in the rescue block. [\#222](https://github.com/chef/chef-zero/pull/222) ([maxlazio](https://github.com/maxlazio)) - support URI specific character in file name [\#204](https://github.com/chef/chef-zero/pull/204) ([byplayer](https://github.com/byplayer)) - Fix attempted fall-through in case statement. [\#175](https://github.com/chef/chef-zero/pull/175) ([andrewdotn](https://github.com/andrewdotn)) - add patch to support chef\_version [\#171](https://github.com/chef/chef-zero/pull/171) ([lamont-granquist](https://github.com/lamont-granquist)) - Fix scary search behavior on terms containing dash character [\#158](https://github.com/chef/chef-zero/pull/158) ([amazon](https://github.com/amazon)) - Update error message for daemon on Windows [\#148](https://github.com/chef/chef-zero/pull/148) ([aerii](https://github.com/aerii)) - add normal tags with empty array [\#139](https://github.com/chef/chef-zero/pull/139) ([cl-lab-k](https://github.com/cl-lab-k)) - Increase WEBrick request timeout to 300 seconds [\#138](https://github.com/chef/chef-zero/pull/138) ([kongslund](https://github.com/kongslund)) - Make ChefZero aware of load balancers [\#109](https://github.com/chef/chef-zero/pull/109) ([joshk0](https://github.com/joshk0)) - Disable sslv3 and a few insecure options [\#106](https://github.com/chef/chef-zero/pull/106) ([sawanoboly](https://github.com/sawanoboly)) ## [v4.7.1](https://github.com/chef/chef-zero/tree/v4.7.1) (2016-07-07) [Full Changelog](https://github.com/chef/chef-zero/compare/v4.7.0...v4.7.1) **Merged pull requests:** - Downgrade info log message to debug [\#221](https://github.com/chef/chef-zero/pull/221) ([stanhu](https://github.com/stanhu)) ## [v4.7.0](https://github.com/chef/chef-zero/tree/v4.7.0) (2016-06-30) [Full Changelog](https://github.com/chef/chef-zero/compare/v4.6.2...v4.7.0) **Merged pull requests:** - Depend on rack \< 2 to restore Ruby 2.1 compat [\#219](https://github.com/chef/chef-zero/pull/219) ([tas50](https://github.com/tas50)) - Add external\_authentication\_uid to actors endpoint for querying [\#217](https://github.com/chef/chef-zero/pull/217) ([kmacgugan](https://github.com/kmacgugan)) ## [v4.6.2](https://github.com/chef/chef-zero/tree/v4.6.2) (2016-04-28) [Full Changelog](https://github.com/chef/chef-zero/compare/v4.6.1...v4.6.2) **Merged pull requests:** - Log responses only at debug log level [\#216](https://github.com/chef/chef-zero/pull/216) ([stevendanna](https://github.com/stevendanna)) ## [v4.6.1](https://github.com/chef/chef-zero/tree/v4.6.1) (2016-04-14) [Full Changelog](https://github.com/chef/chef-zero/compare/v4.6.0...v4.6.1) **Merged pull requests:** - Actually merge key data in user PUT response [\#214](https://github.com/chef/chef-zero/pull/214) ([jkeiser](https://github.com/jkeiser)) - Fix users endpoint in OSC compat mode to use a data store URL [\#213](https://github.com/chef/chef-zero/pull/213) ([jkeiser](https://github.com/jkeiser)) ## [v4.6.0](https://github.com/chef/chef-zero/tree/v4.6.0) (2016-04-14) [Full Changelog](https://github.com/chef/chef-zero/compare/v4.5.0...v4.6.0) **Merged pull requests:** - Fix bugs related to Array vs Enumerator vs Port for options\[:port/host\]. [\#212](https://github.com/chef/chef-zero/pull/212) ([tylercloke](https://github.com/tylercloke)) - Enable listening on more than one address [\#208](https://github.com/chef/chef-zero/pull/208) ([jaymzh](https://github.com/jaymzh)) - Implemented GET /orgs/ORG/users/USER/keys\(/key\) endpoint recently added to server. [\#205](https://github.com/chef/chef-zero/pull/205) ([tylercloke](https://github.com/tylercloke)) - Implement APIv1 behaviors [\#201](https://github.com/chef/chef-zero/pull/201) ([danielsdeleo](https://github.com/danielsdeleo)) - Make user and client keys endpoints pass Pedant specs [\#199](https://github.com/chef/chef-zero/pull/199) ([jrunning](https://github.com/jrunning)) - fix necessary for metadata gem [\#197](https://github.com/chef/chef-zero/pull/197) ([lamont-granquist](https://github.com/lamont-granquist)) ## [v4.5.0](https://github.com/chef/chef-zero/tree/v4.5.0) (2016-01-29) [Full Changelog](https://github.com/chef/chef-zero/compare/v4.4.2...v4.5.0) **Merged pull requests:** - Run chef-zero against master Chef in travis [\#195](https://github.com/chef/chef-zero/pull/195) ([jkeiser](https://github.com/jkeiser)) - Make ACLs for policies/policy\_groups/cookbook\_artifacts work [\#194](https://github.com/chef/chef-zero/pull/194) ([jkeiser](https://github.com/jkeiser)) - Return 410 on /controls so we stop skipping that pedant spec. [\#192](https://github.com/chef/chef-zero/pull/192) ([randomcamel](https://github.com/randomcamel)) - Enable container specs. [\#191](https://github.com/chef/chef-zero/pull/191) ([randomcamel](https://github.com/randomcamel)) - Enable headers pedant tests [\#190](https://github.com/chef/chef-zero/pull/190) ([danielsdeleo](https://github.com/danielsdeleo)) - Enable knife pedant tests [\#189](https://github.com/chef/chef-zero/pull/189) ([danielsdeleo](https://github.com/danielsdeleo)) - Start running policy and cookbook artifact tests [\#187](https://github.com/chef/chef-zero/pull/187) ([jkeiser](https://github.com/jkeiser)) ## [v4.4.2](https://github.com/chef/chef-zero/tree/v4.4.2) (2016-01-15) [Full Changelog](https://github.com/chef/chef-zero/compare/v4.4.1...v4.4.2) **Merged pull requests:** - Make hoovering and deleting parent dir work everywhere for cookbook\_artifacts [\#186](https://github.com/chef/chef-zero/pull/186) ([jkeiser](https://github.com/jkeiser)) - Explain why omnibus/authz/authN/validation checks are skipped [\#185](https://github.com/chef/chef-zero/pull/185) ([danielsdeleo](https://github.com/danielsdeleo)) ## [v4.4.1](https://github.com/chef/chef-zero/tree/v4.4.1) (2016-01-14) [Full Changelog](https://github.com/chef/chef-zero/compare/v4.4.0...v4.4.1) **Merged pull requests:** - Only test master branch and PRs [\#184](https://github.com/chef/chef-zero/pull/184) ([danielsdeleo](https://github.com/danielsdeleo)) - Internal orgs appears to be unused in oc-chef-pedant [\#183](https://github.com/chef/chef-zero/pull/183) ([danielsdeleo](https://github.com/danielsdeleo)) - Fix cookbook\_artifact rspec [\#182](https://github.com/chef/chef-zero/pull/182) ([jkeiser](https://github.com/jkeiser)) - Point chef-server back to master [\#180](https://github.com/chef/chef-zero/pull/180) ([thommay](https://github.com/thommay)) - Ignore the universe endpoint tests in pedant [\#176](https://github.com/chef/chef-zero/pull/176) ([thommay](https://github.com/thommay)) ## [v4.4.0](https://github.com/chef/chef-zero/tree/v4.4.0) (2015-12-11) [Full Changelog](https://github.com/chef/chef-zero/compare/v4.3.2...v4.4.0) **Merged pull requests:** - ChefZero::RSpec support for cookbook\_artifacts. [\#179](https://github.com/chef/chef-zero/pull/179) ([randomcamel](https://github.com/randomcamel)) - /cookbook\_artifacts support for in-memory and FILE\_STORE backends \(not ChefFS\) [\#178](https://github.com/chef/chef-zero/pull/178) ([randomcamel](https://github.com/randomcamel)) - Update and refactor policy and policy\_groups endpoints [\#177](https://github.com/chef/chef-zero/pull/177) ([jkeiser](https://github.com/jkeiser)) - Point at master of oc-chef-pedant and chef [\#174](https://github.com/chef/chef-zero/pull/174) ([stevendanna](https://github.com/stevendanna)) - Upgrade pedant, and enable running in ChefFS mode [\#173](https://github.com/chef/chef-zero/pull/173) ([randomcamel](https://github.com/randomcamel)) - Implement the /policies and /policy\_groups API routes [\#172](https://github.com/chef/chef-zero/pull/172) ([randomcamel](https://github.com/randomcamel)) - Add gemspec files to allow bundler to run from the gem [\#169](https://github.com/chef/chef-zero/pull/169) ([ksubrama](https://github.com/ksubrama)) ## [v4.3.2](https://github.com/chef/chef-zero/tree/v4.3.2) (2015-09-30) [Full Changelog](https://github.com/chef/chef-zero/compare/v4.3.1...v4.3.2) ## [v4.3.1](https://github.com/chef/chef-zero/tree/v4.3.1) (2015-09-30) [Full Changelog](https://github.com/chef/chef-zero/compare/v4.3.0...v4.3.1) **Merged pull requests:** - Translate admin="true" to admin=true [\#166](https://github.com/chef/chef-zero/pull/166) ([jkeiser](https://github.com/jkeiser)) ## [v4.3.0](https://github.com/chef/chef-zero/tree/v4.3.0) (2015-09-02) [Full Changelog](https://github.com/chef/chef-zero/compare/v4.2.3...v4.3.0) **Merged pull requests:** - Allow Hashie to float to 3.x \(no need to be so specific\) [\#164](https://github.com/chef/chef-zero/pull/164) ([jkeiser](https://github.com/jkeiser)) - Autogenerated changelog [\#163](https://github.com/chef/chef-zero/pull/163) ([jkeiser](https://github.com/jkeiser)) - Adding back logic to delete the association request when adding a user to an org \(as well as adding the user to the groups\) [\#160](https://github.com/chef/chef-zero/pull/160) ([tyler-ball](https://github.com/tyler-ball)) - Server api version [\#155](https://github.com/chef/chef-zero/pull/155) ([andrewjamesbrown](https://github.com/andrewjamesbrown)) - Add /organizations/NAME/nodes/NAME/\_identifiers endpoint [\#152](https://github.com/chef/chef-zero/pull/152) ([andrewjamesbrown](https://github.com/andrewjamesbrown)) - Switch to pedant in chef-server repo [\#151](https://github.com/chef/chef-zero/pull/151) ([andrewjamesbrown](https://github.com/andrewjamesbrown)) - Update gem dependencies [\#146](https://github.com/chef/chef-zero/pull/146) ([andrewjamesbrown](https://github.com/andrewjamesbrown)) - Remove dependency on chef [\#140](https://github.com/chef/chef-zero/pull/140) ([terceiro](https://github.com/terceiro)) - CS12 Support [\#117](https://github.com/chef/chef-zero/pull/117) ([marcparadise](https://github.com/marcparadise)) ## [v4.2.3](https://github.com/chef/chef-zero/tree/v4.2.3) (2015-06-19) [Full Changelog](https://github.com/chef/chef-zero/compare/v4.2.2...v4.2.3) **Merged pull requests:** - Make server\_scope: :context work again [\#143](https://github.com/chef/chef-zero/pull/143) ([jkeiser](https://github.com/jkeiser)) ## [v4.2.2](https://github.com/chef/chef-zero/tree/v4.2.2) (2015-05-18) [Full Changelog](https://github.com/chef/chef-zero/compare/v4.2.1...v4.2.2) **Merged pull requests:** - Update version and changelog for 4.2.2 [\#134](https://github.com/chef/chef-zero/pull/134) ([danielsdeleo](https://github.com/danielsdeleo)) - Access server opts in example context not describe context [\#133](https://github.com/chef/chef-zero/pull/133) ([danielsdeleo](https://github.com/danielsdeleo)) - Adding `server\_on\_port` method to socketless server map [\#131](https://github.com/chef/chef-zero/pull/131) ([tyler-ball](https://github.com/tyler-ball)) - Ignore .ruby-version [\#98](https://github.com/chef/chef-zero/pull/98) ([raskchanky](https://github.com/raskchanky)) ## [v4.2.1](https://github.com/chef/chef-zero/tree/v4.2.1) (2015-04-07) [Full Changelog](https://github.com/chef/chef-zero/compare/v4.2.0...v4.2.1) **Merged pull requests:** - Don't pollute global Chef server options [\#125](https://github.com/chef/chef-zero/pull/125) ([jkeiser](https://github.com/jkeiser)) ## [v4.2.0](https://github.com/chef/chef-zero/tree/v4.2.0) (2015-04-06) [Full Changelog](https://github.com/chef/chef-zero/compare/v4.1.0...v4.2.0) **Merged pull requests:** - bump ffi-yajl dep [\#124](https://github.com/chef/chef-zero/pull/124) ([lamont-granquist](https://github.com/lamont-granquist)) - Add :organization and :data\_scope options to with\_chef\_server [\#119](https://github.com/chef/chef-zero/pull/119) ([jkeiser](https://github.com/jkeiser)) ## [v4.1.0](https://github.com/chef/chef-zero/tree/v4.1.0) (2015-04-01) [Full Changelog](https://github.com/chef/chef-zero/compare/v4.0...v4.1.0) **Merged pull requests:** - Socketless Requests [\#121](https://github.com/chef/chef-zero/pull/121) ([danielsdeleo](https://github.com/danielsdeleo)) - Partially Revert 1b2a6e5f107254cce8200a4750035b30265ae0c8 [\#120](https://github.com/chef/chef-zero/pull/120) ([danielsdeleo](https://github.com/danielsdeleo)) - Policyfile Revision ID Validation. [\#116](https://github.com/chef/chef-zero/pull/116) ([danielsdeleo](https://github.com/danielsdeleo)) - Use a Chef version compatible with chef-zero 4.x [\#115](https://github.com/chef/chef-zero/pull/115) ([danielsdeleo](https://github.com/danielsdeleo)) - Support /version; fix some global URIs [\#113](https://github.com/chef/chef-zero/pull/113) ([jaymzh](https://github.com/jaymzh)) ## [v4.0](https://github.com/chef/chef-zero/tree/v4.0) (2015-02-11) [Full Changelog](https://github.com/chef/chef-zero/compare/v3.2.1...v4.0) **Merged pull requests:** - Policyfile get/set API [\#111](https://github.com/chef/chef-zero/pull/111) ([danielsdeleo](https://github.com/danielsdeleo)) ## [v3.2.1](https://github.com/chef/chef-zero/tree/v3.2.1) (2014-11-26) [Full Changelog](https://github.com/chef/chef-zero/compare/v2.2.1...v3.2.1) **Merged pull requests:** - Version bump for 3.2.1. [\#105](https://github.com/chef/chef-zero/pull/105) ([sersut](https://github.com/sersut)) - fix: should set https to rack.url\_scheme \#87 [\#104](https://github.com/chef/chef-zero/pull/104) ([sawanoboly](https://github.com/sawanoboly)) - Add option for logging to a file. [\#103](https://github.com/chef/chef-zero/pull/103) ([jaymzh](https://github.com/jaymzh)) - add CORS header [\#96](https://github.com/chef/chef-zero/pull/96) ([smith](https://github.com/smith)) ## [v2.2.1](https://github.com/chef/chef-zero/tree/v2.2.1) (2014-10-08) [Full Changelog](https://github.com/chef/chef-zero/compare/v3.2...v2.2.1) ## [v3.2](https://github.com/chef/chef-zero/tree/v3.2) (2014-09-27) [Full Changelog](https://github.com/chef/chef-zero/compare/v3.1.3...v3.2) **Merged pull requests:** - Removing 'json' gem dependency, replacing with 'ffi-yajl' [\#93](https://github.com/chef/chef-zero/pull/93) ([tyler-ball](https://github.com/tyler-ball)) ## [v3.1.3](https://github.com/chef/chef-zero/tree/v3.1.3) (2014-09-04) [Full Changelog](https://github.com/chef/chef-zero/compare/v3.1.2...v3.1.3) **Merged pull requests:** - Pass base URI to V1 data store only in true single org mode [\#89](https://github.com/chef/chef-zero/pull/89) ([jkeiser](https://github.com/jkeiser)) ## [v3.1.2](https://github.com/chef/chef-zero/tree/v3.1.2) (2014-08-29) [Full Changelog](https://github.com/chef/chef-zero/compare/v3.1.1...v3.1.2) ## [v3.1.1](https://github.com/chef/chef-zero/tree/v3.1.1) (2014-08-29) [Full Changelog](https://github.com/chef/chef-zero/compare/v3.1...v3.1.1) ## [v3.1](https://github.com/chef/chef-zero/tree/v3.1) (2014-08-29) [Full Changelog](https://github.com/chef/chef-zero/compare/v3.0...v3.1) ## [v3.0](https://github.com/chef/chef-zero/tree/v3.0) (2014-08-22) [Full Changelog](https://github.com/chef/chef-zero/compare/v3.0.0.rc.1...v3.0) ## [v3.0.0.rc.1](https://github.com/chef/chef-zero/tree/v3.0.0.rc.1) (2014-08-22) [Full Changelog](https://github.com/chef/chef-zero/compare/v2.2...v3.0.0.rc.1) **Merged pull requests:** - Support ssl \(--\[no-\]ssl option\) [\#87](https://github.com/chef/chef-zero/pull/87) ([sawanoboly](https://github.com/sawanoboly)) - Get enterprise chef-zero passing oc-chef-pedant [\#84](https://github.com/chef/chef-zero/pull/84) ([jkeiser](https://github.com/jkeiser)) - waffle.io Badge [\#74](https://github.com/chef/chef-zero/pull/74) ([waffle-iron](https://github.com/waffle-iron)) ## [v2.2](https://github.com/chef/chef-zero/tree/v2.2) (2014-06-18) [Full Changelog](https://github.com/chef/chef-zero/compare/v2.1.5...v2.2) **Merged pull requests:** - Allow server to try multiple ports [\#67](https://github.com/chef/chef-zero/pull/67) ([jkeiser](https://github.com/jkeiser)) ## [v2.1.5](https://github.com/chef/chef-zero/tree/v2.1.5) (2014-06-03) [Full Changelog](https://github.com/chef/chef-zero/compare/v2.1.4...v2.1.5) **Merged pull requests:** - Honor :single\_org =\> 'orgname' parameter everywhere [\#66](https://github.com/chef/chef-zero/pull/66) ([jkeiser](https://github.com/jkeiser)) ## [v2.1.4](https://github.com/chef/chef-zero/tree/v2.1.4) (2014-05-28) [Full Changelog](https://github.com/chef/chef-zero/compare/v2.1.3...v2.1.4) ## [v2.1.3](https://github.com/chef/chef-zero/tree/v2.1.3) (2014-05-27) [Full Changelog](https://github.com/chef/chef-zero/compare/v2.1.2...v2.1.3) ## [v2.1.2](https://github.com/chef/chef-zero/tree/v2.1.2) (2014-05-27) [Full Changelog](https://github.com/chef/chef-zero/compare/v2.1.1...v2.1.2) ## [v2.1.1](https://github.com/chef/chef-zero/tree/v2.1.1) (2014-05-27) [Full Changelog](https://github.com/chef/chef-zero/compare/v2.1...v2.1.1) ## [v2.1](https://github.com/chef/chef-zero/tree/v2.1) (2014-05-26) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.6.3...v2.1) **Merged pull requests:** - Add multi-tenancy support and chef local mode tests [\#64](https://github.com/chef/chef-zero/pull/64) ([jkeiser](https://github.com/jkeiser)) ## [v1.6.3](https://github.com/chef/chef-zero/tree/v1.6.3) (2014-02-10) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.7.3...v1.6.3) ## [v1.7.3](https://github.com/chef/chef-zero/tree/v1.7.3) (2014-01-22) [Full Changelog](https://github.com/chef/chef-zero/compare/v2.0.2...v1.7.3) ## [v2.0.2](https://github.com/chef/chef-zero/tree/v2.0.2) (2014-01-21) [Full Changelog](https://github.com/chef/chef-zero/compare/v2.0.1...v2.0.2) **Merged pull requests:** - Read JSON, not a file path [\#52](https://github.com/chef/chef-zero/pull/52) ([sethvargo](https://github.com/sethvargo)) - Fix IPV6 support [\#50](https://github.com/chef/chef-zero/pull/50) ([sethvargo](https://github.com/sethvargo)) - Fix typo [\#47](https://github.com/chef/chef-zero/pull/47) ([gregkare](https://github.com/gregkare)) ## [v2.0.1](https://github.com/chef/chef-zero/tree/v2.0.1) (2014-01-03) [Full Changelog](https://github.com/chef/chef-zero/compare/v2.0.0...v2.0.1) **Merged pull requests:** - Fix clear data when no data was added to chef zero [\#46](https://github.com/chef/chef-zero/pull/46) ([alex-slynko](https://github.com/alex-slynko)) - Fix an issue with an incorrect number of parameters passed to build\_uri [\#45](https://github.com/chef/chef-zero/pull/45) ([sethvargo](https://github.com/sethvargo)) - Make playground items more semantic [\#44](https://github.com/chef/chef-zero/pull/44) ([sethvargo](https://github.com/sethvargo)) ## [v2.0.0](https://github.com/chef/chef-zero/tree/v2.0.0) (2013-12-17) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.7.2...v2.0.0) **Merged pull requests:** - Remove puma and fork a subprocess [\#43](https://github.com/chef/chef-zero/pull/43) ([sethvargo](https://github.com/sethvargo)) ## [v1.7.2](https://github.com/chef/chef-zero/tree/v1.7.2) (2013-11-19) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.7.1...v1.7.2) **Merged pull requests:** - Make Server Options Configurable in RSpec Helper [\#41](https://github.com/chef/chef-zero/pull/41) ([danielsdeleo](https://github.com/danielsdeleo)) ## [v1.7.1](https://github.com/chef/chef-zero/tree/v1.7.1) (2013-11-03) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.7...v1.7.1) ## [v1.7](https://github.com/chef/chef-zero/tree/v1.7) (2013-11-02) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.7.beta.1...v1.7) ## [v1.7.beta.1](https://github.com/chef/chef-zero/tree/v1.7.beta.1) (2013-11-02) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.6.2...v1.7.beta.1) **Merged pull requests:** - improve depsolver error message [\#40](https://github.com/chef/chef-zero/pull/40) ([lamont-granquist](https://github.com/lamont-granquist)) ## [v1.6.2](https://github.com/chef/chef-zero/tree/v1.6.2) (2013-10-11) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.6.1...v1.6.2) ## [v1.6.1](https://github.com/chef/chef-zero/tree/v1.6.1) (2013-10-11) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.6...v1.6.1) **Merged pull requests:** - Range queries with smoke test [\#35](https://github.com/chef/chef-zero/pull/35) ([mattgleeson](https://github.com/mattgleeson)) ## [v1.6](https://github.com/chef/chef-zero/tree/v1.6) (2013-09-13) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.5.6...v1.6) ## [v1.5.6](https://github.com/chef/chef-zero/tree/v1.5.6) (2013-09-13) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.5.5...v1.5.6) ## [v1.5.5](https://github.com/chef/chef-zero/tree/v1.5.5) (2013-08-08) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.5.4...v1.5.5) **Merged pull requests:** - fixup end\_range typos [\#30](https://github.com/chef/chef-zero/pull/30) ([mattgleeson](https://github.com/mattgleeson)) - Add license to gemspec \(fixes \#26\) [\#27](https://github.com/chef/chef-zero/pull/27) ([sethvargo](https://github.com/sethvargo)) ## [v1.5.4](https://github.com/chef/chef-zero/tree/v1.5.4) (2013-07-12) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.5.3...v1.5.4) ## [v1.5.3](https://github.com/chef/chef-zero/tree/v1.5.3) (2013-06-28) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.5.2...v1.5.3) ## [v1.5.2](https://github.com/chef/chef-zero/tree/v1.5.2) (2013-06-27) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.5.1...v1.5.2) ## [v1.5.1](https://github.com/chef/chef-zero/tree/v1.5.1) (2013-06-19) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.5...v1.5.1) **Merged pull requests:** - Allow chef-zero to listen on Unix domain socket. [\#20](https://github.com/chef/chef-zero/pull/20) ([stevendanna](https://github.com/stevendanna)) ## [v1.5](https://github.com/chef/chef-zero/tree/v1.5) (2013-06-12) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.4...v1.5) **Merged pull requests:** - Support daemon mode [\#19](https://github.com/chef/chef-zero/pull/19) ([sethvargo](https://github.com/sethvargo)) ## [v1.4](https://github.com/chef/chef-zero/tree/v1.4) (2013-06-07) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.4.0.alpha...v1.4) **Merged pull requests:** - Downgrade Puma so Chef Zero runs on Windows [\#18](https://github.com/chef/chef-zero/pull/18) ([sethvargo](https://github.com/sethvargo)) ## [v1.4.0.alpha](https://github.com/chef/chef-zero/tree/v1.4.0.alpha) (2013-06-07) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.3...v1.4.0.alpha) ## [v1.3](https://github.com/chef/chef-zero/tree/v1.3) (2013-06-06) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.2.1...v1.3) ## [v1.2.1](https://github.com/chef/chef-zero/tree/v1.2.1) (2013-06-05) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.2...v1.2.1) ## [v1.2](https://github.com/chef/chef-zero/tree/v1.2) (2013-06-03) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.1.3...v1.2) **Merged pull requests:** - Fix usage info to be consistent with latest version [\#12](https://github.com/chef/chef-zero/pull/12) ([andrewgross](https://github.com/andrewgross)) ## [v1.1.3](https://github.com/chef/chef-zero/tree/v1.1.3) (2013-05-30) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.1.2...v1.1.3) ## [v1.1.2](https://github.com/chef/chef-zero/tree/v1.1.2) (2013-05-29) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.1.1...v1.1.2) ## [v1.1.1](https://github.com/chef/chef-zero/tree/v1.1.1) (2013-05-28) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.1...v1.1.1) **Merged pull requests:** - fix undefined method .list on CookbookData [\#10](https://github.com/chef/chef-zero/pull/10) ([reset](https://github.com/reset)) ## [v1.1](https://github.com/chef/chef-zero/tree/v1.1) (2013-05-28) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.0.1...v1.1) ## [v1.0.1](https://github.com/chef/chef-zero/tree/v1.0.1) (2013-05-20) [Full Changelog](https://github.com/chef/chef-zero/compare/v1.0...v1.0.1) **Merged pull requests:** - Fix frozen version [\#9](https://github.com/chef/chef-zero/pull/9) ([sethvargo](https://github.com/sethvargo)) ## [v1.0](https://github.com/chef/chef-zero/tree/v1.0) (2013-05-20) [Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.13...v1.0) ## [v0.9.13](https://github.com/chef/chef-zero/tree/v0.9.13) (2013-05-20) [Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.12...v0.9.13) ## [v0.9.12](https://github.com/chef/chef-zero/tree/v0.9.12) (2013-05-20) [Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.11...v0.9.12) **Merged pull requests:** - Move to Puma [\#8](https://github.com/chef/chef-zero/pull/8) ([sethvargo](https://github.com/sethvargo)) ## [v0.9.11](https://github.com/chef/chef-zero/tree/v0.9.11) (2013-05-19) [Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.9...v0.9.11) **Merged pull requests:** - Add '-d' flag [\#7](https://github.com/chef/chef-zero/pull/7) ([sethvargo](https://github.com/sethvargo)) ## [v0.9.9](https://github.com/chef/chef-zero/tree/v0.9.9) (2013-05-14) [Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.8...v0.9.9) **Merged pull requests:** - Remove dependency on Chef gem \(adds JRuby support\) [\#6](https://github.com/chef/chef-zero/pull/6) ([reset](https://github.com/reset)) - Assume application/json is acceptable if no Accept header was sent. [\#4](https://github.com/chef/chef-zero/pull/4) ([stevendanna](https://github.com/stevendanna)) ## [v0.9.8](https://github.com/chef/chef-zero/tree/v0.9.8) (2013-04-29) [Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.7...v0.9.8) ## [v0.9.7](https://github.com/chef/chef-zero/tree/v0.9.7) (2013-04-17) [Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.6...v0.9.7) ## [v0.9.6](https://github.com/chef/chef-zero/tree/v0.9.6) (2013-04-17) [Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.5...v0.9.6) ## [v0.9.5](https://github.com/chef/chef-zero/tree/v0.9.5) (2013-01-21) [Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.4...v0.9.5) ## [v0.9.4](https://github.com/chef/chef-zero/tree/v0.9.4) (2013-01-18) [Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.3...v0.9.4) ## [v0.9.3](https://github.com/chef/chef-zero/tree/v0.9.3) (2013-01-12) [Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.2...v0.9.3) ## [v0.9.2](https://github.com/chef/chef-zero/tree/v0.9.2) (2012-12-31) [Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.1...v0.9.2) ## [v0.9.1](https://github.com/chef/chef-zero/tree/v0.9.1) (2012-12-24) [Full Changelog](https://github.com/chef/chef-zero/compare/v0.9...v0.9.1) ## [v0.9](https://github.com/chef/chef-zero/tree/v0.9) (2012-12-24) [Full Changelog](https://github.com/chef/chef-zero/compare/666374b272a8851a2c57530a71a6183d4d06a648...v0.9)chef-zero-15.0.0/CODE_OF_CONDUCT.md000066400000000000000000000001331362334302100162500ustar00rootroot00000000000000Please refer to the Chef Community Code of Conduct at https://www.chef.io/code-of-conduct/ chef-zero-15.0.0/Gemfile000066400000000000000000000010241362334302100147440ustar00rootroot00000000000000source "https://rubygems.org" gemspec # gem 'rest-client', :git => 'https://github.com/chef/rest-client.git' group :pedant do gem "oc-chef-pedant", git: "https://github.com/chef/chef-server.git" end group :development, :test do gem "chefstyle" gem "rake" gem "rspec", "~> 3.0" end if ENV["GEMFILE_MOD"] puts "GEMFILE_MOD: #{ENV["GEMFILE_MOD"]}" instance_eval(ENV["GEMFILE_MOD"]) else gem "chef", "~> 14.0" gem "ohai", "~> 14.0" end group :debug do gem "pry" gem "pry-byebug" gem "pry-stack_explorer" end chef-zero-15.0.0/LICENSE000066400000000000000000000251421362334302100144650ustar00rootroot00000000000000 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-15.0.0/README.md000066400000000000000000000133221362334302100147340ustar00rootroot00000000000000# Chef Zero [![Build status](https://badge.buildkite.com/943881fa8cbc4bede24eebf34685a49fecd39ea88a316f60b1.svg?branch=master)](https://buildkite.com/chef-oss/chef-chef-zero-master-verify) [![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. ``` chef-zero-15.0.0/Rakefile000066400000000000000000000032541362334302100151250ustar00rootroot00000000000000require "bundler/gem_tasks" def run_oc_pedant(env = {}) ENV.update(env) require File.expand_path("spec/run_oc_pedant") end ENV_DOCS = <<~END.freeze Environment: - RSPEC_OPTS Options to pass to RSpec e.g. RSPEC_OPTS="--fail-fast --profile 5" - PEDANT_OPTS Options to pass to oc-chef-pedant e.g. PEDANT_OPTS="--focus-keys --skip-users" - LOG_LEVEL Set the log level (default: warn) e.g. LOG_LEVEL=debug END task default: :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" desc "Run Chefstyle tests" RuboCop::RakeTask.new(:style) do |task| task.options += ["--display-cop-names", "--no-color"] end rescue LoadError puts "chefstyle gem is not installed. bundle install first to make sure all dependencies are installed." end begin require "yard" YARD::Rake::YardocTask.new(:docs) rescue LoadError puts "yard is not available. bundle install first to make sure all dependencies are installed." end chef-zero-15.0.0/VERSION000066400000000000000000000000061362334302100145200ustar00rootroot0000000000000015.0.0chef-zero-15.0.0/bin/000077500000000000000000000000001362334302100142245ustar00rootroot00000000000000chef-zero-15.0.0/bin/chef-zero000077500000000000000000000054601362334302100160410ustar00rootroot00000000000000#!/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/dist" 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: #{ChefZero::Dist::CLIENT} [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 #{ChefZero::Dist::CLIENT}' will fork the process." else abort "Process.daemon requires Ruby >= 1.9" end end else server.start(true) end chef-zero-15.0.0/chef-zero.gemspec000066400000000000000000000016611362334302100167070ustar00rootroot00000000000000$:.unshift(File.dirname(__FILE__) + "/lib") require "chef_zero/version" Gem::Specification.new do |s| s.name = "chef-zero" s.version = ChefZero::VERSION s.summary = "Self-contained, easy-setup, fast-start in-memory Chef server for testing and solo setup purposes" s.description = s.summary s.author = "Chef Software, Inc." s.email = "oss@chef.io" s.homepage = "https://github.com/chef/chef-zero" s.license = "Apache-2.0" s.required_ruby_version = ">= 2.6" s.add_dependency "mixlib-log", ">= 2.0", "< 4.0" s.add_dependency "hashie", ">= 2.0", "< 5.0" s.add_dependency "uuidtools", "~> 2.1" s.add_dependency "ffi-yajl", "~> 2.2" s.add_dependency "rack", "~> 2.0", ">= 2.0.6" s.bindir = "bin" s.executables = ["chef-zero"] s.require_path = "lib" s.files = %w{LICENSE Gemfile Rakefile} + Dir.glob("*.gemspec") + Dir.glob("{lib,spec}/**/*", File::FNM_DOTMATCH).reject { |f| File.directory?(f) } end chef-zero-15.0.0/lib/000077500000000000000000000000001362334302100142225ustar00rootroot00000000000000chef-zero-15.0.0/lib/chef_zero.rb000066400000000000000000000067201362334302100165200ustar00rootroot00000000000000module ChefZero require_relative "chef_zero/log" MIN_API_VERSION = 0 MAX_API_VERSION = 2 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".freeze 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".freeze 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".freeze end chef-zero-15.0.0/lib/chef_zero/000077500000000000000000000000001362334302100161665ustar00rootroot00000000000000chef-zero-15.0.0/lib/chef_zero/chef_data/000077500000000000000000000000001362334302100200645ustar00rootroot00000000000000chef-zero-15.0.0/lib/chef_zero/chef_data/acl_path.rb000066400000000000000000000121351362334302100221660ustar00rootroot00000000000000module 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}.freeze TOP_DATA_TYPES = %w{containers organizations users}.freeze # 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 # /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 private_class_method 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 private_class_method 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-15.0.0/lib/chef_zero/chef_data/cookbook_data.rb000066400000000000000000000157601362334302100232210ustar00rootroot00000000000000require "digest/md5" require "hashie" module ChefZero class Mash < ::Hashie::Mash disable_warnings end module ChefData module CookbookData class << self def to_hash(cookbook, name, version = nil) frozen = false if cookbook.key?(:frozen) frozen = cookbook[:frozen] cookbook = cookbook.dup cookbook.delete(:frozen) end result = files_from(cookbook) recipe_names = result[:all_files].select do |file| file[:name].start_with?("recipes/") end.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 metadata_from(directory, name, version, recipe_names) metadata = PretendCookbookMetadata.new(PretendCookbook.new(name, recipe_names)) # If both .rb and .json exist, read .json if has_child(directory, "metadata.json") metadata.from_json(read_file(directory, "metadata.json")) elsif 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 end result = {} metadata.to_hash.each_pair do |key, value| result[key.to_sym] = value end result[:version] = version if version result end private def files_from(directory) # TODO some support .rb only result = load_files(directory) set_specificity(result, :templates) set_specificity(result, :files) result = { all_files: result, } result end def has_child(directory, name) if directory.is_a?(Hash) directory.key?(name) else directory.child(name).exists? end end def read_file(directory, name) if directory.is_a?(Hash) directory[name] else directory.child(name).read end end def filename(directory, name) if directory.respond_to?(:file_path) File.join(directory.file_path, name) else nil end end def 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 list(directory) if directory.is_a?(Hash) directory.keys else directory.children.map(&:name) end end def load_child_files(parent, key, recursive, part) result = load_files(get_directory(parent, key), recursive, part) result.each do |file| file[:path] = "#{key}/#{file[:path]}" end result end def load_files(directory, recursive = true, part = nil) result = [] if directory list(directory).each do |child_name| dir = get_directory(directory, child_name) if dir child_part = child_name if part.nil? if recursive result += load_child_files(directory, child_name, recursive, child_part) end else result += load_file(read_file(directory, child_name), child_name, part) end end end result end def load_file(value, name, part = nil) specific_name = part ? "#{part}/#{name}" : name [{ name: specific_name, path: name, checksum: Digest::MD5.hexdigest(value), specificity: "default", }] end def set_specificity(files, type) files.each do |file| next unless file[:name].split("/")[0] == type.to_s parts = file[:path].split("/") file[:specificity] = if parts.size == 2 "default" else parts[1] end end end end # 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) name(cookbook.name) 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] = Mash.new end end def from_json(json) 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 provides(cookbook, *version_constraints) cookbook_arg(:providing, 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 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 end end CookbookData = ChefData::CookbookData end chef-zero-15.0.0/lib/chef_zero/chef_data/data_normalizer.rb000066400000000000000000000230471362334302100235720ustar00rootroot00000000000000require_relative "../../chef_zero" require_relative "../rest_base" require_relative "default_creator" module ChefZero module ChefData class DataNormalizer COOKBOOK_SEGMENTS = %w{ resources providers recipes definitions libraries attributes files templates root_files }.freeze def self.normalize_acls(acls) ChefData::DefaultCreator::PERMISSIONS.each do |perm| acls[perm] ||= {} acls[perm]["groups"] ||= [] if acls[perm].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 unless 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 unless 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, api_version: 2) # TODO I feel dirty if method == "PUT" && api_version < 2 cookbook["all_files"] = cookbook.delete(["root_files"]) { [] } COOKBOOK_SEGMENTS.each do |segment| next unless cookbook.key? segment cookbook[segment].each do |file| file["name"] = "#{segment}/#{file["name"]}" cookbook["all_files"] << file end cookbook.delete(segment) end elsif method != "PUT" if cookbook.key? "all_files" cookbook["all_files"].each do |file| if file.is_a?(Hash) && file.key?("checksum") file["url"] ||= endpoint.build_uri(base_uri, org_prefix + ["file_store", "checksums", file["checksum"]]) end end # down convert to old style manifest, ensuring we don't send all_files on the wire and that we correctly divine segments # any file that's not in an old segment is just dropped on the floor. if api_version < 2 # the spec appears to think we should send empty arrays for each segment, so let's do that COOKBOOK_SEGMENTS.each { |seg| cookbook[seg] ||= [] } cookbook["all_files"].each do |file| segment, name = file["name"].split("/") # root_files have no segment prepended if name.nil? name = segment segment = "root_files" end file.delete("full_path") next unless COOKBOOK_SEGMENTS.include? segment file["name"] = name cookbook[segment] << file end cookbook.delete("all_files") 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-15.0.0/lib/chef_zero/chef_data/default_creator.rb000066400000000000000000000352421362334302100235620ustar00rootroot00000000000000require_relative "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_writer :deleted PERMISSIONS = %w{create read update delete grant}.freeze DEFAULT_SUPERUSERS = %w{pivotal}.freeze 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" => {}, }.freeze 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 unless 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 unless 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", "containers/cookbooks", "containers/cookbook_artifacts", "containers/data" { "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/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)) unless 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 true when 2 path[0] == "organizations" || (path[0] == "acls" && path[1] != "root") when 3 # If it has a container, it is a directory. path[0] == "organizations" && (path[2] == "acls" || data.exists?(path[0..1] + [ "containers", path[2] ])) when 4 path[0] == "organizations" && ( (path[2] == "acls" && path[1] != "root") || %w{cookbooks cookbook_artifacts data policies policy_groups}.include?(path[2])) else false end end end end end chef-zero-15.0.0/lib/chef_zero/data_store/000077500000000000000000000000001362334302100203135ustar00rootroot00000000000000chef-zero-15.0.0/lib/chef_zero/data_store/data_already_exists_error.rb000066400000000000000000000015561362334302100260710ustar00rootroot00000000000000# # 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_relative "data_error" module ChefZero module DataStore class DataAlreadyExistsError < DataError def initialize(path, cause = nil) super end end end end chef-zero-15.0.0/lib/chef_zero/data_store/data_error.rb000066400000000000000000000017601362334302100227660ustar00rootroot00000000000000# # 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-15.0.0/lib/chef_zero/data_store/data_not_found_error.rb000066400000000000000000000015511362334302100250370ustar00rootroot00000000000000# # 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_relative "data_error" module ChefZero module DataStore class DataNotFoundError < DataError def initialize(path, cause = nil) super end end end end chef-zero-15.0.0/lib/chef_zero/data_store/default_facade.rb000066400000000000000000000105531362334302100235530ustar00rootroot00000000000000require_relative "interface_v2" require_relative "../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) 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 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 unless deleted raise end end end def delete_dir(path, *options) deleted = default_creator.deleted(path) begin real_store.delete_dir(path, *options) rescue DataNotFoundError unless 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-15.0.0/lib/chef_zero/data_store/interface_v1.rb000066400000000000000000000035271362334302100232150ustar00rootroot00000000000000module 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-15.0.0/lib/chef_zero/data_store/interface_v2.rb000066400000000000000000000010341362334302100232050ustar00rootroot00000000000000require_relative "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-15.0.0/lib/chef_zero/data_store/memory_store.rb000066400000000000000000000020651362334302100233670ustar00rootroot00000000000000# # 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_relative "v2_to_v1_adapter" require_relative "memory_store_v2" require_relative "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-15.0.0/lib/chef_zero/data_store/memory_store_v2.rb000066400000000000000000000100311362334302100237660ustar00rootroot00000000000000# # 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_relative "data_already_exists_error" require_relative "data_not_found_error" require_relative "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.key?(name) unless options.include?(:recursive) raise DataAlreadyExistsError.new(path + [name]) end else parent[name] = {} end end def create(path, name, data, *options) unless data.is_a?(String) raise "set only works with strings (given data: #{data.inspect})" end parent = _get(path, options.include?(:create_dir)) if parent.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) unless 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]) unless parent.key?(path[-1]) raise DataNotFoundError.new(path) end unless 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]) unless parent.key?(path[-1]) raise DataNotFoundError.new(path) end unless 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) unless dir.is_a? Hash raise "list only works with directories (#{path} = #{dir.class})" end dir.keys.sort end def exists?(path, options = {}) value = _get(path) if value.is_a?(Hash) && !options[:allow_dirs] raise "exists? does not work with directories (#{path} = #{value.class})" end true rescue DataNotFoundError false end def exists_dir?(path) dir = _get(path) unless dir.is_a? Hash raise "exists_dir? only works with directories (#{path} = #{dir.class})" end true rescue DataNotFoundError false end private def _get(path, create_dir = false) value = @data path.each_with_index do |path_part, index| unless value.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-15.0.0/lib/chef_zero/data_store/raw_file_store.rb000066400000000000000000000076251362334302100236560ustar00rootroot00000000000000# # 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_relative "data_already_exists_error" require_relative "data_not_found_error" require_relative "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) IO.read(path_to(path)) rescue Errno::ENOENT raise DataNotFoundError.new(path) 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) File.delete(path_to(path)) rescue Errno::ENOENT raise DataNotFoundError.new(path) end def delete_dir(path, *options) if options.include?(:recursive) unless 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) Dir.entries(path_to(path)).select { |entry| entry != "." && entry != ".." }.to_a rescue Errno::ENOENT raise DataNotFoundError.new(path) end def exists?(path, options = {}) File.exist?(path_to(path)) end def exists_dir?(path) File.exist?(path_to(path)) end end end end chef-zero-15.0.0/lib/chef_zero/data_store/v1_to_v2_adapter.rb000066400000000000000000000103601362334302100237770ustar00rootroot00000000000000require_relative "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 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 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-15.0.0/lib/chef_zero/data_store/v2_to_v1_adapter.rb000066400000000000000000000051021362334302100237750ustar00rootroot00000000000000# # 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_relative "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 yield rescue DataAlreadyExistsError => e raise DataAlreadyExistsError.new(e.path[2..-1], e) rescue DataNotFoundError => e raise DataNotFoundError.new(e.path[2..-1], e) end def fix_path(path) [ "organizations", single_org ] + path end end end end chef-zero-15.0.0/lib/chef_zero/dist.rb000066400000000000000000000002521362334302100174550ustar00rootroot00000000000000module ChefZero class Dist # When referencing a product directly PRODUCT = "Chef Zero".freeze # The binary alias CLIENT = "chef-zero".freeze end end chef-zero-15.0.0/lib/chef_zero/endpoints/000077500000000000000000000000001362334302100201715ustar00rootroot00000000000000chef-zero-15.0.0/lib/chef_zero/endpoints/acl_endpoint.rb000066400000000000000000000026311362334302100231570ustar00rootroot00000000000000require "ffi_yajl" require_relative "../rest_base" require_relative "../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)).to_s }) end end end end chef-zero-15.0.0/lib/chef_zero/endpoints/acls_endpoint.rb000066400000000000000000000025041362334302100233410ustar00rootroot00000000000000require "ffi_yajl" require_relative "../rest_base" require_relative "../chef_data/data_normalizer" require_relative "../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) unless 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-15.0.0/lib/chef_zero/endpoints/actor_default_key_endpoint.rb000066400000000000000000000050371362334302100261070ustar00rootroot00000000000000require_relative "../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-15.0.0/lib/chef_zero/endpoints/actor_endpoint.rb000066400000000000000000000126431362334302100235340ustar00rootroot00000000000000require "ffi_yajl" require_relative "rest_object_endpoint" require_relative "../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 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-15.0.0/lib/chef_zero/endpoints/actor_key_endpoint.rb000066400000000000000000000034011362334302100243740ustar00rootroot00000000000000require_relative "../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-15.0.0/lib/chef_zero/endpoints/actor_keys_endpoint.rb000066400000000000000000000077011362334302100245660ustar00rootroot00000000000000require_relative "../rest_base" module ChefZero module Endpoints # /users/USER/keys # /organizations/ORG/clients/CLIENT/keys class ActorKeysEndpoint < RestBase DEFAULT_PUBLIC_KEY_NAME = "default".freeze DATE_FORMAT = "%FT%TZ".freeze # 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-15.0.0/lib/chef_zero/endpoints/actors_endpoint.rb000066400000000000000000000071361362334302100237200ustar00rootroot00000000000000require "ffi_yajl" require_relative "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], insensitive: true) 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, opts = {}) 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] && is_equal(record[key], value, opts[:insensitive]) end end to_json(new_results) end def is_equal(a, b, ignore_case) ignore_case ? a.casecmp(b) == 0 : a == b end end end end chef-zero-15.0.0/lib/chef_zero/endpoints/authenticate_user_endpoint.rb000066400000000000000000000021111362334302100261250ustar00rootroot00000000000000require "ffi_yajl" require_relative "../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-15.0.0/lib/chef_zero/endpoints/container_endpoint.rb000066400000000000000000000011751362334302100244040ustar00rootroot00000000000000require "ffi_yajl" require_relative "rest_object_endpoint" require_relative "../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-15.0.0/lib/chef_zero/endpoints/containers_endpoint.rb000066400000000000000000000015261362334302100245670ustar00rootroot00000000000000require "ffi_yajl" require_relative "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(&: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-15.0.0/lib/chef_zero/endpoints/controls_endpoint.rb000066400000000000000000000007001362334302100242560ustar00rootroot00000000000000require_relative "../dist" module 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, #{ChefZero::Dist::CLIENT} says 410.") end def post(request) error(410, "Server says 410, #{ChefZero::Dist::CLIENT} says 410.") end end end end chef-zero-15.0.0/lib/chef_zero/endpoints/cookbook_artifact_endpoint.rb000066400000000000000000000013721362334302100261040ustar00rootroot00000000000000require_relative "../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 } json_response(200, response_data) end end end end chef-zero-15.0.0/lib/chef_zero/endpoints/cookbook_artifact_identifier_endpoint.rb000066400000000000000000000052171362334302100303100ustar00rootroot00000000000000require_relative "../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, get_data(request)) 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 cb_data = normalize(request, request.body) set_data(request, nil, to_json(cb_data), :create_dir, :create) already_json_response(201, request.body) end # DELETE /organizations/ORG/cookbook_artifacts/COOKBOOK/IDENTIFIER def delete(request) doomed_cookbook_json = get_data(request) identified_cookbook_data = normalize(request, 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 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) cookbook = parse_json(cookbook_artifact_data) ChefData::DataNormalizer.normalize_cookbook(self, request.rest_path[0..1], cookbook, request.rest_path[3], request.rest_path[4], request.base_uri, request.method, true, api_version: request.api_version) end end end end chef-zero-15.0.0/lib/chef_zero/endpoints/cookbook_artifacts_endpoint.rb000066400000000000000000000021101362334302100262560ustar00rootroot00000000000000require_relative "../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 json_response(200, data) end end end end chef-zero-15.0.0/lib/chef_zero/endpoints/cookbook_endpoint.rb000066400000000000000000000024551362334302100242320ustar00rootroot00000000000000require_relative "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-15.0.0/lib/chef_zero/endpoints/cookbook_version_endpoint.rb000066400000000000000000000125121362334302100257720ustar00rootroot00000000000000require "ffi_yajl" require_relative "rest_object_endpoint" require_relative "../rest_error_response" require_relative "../chef_data/data_normalizer" require_relative "../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) unless 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, populate_defaults(request, 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, normalize: false)) 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.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 -= 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, normalize: true) # Inject URIs into each cookbook file cookbook = FFI_Yajl::Parser.parse(response_json) cookbook["chef_type"] ||= "cookbook_version" cookbook["json_class"] ||= "Chef::CookbookVersion" 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, false, api_version: request.api_version) if normalize 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-15.0.0/lib/chef_zero/endpoints/cookbooks_base.rb000066400000000000000000000055641362334302100235130ustar00rootroot00000000000000require "ffi_yajl" require_relative "../rest_base" require_relative "../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 format_universe_list(request, cookbooks_list) results = {} cookbooks_list.each do |name, versions| results[name] ||= {} versions.each do |version| cookbook_data = FFI_Yajl::Parser.parse(get_data(request, request.rest_path[0..1] + [ "cookbooks", name, version ], :nil)) results[name][version] ||= { "dependencies" => cookbook_data["metadata"]["dependencies"], "location_path" => build_uri(request.base_uri, request.rest_path[0..1] + ["cookbooks", name, version]), "location_type" => "chef_server", } end 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) cookbook["all_files"].inject([]) do |acc, file| part, name = file["name"].split("/") next acc unless part == "recipes" || File.extname(name) != ".rb" if name == "default.rb" acc << cookbook_name else acc << "#{cookbook_name}::#{File.basename(name, ".rb")}" end end end end end end chef-zero-15.0.0/lib/chef_zero/endpoints/cookbooks_endpoint.rb000066400000000000000000000010401362334302100244020ustar00rootroot00000000000000require_relative "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-15.0.0/lib/chef_zero/endpoints/data_bag_endpoint.rb000066400000000000000000000024011362334302100241350ustar00rootroot00000000000000require "ffi_yajl" require_relative "rest_list_endpoint" require_relative "data_bag_item_endpoint" require_relative "../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-15.0.0/lib/chef_zero/endpoints/data_bag_item_endpoint.rb000066400000000000000000000015161362334302100251610ustar00rootroot00000000000000require "ffi_yajl" require_relative "rest_object_endpoint" require_relative "data_bag_item_endpoint" require_relative "../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-15.0.0/lib/chef_zero/endpoints/data_bags_endpoint.rb000066400000000000000000000014561362334302100243310ustar00rootroot00000000000000require "ffi_yajl" require_relative "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(&: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])).to_s }) end end end end end chef-zero-15.0.0/lib/chef_zero/endpoints/dummy_endpoint.rb000066400000000000000000000021571362334302100235560ustar00rootroot00000000000000 # 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-15.0.0/lib/chef_zero/endpoints/environment_cookbook_endpoint.rb000066400000000000000000000016171362334302100266550ustar00rootroot00000000000000require "ffi_yajl" require_relative "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-15.0.0/lib/chef_zero/endpoints/environment_cookbook_versions_endpoint.rb000066400000000000000000000132061362334302100306020ustar00rootroot00000000000000require "ffi_yajl" require_relative "../rest_base" require_relative "../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}") unless cookbook_names.include?($1) raise RestErrorResponse.new(412, "No such cookbook version for cookbook #{$1}: #{$2}") unless 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}") unless 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) unless 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", false, api_version: request.api_version) 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 unless 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 unless new_desired_versions.key?(dep_name) new_unsolved += [dep_name] # If the dep is missing, we will try other versions of the cookbook that might not have the bad dep. unless 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 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 unless 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-15.0.0/lib/chef_zero/endpoints/environment_cookbooks_endpoint.rb000066400000000000000000000013571362334302100270410ustar00rootroot00000000000000require "ffi_yajl" require_relative "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-15.0.0/lib/chef_zero/endpoints/environment_endpoint.rb000066400000000000000000000016551362334302100247710ustar00rootroot00000000000000require "ffi_yajl" require_relative "rest_object_endpoint" require_relative "../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-15.0.0/lib/chef_zero/endpoints/environment_nodes_endpoint.rb000066400000000000000000000013341362334302100261530ustar00rootroot00000000000000require "ffi_yajl" require_relative "../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-15.0.0/lib/chef_zero/endpoints/environment_recipes_endpoint.rb000066400000000000000000000013661362334302100265020ustar00rootroot00000000000000require "ffi_yajl" require_relative "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-15.0.0/lib/chef_zero/endpoints/environment_role_endpoint.rb000066400000000000000000000022621362334302100260050ustar00rootroot00000000000000require "ffi_yajl" require_relative "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-15.0.0/lib/chef_zero/endpoints/file_store_file_endpoint.rb000066400000000000000000000011031362334302100255430ustar00rootroot00000000000000require_relative "../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-15.0.0/lib/chef_zero/endpoints/group_endpoint.rb000066400000000000000000000011311362334302100235460ustar00rootroot00000000000000require "ffi_yajl" require_relative "rest_object_endpoint" require_relative "../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-15.0.0/lib/chef_zero/endpoints/groups_endpoint.rb000066400000000000000000000004171362334302100237370ustar00rootroot00000000000000require "ffi_yajl" require_relative "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-15.0.0/lib/chef_zero/endpoints/license_endpoint.rb000066400000000000000000000012121362334302100240340ustar00rootroot00000000000000require "ffi_yajl" require_relative "../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-15.0.0/lib/chef_zero/endpoints/node_endpoint.rb000066400000000000000000000017161362334302100233500ustar00rootroot00000000000000require "ffi_yajl" require_relative "rest_object_endpoint" require_relative "../chef_data/data_normalizer" module ChefZero module Endpoints # /nodes/ID class NodeEndpoint < RestObjectEndpoint def put(request) data = parse_json(request.body) if data.key?("policy_name") && policy_name_invalid?(data["policy_name"]) return error(400, "Field 'policy_name' invalid", pretty: false) end if data.key?("policy_group") && policy_name_invalid?(data["policy_group"]) return error(400, "Field 'policy_group' invalid", pretty: false) end super(request) end def head(request) head_request(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-15.0.0/lib/chef_zero/endpoints/node_identifiers_endpoint.rb000066400000000000000000000013211362334302100257250ustar00rootroot00000000000000require "ffi_yajl" require_relative "../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-15.0.0/lib/chef_zero/endpoints/nodes_endpoint.rb000066400000000000000000000017561362334302100235370ustar00rootroot00000000000000require "ffi_yajl" require_relative "rest_object_endpoint" require_relative "../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.key?("policy_name") && policy_name_invalid?(data["policy_name"]) return error(400, "Field 'policy_name' invalid", pretty: false) end if data.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-15.0.0/lib/chef_zero/endpoints/not_found_endpoint.rb000066400000000000000000000004531362334302100244130ustar00rootroot00000000000000require "ffi_yajl" module ChefZero module Endpoints class NotFoundEndpoint def call(request) [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-15.0.0/lib/chef_zero/endpoints/organization_association_request_endpoint.rb000066400000000000000000000012161362334302100312660ustar00rootroot00000000000000require "ffi_yajl" require_relative "../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] delete_data(request, path) json_response(200, { "id" => id, "username" => username }) end end end end chef-zero-15.0.0/lib/chef_zero/endpoints/organization_association_requests_endpoint.rb000066400000000000000000000017541362334302100314600ustar00rootroot00000000000000require "ffi_yajl" require_relative "../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-15.0.0/lib/chef_zero/endpoints/organization_authenticate_user_endpoint.rb000066400000000000000000000013601362334302100307160ustar00rootroot00000000000000require "ffi_yajl" require_relative "../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-15.0.0/lib/chef_zero/endpoints/organization_endpoint.rb000066400000000000000000000031731362334302100251260ustar00rootroot00000000000000require "ffi_yajl" require_relative "../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)).to_s, "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-15.0.0/lib/chef_zero/endpoints/organization_user_base.rb000066400000000000000000000004311362334302100252500ustar00rootroot00000000000000require "ffi_yajl" require_relative "../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-15.0.0/lib/chef_zero/endpoints/organization_user_default_key_endpoint.rb000066400000000000000000000007401362334302100305350ustar00rootroot00000000000000require_relative "../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-15.0.0/lib/chef_zero/endpoints/organization_user_endpoint.rb000066400000000000000000000016461362334302100261670ustar00rootroot00000000000000require "ffi_yajl" require_relative "../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-15.0.0/lib/chef_zero/endpoints/organization_user_key_endpoint.rb000066400000000000000000000007651362334302100270400ustar00rootroot00000000000000require_relative "../rest_base" require_relative "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-15.0.0/lib/chef_zero/endpoints/organization_user_keys_endpoint.rb000066400000000000000000000010011362334302100272030ustar00rootroot00000000000000require_relative "../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-15.0.0/lib/chef_zero/endpoints/organization_users_endpoint.rb000066400000000000000000000031421362334302100263430ustar00rootroot00000000000000require "ffi_yajl" require_relative "../rest_base" require_relative "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-15.0.0/lib/chef_zero/endpoints/organization_validator_key_endpoint.rb000066400000000000000000000013141362334302100300360ustar00rootroot00000000000000require "ffi_yajl" require_relative "../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-15.0.0/lib/chef_zero/endpoints/organizations_endpoint.rb000066400000000000000000000040521362334302100253060ustar00rootroot00000000000000require "ffi_yajl" require_relative "../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)).to_s, "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-15.0.0/lib/chef_zero/endpoints/policies_endpoint.rb000066400000000000000000000013411362334302100242240ustar00rootroot00000000000000require_relative "../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 json_response(200, response_data) end end end end chef-zero-15.0.0/lib/chef_zero/endpoints/policy_endpoint.rb000066400000000000000000000013111362334302100237110ustar00rootroot00000000000000require_relative "../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) } 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) json_response(200, data) end end end end chef-zero-15.0.0/lib/chef_zero/endpoints/policy_group_endpoint.rb000066400000000000000000000026131362334302100251330ustar00rootroot00000000000000require "ffi_yajl" require_relative "../rest_base" require_relative "../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-15.0.0/lib/chef_zero/endpoints/policy_group_policy_endpoint.rb000066400000000000000000000076111362334302100265150ustar00rootroot00000000000000require "ffi_yajl" require_relative "../rest_base" require_relative "../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] unless 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) json_response(200, full_policy_doc) end end end end chef-zero-15.0.0/lib/chef_zero/endpoints/policy_groups_endpoint.rb000066400000000000000000000025531362334302100253210ustar00rootroot00000000000000require "ffi_yajl" require_relative "../rest_base" require_relative "../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-15.0.0/lib/chef_zero/endpoints/policy_revision_endpoint.rb000066400000000000000000000015201362334302100256310ustar00rootroot00000000000000require_relative "../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]) 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) json_response(200, policyfile_data) end end end end chef-zero-15.0.0/lib/chef_zero/endpoints/policy_revisions_endpoint.rb000066400000000000000000000007551362334302100260250ustar00rootroot00000000000000require_relative "../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) already_json_response(201, request.body) end end end end chef-zero-15.0.0/lib/chef_zero/endpoints/principal_endpoint.rb000066400000000000000000000032641362334302100244040ustar00rootroot00000000000000require "ffi_yajl" require_relative "../../chef_zero" require_relative "../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-15.0.0/lib/chef_zero/endpoints/rest_list_endpoint.rb000066400000000000000000000023611362334302100244300ustar00rootroot00000000000000require "ffi_yajl" require_relative "../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])).to_s 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(&: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])).to_s }) 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-15.0.0/lib/chef_zero/endpoints/rest_object_endpoint.rb000066400000000000000000000050171362334302100247240ustar00rootroot00000000000000require "ffi_yajl" require_relative "../rest_base" require_relative "../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 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-15.0.0/lib/chef_zero/endpoints/role_endpoint.rb000066400000000000000000000007321362334302100233610ustar00rootroot00000000000000require "ffi_yajl" require_relative "rest_object_endpoint" require_relative "../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-15.0.0/lib/chef_zero/endpoints/role_environments_endpoint.rb000066400000000000000000000005711362334302100261710ustar00rootroot00000000000000require "ffi_yajl" require_relative "../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-15.0.0/lib/chef_zero/endpoints/sandbox_endpoint.rb000066400000000000000000000015331362334302100240560ustar00rootroot00000000000000require_relative "../rest_base" require_relative "../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| unless 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-15.0.0/lib/chef_zero/endpoints/sandboxes_endpoint.rb000066400000000000000000000030101362334302100243760ustar00rootroot00000000000000require "ffi_yajl" require_relative "../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-15.0.0/lib/chef_zero/endpoints/search_endpoint.rb000066400000000000000000000167431362334302100236760ustar00rootroot00000000000000require "ffi_yajl" require_relative "rest_object_endpoint" require_relative "../chef_data/data_normalizer" require_relative "../rest_error_response" require_relative "../solr/solr_parser" require_relative "../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] unless 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, val| result[key] = val 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 # 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.is_a?(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.is_a?(Array) 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-15.0.0/lib/chef_zero/endpoints/searches_endpoint.rb000066400000000000000000000007711362334302100242200ustar00rootroot00000000000000require_relative "../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-15.0.0/lib/chef_zero/endpoints/server_api_version_endpoint.rb000066400000000000000000000006171362334302100263260ustar00rootroot00000000000000require_relative "../rest_base" module ChefZero module Endpoints # /server_api_version class ServerAPIVersionEndpoint < RestBase API_VERSION = 2 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-15.0.0/lib/chef_zero/endpoints/system_recovery_endpoint.rb000066400000000000000000000020151362334302100256560ustar00rootroot00000000000000require "ffi_yajl" require_relative "../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) unless 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]) unless 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-15.0.0/lib/chef_zero/endpoints/universe_endpoint.rb000066400000000000000000000005251362334302100242600ustar00rootroot00000000000000require "ffi_yajl" require_relative "rest_object_endpoint" require_relative "../chef_data/data_normalizer" module ChefZero module Endpoints # /universe class UniverseEndpoint < CookbooksBase def get(request) json_response(200, format_universe_list(request, all_cookbooks_list(request))) end end end end chef-zero-15.0.0/lib/chef_zero/endpoints/user_association_request_endpoint.rb000066400000000000000000000031511362334302100275400ustar00rootroot00000000000000require "ffi_yajl" require_relative "../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-15.0.0/lib/chef_zero/endpoints/user_association_requests_count_endpoint.rb000066400000000000000000000010661362334302100311360ustar00rootroot00000000000000require "ffi_yajl" require_relative "../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-15.0.0/lib/chef_zero/endpoints/user_association_requests_endpoint.rb000066400000000000000000000011571362334302100277270ustar00rootroot00000000000000require "ffi_yajl" require_relative "../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-15.0.0/lib/chef_zero/endpoints/user_organizations_endpoint.rb000066400000000000000000000013241362334302100263430ustar00rootroot00000000000000require "ffi_yajl" require_relative "../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-15.0.0/lib/chef_zero/endpoints/version_endpoint.rb000066400000000000000000000004251362334302100241040ustar00rootroot00000000000000require_relative "../rest_base" require_relative "../dist" module ChefZero module Endpoints # /version class VersionEndpoint < RestBase def get(request) text_response(200, "#{ChefZero::Dist::CLIENT} #{ChefZero::VERSION}\n") end end end end chef-zero-15.0.0/lib/chef_zero/log.rb000066400000000000000000000001231362334302100172700ustar00rootroot00000000000000require "mixlib/log" module ChefZero class Log extend Mixlib::Log end end chef-zero-15.0.0/lib/chef_zero/rest_base.rb000066400000000000000000000253411362334302100204670ustar00rootroot00000000000000require_relative "rest_request" require_relative "rest_error_response" require_relative "data_store/data_not_found_error" require_relative "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) version = request.api_version 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 rescue ArgumentError json_response(406, { "username" => request.requestor }, request_version: -1, response_version: -1) end def call(request) response = check_api_version(request) return response unless response.nil? method = request.method.downcase.to_sym unless respond_to?(method) accept_methods = %i{get put post delete}.select { |m| 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 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 unless request.env["HTTP_ACCEPT"] accepts = request.env["HTTP_ACCEPT"].split(/,\s*/).map { |x| x.split(";", 2)[0].strip } 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| self.class.rfc2396_parser.unescape(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 # rfc090 returns 404 error or 200 with an emtpy body # @param [ChefZero::RestRequest] request The HTTP request object # # @return (see #json_response) # def head_request(request) get_data(request) # will raise 404 if non-existant json_response(200, nil) 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| rfc2396_parser.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 def self.rfc2396_parser @parser ||= URI::RFC2396_Parser.new end end end chef-zero-15.0.0/lib/chef_zero/rest_error_response.rb000066400000000000000000000004001362334302100226110ustar00rootroot00000000000000module 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-15.0.0/lib/chef_zero/rest_request.rb000066400000000000000000000035211362334302100212410ustar00rootroot00000000000000require "rack/request" require "cgi" module ChefZero class RestRequest def initialize(env, rest_base_prefix = []) @env = env @rest_base_prefix = rest_base_prefix end attr_reader :env attr_accessor :rest_base_prefix def base_uri # 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 Integer(@env["HTTP_X_OPS_SERVER_API_VERSION"] || 0) end def api_v0? api_version == 0 end def requestor @env["HTTP_X_OPS_USERID"] end def method @env["REQUEST_METHOD"] end def rest_path @rest_path ||= rest_base_prefix + env["PATH_INFO"].split("/").select { |part| part != "" } end def 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] = self.class.rfc2396_parser.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" unless body.end_with?("\n") result << "--- END #{method} BODY ---" end result end def self.rfc2396_parser @parser ||= URI::RFC2396_Parser.new end end end chef-zero-15.0.0/lib/chef_zero/rest_router.rb000066400000000000000000000034251362334302100210740ustar00rootroot00000000000000require "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-15.0.0/lib/chef_zero/rspec.rb000066400000000000000000000302631362334302100176330ustar00rootroot00000000000000require "tempfile" require_relative "server" require_relative "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 #{ChefZero::Dist::PRODUCT} server #{description}", *tags do extend WhenTheChefServerClassMethods include WhenTheChefServerInstanceMethods # Take the passed-in options # rubocop:disable Lint/UnderscorePrefixedVariableName 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 # rubocop:enable Lint/UnderscorePrefixedVariableName # Merge in chef_server_options from let(:chef_server_options) def chef_server_options # rubocop:disable Lint/NestedMethodDefinition chef_server_options = self.class.chef_server_options.dup chef_server_options = chef_server_options.merge(chef_zero_opts) if 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 #{ChefZero::Dist::PRODUCT} 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.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.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-15.0.0/lib/chef_zero/server.rb000066400000000000000000000665641362334302100200420ustar00rootroot00000000000000# # Author:: John Keiser () # Copyright:: Copyright (c) 2012-2017, Chef Software 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_relative "../chef_zero" require_relative "socketless_server_map" require_relative "chef_data/cookbook_data" require_relative "chef_data/acl_path" require_relative "rest_router" require_relative "data_store/memory_store_v2" require_relative "data_store/v1_to_v2_adapter" require_relative "data_store/default_facade" require_relative "version" require "chef_zero/dist.rb" require_relative "endpoints/rest_list_endpoint" require_relative "endpoints/authenticate_user_endpoint" require_relative "endpoints/acls_endpoint" require_relative "endpoints/acl_endpoint" require_relative "endpoints/actor_endpoint" require_relative "endpoints/actors_endpoint" require_relative "endpoints/actor_key_endpoint" require_relative "endpoints/organization_user_key_endpoint" require_relative "endpoints/organization_user_default_key_endpoint" require_relative "endpoints/organization_user_keys_endpoint" require_relative "endpoints/actor_default_key_endpoint" require_relative "endpoints/actor_keys_endpoint" require_relative "endpoints/cookbooks_endpoint" require_relative "endpoints/cookbook_endpoint" require_relative "endpoints/cookbook_version_endpoint" require_relative "endpoints/cookbook_artifacts_endpoint" require_relative "endpoints/cookbook_artifact_endpoint" require_relative "endpoints/cookbook_artifact_identifier_endpoint" require_relative "endpoints/containers_endpoint" require_relative "endpoints/container_endpoint" require_relative "endpoints/controls_endpoint" require_relative "endpoints/dummy_endpoint" require_relative "endpoints/data_bags_endpoint" require_relative "endpoints/data_bag_endpoint" require_relative "endpoints/data_bag_item_endpoint" require_relative "endpoints/groups_endpoint" require_relative "endpoints/group_endpoint" require_relative "endpoints/environment_endpoint" require_relative "endpoints/environment_cookbooks_endpoint" require_relative "endpoints/environment_cookbook_endpoint" require_relative "endpoints/environment_cookbook_versions_endpoint" require_relative "endpoints/environment_nodes_endpoint" require_relative "endpoints/environment_recipes_endpoint" require_relative "endpoints/environment_role_endpoint" require_relative "endpoints/license_endpoint" require_relative "endpoints/node_endpoint" require_relative "endpoints/nodes_endpoint" require_relative "endpoints/node_identifiers_endpoint" require_relative "endpoints/organizations_endpoint" require_relative "endpoints/organization_endpoint" require_relative "endpoints/organization_association_requests_endpoint" require_relative "endpoints/organization_association_request_endpoint" require_relative "endpoints/organization_authenticate_user_endpoint" require_relative "endpoints/organization_users_endpoint" require_relative "endpoints/organization_user_endpoint" require_relative "endpoints/organization_validator_key_endpoint" require_relative "endpoints/policies_endpoint" require_relative "endpoints/policy_endpoint" require_relative "endpoints/policy_revisions_endpoint" require_relative "endpoints/policy_revision_endpoint" require_relative "endpoints/policy_groups_endpoint" require_relative "endpoints/policy_group_endpoint" require_relative "endpoints/policy_group_policy_endpoint" require_relative "endpoints/principal_endpoint" require_relative "endpoints/role_endpoint" require_relative "endpoints/role_environments_endpoint" require_relative "endpoints/sandboxes_endpoint" require_relative "endpoints/sandbox_endpoint" require_relative "endpoints/searches_endpoint" require_relative "endpoints/search_endpoint" require_relative "endpoints/system_recovery_endpoint" require_relative "endpoints/user_association_requests_endpoint" require_relative "endpoints/user_association_requests_count_endpoint" require_relative "endpoints/user_association_request_endpoint" require_relative "endpoints/user_organizations_endpoint" require_relative "endpoints/file_store_file_endpoint" require_relative "endpoints/not_found_endpoint" require_relative "endpoints/version_endpoint" require_relative "endpoints/server_api_version_endpoint" require_relative "endpoints/universe_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", ].freeze def initialize(options = {}) @options = DEFAULT_OPTIONS.merge(options) if @options[:single_org] && !@options.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.is_a?(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 #{ChefZero::Dist::PRODUCT} (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 #{ChefZero::Dist::PRODUCT}..." @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(&: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 unless @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("#{ChefZero::Dist::PRODUCT} 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, _, 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" unless 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) ], [ "/organizations/*/universe", UniverseEndpoint.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-15.0.0/lib/chef_zero/socketless_server_map.rb000066400000000000000000000042021362334302100231130ustar00rootroot00000000000000# # 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" require_relative "dist" 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 #{ChefZero::Dist::PRODUCT} server on given port #{port.inspect}" unless server server.handle_socketless_request(request_env) end end end chef-zero-15.0.0/lib/chef_zero/solr/000077500000000000000000000000001362334302100171455ustar00rootroot00000000000000chef-zero-15.0.0/lib/chef_zero/solr/query/000077500000000000000000000000001362334302100203125ustar00rootroot00000000000000chef-zero-15.0.0/lib/chef_zero/solr/query/binary_operator.rb000066400000000000000000000025731362334302100240450ustar00rootroot00000000000000module 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-15.0.0/lib/chef_zero/solr/query/phrase.rb000066400000000000000000000011011362334302100221120ustar00rootroot00000000000000require_relative "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(&:regexp_string).join("#{NON_WORD_CHARACTER}+"), literal_string) end def to_s "Phrase(\"#{@regexp_string}\")" end end end end end chef-zero-15.0.0/lib/chef_zero/solr/query/range_query.rb000066400000000000000000000021231362334302100231560ustar00rootroot00000000000000module 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 unless @from_inclusive end end unless @to == "*" case value <=> @to when 1 return false when 0 return false unless @to_inclusive end end return true end end def matches_doc?(doc) matches_values?(doc[DEFAULT_FIELD]) end DEFAULT_FIELD = "text".freeze end end end end chef-zero-15.0.0/lib/chef_zero/solr/query/regexpable_query.rb000066400000000000000000000015361362334302100242070ustar00rootroot00000000000000module 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".freeze WORD_CHARACTER = "[A-Za-z0-9@._':\-]".freeze NON_WORD_CHARACTER = "[^A-Za-z0-9@._':\-]".freeze end end end end chef-zero-15.0.0/lib/chef_zero/solr/query/subquery.rb000066400000000000000000000011731362334302100225200ustar00rootroot00000000000000module 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-15.0.0/lib/chef_zero/solr/query/term.rb000066400000000000000000000024341362334302100216110ustar00rootroot00000000000000require_relative "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 += 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-15.0.0/lib/chef_zero/solr/query/unary_operator.rb000066400000000000000000000021621362334302100237110ustar00rootroot00000000000000module 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-15.0.0/lib/chef_zero/solr/solr_doc.rb000066400000000000000000000025621362334302100213030ustar00rootroot00000000000000module 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-15.0.0/lib/chef_zero/solr/solr_parser.rb000066400000000000000000000140331362334302100220260ustar00rootroot00000000000000require_relative "query/binary_operator" require_relative "query/unary_operator" require_relative "query/term" require_relative "query/phrase" require_relative "query/range_query" require_relative "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 unless 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 !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") unless 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") unless right end_range = next_token parse_error(right, "Expected end range '#{end_range}") unless ["}", "]"].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".freeze end end end chef-zero-15.0.0/lib/chef_zero/version.rb000066400000000000000000000000601362334302100201740ustar00rootroot00000000000000module ChefZero VERSION = "15.0.0".freeze end chef-zero-15.0.0/playground/000077500000000000000000000000001362334302100156405ustar00rootroot00000000000000chef-zero-15.0.0/playground/.chef/000077500000000000000000000000001362334302100166235ustar00rootroot00000000000000chef-zero-15.0.0/playground/.chef/knife.rb000066400000000000000000000004711362334302100202460ustar00rootroot00000000000000chef_repo = File.join(File.dirname(__FILE__), "..") chef_server_url "http://127.0.0.1:8889" node_name "stickywicket" client_key File.join(File.dirname(__FILE__), "stickywicket.pem") cookbook_path "#{chef_repo}/cookbooks" cache_type "BasicFile" cache_options :path => "#{chef_repo}/checksums" chef-zero-15.0.0/playground/.chef/stickywicket.pem000066400000000000000000000032171362334302100220460ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA0sOY9tHvVtLZ6xmVmH8d8LrRrNcWOXbrvvCrai+T3GtRvRSL hksLrpOpD0L9EHM6NdThNF/eGA9Oq+UKAe6yXR0hwsKuxKXqQ8SEmlhZZ9GiuggD B/zYD3ItB6SGpdkRe7kQqTChQyrIXqbRkJqxoTXLyeJDF0sCyTdp3L8IZCUWodM8 oV9TlQBJHYtG1gLUwIi8kcMVEoCn2Q8ltCj0/ftnwhTtwO52RkWA0uYOLGVayHsL SCFfx+ACWPU/oWCwW5/KBqb3veTv0aEg/nh0QsFzRLoTx6SRFI5dT2Nf8iiJe4WC UG8WKEB2G8QPnxsxfOPYDBdTJ4CXEi2e+z41VQIDAQABAoIBAALhqbW2KQ+G0nPk ZacwFbi01SkHx8YBWjfCEpXhEKRy0ytCnKW5YO+CFU2gHNWcva7+uhV9OgwaKXkw KHLeUJH1VADVqI4Htqw2g5mYm6BPvWnNsjzpuAp+BR+VoEGkNhj67r9hatMAQr0I itTvSH5rvd2EumYXIHKfz1K1SegUk1u1EL1RcMzRmZe4gDb6eNBs9Sg4im4ybTG6 pPIytA8vBQVWhjuAR2Tm+wZHiy0Az6Vu7c2mS07FSX6FO4E8SxWf8idaK9ijMGSq FvIS04mrY6XCPUPUC4qm1qNnhDPpOr7CpI2OO98SqGanStS5NFlSFXeXPpM280/u fZUA0AECgYEA+x7QUnffDrt7LK2cX6wbvn4mRnFxet7bJjrfWIHf+Rm0URikaNma h0/wNKpKBwIH+eHK/LslgzcplrqPytGGHLOG97Gyo5tGAzyLHUWBmsNkRksY2sPL uHq6pYWJNkqhnWGnIbmqCr0EWih82x/y4qxbJYpYqXMrit0wVf7yAgkCgYEA1twI gFaXqesetTPoEHSQSgC8S4D5/NkdriUXCYb06REcvo9IpFMuiOkVUYNN5d3MDNTP IdBicfmvfNELvBtXDomEUD8ls1UuoTIXRNGZ0VsZXu7OErXCK0JKNNyqRmOwcvYL JRqLfnlei5Ndo1lu286yL74c5rdTLs/nI2p4e+0CgYB079ZmcLeILrmfBoFI8+Y/ gJLmPrFvXBOE6+lRV7kqUFPtZ6I3yQzyccETZTDvrnx0WjaiFavUPH27WMjY01S2 TMtO0Iq1MPsbSrglO1as8MvjB9ldFcvp7gy4Q0Sv6XT0yqJ/S+vo8Df0m+H4UBpU f5o6EwBSd/UQxwtZIE0lsQKBgQCswfjX8Eg8KL/lJNpIOOE3j4XXE9ptksmJl2sB jxDnQYoiMqVO808saHVquC/vTrpd6tKtNpehWwjeTFuqITWLi8jmmQ+gNTKsC9Gn 1Pxf2Gb67PqnEpwQGln+TRtgQ5HBrdHiQIi+5am+gnw89pDrjjO5rZwhanAo6KPJ 1zcPNQKBgQDxFu8v4frDmRNCVaZS4f1B6wTrcMrnibIDlnzrK9GG6Hz1U7dDv8s8 Nf4UmeMzDXjlPWZVOvS5+9HKJPdPj7/onv8B2m18+lcgTTDJBkza7R1mjL1Cje/Z KcVGsryKN6cjE7yCDasnA7R2rVBV/7NWeJV77bmzT5O//rW4yIfUIg== -----END RSA PRIVATE KEY----- chef-zero-15.0.0/playground/checksums/000077500000000000000000000000001362334302100176255ustar00rootroot0000000000000054fb547c97dba6cb20343d8a56af7227d6a0a989567af7551a4d9ee8d31893c8000066400000000000000000000000001362334302100303150ustar00rootroot00000000000000chef-zero-15.0.0/playground/checksums615eca02f4433fcb2b3bdeb7b0050e2485b2ae161f64c4c54b508713a076eb52000066400000000000000000000000001362334302100302710ustar00rootroot00000000000000chef-zero-15.0.0/playground/checksums7b8dba91d144fd94920d30c4cd5fe01798a2172713ae44adf17a2ee2f1025425000066400000000000000000000000001362334302100302360ustar00rootroot00000000000000chef-zero-15.0.0/playground/checksumsb21d7134123dc093c355621f59da98a4c19e6817b36209e50a90764a5abd22dd000066400000000000000000000000001362334302100300230ustar00rootroot00000000000000chef-zero-15.0.0/playground/checksumsf6b930d7229704307f8a5d71a594081767c60e71beb3b085af95c99d908438be000066400000000000000000000000001362334302100300130ustar00rootroot00000000000000chef-zero-15.0.0/playground/checksumschef-zero-15.0.0/playground/cookbooks/000077500000000000000000000000001362334302100176315ustar00rootroot00000000000000chef-zero-15.0.0/playground/cookbooks/apache2/000077500000000000000000000000001362334302100211345ustar00rootroot00000000000000chef-zero-15.0.0/playground/cookbooks/apache2/metadata.rb000066400000000000000000000000371362334302100232410ustar00rootroot00000000000000name "apache2" version "1.0.0" chef-zero-15.0.0/playground/cookbooks/apache2/recipes/000077500000000000000000000000001362334302100225665ustar00rootroot00000000000000chef-zero-15.0.0/playground/cookbooks/apache2/recipes/default.rb000066400000000000000000000001311362334302100245320ustar00rootroot00000000000000package "apache2" template "/etc/apache2/sites-enabled" do source "site.conf.erb" end chef-zero-15.0.0/playground/cookbooks/apache2/templates/000077500000000000000000000000001362334302100231325ustar00rootroot00000000000000chef-zero-15.0.0/playground/cookbooks/apache2/templates/default/000077500000000000000000000000001362334302100245565ustar00rootroot00000000000000chef-zero-15.0.0/playground/cookbooks/apache2/templates/default/site.conf.erb000066400000000000000000000000631362334302100271370ustar00rootroot00000000000000# This is just an empty file, but you get the idea chef-zero-15.0.0/playground/cookbooks/php/000077500000000000000000000000001362334302100204205ustar00rootroot00000000000000chef-zero-15.0.0/playground/cookbooks/php/metadata.rb000066400000000000000000000000551362334302100225250ustar00rootroot00000000000000name "php" version "1.0.0" depends "apache2" chef-zero-15.0.0/playground/cookbooks/php/recipes/000077500000000000000000000000001362334302100220525ustar00rootroot00000000000000chef-zero-15.0.0/playground/cookbooks/php/recipes/default.rb000066400000000000000000000000161362334302100240200ustar00rootroot00000000000000package "php" chef-zero-15.0.0/playground/data_bags/000077500000000000000000000000001362334302100175455ustar00rootroot00000000000000chef-zero-15.0.0/playground/data_bags/dns/000077500000000000000000000000001362334302100203315ustar00rootroot00000000000000chef-zero-15.0.0/playground/data_bags/dns/services.json000066400000000000000000000001511362334302100230440ustar00rootroot00000000000000{ "id": "services", "payment_gateway": "money.example.com", "authentication": "auth.example.com" } chef-zero-15.0.0/playground/data_bags/passwords/000077500000000000000000000000001362334302100215725ustar00rootroot00000000000000chef-zero-15.0.0/playground/data_bags/passwords/github.json000066400000000000000000000002321362334302100237440ustar00rootroot00000000000000{ "id": "github", "development": { "token": "ABC123" }, "staging": { "token": "DEF456" }, "production": { "token": "HIJ789" } } chef-zero-15.0.0/playground/data_bags/passwords/twitter.json000066400000000000000000000002411362334302100241640ustar00rootroot00000000000000{ "id": "twitter", "development": { "api_key": "ABC123" }, "staging": { "api_key": "DEF456" }, "production": { "api_key": "HIJ789" } } chef-zero-15.0.0/playground/data_bags/users/000077500000000000000000000000001362334302100207065ustar00rootroot00000000000000chef-zero-15.0.0/playground/data_bags/users/jkeiser.json000066400000000000000000000000571362334302100232370ustar00rootroot00000000000000{ "id": "jkeiser", "name": "John Keiser" } chef-zero-15.0.0/playground/data_bags/users/schisamo.json000066400000000000000000000000631362334302100234060ustar00rootroot00000000000000{ "id": "schisamo", "name": "Seth Chisamore" } chef-zero-15.0.0/playground/data_bags/users/sethvargo.json000066400000000000000000000000601362334302100235770ustar00rootroot00000000000000{ "id": "sethvargo", "name": "Seth Vargo" } chef-zero-15.0.0/playground/environments/000077500000000000000000000000001362334302100203675ustar00rootroot00000000000000chef-zero-15.0.0/playground/environments/production.json000066400000000000000000000003721362334302100234520ustar00rootroot00000000000000{ "name": "production", "description": "This is just production", "json_class": "Chef::Environment", "chef_type": "environment", "cookbook_versions": { "apache": ">= 1.0.0" }, "default_attributes": {}, "override_attributes": {} } chef-zero-15.0.0/playground/environments/staging.json000066400000000000000000000003511362334302100227150ustar00rootroot00000000000000{ "name": "staging", "description": "This is just like production, but not", "json_class": "Chef::Environment", "chef_type": "environment", "default_attributes": { "some_key": "value" }, "override_attributes": {} } chef-zero-15.0.0/playground/nodes/000077500000000000000000000000001362334302100167505ustar00rootroot00000000000000chef-zero-15.0.0/playground/nodes/desktop.json000066400000000000000000000002001362334302100213040ustar00rootroot00000000000000{ "name": "desktop", "chef_type": "node", "json_class": "Chef::Node", "chef_environment": "staging", "run_list": [] } chef-zero-15.0.0/playground/nodes/dns.json000066400000000000000000000001751362334302100204320ustar00rootroot00000000000000{ "name": "dns", "chef_type": "node", "json_class": "Chef::Node", "chef_environment": "_default", "run_list": [] } chef-zero-15.0.0/playground/nodes/lb.json000066400000000000000000000001761362334302100202440ustar00rootroot00000000000000{ "name": "lb", "chef_type": "node", "json_class": "Chef::Node", "chef_environment": "production", "run_list": [] } chef-zero-15.0.0/playground/nodes/ldap.json000066400000000000000000000001761362334302100205670ustar00rootroot00000000000000{ "name": "ldap", "chef_type": "node", "json_class": "Chef::Node", "chef_environment": "_default", "run_list": [] } chef-zero-15.0.0/playground/nodes/www.json000066400000000000000000000001751362334302100204720ustar00rootroot00000000000000{ "name": "www", "chef_type": "node", "json_class": "Chef::Node", "chef_environment": "_default", "run_list": [] } chef-zero-15.0.0/spec/000077500000000000000000000000001362334302100144065ustar00rootroot00000000000000chef-zero-15.0.0/spec/run_oc_pedant.rb000066400000000000000000000152461362334302100175630ustar00rootroot00000000000000#!/usr/bin/env ruby require "json" 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(&: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) unless File.exist?(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}") unless File.exist?("#{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 test_secrets = JSON.parse(File.read("spec/support/secrets.json")) ENV["SUPERUSER_KEY"] = test_secrets["chef-server"]["superuser_key"] ENV["WEBUI_KEY"] = test_secrets["chef-server"]["webui_key"] 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", ] 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", # Chef Zero does not intend to support oc_id authentication/authorization # the way erchef does. "--skip-oc_id", # 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 # 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. This is also used to skip the user acl # tests from chef-server as the user acls are not supported in chef-zero # at this time. "--skip-chef-zero-quirks", ] # 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-15.0.0/spec/search_spec.rb000066400000000000000000000015111362334302100172100ustar00rootroot00000000000000require "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-15.0.0/spec/server_spec.rb000066400000000000000000000057201362334302100172570ustar00rootroot00000000000000require "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-15.0.0/spec/socketless_server_map_spec.rb000066400000000000000000000044151362334302100223530ustar00rootroot00000000000000require "chef_zero/socketless_server_map" describe "Socketless Mode" do let(:server_map) { ChefZero::SocketlessServerMap.instance.tap(&: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-15.0.0/spec/support/000077500000000000000000000000001362334302100161225ustar00rootroot00000000000000chef-zero-15.0.0/spec/support/oc_pedant.rb000066400000000000000000000132011362334302100204000ustar00rootroot00000000000000# 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" 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] = %i{validation authentication authorization} verify_error_messages false ruby_users_endpoint? false ruby_acls_endpoint? false ruby_org_assoc? false chef_12? true chef-zero-15.0.0/spec/support/secrets.json000066400000000000000000000066301362334302100204720ustar00rootroot00000000000000{ "chef-server": { "superuser_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEApNCkX2k+lFGDWRVhX4uClaVQrumG9XXvk6X7M2izrIg7RzMP\nDk4thhZkpx5gr22By7PZQdMEjWC/Zo8MBjtoJ0GV0jw8npefbU1MGKs2dtpYgo0N\nFq8fX8MdFPu4h2W3g0dMEdhT8icc2H4EjhZmdeUhUn3RIEt2duCgp3YDYnUUZx3j\nN7MHcTIdzD58ikr6zQrZzHOv+OOI86Xk9EpyEEQizOLoQxkICNrhqN7ElQDuvXaX\nBSBrYDRKH2umBMMcXzvsR/SvkmqxoEESSpIlW8zeKAWQ+znNjDC0tmTg7jZmgSP7\nsiKrwo4t4ebjcmjpIoi/JKww/nGN3Uhz1ZOZuwIDAQABAoIBAQCaJQD2s0nyEeKU\nuKhfYe155Cl3zbWJcQnmv4AXbr9MiAVY6+oS6Q8ur1bn7kNjDzoruENjiuZhC7E3\nTGZklb8tp+tluyy+7vQOmBKpp8fClSfewekR5CultqhGbb8B8yIVR+NfdUHd4rLZ\nz9KWyWB+txPZQQ8L80gSmrfmpzs3IuT7oPvmtBU1Wq9QapC4n/rUohHUpUV1du4G\n0wCIF4zQTg6cbYW2YXozwVQvw+P7P3RVEqZt+aZlbVcy0fNr6jNao0hi1KFC9OH2\nVjjU+PioreoA/NU3aZPIUzmJpWtsu31yuOZxXmytAkYooCZgiEQNEHnJlNPv0RmC\n6BPMzVoBAoGBAM7yZoSNJpzdP/q1/4+H3zyy7o4I0VTW9u/GqUzhnbjm5poK30X9\nYXh/7WOVV0OoVqdO6ljRKygP3Oggf41ZEbi1C6bbsO57pksBWgx9bD9V35XscZ0J\nF1ERe//kMHwVQy74R8/cIuRwm75haLSBj5/fwGbLeeVDglJkCVqPjtuBAoGBAMvh\nqsAGG5k9u6voTcXlFwS+B5YjULhK4NSxdJ2BnOxzYzxQ3IYQZMlb2xt8yZYx/ZZK\nwjkr9rcAPEQIQZ2A6NUbGq6qCD7sSmg6UAi0CgiqTokQ/Wtag0UDvFMzwerdg/On\n37uxffpxpte8z1jYi/MxRaoTYueuc1UVnqofVIM7AoGBALZJzwPzUY/bVAADUJmd\nlYZiFsAGBF42/E05MOgH1GaK/ZWy/fkouDLsfK67XaK7JZk6ajLSDLG9R1kxRym6\ny2FoGFtiKPfo8xIenrNhx3gCrG/jVjB9UYyXWiKNXifukr9M8/SkdBfFGWsZYqGd\nfmXVMiVaFoVcce8hLxwWWEABAoGBAKcyhKX/HEj6YFqlIoqkydDAylXs1jicZ27l\nrF2yum8KXZpMMdzbutuKsdAD8Ql0K6NB4a+jByuiTMn5/11cJxUEqkgM9sArZQW+\ntH2+r+/VQpyTS0/rpXVGj/2nl2K1kI2T4R36e/aTl6CanWweAf9JK/lC9rxKyxg+\np6SaFuObAoGACP6TKCkp2oymXlKgdUUgPrnsaz2VAw8jD5QHtx10U4wty0C8gxsk\nMLe00h09iLPyFmvJpD+MgbxV/r6RrZeVdsKdU/5LG52YgiVSTaizyy+ciEfW7xoQ\nCL5EtZd8Cn5OKinBEzzFpELqunlqepIKCIDOcLKz/cjR+3a+E6Zx5Wo=\n-----END RSA PRIVATE KEY-----", "webui_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEApNCkX2k+lFGDWRVhX4uClaVQrumG9XXvk6X7M2izrIg7RzMP\nDk4thhZkpx5gr22By7PZQdMEjWC/Zo8MBjtoJ0GV0jw8npefbU1MGKs2dtpYgo0N\nFq8fX8MdFPu4h2W3g0dMEdhT8icc2H4EjhZmdeUhUn3RIEt2duCgp3YDYnUUZx3j\nN7MHcTIdzD58ikr6zQrZzHOv+OOI86Xk9EpyEEQizOLoQxkICNrhqN7ElQDuvXaX\nBSBrYDRKH2umBMMcXzvsR/SvkmqxoEESSpIlW8zeKAWQ+znNjDC0tmTg7jZmgSP7\nsiKrwo4t4ebjcmjpIoi/JKww/nGN3Uhz1ZOZuwIDAQABAoIBAQCaJQD2s0nyEeKU\nuKhfYe155Cl3zbWJcQnmv4AXbr9MiAVY6+oS6Q8ur1bn7kNjDzoruENjiuZhC7E3\nTGZklb8tp+tluyy+7vQOmBKpp8fClSfewekR5CultqhGbb8B8yIVR+NfdUHd4rLZ\nz9KWyWB+txPZQQ8L80gSmrfmpzs3IuT7oPvmtBU1Wq9QapC4n/rUohHUpUV1du4G\n0wCIF4zQTg6cbYW2YXozwVQvw+P7P3RVEqZt+aZlbVcy0fNr6jNao0hi1KFC9OH2\nVjjU+PioreoA/NU3aZPIUzmJpWtsu31yuOZxXmytAkYooCZgiEQNEHnJlNPv0RmC\n6BPMzVoBAoGBAM7yZoSNJpzdP/q1/4+H3zyy7o4I0VTW9u/GqUzhnbjm5poK30X9\nYXh/7WOVV0OoVqdO6ljRKygP3Oggf41ZEbi1C6bbsO57pksBWgx9bD9V35XscZ0J\nF1ERe//kMHwVQy74R8/cIuRwm75haLSBj5/fwGbLeeVDglJkCVqPjtuBAoGBAMvh\nqsAGG5k9u6voTcXlFwS+B5YjULhK4NSxdJ2BnOxzYzxQ3IYQZMlb2xt8yZYx/ZZK\nwjkr9rcAPEQIQZ2A6NUbGq6qCD7sSmg6UAi0CgiqTokQ/Wtag0UDvFMzwerdg/On\n37uxffpxpte8z1jYi/MxRaoTYueuc1UVnqofVIM7AoGBALZJzwPzUY/bVAADUJmd\nlYZiFsAGBF42/E05MOgH1GaK/ZWy/fkouDLsfK67XaK7JZk6ajLSDLG9R1kxRym6\ny2FoGFtiKPfo8xIenrNhx3gCrG/jVjB9UYyXWiKNXifukr9M8/SkdBfFGWsZYqGd\nfmXVMiVaFoVcce8hLxwWWEABAoGBAKcyhKX/HEj6YFqlIoqkydDAylXs1jicZ27l\nrF2yum8KXZpMMdzbutuKsdAD8Ql0K6NB4a+jByuiTMn5/11cJxUEqkgM9sArZQW+\ntH2+r+/VQpyTS0/rpXVGj/2nl2K1kI2T4R36e/aTl6CanWweAf9JK/lC9rxKyxg+\np6SaFuObAoGACP6TKCkp2oymXlKgdUUgPrnsaz2VAw8jD5QHtx10U4wty0C8gxsk\nMLe00h09iLPyFmvJpD+MgbxV/r6RrZeVdsKdU/5LG52YgiVSTaizyy+ciEfW7xoQ\nCL5EtZd8Cn5OKinBEzzFpELqunlqepIKCIDOcLKz/cjR+3a+E6Zx5Wo=\n-----END RSA PRIVATE KEY-----" } } chef-zero-15.0.0/spec/support/stickywicket.pem000066400000000000000000000032171362334302100213450ustar00rootroot00000000000000-----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-----