diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/chef_zero/endpoints/node_endpoint.rb | 14 | ||||
-rw-r--r-- | lib/chef_zero/endpoints/nodes_endpoint.rb | 35 | ||||
-rw-r--r-- | lib/chef_zero/endpoints/organization_policies_endpoint.rb | 100 | ||||
-rw-r--r-- | lib/chef_zero/endpoints/organization_policy_groups_endpoint.rb | 262 | ||||
-rw-r--r-- | lib/chef_zero/rest_base.rb | 39 | ||||
-rw-r--r-- | lib/chef_zero/server.rb | 15 |
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) ], |