class Chef::Resource::ChefAcl

Public Instance Methods

acl_path(path) click to toggle source

Takes a normal path and finds the Chef path to get / set its ACL.

nodes/x -> nodes/x/_acl nodes -> containers/nodes/_acl '' -> organizations/_acl (the org acl) /organizations/foo -> /organizations/foo/organizations/_acl /users/foo -> /users/foo/_acl /organizations/foo/nodes/x -> /organizations/foo/nodes/x/_acl

# File lib/chef/resource/chef_acl.rb, line 333
def acl_path(path)
  parts = path.split("/").select { |x| x != "" }.to_a
  prefix = (path[0] == "/") ? "/" : ""

  case parts.size
  when 0
    # /, empty (relative root)
    # The root of the server has no publicly visible ACLs.  Only nodes/*, etc.
    if prefix == ""
      ::File.join("organizations", "_acl")
    end

  when 1
    # nodes, roles, etc.
    # The top level organizations and users containers have no publicly
    # visible ACLs.  Only nodes/*, etc.
    if prefix == ""
      ::File.join("containers", path, "_acl")
    end

  when 2
    # /organizations/NAME, /users/NAME, nodes/NAME, roles/NAME, etc.
    if prefix == "/" && parts[0] == "organizations"
      ::File.join(path, "organizations", "_acl")
    else
      ::File.join(path, "_acl")
    end

  when 3
    # /organizations/NAME/nodes, cookbooks/NAME/VERSION, etc.
    if prefix == "/"
      ::File.join("/", parts[0], parts[1], "containers", parts[2], "_acl")
    else
      ::File.join(parts[0], parts[1], "_acl")
    end

  when 4
    # /organizations/NAME/nodes/NAME, cookbooks/NAME/VERSION/BLAH
    # /organizations/NAME/nodes/NAME, cookbooks/NAME/VERSION, etc.
    if prefix == "/"
      ::File.join(path, "_acl")
    else
      ::File.join(parts[0], parts[1], "_acl")
    end

  else
    # /organizations/NAME/cookbooks/NAME/VERSION/..., cookbooks/NAME/VERSION/A/B/...
    if prefix == "/"
      ::File.join("/", parts[0], parts[1], parts[2], parts[3], "_acl")
    else
      ::File.join(parts[0], parts[1], "_acl")
    end
  end
end
add_rights(acl_path, json) click to toggle source
# File lib/chef/resource/chef_acl.rb, line 200
def add_rights(acl_path, json)
  if new_resource.rights
    new_resource.rights.each do |rights|
      if rights[:permissions].delete(:all)
        rights[:permissions] |= current_acl(acl_path).keys
      end

      Array(rights[:permissions]).each do |permission|
        ace = json[permission.to_s] ||= {}
        # WTF, no distinction between users and clients?  The Chef API doesn't
        # let us distinguish, so we have no choice :/  This means that:
        # 1. If you specify :users => 'foo', and client 'foo' exists, it will
        #    pick that (whether user 'foo' exists or not)
        # 2. If you specify :clients => 'foo', and user 'foo' exists but
        #    client 'foo' does not, it will pick user 'foo' and put it in the
        #    ACL
        # 3. If an existing item has user 'foo' on it and you specify :clients
        #    => 'foo' instead, idempotence will not notice that anything needs
        #    to be updated and nothing will happen.
        if rights[:users]
          ace["actors"] ||= []
          ace["actors"] |= Array(rights[:users])
        end
        if rights[:clients]
          ace["actors"] ||= []
          ace["actors"] |= Array(rights[:clients])
        end
        if rights[:groups]
          ace["groups"] ||= []
          ace["groups"] |= Array(rights[:groups])
        end
      end
    end
  end
end
create_acl(path) click to toggle source

Update the ACL if necessary.

# File lib/chef/resource/chef_acl.rb, line 82
def create_acl(path)
  changed = false
  # There may not be an ACL path for some valid paths (/ and /organizations,
  # for example).  We want to recurse into these, but we don't want to try to
  # update nonexistent ACLs for them.
  acl = acl_path(path)
  if acl
    # It's possible to make a custom container
    current_json = current_acl(acl)
    if current_json

      # Compare the desired and current json for the ACL, and update if different.
      modify = {}
      desired_acl(acl).each do |permission, desired_json|
        differences = json_differences(sort_values(current_json[permission]), sort_values(desired_json))

        if differences.size > 0
          # Verify we aren't trying to destroy grant permissions
          if permission == "grant" && desired_json["actors"] == [] && desired_json["groups"] == []
            # NOTE: if superusers exist, this should turn into a warning.
            raise "chef_acl attempted to remove all actors from GRANT!  I'm sorry Dave, I can't let you remove access to an object with no hope of recovery."
          end

          modify[differences] ||= {}
          modify[differences][permission] = desired_json
        end
      end

      if modify.size > 0
        changed = true
        description = [ "update acl #{path} at #{rest_url(path)}" ] + modify.flat_map do |diffs, permissions|
          diffs.map { |diff| "  #{permissions.keys.join(", ")}:#{diff}" }
        end
        converge_by description do
          modify.values.each do |permissions|
            permissions.each do |permission, desired_json|
              rest.put(rest_url("#{acl}/#{permission}"), { permission => desired_json })
            end
          end
        end
      end
    end
  end

  # If we have been asked to recurse, do so.
  # If recurse is on_change, then we will recurse if there is no ACL, or if
  # the ACL has changed.
  if new_resource.recursive == true || (new_resource.recursive == :on_change && (!acl || changed))
    children, _error = list(path, "*")
    children.parallel_each do |child|
      next if child.split("/")[-1] == "containers"

      create_acl(child)
    end
    # containers mess up our descent, so we do them last
    children.parallel_each do |child|
      next if child.split("/")[-1] != "containers"

      create_acl(child)
    end

  end
