diff options
author | John Keiser <jkeiser@opscode.com> | 2014-08-19 23:53:36 -0700 |
---|---|---|
committer | John Keiser <jkeiser@opscode.com> | 2014-08-22 09:20:49 -0700 |
commit | 0bccfd74679ae382d04e319972353f9a692bbf30 (patch) | |
tree | 35c0b14677bc4ae9dd9974b08721caf711b70659 /lib | |
parent | 91768b0e9a0a151635a0a7a96823113b200c67c6 (diff) | |
download | chef-zero-0bccfd74679ae382d04e319972353f9a692bbf30.tar.gz |
Move all defaults to DefaultCreator, calculate on fly,
remember ownership
Diffstat (limited to 'lib')
-rw-r--r-- | lib/chef_zero/chef_data/acl_path.rb | 141 | ||||
-rw-r--r-- | lib/chef_zero/chef_data/default_creator.rb | 428 | ||||
-rw-r--r-- | lib/chef_zero/data_normalizer.rb | 5 | ||||
-rw-r--r-- | lib/chef_zero/data_store/default_facade.rb | 375 | ||||
-rw-r--r-- | lib/chef_zero/data_store/memory_store.rb | 2 | ||||
-rw-r--r-- | lib/chef_zero/data_store/memory_store_v2.rb | 2 | ||||
-rw-r--r-- | lib/chef_zero/endpoints/acl_base.rb | 82 | ||||
-rw-r--r-- | lib/chef_zero/endpoints/acl_endpoint.rb | 13 | ||||
-rw-r--r-- | lib/chef_zero/endpoints/acls_endpoint.rb | 15 | ||||
-rw-r--r-- | lib/chef_zero/endpoints/cookbook_version_endpoint.rb | 3 | ||||
-rw-r--r-- | lib/chef_zero/endpoints/data_bag_endpoint.rb | 1 | ||||
-rw-r--r-- | lib/chef_zero/endpoints/data_bags_endpoint.rb | 2 | ||||
-rw-r--r-- | lib/chef_zero/endpoints/rest_object_endpoint.rb | 3 | ||||
-rw-r--r-- | lib/chef_zero/rest_base.rb | 101 | ||||
-rw-r--r-- | lib/chef_zero/rest_error_response.rb | 2 | ||||
-rw-r--r-- | lib/chef_zero/server.rb | 7 |
16 files changed, 693 insertions, 489 deletions
diff --git a/lib/chef_zero/chef_data/acl_path.rb b/lib/chef_zero/chef_data/acl_path.rb new file mode 100644 index 0000000..4592342 --- /dev/null +++ b/lib/chef_zero/chef_data/acl_path.rb @@ -0,0 +1,141 @@ +module 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 cookbooks containers data environments groups nodes roles sandboxes) + TOP_DATA_TYPES = %w(containers organizations users) + + # ACL data paths for a partition are: + # / -> /acls/root + # /TYPE -> /acls/containers/TYPE + # /TYPE/NAME -> /acls/TYPE/NAME + # + # The root partition "/" has its own acls, so it looks like this: + # + # / -> /acls/root + # /users -> /acls/containers/users + # /organizations -> /acls/containers/organizations + # /users/schlansky -> /acls/users/schlansky + # + # Each organization is its own partition, so it looks like this: + # + # /organizations/blah -> /organizations/blah/acls/root + # /organizations/blah/roles -> /organizations/blah/acls/containers/roles + # /organizations/blah/roles/web -> /organizations/blah/acls/roles/web + # /organizations/ORG is its own partition. ACLs for anything under it follow + + # This method takes a Chef REST path and returns the chef-zero path + # used to look up the ACL. If an object does not have an ACL directly, + # it will return nil. Paths like /organizations/ORG/data/bag/item will + # return nil, because it is the parent path (data/bag) that has an ACL. + def self.get_acl_data_path(path) + # Things under organizations have their own acls hierarchy + if path[0] == 'organizations' && path.size >= 2 + under_org = partition_acl_data_path(path[2..-1], ORG_DATA_TYPES) + if under_org + path[0..1] + under_org + end + else + partition_acl_data_path(path, TOP_DATA_TYPES) + end + end + + # Reverse transform from acl_data_path to path. + # /acls/root -> / + # /acls/containers/TYPE -> /TYPE + # /acls/TYPE/NAME -> /TYPE/NAME + # /organizations/ORG/acls/root -> / + # /organizations/ORG/acls/containers/TYPE -> /organizations/ORG/TYPE + # /organizations/ORG/acls/TYPE/NAME -> /organizations/ORG/TYPE/NAME + # + def self.get_object_path(acl_data_path) + if acl_data_path[0] == 'acls' + if acl_data_path[1] == 'root' + [] + elsif acl_data_path[1] == 'containers' + [acl_data_path[2]] + else + acl_data_path[1..2] + end + elsif acl_data_path[0] == 'organizations' && acl_data_path[2] == 'acls' + if acl_data_path[3] == 'root' + acl_data_path[0..1] + elsif acl_data_path[3] == 'containers' + acl_data_path[0..1] + [ acl_data_path[4] ] + else + acl_data_path[0..1] + acl_data_path[3..4] + end + end + end + + # Method *assumes* acl_data_path is valid. + # /organizations/BLAH's parent is /organizations + # + # An example traversal up the whole tree: + # /organizations/foo/acls/nodes/mario -> + # /organizations/foo/acls/containers/nodes -> + # /organizations/foo/acls/containers/containers -> + # /organizations/foo/acls/root -> + # /acls/containers/organizations -> + # /acls/containers/containers -> + # /acls/root -> + # nil + def self.parent_acl_data_path(acl_data_path) + if acl_data_path[0] == 'organizations' + under_org = partition_parent_acl_data_path(acl_data_path[2..-1]) + if under_org + acl_data_path[0..1] + under_org + else + # ACL data path is /organizations/X/acls/root; therefore parent is "/organizations" + [ 'acls', 'containers', 'organizations' ] + end + else + partition_parent_acl_data_path(acl_data_path) + end + end + + private + + # /acls/root -> nil + # /acls/containers/containers -> /acls/root + # /acls/TYPE/X -> /acls/containers/TYPE + # + # Method *assumes* acl_data_path is valid. + # Returns nil if the path is /acls/root + def self.partition_parent_acl_data_path(acl_data_path) + if acl_data_path.size == 3 + if acl_data_path == %w(acls containers containers) + [ 'acls', 'root' ] + else + [ 'acls', 'containers', acl_data_path[1]] + end + else + nil + end + end + + def self.partition_acl_data_path(path, data_types) + if path.size == 0 + [ 'acls', 'root'] + elsif data_types.include?(path[0]) + if path.size == 0 + [ 'acls', 'containers', path[0] ] + elsif path.size == 2 + [ 'acls', path[0], path[1] ] + end + end + end + end + end +end diff --git a/lib/chef_zero/chef_data/default_creator.rb b/lib/chef_zero/chef_data/default_creator.rb new file mode 100644 index 0000000..564a84c --- /dev/null +++ b/lib/chef_zero/chef_data/default_creator.rb @@ -0,0 +1,428 @@ +require 'chef_zero/chef_data/acl_path' + +module ChefZero + module ChefData + # + # The DefaultCreator creates default values when you ask for them. + # - It relies on created and deleted being called when things get + # created and deleted, so that it knows the owners of said objects + # and knows to eliminate default values on delete. + # - get, list and exists? get data. + # + class DefaultCreator + def initialize(data, single_org, osc_compat, superusers = nil) + @data = data + @single_org = single_org + @osc_compat = osc_compat + @superusers = superusers || DEFAULT_SUPERUSERS + clear + end + + attr_reader :data + attr_reader :single_org + attr_reader :osc_compat + attr_reader :creators + attr_reader :deleted + + PERMISSIONS = %w(create read update delete grant) + DEFAULT_SUPERUSERS = %w(pivotal) + + def clear + @creators = { [] => @superusers } + @deleted = {} + end + + def deleted(path) + # acl deletes mean nothing, they are entirely subservient to their + # parent object + unless path[0] == 'acls' || (path[0] == 'organizations' && path[2] == 'acls') + result = exists?(path) + @deleted[path] = true + result + end + false + end + + def deleted?(path) + 1.upto(path.size) do |index| + return true if @deleted[path[0..-index]] + end + false + end + + def created(path, creator) + @creators[path] = [ creator ] + @deleted.delete(path) if @deleted[path] + end + + def superusers + @creators[[]] + end + + def get(path) + return nil if deleted?(path) + + 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 + 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' ]) + else + data.list(AclPath.get_object_path(path)) + end + + when 'containers' + [ 'containers', 'users' ] + + when 'users' + result = superusers + data.list([ 'organizations' ]).each do |org| + result += data.list([ 'organizations', org, 'users' ]).uniq + end + result + + 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' => {}, + 'cookbooks' => {}, + 'data' => {}, + 'environments' => %w(_default), + 'file_store' => { + 'checksums' => {} + }, + 'nodes' => {}, + 'roles' => {}, + 'sandboxes' => {}, + 'users' => {}, + + 'org' => {}, + 'containers' => %w(clients containers cookbooks data environments groups nodes roles sandboxes), + 'groups' => %w(admins billing-admins clients users), + 'association_requests' => {} + } + + def list_org_default(path) + if path.size >= 3 && path[2] == 'acls' + if path.size == 3 + return [ 'root' ] + data.list(path[0..1] + [ 'containers' ]) + else + return data.list(AclPath.get_object_path(path)) + end + end + + value = DEFAULT_ORG_SPINE + 2.upto(path.size-1) do |index| + value = nil if @deleted[path[0..index]] + break if !value + value = value[path[index]] + end + + result = if value.is_a?(Hash) + value.keys + elsif value + value + end + + if path.size == 3 + if path[2] == 'clients' + result << "#{path[1]}-validator" + if osc_compat + result << "#{path[1]}-webui" + end + elsif path[2] == 'users' + if osc_compat + result << 'admin' + else + result += @creators[path[0..1]] if @creators[path[0..1]] + end + end + end + + result + end + + def get_org_default(path) + if path[2] == 'acls' + get_org_acl_default(path) + + elsif path.size >= 4 + if !osc_compat && path[2] == 'users' + if @creators[path[0..1]] && @creators[path[0..1]].include?(path[3]) + return {} + end + end + + 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 = JSON.parse(data.get(path[0..1] + [ 'users', name ]), :create_additions => false) + user['admin'] + end + admins += data.list(path[0..1] + [ 'clients' ]).select do |name| + client = JSON.parse(data.get(path[0..1] + [ 'clients', name ]), :create_additions => false) + client['admin'] + end + admins += @creators[path[0..1]] if @creators[path[0..1]] + { 'actors' => admins.uniq } + + when "groups/billing-admins" + {} + + when "groups/clients" + { 'clients' => data.list(path[0..1] + [ 'clients' ]) } + + when "groups/users" + users = data.list(path[0..1] + [ 'users' ]) + users += @creators[path[0..1]] if @creators[path[0..1]] + { 'users' => users.uniq } + + when "org" + {} + + end + end + end + + def get_org_acl_default(path) + object_path = AclPath.get_object_path(path) + return nil if !data_exists?(object_path) + basic_acl = + case path[3..-1].join('/') + when 'root', 'containers/containers', 'containers/groups' + { + 'create' => { 'groups' => %w(admins) }, + 'read' => { 'groups' => %w(admins users) }, + 'update' => { 'groups' => %w(admins) }, + 'delete' => { 'groups' => %w(admins) }, + 'grant' => { 'groups' => %w(admins) }, + } + when 'containers/cookbooks', 'containers/environments', 'containers/roles' + { + 'create' => { 'groups' => %w(admins users) }, + 'read' => { 'groups' => %w(admins users clients) }, + 'update' => { 'groups' => %w(admins users) }, + 'delete' => { 'groups' => %w(admins users) }, + 'grant' => { 'groups' => %w(admins) }, + } + when 'containers/cookbooks', 'containers/data' + { + 'create' => { 'groups' => %w(admins users clients) }, + 'read' => { 'groups' => %w(admins users clients) }, + 'update' => { 'groups' => %w(admins users clients) }, + 'delete' => { 'groups' => %w(admins users clients) }, + 'grant' => { 'groups' => %w(admins) }, + } + when 'containers/nodes' + { + 'create' => { 'groups' => %w(admins users clients) }, + 'read' => { 'groups' => %w(admins users clients) }, + 'update' => { 'groups' => %w(admins users) }, + 'delete' => { 'groups' => %w(admins users) }, + 'grant' => { 'groups' => %w(admins) }, + } + when 'containers/clients' + { + 'create' => { 'groups' => %w(admins) }, + 'read' => { 'groups' => %w(admins users) }, + 'update' => { 'groups' => %w(admins) }, + 'delete' => { 'groups' => %w(admins users) }, + 'grant' => { 'groups' => %w(admins) }, + } + when 'containers/sandboxes' + { + 'create' => { 'groups' => %w(admins users) }, + 'read' => { 'groups' => %w(admins) }, + 'update' => { 'groups' => %w(admins) }, + 'delete' => { 'groups' => %w(admins) }, + 'grant' => { 'groups' => %w(admins) }, + } + when 'groups/admins', 'groups/clients', 'groups/users' + { + 'create' => { 'groups' => %w(admins) }, + 'read' => { 'groups' => %w(admins) }, + 'update' => { 'groups' => %w(admins) }, + 'delete' => { 'groups' => %w(admins) }, + 'grant' => { 'groups' => %w(admins) }, + } + when 'groups/billing-admins' + { + 'create' => { 'groups' => %w() }, + 'read' => { 'groups' => %w(billing-admins) }, + 'update' => { 'groups' => %w(billing-admins) }, + 'delete' => { 'groups' => %w() }, + 'grant' => { 'groups' => %w() }, + } + else + {} + end + + default_acl(path, basic_acl) + end + + def get_owners(acl_path) + owners = [] + + path = AclPath.get_object_path(acl_path) + if path + + # Add the actual owner + if @creators[path] + owners += @creators[path] + end + + # The objects that were created with the org itself have the peculiar + # property of missing superusers from their acl. + # if !exists?(path) + owners += superusers + # end + + # Clients need to be in their own acl list, except the validator created with the org + # (which we test for with exists?, which only looks at the defaults) + if path.size == 4 && path[0] == 'organizations' && path[2] == 'clients' && !exists?(path) + owners |= [ path[3] ] + end + + end + + owners.uniq + end + + def default_acl(acl_path, acl={}) + owners = nil + container_acl = nil + PERMISSIONS.each do |perm| + acl[perm] ||= {} + acl[perm]['actors'] ||= begin + owners ||= get_owners(acl_path) + container_acl ||= get_container_acl(acl_path) || {} + if container_acl[perm] && container_acl[perm]['actors'] + owners | container_acl[perm]['actors'] + else + owners + end + end + acl[perm]['groups'] ||= begin + # When we create containers, we don't merge groups (not sure why). + if acl_path[0] == 'organizations' && acl_path[3] == 'containers' + [] + else + container_acl ||= get_container_acl(request, acl_path) || {} + (container_acl[perm] ? container_acl[perm]['groups'] : []) || [] + end + end + end + acl + end + + def get_container_acl(acl_path) + parent_path = AclPath.parent_acl_data_path(acl_path) + if parent_path + JSON.parse(data.get(parent_path), :create_additions => false) + else + nil + end + end + + def data_exists?(path) + if is_dir?(path) + data.exists_dir?(path) + else + data.exists?(path) + end + end + + def is_dir?(path) + case path.size + when 0, 1 + return true + when 2 + return path[0] == 'organizations' || (path[0] == 'acls' && path[1] != 'root') + when 3 + # If it has a container, it is a directory. + return path[0] == 'organizations' && + (path[2] == 'acls' || data.exists?(path[0..1] + [ 'containers', path[2] ])) + when 4 + return path[0] == 'organizations' && ( + (path[2] == 'acls' && path[1] != 'root') || + %w(cookbooks data).include?(path[2])) + else + return false + end + end + end + end +end diff --git a/lib/chef_zero/data_normalizer.rb b/lib/chef_zero/data_normalizer.rb index 93ba370..89edfeb 100644 --- a/lib/chef_zero/data_normalizer.rb +++ b/lib/chef_zero/data_normalizer.rb @@ -1,13 +1,14 @@ require 'chef_zero' require 'chef_zero/rest_base' +require 'chef_zero/chef_data/default_creator' module ChefZero class DataNormalizer def self.normalize_acls(acls) - %w(create read update delete grant).each do |perm| + ChefData::DefaultCreator::PERMISSIONS.each do |perm| acls[perm] ||= {} acls[perm]['actors'] ||= [] - acls[perm]['groups'] ||= [ ] + acls[perm]['groups'] ||= [] end acls end diff --git a/lib/chef_zero/data_store/default_facade.rb b/lib/chef_zero/data_store/default_facade.rb index 74d1ba4..abc3f47 100644 --- a/lib/chef_zero/data_store/default_facade.rb +++ b/lib/chef_zero/data_store/default_facade.rb @@ -1,151 +1,77 @@ require 'chef_zero/data_store/interface_v2' +require 'chef_zero/chef_data/default_creator' module ChefZero module DataStore + # + # The DefaultFacade exists to layer defaults on top of an existing data + # store. When you create an org, you just create the directory itself: + # the rest of the org (such as environments/_default) will not actually + # exist anywhere, but when you get(/organizations/org/environments/_default), + # the DefaultFacade will create one for you on the fly. + # + # acls in particular are instantiated on the fly using this method. + # class DefaultFacade < ChefZero::DataStore::InterfaceV2 - def initialize(real_store, osc_compat, superusers = nil) + def initialize(real_store, single_org, osc_compat, superusers = nil) @real_store = real_store - @osc_compat = osc_compat - @superusers = superusers || (osc_compat ? [] : DefaultFacade::DEFAULT_SUPERUSERS) + @default_creator = ChefData::DefaultCreator.new(self, single_org, osc_compat, superusers) clear end attr_reader :real_store - attr_reader :osc_compat - attr_reader :superusers - - DEFAULT_SUPERUSERS = [ 'pivotal' ] - - def default(path, name=nil) - value = @defaults - for part in path - break if !value - value = value[part] - end - value = value[name] if value && name - if value.is_a?(Proc) - return value.call(self, path) - else - if value.nil? - # ACLs are a special case: defaults for them exist as long as the - # underlying object does - if (path[0] == 'acls' || (path[0] == 'organizations' && path[2] == 'acls')) && - target_object_exists?(path) - return '{}' - end - end - return value - end - end - - def target_object_exists?(acl_path) - if acl_path[0] == 'organizations' - org_path = acl_path[0..1] - object_part = acl_path[3..-1] - if object_part == [ 'organization' ] - exists_dir?(org_path) - else - path = org_path + object_part - if object_part.size == 2 && %w(cookbooks data).include?(object_part[0]) - exists_dir?(path) - else - exists?(path) - end - end - elsif acl_path[0] == 'acls' - exists?(acl_path[1..-1]) - end - end - - def delete_default(path) - value = @defaults - for part in path[0..-2] - break if !value - value = value[part] - end - if value - !!value.delete(path[-1]) - else - false - end - end + attr_reader :default_creator def clear real_store.clear if real_store.respond_to?(:clear) - @defaults = { - 'organizations' => {}, - 'acls' => {} - } - unless osc_compat - @defaults['users'] = {} - @defaults['superusers'] = {} - - superusers.each do |superuser| - @defaults['users'][superuser] = '{}' - @defaults['superusers'][superuser] = '{}' - end - end + default_creator.clear end def create_dir(path, name, *options) - if default(path, name) && !options.include?(:recursive) + 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(path) + if default_creator.exists?(path) real_store.create_dir(path, name, :recursive, *options) else raise end end - # If the org hasn't been created, create its defaults - if path.size > 0 && path[0] == 'organizations' - options_hash = options.last - requestor = options_hash.is_a?(Hash) ? options_hash[:requestor] : nil - if path.size == 1 - orgname = name - else - orgname = path[1] - end - @defaults['organizations'][orgname] ||= DefaultFacade.org_defaults(orgname, requestor, superusers, osc_compat) - end + options_hash = options.last.is_a?(Hash) ? options.last : {} + default_creator.created(path + [ name ], options_hash[:requestor]) end def create(path, name, data, *options) - if default(path, name) && !options.include?(:create_dir) + 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(path) + if default_creator.exists?(path) real_store.create(path, name, data, :create_dir, *options) else raise end end - # If the org hasn't been created, create its defaults - if path.size > 0 && path[0] == 'organizations' - options_hash = options.last - requestor = options_hash.is_a?(Hash) ? options_hash[:requestor] : nil - if path.size == 1 - @defaults['organizations'][name] ||= DefaultFacade.org_defaults(name, options[:requestor], superusers, osc_compat) - else - @defaults['organizations'][path[1]] ||= DefaultFacade.org_defaults(path[1], options[:requestor], suepruserse, osc_compat) - end - end + + options_hash = options.last || {} + default_creator.created(path + [ name ], options_hash[:requestor]) end def get(path, request=nil) begin real_store.get(path, request) rescue DataNotFoundError - result = default(path) + result = default_creator.get(path) if result - result + JSON.pretty_generate(result) else raise end @@ -156,7 +82,7 @@ module ChefZero begin real_store.set(path, data, *options) rescue DataNotFoundError - if default(path) + if default_creator.exists?(path) real_store.set(path, data, :create, :create_dir, *options) else raise @@ -165,20 +91,18 @@ module ChefZero end def delete(path) - deleted = delete_default(path) + deleted = default_creator.deleted(path) begin real_store.delete(path) rescue DataNotFoundError - if deleted - return - else + if !deleted raise end end end def delete_dir(path, *options) - deleted = delete_default(path) + deleted = default_creator.deleted(path) begin real_store.delete_dir(path, *options) rescue DataNotFoundError @@ -189,8 +113,7 @@ module ChefZero end def list(path) - default_results = default(path) - default_results = default_results.keys if default_results + default_results = default_creator.list(path) begin real_results = real_store.list(path) if default_results @@ -208,241 +131,11 @@ module ChefZero end def exists?(path) - real_store.exists?(path) || default(path) + real_store.exists?(path) || default_creator.exists?(path) end def exists_dir?(path) - real_store.exists_dir?(path) || default(path) - end - - def self.org_defaults(name, creator, superusers, osc_compat) - result = { - 'clients' => { - "#{name}-validator" => '{ "validator": true }' - }, - 'cookbooks' => {}, - 'data' => {}, - 'environments' => { - '_default' => '{ "description": "The default Chef environment" }' - }, - 'file_store' => { - 'checksums' => {} - }, - 'nodes' => {}, - 'roles' => {}, - 'sandboxes' => {}, - 'users' => {}, - - 'org' => '{}', - 'containers' => { - 'clients' => '{}', - 'containers' => '{}', - 'cookbooks' => '{}', - 'data' => '{}', - 'environments' => '{}', - 'groups' => '{}', - 'nodes' => '{}', - 'roles' => '{}', - 'sandboxes' => '{}' - }, - 'groups' => { - 'admins' => admins_group(creator), - 'billing-admins' => '{}', - 'clients' => clients_group, - 'users' => users_group(creator), - }, - 'acls' => { - 'clients' => {}, - 'containers' => { - 'cookbooks' => fill_acls(creator, { - :create => %w(admins users), - :read => %w(admins users clients), - :update => %w(admins users), - :delete => %w(admins users), - :grant => %w(admins) - }), - 'environments' => fill_acls(creator, { - :create => %w(admins users), - :read => %w(admins users clients), - :update => %w(admins users), - :delete => %w(admins users), - :grant => %w(admins) - }), - 'roles' => fill_acls(creator, { - :create => %w(admins users), - :read => %w(admins users clients), - :update => %w(admins users), - :delete => %w(admins users), - :grant => %w(admins) - }), - 'data' => fill_acls(creator, { - :create => %w(admins users clients), - :read => %w(admins users clients), - :update => %w(admins users clients), - :delete => %w(admins users clients), - :grant => %w(admins) - }), - 'nodes' => fill_acls(creator, { - :create => %w(admins users clients), - :read => %w(admins users clients), - :update => %w(admins users), - :delete => %w(admins users), - :grant => %w(admins) - }), - 'clients' => fill_acls(creator, { - :create => %w(admins), - :read => %w(admins users), - :update => %w(admins), - :delete => %w(admins users), - :grant => %w(admins) - }), - 'groups' => fill_acls(creator, { - :create => %w(admins), - :read => %w(admins users), - :update => %w(admins), - :delete => %w(admins), - :grant => %w(admins) - }), - 'containers' => fill_acls(creator, { - :create => %w(admins), - :read => %w(admins users), - :update => %w(admins), - :delete => %w(admins), - :grant => %w(admins) - }), - 'sandboxes' => fill_acls(creator, { - :create => %w(admins users), - :read => %w(admins), - :update => %w(admins), - :delete => %w(admins), - :grant => %w(admins) - }) - }, - 'cookbooks' => {}, - 'data' => {}, - 'environments' => {}, - 'groups' => { - # It's a little weird that the default acls for groups - # allows users to read, but these groups don't. - 'admins' => '{ "read": { "groups": [ "admins" ] } }', - 'clients' => '{ "read": { "groups": [ "admins" ] } }', - 'users' => '{ "read": { "groups": [ "admins" ] } }', - 'billing-admins' => '{ - "create": { "groups": [ ] }, - "read": { "groups": [ "billing-admins" ] }, - "update": { "groups": [ "billing-admins" ] }, - "delete": { "groups": [ ] }, - "grant": { "groups": [ ] } - }', - }, - 'nodes' => {}, - 'roles' => {}, - 'organization' => org_acls, - 'organizations' => fill_acls(creator, { - :create => %w(admins), - :read => %w(admins users), - :update => %w(admins), - :delete => %w(admins), - :grant => %w(admins) - }), - 'sandboxes' => {} - }, - 'association_requests' => {} - } - - if osc_compat - result['users']['admin'] = '{ "admin": "true" }' - result['clients']["#{name}-webui"] = '{ "admin": true }' - else - result['users'][creator] = '{}' - end - - result - end - - private - - def self.org_acls - proc do |data, path| - superusers = data.list([ 'superusers' ]) - acls = { - 'create' => { - 'actors' => superusers, - 'groups' => %w(admins) - }, - 'read' => { - 'actors' => superusers, - 'groups' => %w(admins users) - }, - 'update' => { - 'actors' => superusers, - 'groups' => %w(admins) - }, - 'delete' => { - 'actors' => superusers, - 'groups' => %w(admins) - }, - 'grant' => { - 'actors' => superusers, - 'groups' => %w(admins) - } - } - JSON.pretty_generate(acls) - end - end - - def self.fill_acls(creator, group_acls) - acls = { - 'create' => { - 'actors' => [ creator ], - 'groups' => group_acls[:create] - }, - 'read' => { - 'actors' => [ creator ], - 'groups' => group_acls[:read] - }, - 'update' => { - 'actors' => [ creator ], - 'groups' => group_acls[:update] - }, - 'delete' => { - 'actors' => [ creator ], - 'groups' => group_acls[:delete] - }, - 'grant' => { - 'actors' => [ creator ], - 'groups' => group_acls[:grant] - } - } - return JSON.pretty_generate(acls) - end - - def self.admins_group(creator) - proc do |data, path| - admins = data.list(path[0..1] + [ 'users' ]).select do |name| - user = JSON.parse(data.get(path[0..1] + [ 'users', name ]), :create_additions => false) - user['admin'] - end - admins += data.list(path[0..1] + [ 'clients' ]).select do |name| - client = JSON.parse(data.get(path[0..1] + [ 'clients', name ]), :create_additions => false) - client['admin'] - end - JSON.pretty_generate({ 'actors' => ([ creator ] + admins).uniq }) - end - end - - def self.clients_group - proc do |data, path| - clients = data.list(path[0..1] + [ 'clients' ]) - JSON.pretty_generate({ 'clients' => clients }) - end - end - - def self.users_group(creator) - proc do |data, path| - users = data.list(path[0..1] + [ 'users' ]) - JSON.pretty_generate({ 'users' => ([ creator ] + users).uniq }) - end + real_store.exists_dir?(path) || default_creator.exists?(path) end end end diff --git a/lib/chef_zero/data_store/memory_store.rb b/lib/chef_zero/data_store/memory_store.rb index 2c8251d..bfbe9d6 100644 --- a/lib/chef_zero/data_store/memory_store.rb +++ b/lib/chef_zero/data_store/memory_store.rb @@ -25,7 +25,7 @@ module ChefZero class MemoryStore < ChefZero::DataStore::V2ToV1Adapter def initialize super - @real_store = ChefZero::DataStore::DefaultFacade.new(ChefZero::DataStore::MemoryStoreV2.new, true) + @real_store = ChefZero::DataStore::DefaultFacade.new(ChefZero::DataStore::MemoryStoreV2.new, 'chef', true) clear end end diff --git a/lib/chef_zero/data_store/memory_store_v2.rb b/lib/chef_zero/data_store/memory_store_v2.rb index 864d192..9f8b80e 100644 --- a/lib/chef_zero/data_store/memory_store_v2.rb +++ b/lib/chef_zero/data_store/memory_store_v2.rb @@ -114,7 +114,7 @@ module ChefZero begin value = _get(path) if value.is_a?(Hash) && !options[:allow_dirs] - raise "exists? does not work with directories (#{path} = #{dir.class})" + raise "exists? does not work with directories (#{path} = #{value.class})" end return true rescue DataNotFoundError diff --git a/lib/chef_zero/endpoints/acl_base.rb b/lib/chef_zero/endpoints/acl_base.rb deleted file mode 100644 index ca85590..0000000 --- a/lib/chef_zero/endpoints/acl_base.rb +++ /dev/null @@ -1,82 +0,0 @@ -require 'json' -require 'chef_zero/rest_base' -require 'chef_zero/data_normalizer' -require 'chef_zero/data_store/default_facade' - -module ChefZero - module Endpoints - # Extended by AclEndpoint and AclsEndpoint - class AclBase < RestBase - def get_acls(request, path) - acls = get_data(request, acl_path(path)) - acls = JSON.parse(acls, :create_additions => false) - - owners = nil - container_acls = nil - %w(create read update delete grant).each do |perm| - acls[perm] ||= {} - acls[perm]['actors'] ||= begin - # owners = the superusers (and special case for clients owning themselves) - owners ||= get_owners(path) - container_acls ||= get_container_acls(request, path) - if container_acls - owners | container_acls[perm]['actors'] - else - owners - end - end - acls[perm]['groups'] ||= begin - # When we create containers, we don't merge groups (not sure why). - if path[0] == 'organizations' && path[2] == 'containers' - [] - else - container_acls ||= get_container_acls(request, path) - container_acls ? container_acls[perm]['groups'] : [] - end - end - end - acls - end - - private - - def get_owners(path) - # The objects that were created with the org itself, and containers for - # some reason, have the peculiar property of missing superusers from their acls. - if is_created_with_org?(path, false) || path[0] == 'organizations' && path[2] == 'containers' - owners = [] - else - owners = superusers - # Clients need to be in their own acl list - if path.size == 4 && path[0] == 'organizations' && path[2] == 'clients' - owners |= [ path[3] ] - end - end - owners - end - - def get_container_acls(request, path) - if path[0] == 'organizations' - if %w(clients cookbooks containers data environments groups nodes roles sandboxes).include?(path[2]) - return get_acls(request, path[0..1] + [ 'containers', path[2] ]) - end - end - return nil - end - - def superusers - data_store.list([ 'superusers' ]) - end - - def is_created_with_org?(path, osc_compat = false) - return false if path.size == 0 || path[0] != 'organizations' - value = DataStore::DefaultFacade.org_defaults(path[1], 'pivotal', [], osc_compat) - for part in path[2..-1] - break if !value - value = value[part] - end - return !!value - end - end - end -end diff --git a/lib/chef_zero/endpoints/acl_endpoint.rb b/lib/chef_zero/endpoints/acl_endpoint.rb index 13a5e6b..e0310c7 100644 --- a/lib/chef_zero/endpoints/acl_endpoint.rb +++ b/lib/chef_zero/endpoints/acl_endpoint.rb @@ -1,5 +1,6 @@ require 'json' -require 'chef_zero/endpoints/acl_base' +require 'chef_zero/rest_base' +require 'chef_zero/chef_data/acl_path' module ChefZero module Endpoints @@ -13,14 +14,16 @@ module ChefZero # /users/NAME/_acl/PERM # # Where PERM is create,read,update,delete,grant - class AclEndpoint < AclBase + class AclEndpoint < RestBase def validate_request(request) - path = acl_path(request.rest_path[0..-3]) # Strip off _acl/PERM + path = request.rest_path[0..-3] + 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) # Strip off _acl/PERM perm = request.rest_path[-1] - if !%w(read create update delete grant).include?(perm) + 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 - [path, perm] + [acl_path, perm] end def get(request) diff --git a/lib/chef_zero/endpoints/acls_endpoint.rb b/lib/chef_zero/endpoints/acls_endpoint.rb index 872ecce..41fb874 100644 --- a/lib/chef_zero/endpoints/acls_endpoint.rb +++ b/lib/chef_zero/endpoints/acls_endpoint.rb @@ -1,5 +1,7 @@ require 'json' -require 'chef_zero/endpoints/acl_base' +require 'chef_zero/rest_base' +require 'chef_zero/data_normalizer' +require 'chef_zero/chef_data/acl_path' module ChefZero module Endpoints @@ -10,11 +12,16 @@ module ChefZero # or # /organizations/ORG/organization/_acl # /users/NAME/_acl - class AclsEndpoint < AclBase + 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' && path[2] == 'organizations' - acls = DataNormalizer.normalize_acls(get_acls(request, path)) + path = path[0..1] if path.size == 3 && path[0] == 'organizations' && %w(organization organizations).include?(path[2]) + acl_path = ChefData::AclPath.get_acl_data_path(path) + if !acl_path + raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") + end + acls = JSON.parse(get_data(request, acl_path), :create_additions => false) + acls = DataNormalizer.normalize_acls(acls) json_response(200, acls) end diff --git a/lib/chef_zero/endpoints/cookbook_version_endpoint.rb b/lib/chef_zero/endpoints/cookbook_version_endpoint.rb index a3ac7b4..098c6c6 100644 --- a/lib/chef_zero/endpoints/cookbook_version_endpoint.rb +++ b/lib/chef_zero/endpoints/cookbook_version_endpoint.rb @@ -62,7 +62,6 @@ module ChefZero cookbook_path = request.rest_path[0..1] + ['cookbooks', cookbook_name] if exists_data_dir?(request, cookbook_path) && list_data(request, cookbook_path).size == 0 delete_data_dir(request, cookbook_path) - delete_acl(cookbook_path) end # Hoover deleted files, if they exist @@ -98,7 +97,7 @@ module ChefZero # This deals with an exception on delete, but things can still get deleted # that shouldn't be. begin - data_store.delete(request.rest_path[0..1] + ['file_store', 'checksums', checksum]) + delete_data(request, request.rest_path[0..1] + ['file_store', 'checksums', checksum], :data_store_exceptions) rescue ChefZero::DataStore::DataNotFoundError end end diff --git a/lib/chef_zero/endpoints/data_bag_endpoint.rb b/lib/chef_zero/endpoints/data_bag_endpoint.rb index ce7e263..41e87ae 100644 --- a/lib/chef_zero/endpoints/data_bag_endpoint.rb +++ b/lib/chef_zero/endpoints/data_bag_endpoint.rb @@ -34,7 +34,6 @@ module ChefZero def delete(request) key = request.rest_path[3] delete_data_dir(request, request.rest_path, :recursive) - delete_acl(request.rest_path) json_response(200, { 'chef_type' => 'data_bag', 'json_class' => 'Chef::DataBag', diff --git a/lib/chef_zero/endpoints/data_bags_endpoint.rb b/lib/chef_zero/endpoints/data_bags_endpoint.rb index c3e5970..732c1a5 100644 --- a/lib/chef_zero/endpoints/data_bags_endpoint.rb +++ b/lib/chef_zero/endpoints/data_bags_endpoint.rb @@ -14,7 +14,7 @@ module ChefZero elsif exists_data_dir?(request, request.rest_path[0..1] + ['data', name]) error(409, "Object already exists") else - data_store.create_dir(request.rest_path[0..1] + ['data'], name, :recursive) + create_data_dir(request, request.rest_path[0..1] + ['data'], name, :recursive) json_response(201, {"uri" => "#{build_uri(request.base_uri, request.rest_path + [name])}"}) end end diff --git a/lib/chef_zero/endpoints/rest_object_endpoint.rb b/lib/chef_zero/endpoints/rest_object_endpoint.rb index 5947d6d..fb821e0 100644 --- a/lib/chef_zero/endpoints/rest_object_endpoint.rb +++ b/lib/chef_zero/endpoints/rest_object_endpoint.rb @@ -28,7 +28,7 @@ module ChefZero rename = key != request.rest_path[-1] if rename begin - data_store.create(request.rest_path[0..1] + request.rest_path[2..-2], key, request.body) + create_data(request, request.rest_path[0..1] + request.rest_path[2..-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 @@ -43,7 +43,6 @@ module ChefZero def delete(request) result = get_data(request) delete_data(request) - delete_acl(request.rest_path) already_json_response(200, populate_defaults(request, result)) end diff --git a/lib/chef_zero/rest_base.rb b/lib/chef_zero/rest_base.rb index 5d44322..f64343b 100644 --- a/lib/chef_zero/rest_base.rb +++ b/lib/chef_zero/rest_base.rb @@ -1,6 +1,7 @@ require 'chef_zero/rest_request' require 'chef_zero/rest_error_response' require 'chef_zero/data_store/data_not_found_error' +require 'chef_zero/chef_data/acl_path' module ChefZero class RestBase @@ -52,27 +53,43 @@ module ChefZero 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) + def list_data(request, rest_path=nil, *options) rest_path ||= request.rest_path begin data_store.list(rest_path) rescue DataStore::DataNotFoundError - raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}") + 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) + def delete_data(request, rest_path=nil, *options) rest_path ||= request.rest_path begin - data_store.delete(rest_path) + data_store.delete(rest_path, *options) + rescue DataStore::DataNotFoundError + if options.include?(:data_store_exceptions) + raise + else + raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") + end + end + + begin + acl_path = ChefData::AclPath.get_acl_data_path(rest_path) + data_store.delete(acl_path) if acl_path rescue DataStore::DataNotFoundError - raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") end end @@ -81,7 +98,17 @@ module ChefZero begin data_store.delete_dir(rest_path, *options) rescue DataStore::DataNotFoundError - raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") + if options.include?(:data_store_exceptions) + raise + else + raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") + end + end + + begin + acl_path = ChefData::AclPath.get_acl_data_path(rest_path) + data_store.delete(acl_path) if acl_path + rescue DataStore::DataNotFoundError end end @@ -90,29 +117,49 @@ module ChefZero begin data_store.set(rest_path, data, *options) rescue DataStore::DataNotFoundError - raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") + if options.include?(:data_store_exceptions) + raise + else + raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") + end end end def create_data_dir(request, rest_path, name, *options) rest_path ||= request.rest_path begin - data_store.create_dir(rest_path, name, *options) + data_store.create_dir(rest_path, name, *options, :requestor => request.requestor) rescue DataStore::DataNotFoundError - raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, request.rest_path)}") + if options.include?(:data_store_exceptions) + raise + else + raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, request.rest_path)}") + end rescue DataStore::DataAlreadyExistsError - raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, request.rest_path + [name])}") + if options.include?(:data_store_exceptions) + raise + else + raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, request.rest_path + [name])}") + end end end def create_data(request, rest_path, name, data, *options) rest_path ||= request.rest_path begin - data_store.create(rest_path, name, data, *options) + data_store.create(rest_path, name, data, *options, :requestor => request.requestor) rescue DataStore::DataNotFoundError - raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, request.rest_path)}") + if options.include?(:data_store_exceptions) + raise + else + raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, request.rest_path)}") + end rescue DataStore::DataAlreadyExistsError - raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, request.rest_path + [name])}") + if options.include?(:data_store_exceptions) + raise + else + raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, request.rest_path + [name])}") + end end end @@ -158,33 +205,5 @@ module ChefZero def populate_defaults(request, response) response end - - def delete_acl(path) - # On delete we have to delete the corresponding acl - acl = acl_path(path) - if acl - begin - data_store.delete(acl) - rescue DataStore::DataNotFoundError - end - end - end - - def acl_path(path) - if path[0] == 'organizations' && path.size > 2 - if path.size == 4 - acl_path = path[0..1] + [ 'acls' ] + path[2..3] - elsif path.size == 3 && %w(organization organizations).include?(path[2]) - acl_path = path[0..1] + [ 'acls', path[2] ] - elsif path.size == 3 - acl_path = path[0..1] + [ 'acls', 'containers', path[2] ] - end - elsif path[0] == 'organizations' && path.size == 2 - acl_path = path + %w(acls organizations) - else - acl_path = [ 'acls' ] + path - end - acl_path - end end end diff --git a/lib/chef_zero/rest_error_response.rb b/lib/chef_zero/rest_error_response.rb index 2edca25..e75d427 100644 --- a/lib/chef_zero/rest_error_response.rb +++ b/lib/chef_zero/rest_error_response.rb @@ -1,5 +1,5 @@ module ChefZero - class RestErrorResponse < Exception + class RestErrorResponse < StandardError def initialize(response_code, error) @response_code = response_code @error = error diff --git a/lib/chef_zero/server.rb b/lib/chef_zero/server.rb index 03d196b..c952278 100644 --- a/lib/chef_zero/server.rb +++ b/lib/chef_zero/server.rb @@ -145,12 +145,12 @@ module ChefZero # def data_store @data_store ||= begin - result = @options[:data_store] || DataStore::DefaultFacade.new(DataStore::MemoryStoreV2.new, options[:osc_compat]) + 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[:osc_compat]) + result = ChefZero::DataStore::DefaultFacade.new(result, options[:single_org], options[:osc_compat]) end else @@ -393,9 +393,6 @@ module ChefZero def clear_data data_store.clear - if options[:single_org] - data_store.create_dir([ 'organizations' ], options[:single_org]) - end end def request_handler(&block) |