summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/chef_zero/endpoints/node_endpoint.rb14
-rw-r--r--lib/chef_zero/endpoints/nodes_endpoint.rb35
-rw-r--r--lib/chef_zero/endpoints/organization_policies_endpoint.rb100
-rw-r--r--lib/chef_zero/endpoints/organization_policy_groups_endpoint.rb262
-rw-r--r--lib/chef_zero/rest_base.rb39
-rw-r--r--lib/chef_zero/server.rb15
6 files changed, 458 insertions, 7 deletions
diff --git a/lib/chef_zero/endpoints/node_endpoint.rb b/lib/chef_zero/endpoints/node_endpoint.rb
index 223ec9f..f2bb8ba 100644
--- a/lib/chef_zero/endpoints/node_endpoint.rb
+++ b/lib/chef_zero/endpoints/node_endpoint.rb
@@ -6,6 +6,20 @@ module ChefZero
module Endpoints
# /nodes/ID
class NodeEndpoint < RestObjectEndpoint
+ def put(request)
+ data = parse_json(request.body)
+
+ if data.has_key?("policy_name") && policy_name_invalid?(data["policy_name"])
+ return error(400, "Field 'policy_name' invalid", :pretty => false)
+ end
+
+ if data.has_key?("policy_group") && policy_name_invalid?(data["policy_group"])
+ return error(400, "Field 'policy_group' invalid", :pretty => false)
+ end
+
+ super(request)
+ end
+
def populate_defaults(request, response_json)
node = FFI_Yajl::Parser.parse(response_json, :create_additions => false)
node = ChefData::DataNormalizer.normalize_node(node, request.rest_path[3])
diff --git a/lib/chef_zero/endpoints/nodes_endpoint.rb b/lib/chef_zero/endpoints/nodes_endpoint.rb
new file mode 100644
index 0000000..8b9d852
--- /dev/null
+++ b/lib/chef_zero/endpoints/nodes_endpoint.rb
@@ -0,0 +1,35 @@
+require 'ffi_yajl'
+require 'chef_zero/endpoints/rest_object_endpoint'
+require 'chef_zero/chef_data/data_normalizer'
+
+module ChefZero
+ module Endpoints
+ # /nodes
+ class NodesEndpoint < RestListEndpoint
+
+ def post(request)
+ # /nodes validation
+ if request.rest_path.last == "nodes"
+ data = parse_json(request.body)
+
+ if data.has_key?("policy_name") && policy_name_invalid?(data["policy_name"])
+ return error(400, "Field 'policy_name' invalid", :pretty => false)
+ end
+
+ if data.has_key?("policy_group") && policy_name_invalid?(data["policy_group"])
+ return error(400, "Field 'policy_group' invalid", :pretty => false)
+ end
+ end
+
+ super(request)
+ end
+
+ def populate_defaults(request, response_json)
+ node = FFI_Yajl::Parser.parse(response_json, :create_additions => false)
+ node = ChefData::DataNormalizer.normalize_node(node, request.rest_path[3])
+ FFI_Yajl::Encoder.encode(node, :pretty => true)
+ end
+ end
+ end
+end
+
diff --git a/lib/chef_zero/endpoints/organization_policies_endpoint.rb b/lib/chef_zero/endpoints/organization_policies_endpoint.rb
new file mode 100644
index 0000000..a495d9b
--- /dev/null
+++ b/lib/chef_zero/endpoints/organization_policies_endpoint.rb
@@ -0,0 +1,100 @@
+
+module ChefZero
+ module Endpoints
+ # /organizations/NAME/policies
+ class OrganizationPoliciesEndpoint < RestBase
+ def hashify_list(list)
+ list.reduce({}) { |acc, obj| acc.merge( obj => {} ) }
+ end
+
+ def get(request)
+
+ # vanilla /policies.
+ if request.rest_path.last == "policies"
+ response_data = {}
+ policy_names = list_data_or_else(request, nil, [])
+ 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_or_else(request, policy_path + ["revisions"], {})
+
+ response_data[policy_name] = {
+ uri: policy_uri,
+ revisions: hashify_list(revisions)
+ }
+ end
+
+ return json_response(200, response_data)
+ end
+
+ # /policies/:policy_name
+ if request.rest_path[-2] == "policies"
+ if !exists_data_dir?(request)
+ return error(404, "Item not found" )
+ else
+ revisions = list_data(request, request.rest_path + ["revisions"])
+ data = { revisions: hashify_list(revisions) }
+ return json_response(200, data)
+ end
+ end
+
+ # /policies/:policy_name/revisions/:revision_id
+ if request.rest_path[-2] == "revisions"
+ if !exists_data?(request, nil)
+ return error(404, "Revision ID #{request.rest_path.last} not found" )
+ else
+ data = get_data(request)
+ return already_json_response(200, data)
+ end
+ end
+ end
+
+ def post(request)
+ if request.rest_path.last == "revisions"
+ # POST /policies/:policy_name/revisions
+ # we want to create /policies/{policy_name}/revisions/{revision_id}
+ policyfile_data = parse_json(request.body)
+ uri_policy_name = request.rest_path[-2]
+
+ if exists_data?(request, request.rest_path + [policyfile_data["revision_id"]])
+ return error(409, "Revision ID #{policyfile_data["revision_id"]} already exists.")
+ end
+
+ if policyfile_data["name"] != uri_policy_name
+ return error(400, "URI policy name #{uri_policy_name} does not match JSON policy name #{policyfile_data["name"]}")
+ end
+
+ revision_path = request.rest_path + [policyfile_data["revision_id"]]
+ set_data(request, revision_path, request.body, *set_opts)
+ return already_json_response(201, request.body)
+ end
+ end
+
+ def delete(request)
+ # /policies/:policy_name/revisions/:revision_id
+ if request.rest_path[-2] == "policies"
+ revisions = list_data(request, request.rest_path + ["revisions"])
+ data = { revisions: hashify_list(revisions) }
+
+ delete_data_dir(request, nil, :recursive)
+ return json_response(200, data)
+ end
+
+ if request.rest_path[-2] == "revisions"
+ if exists_data?(request)
+ policyfile_data = get_data(request)
+ delete_data(request)
+ return already_json_response(200, policyfile_data)
+ else
+ return error(404, "Revision ID #{request.rest_path.last} not found")
+ end
+ end
+ end
+
+ private
+ def set_opts
+ [ :create_dir ]
+ end
+ end
+ end
+end
diff --git a/lib/chef_zero/endpoints/organization_policy_groups_endpoint.rb b/lib/chef_zero/endpoints/organization_policy_groups_endpoint.rb
new file mode 100644
index 0000000..51e0812
--- /dev/null
+++ b/lib/chef_zero/endpoints/organization_policy_groups_endpoint.rb
@@ -0,0 +1,262 @@
+require 'ffi_yajl'
+require 'chef_zero/rest_base'
+
+module ChefZero
+ module Endpoints
+ # /organizations/{organization}/policy_groups/{policy_group}/policies/{policy_name}
+ # GET / PUT / DELETE
+ #
+ # in the data store, this REST path actually stores the revision ID of ${policy_name} that's currently
+ # associated with ${policy_group}.
+ class OrganizationPolicyGroupsEndpoint < RestBase
+
+ def fetch_uri_params(request)
+ {
+ org_name: request.rest_path[1],
+ policy_group_name: request.rest_path[3],
+ policy_name: request.rest_path[5]
+ }
+ end
+
+ # Either return all the policy groups with URIs and list of revisions, or...
+ # Return the policy document for the given policy group and policy name.
+ def get(request)
+
+ # vanilla /policy_groups.
+ if request.rest_path.last == "policy_groups"
+ policy_group_names = list_data_or_else(request, nil, [])
+ # no policy groups, so sad.
+ if policy_group_names.size == 0
+ return already_json_response(200, '{}')
+ else
+
+ response_data = {}
+ # each policy group has policies and associated revisions under
+ # /policy_groups/{group name}/policies/{policy name}.
+ policy_group_names.each do |group_name|
+
+ response_data[group_name] = {
+ uri: build_uri(request.base_uri, request.rest_path + [group_name]),
+ policies: {}
+ }
+
+ policy_group_path = request.rest_path + [group_name]
+ policy_group_policies_path = policy_group_path + ["policies"]
+ policy_list = list_data(request, policy_group_policies_path)
+
+ # build the list of policies with their revision ID associated with this policy group.
+ policy_list.each do |policy_name|
+ policy_group_policy_path = policy_group_policies_path + [policy_name]
+ revision_id = get_data_or_else(request, policy_group_policy_path, "no revision ID found")
+ response_data[group_name][:policies][policy_name] = {
+ revision_id: revision_id
+ }
+ end
+
+ response_data[group_name].delete(:policies) if response_data[group_name][:policies].size == 0
+ end
+
+ return json_response(200, response_data)
+ end # if policy_groups.size > 0
+ end # end /policy_groups
+
+ # /policy_groups/{policy_group}
+ if request.rest_path.last(2).first == "policy_groups"
+ data = {
+ uri: build_uri(request.base_uri, request.rest_path),
+ policies: get_policy_group_policies(request)
+ }
+ return json_response(200, data)
+ end
+
+ # /policy_groups/{policy_group}/policies/{policy_name}
+ if request.rest_path.last(2).first == "policies"
+ uri_params = fetch_uri_params(request)
+
+ # fetch /organizations/{organization}/policies/{policy_name}/revisions/{revision_id}
+ revision_id = parse_json(get_data(request))
+ result = get_data(request, ["organizations", uri_params[:org_name], "policies",
+ uri_params[:policy_name], "revisions", revision_id], :nil)
+ return already_json_response(200, result)
+ end
+ end # end get()
+
+ # 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.
+
+ # 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
+
+ ## 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
+
+
+ def cookbook_locks_valid?(cookbook_locks)
+ if !cookbook_locks.is_a?(Hash)
+ return [false, "Field 'cookbook_locks' invalid"]
+ end
+
+ cookbook_locks.each do |name, lock_data|
+ if !lock_data.is_a?(Hash)
+ return [false, "Field 'cookbook_locks' invalid"]
+ end
+
+ if !lock_data.has_key?("identifier")
+ return [false, "Field 'identifier' missing"]
+ end
+
+ if lock_data["identifier"].length > 255
+ return [false, "Field 'identifier' invalid"]
+ end
+
+ if lock_data.has_key?("dotted_decimal_identifier") &&
+ lock_data["dotted_decimal_identifier"] !~ /\d+\.\d+\.\d+/
+ return [false, "Field 'dotted_decimal_identifier' is not a valid version"]
+ end
+ end
+ return [true, "no error to return"]
+ end
+
+ def validate_policyfile(policyfile_data)
+ if !policyfile_data.has_key?("revision_id")
+ return [false, "Field 'revision_id' missing"]
+ elsif policyfile_data["revision_id"] !~ /^[\-[:alnum:]_\.\:]{1,255}$/
+ return [false, "Field 'revision_id' invalid"]
+ end
+
+ if !policyfile_data.has_key?("name")
+ return [false, "Field 'name' missing"]
+ elsif policyfile_data["name"] !~ /^[\-[:alnum:]_\.\:]{1,255}$/
+ return [false, "Field 'name' invalid"]
+ end
+
+ if !policyfile_data.has_key?("run_list")
+ return [false, "Field 'run_list' missing"]
+ elsif !(policyfile_data["run_list"].is_a?(Array) &&
+ policyfile_data["run_list"].all? { |r| r =~ /\Arecipe\[[^\s]+::[^\s]+\]\Z/ })
+ return [false, "Field 'run_list' is not a valid run list"]
+ end
+
+ if !policyfile_data.has_key?("cookbook_locks")
+ return [false, "Field 'cookbook_locks' missing"]
+ else
+ # change this logic if there are more validations after this.
+ return cookbook_locks_valid?(policyfile_data["cookbook_locks"])
+ end
+
+ return [true, "no error to return"]
+ end
+
+ def put(request)
+
+ # validate request body.
+ policyfile_data = parse_json(request.body)
+
+ is_valid, error_msg = validate_policyfile(policyfile_data)
+ if !is_valid
+ return error(400, error_msg)
+ end
+
+ if request.rest_path.last != policyfile_data["name"]
+ return error(400, "Field 'name' invalid : #{request.rest_path.last} does not match #{policyfile_data["name"]}")
+ end
+
+ uri_params = fetch_uri_params(request)
+ org_path = request.rest_path.first(2)
+
+
+ new_policyfile_data = parse_json( request.body )
+
+ # get the current list of revisions of this policyfile.
+ policyfile_path = request.rest_path[0..1] + ["policies", uri_params[:policy_name]]
+
+ policy_revisions = list_data_or_else(request, policyfile_path + ["revisions"], [])
+
+ # if the given policy+revision doesn't exist, create it..
+ if !policy_revisions.include?(new_policyfile_data["revision_id"])
+ new_revision_path = policyfile_path +["revisions", new_policyfile_data["revision_id"]]
+ set_data(request, new_revision_path, request.body, *set_opts)
+ created_policy = true
+ end
+
+ no_revision_set = "no revision ID set"
+
+ # this request's data path just stores the revision ID currently associated with the policy group.
+ existing_revision_id = get_data_or_else(request, nil, no_revision_set)
+
+ # if named policy exists and the given revision ID exists, associate the revision ID with the policy
+ # group.
+ if existing_revision_id != new_policyfile_data["revision_id"]
+ set_data(request, nil, to_json(new_policyfile_data["revision_id"]), *set_opts)
+ updated_association = true
+ end
+
+ code = (existing_revision_id == no_revision_set) ? 201 : 200
+
+ return already_json_response(code, request.body)
+ end
+
+ def delete(request)
+ # /policy_groups/{policy_group}
+ if request.rest_path.last(2).first == "policy_groups"
+
+ policy_group_policies = get_policy_group_policies(request)
+
+ if exists_data_dir?(request, request.rest_path + ["policies"])
+ delete_data_dir(request, request.rest_path + ["policies"], :recursive)
+ end
+
+ data = {
+ uri: build_uri(request.base_uri, request.rest_path),
+ policies: policy_group_policies
+ }
+ return json_response(200, data)
+ end
+
+ # "/policy_groups/some_policy_group/policies/some_policy_name"
+ if request.rest_path.last(2).first == "policies"
+ 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_path = request.rest_path.first(2) + ["policies", request.rest_path.last,
+ "revisions", current_revision_id]
+
+ full_policy_doc = get_data(request, policy_path)
+ return already_json_response(200, full_policy_doc)
+ end
+
+ return error(404, "Don't know what to do with path #{request.rest_path}")
+ end
+
+ private
+ def set_opts
+ [ :create_dir ]
+ end
+ end
+ end
+end
diff --git a/lib/chef_zero/rest_base.rb b/lib/chef_zero/rest_base.rb
index 715d705..b4f8605 100644
--- a/lib/chef_zero/rest_base.rb
+++ b/lib/chef_zero/rest_base.rb
@@ -195,12 +195,13 @@ module ChefZero
data_store.exists_dir?(rest_path)
end
- def error(response_code, error)
- json_response(response_code, {"error" => [error]})
+ def error(response_code, error, opts={})
+ json_response(response_code, {"error" => [error]}, 0, 0, opts)
end
- def json_response(response_code, json, request_version=0, response_version=0)
- already_json_response(response_code, FFI_Yajl::Encoder.encode(json, :pretty => true), request_version, response_version)
+ def json_response(response_code, json, request_version=0, response_version=0, opts={pretty: true})
+ do_pretty_json = opts[:pretty] && true
+ already_json_response(response_code, FFI_Yajl::Encoder.encode(json, :pretty => do_pretty_json), request_version, response_version)
end
def text_response(response_code, text)
@@ -238,5 +239,35 @@ module ChefZero
def populate_defaults(request, response)
response
end
+
+ def parse_json(json)
+ FFI_Yajl::Parser.parse(json, create_additions: false)
+ end
+
+ def to_json(data)
+ FFI_Yajl::Encoder.encode(data, :pretty => true)
+ end
+
+ def get_data_or_else(request, path, or_else_value)
+ if exists_data?(request, path)
+ parse_json(get_data(request, path))
+ else
+ or_else_value
+ end
+ end
+
+ def list_data_or_else(request, path, or_else_value)
+ if exists_data_dir?(request, path)
+ list_data(request, path)
+ else
+ or_else_value
+ end
+ end
+
+ def policy_name_invalid?(name)
+ !name.is_a?(String) ||
+ name.size > 255 ||
+ name =~ /[+ !]/
+ end
end
end
diff --git a/lib/chef_zero/server.rb b/lib/chef_zero/server.rb
index 016f299..5723ae4 100644
--- a/lib/chef_zero/server.rb
+++ b/lib/chef_zero/server.rb
@@ -61,17 +61,19 @@ require 'chef_zero/endpoints/environment_recipes_endpoint'
require 'chef_zero/endpoints/environment_role_endpoint'
require 'chef_zero/endpoints/license_endpoint'
require 'chef_zero/endpoints/node_endpoint'
+require 'chef_zero/endpoints/nodes_endpoint'
require 'chef_zero/endpoints/node_identifiers_endpoint'
require 'chef_zero/endpoints/organizations_endpoint'
require 'chef_zero/endpoints/organization_endpoint'
require 'chef_zero/endpoints/organization_association_requests_endpoint'
require 'chef_zero/endpoints/organization_association_request_endpoint'
require 'chef_zero/endpoints/organization_authenticate_user_endpoint'
+require 'chef_zero/endpoints/organization_policies_endpoint'
+require 'chef_zero/endpoints/organization_policy_groups_endpoint'
require 'chef_zero/endpoints/organization_users_endpoint'
require 'chef_zero/endpoints/organization_user_endpoint'
require 'chef_zero/endpoints/organization_validator_key_endpoint'
require 'chef_zero/endpoints/principal_endpoint'
-require 'chef_zero/endpoints/policies_endpoint'
require 'chef_zero/endpoints/role_endpoint'
require 'chef_zero/endpoints/role_environments_endpoint'
require 'chef_zero/endpoints/sandboxes_endpoint'
@@ -89,6 +91,7 @@ require 'chef_zero/endpoints/version_endpoint'
require 'chef_zero/endpoints/server_api_version_endpoint'
module ChefZero
+
class Server
DEFAULT_OPTIONS = {
@@ -541,10 +544,16 @@ module ChefZero
[ "/organizations/*/environments/*/nodes", EnvironmentNodesEndpoint.new(self) ],
[ "/organizations/*/environments/*/recipes", EnvironmentRecipesEndpoint.new(self) ],
[ "/organizations/*/environments/*/roles/*", EnvironmentRoleEndpoint.new(self) ],
- [ "/organizations/*/nodes", RestListEndpoint.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", OrganizationPoliciesEndpoint.new(self) ],
+ [ "/organizations/*/policies/*", OrganizationPoliciesEndpoint.new(self) ],
+ [ "/organizations/*/policies/*/revisions", OrganizationPoliciesEndpoint.new(self) ],
+ [ "/organizations/*/policies/*/revisions/*", OrganizationPoliciesEndpoint.new(self) ],
+ [ "/organizations/*/policy_groups", OrganizationPolicyGroupsEndpoint.new(self) ],
+ [ "/organizations/*/policy_groups/*", OrganizationPolicyGroupsEndpoint.new(self) ],
+ [ "/organizations/*/policy_groups/*/policies/*", OrganizationPolicyGroupsEndpoint.new(self) ],
[ "/organizations/*/principals/*", PrincipalEndpoint.new(self) ],
[ "/organizations/*/roles", RestListEndpoint.new(self) ],
[ "/organizations/*/roles/*", RoleEndpoint.new(self) ],