end
current_acl(acl_path) click to toggle source

Get the current ACL for the given path

# File lib/chef/resource/chef_acl.rb, line 147
def current_acl(acl_path)
  @current_acls ||= {}
  unless @current_acls.key?(acl_path)
    @current_acls[acl_path] = begin
      rest.get(rest_url(acl_path))
                              rescue Net::HTTPClientException => e
                                unless e.response.code == "404" && new_resource.path.split("/").any? { |p| p == "*" }
                                  raise
                                end
    end
  end
  @current_acls[acl_path]
end
desired_acl(acl_path) click to toggle source

Get the desired acl for the given acl path

# File lib/chef/resource/chef_acl.rb, line 162
def desired_acl(acl_path)
  result = new_resource.raw_json ? new_resource.raw_json.dup : {}

  # Calculate the JSON based on rights
  add_rights(acl_path, result)

  if new_resource.complete
    result = Chef::ChefFS::DataHandler::AclDataHandler.new.normalize(result, nil)
  else
    # If resource is incomplete, use current json to fill any holes
    current_acl(acl_path).each do |permission, perm_hash|
      if !result[permission]
        result[permission] = perm_hash.dup
      else
        result[permission] = result[permission].dup
        perm_hash.each do |type, actors|
          if !result[permission][type]
            result[permission][type] = actors
          else
            result[permission][type] = result[permission][type].dup
            result[permission][type] |= actors
          end
        end
      end
    end

    remove_rights(result)
  end
  result
end
list(path, child) click to toggle source

Lists the securable children under a path (the ones that either have ACLs or have children with ACLs).

list('nodes', 'x') -> [ 'nodes/x' ] list('nodes', '*') -> [ 'nodes/x', 'nodes/y', 'nodes/z' ] list('', '*') -> [ 'clients', 'environments', 'nodes', 'roles', … ] list('/', '*') -> [ '/organizations'] list('cookbooks', 'x') -> [ 'cookbooks/x' ] list('cookbooks/x', '*') -> [ ] # Individual cookbook versions do not have their own ACLs list('/organizations/foo/nodes', '*') -> [ '/organizations/foo/nodes/x', '/organizations/foo/nodes/y' ]

The list of children of an organization is == the list of containers. If new containers are added, the list of children will grow. This allows the system to extend to new types of objects and allow cheffish to work with them.

# File lib/chef/resource/chef_acl.rb, line 404
def list(path, child)
  # TODO make ChefFS understand top level organizations and stop doing this altogether.
  parts = path.split("/").select { |x| x != "" }.to_a
  absolute = (path[0] == "/")
  if absolute && parts[0] == "organizations"
    return [ [], "ACLs cannot be set on children of #{path}" ] if parts.size > 3
  else
    return [ [], "ACLs cannot be set on children of #{path}" ] if parts.size > 1
  end

  error = nil

  if child == "*"
    case parts.size
    when 0
      # /*, *
      if absolute
        results = [ "/organizations", "/users" ]
      else
        results, error = rest_list("containers")
      end

    when 1
      # /organizations/*, /users/*, roles/*, nodes/*, etc.
      results, error = rest_list(path)
      unless error
        results = results.map { |result| ::File.join(path, result) }
      end

    when 2
      # /organizations/NAME/*
      results, error = rest_list(::File.join(path, "containers"))
      unless error
        results = results.map { |result| ::File.join(path, result) }
      end

    when 3
      # /organizations/NAME/TYPE/*
      results, error = rest_list(path)
      unless error
        results = results.map { |result| ::File.join(path, result) }
      end
    end

  else
    if child == "data_bags" &&
        (parts.size == 0 || (parts.size == 2 && parts[0] == "organizations"))
      child = "data"
    end

    if absolute
      # /<child>, /users/<child>, /organizations/<child>, /organizations/foo/<child>, /organizations/foo/nodes/<child> ...
      results = [ ::File.join("/", parts[0..2], child) ]
    elsif parts.size == 0
      # <child> (nodes, roles, etc.)
      results = [ child ]
    else
      # nodes/<child>, roles/<child>, etc.
      results = [ ::File.join(parts[0], child) ]
    end
  end

  [ results, error ]
end
load_current_resource() click to toggle source
# File lib/chef/resource/chef_acl.rb, line 260
def load_current_resource; end
match_paths(path) click to toggle source

Matches chef_acl paths like nodes, nodes/*.

Examples

match_paths('nodes'): [ 'nodes' ] match_paths('nodes/*'): [ 'nodes/x', 'nodes/y', 'nodes/z' ] match_paths('*'): [ 'clients', 'environments', 'nodes', 'roles', … ] match_paths('/'): [ '/' ] match_paths(''): [ '' ] match_paths('/*'): [ '/organizations', '/users' ] match_paths('/organizations//'): [ '/organizations/foo/clients', '/organizations/foo/environments', …, '/organizations/bar/clients', '/organizations/bar/environments', … ]

# File lib/chef/resource/chef_acl.rb, line 274
def match_paths(path)
  # Turn multiple slashes into one
  # nodes//x -> nodes/x
  path = path.gsub(%r{[/]+}, "/")
  # If it's absolute, start the matching with /.  If it's relative, start with '' (relative root).
  if path[0] == "/"
    matches = [ "/" ]
  else
    matches = [ "" ]
  end

  # Split the path, and get rid of the empty path at the beginning and end
  # (/a/b/c/ -> [ 'a', 'b', 'c' ])
  parts = path.split("/").select { |x| x != "" }.to_a

  # Descend until we find the matches:
  # path = 'a/b/c'
  # parts = [ 'a', 'b', 'c' ]
  # Starting matches = [ '' ]
  parts.each_with_index do |part, index|
    # For each match, list <match>/<part> and set matches to that.
    #
    # Example: /*/foo
    # 1. To start,
    #    matches = [ '/' ], part = '*'.
    #    list('/', '*')                = [ '/organizations, '/users' ]
    # 2. matches = [ '/organizations', '/users' ], part = 'foo'
    #    list('/organizations', 'foo') = [ '/organizations/foo' ]
    #    list('/users', 'foo')         = [ '/users/foo' ]
    #
    # Result: /*/foo = [ '/organizations/foo', '/users/foo' ]
    #
    matches = matches.parallel_map do |pth|
      found, error = list(pth, part)
      if error
        if parts[0..index - 1].all? { |p| p != "*" }
          raise error
        end

        []
      else
        found
      end
    end.flatten.to_a
  end

  matches
end
remove_rights(*values) click to toggle source

remove_rights :read, :users => 'jkeiser', :groups => [ 'admins', 'users' ] remove_rights [ :create, :read ], :users => [ 'jkeiser', 'adam' ] remove_rights :all, :users => [ 'jkeiser', 'adam' ]

# File lib/chef/resource/chef_acl.rb, line 44
def remove_rights(*values)
  if values.size == 0
    @remove_rights
  else
    args = values.pop
    args[:permissions] ||= []
    values.each do |value|
      args[:permissions] |= Array(value)
    end
    @remove_rights ||= []
    @remove_rights << args
  end
end
rest_list(path) click to toggle source
# File lib/chef/resource/chef_acl.rb, line 473
def rest_list(path)
  # All our rest lists are hashes where the keys are the names
  [ rest.get(rest_url(path)).keys, nil ]
rescue Net::HTTPClientException => e
  if e.response.code == "405" || e.response.code == "404"
    parts = path.split("/").select { |p| p != "" }.to_a

    # We KNOW we expect these to exist.  Other containers may or may not.
    unless (parts.size == 1 || (parts.size == 3 && parts[0] == "organizations")) &&
        %w{clients containers cookbooks data environments groups nodes roles}.include?(parts[-1])
      return [ [], "Cannot get list of #{path}: HTTP response code #{e.response.code}" ]
    end
  end
  raise
end
rest_url(path) click to toggle source
# File lib/chef/resource/chef_acl.rb, line 469
def rest_url(path)
  path[0] == "/" ? URI.join(rest.url, path) : path
end
rights(*values) click to toggle source

rights :read, :users => 'jkeiser', :groups => [ 'admins', 'users' ] rights [ :create, :read ], :users => [ 'jkeiser', 'adam' ] rights :all, :users => 'jkeiser'

# File lib/chef/resource/chef_acl.rb, line 27
def rights(*values)
  if values.size == 0
    @rights
  else
    args = values.pop
    args[:permissions] ||= []
    values.each do |value|
      args[:permissions] |= Array(value)
    end
    @rights ||= []
    @rights << args
  end
end
sort_values(json) click to toggle source
# File lib/chef/resource/chef_acl.rb, line 193
def sort_values(json)
  json.each do |key, value|
    json[key] = value.sort if value.is_a?(Array)
  end
  json
end