require 'rubygems' require_gem 'builder' base = File.dirname(__FILE__) require File.join(base, 'libxml_loader') require File.join(base, 's33r_exception') module S33r # S3 ACL handling. # # NB an individual ACL for an object can only contain <= 100 grants. module S3ACL # An S3 ACL document, incorporating one or more Grants # (see http://docs.amazonwebservices.com/AmazonS3/2006-03-01/UsingACL.html). # # Represents both retrieved ACL XML or can be built up # using objects and converted to XML. # NB the Policy is oblivious to the resource it is going # to be applied to. class Policy # List of grants to be applied. attr_accessor :grants, :owner # +owner+: S33r::S3ACL::CanonicalUser instance def initialize(owner, grants=[]) @grants = grants @owner = owner end # Create an Policy instance from a raw Access Control Policy XML document. # # +acl_xml+ is a raw Access Control Policy XML string (NOT libxml Document or Node). # # Returns nil if the ACL XML is nil. def self.from_xml(acl_xml) return nil if acl_xml.nil? acl_xml = S33r.remove_namespace(acl_xml) doc = XML.get_xml_doc(acl_xml) owner_xml = doc.find('//Owner').to_a.first owner = CanonicalUser.from_xml(owner_xml) grants = [] doc.find('//AccessControlList/Grant').to_a.each do |g| grantee_xml = g.find('Grantee').to_a.first grantee = Grantee.from_xml(grantee_xml) permission = g.xget('Permission') grants << Grant.new(grantee, permission) end Policy.new(owner, grants) end # Generate AccessControlPolicy XML document. def to_xml xml_str = "" xml = Builder::XmlMarkup.new(:target => xml_str, :indent => 0) xml.instruct! # Access control policy XML. xml.AccessControlPolicy({"xmlns" => RESPONSE_NAMESPACE_URI}) { xml.Owner { xml.ID owner.user_id xml.DisplayName owner.display_name } xml.AccessControlList { grants.each do |grant| xml << grant.to_xml end } } xml_str end # Add a grant to the ACL document. # # Returns true if grant was added; # false otherwise (grant already exists). def add_grant(grant) unless @grants.include?(grant) @grants << grant end self end # Remove a grant from the ACL document. Note that if you # set a grant for an AmazonCustomer, you want be able to remove it by # specifying the same grant. This is because grants set by AmazonCustomer # are converted at the S3 end into CanonicalUser grants - so you will need # to remove a CanonicalUser grant instead. See Grant.for_amazon_customer # for a few more details. # # Returns true if grant was removed; # false if it wasn't in the document. def remove_grant(grant) @grants.delete_if { |g| grant == g } self end # Does the ACL contain a grant for public reads? # (i.e. grants holds a Grant object for :all_users with :read permission) def public_readable? pr_grant = Grant.public_read_grant grants.each do |g| return true if pr_grant == g end return false end # Add a public READ permission to this instance. def add_public_read_grant add_grant(Grant.public_read_grant) end # Remove the public READ permission from this instance. def remove_public_read_grant remove_grant(Grant.public_read_grant) end # String representation of the policy. def to_s @grants.inject('') {|acc, grant| acc += "* " + grant.to_s + "\n"} end end # Representation of an S3 Grant # (see http://docs.amazonwebservices.com/AmazonS3/2006-03-01/UsingGrantees.html). # # A Grant consists of a Grantee and a permission they are to be # assigned. class Grant attr_accessor :grantee, :permission # permission: one of the keys in the PERMISSIONS hash or a raw permission string def initialize(grantee, permission) @grantee = grantee if permission.is_a? String @permission = permission else @permission = PERMISSIONS[permission] end raise S3Exception::InvalidPermission, \ "Permission #{permission.to_s} is not a valid permission specifier" if @permission.nil? end # Note that setting a grant for an Amazon customer is the # same as setting a grant for the CanonicalUser who owns the # specified email address. So when you get the ACL back, it will # actually contain a CanonicalUser grant. def Grant.for_amazon_customer(email_address, permission) Grant.new(AmazonCustomer.new(email_address), permission) end def Grant.for_canonical_user(id, display_name, permission) Grant.new(CanonicalUser.new(id, display_name), permission) end def Grant.for_group(group_type, permission) Grant.new(Group.new(group_type), permission) end # Generator for a Grant which gives READ permissions to the AllUsers # group type. def Grant.public_read_grant Grant.new(Group.new(:all_users), :read) end # Convert a Grant object into an XML fragment. def to_xml xml_str = "" xml = S33r::OrderlyXmlMarkup.new(:target => xml_str, :indent => 0) # element. xml.Grant { xml.Grantee({"xmlns:#{NAMESPACE}" => NAMESPACE_URI, "xsi:type" => @grantee.grantee_type}) { case @grantee.grantee_type when GRANTEE_TYPES[:amazon_customer] xml.EmailAddress @grantee.email_address when GRANTEE_TYPES[:canonical_user] xml.ID @grantee.user_id xml.DisplayName @grantee.display_name when GRANTEE_TYPES[:group] xml.URI GROUP_ACL_URI_BASE + @grantee.group_type end } xml.Permission @permission } xml_str end def ==(obj) if !obj.is_a?(Grant) return false end if obj.permission != self.permission or obj.grantee != self.grantee return false end return true end def to_s "#{@grantee.to_s} has permission #{@permission}" end end # Abstract representation of an S3 Grantee. class Grantee attr_reader :grantee_type def ==(obj) if !obj.is_a?(Grantee) return false end instance_variables.each do |var| method_name = var.gsub(/^@/, '') if self.send(method_name) != obj.send(method_name) return false end end return true end def method_missing(*args) nil end def self.from_xml(grantee_xml) grantee_type = grantee_xml['type'] case grantee_type when GRANTEE_TYPES[:amazon_customer] AmazonCustomer.new(grantee_xml.xget('EmailAddress')) when GRANTEE_TYPES[:canonical_user] CanonicalUser.from_xml(grantee_xml) when GRANTEE_TYPES[:group] uri = grantee_xml.xget('URI') # last part of the path is the group type path = uri.gsub(/#{GROUP_ACL_URI_BASE}/, '') group_type = :all_users S3_GROUP_TYPES.each { |k,v| group_type = k if v == path } Group.new(group_type) end end end # An Amazon customer for the purposes of assigning permissions. class AmazonCustomer < Grantee attr_accessor :email_address def initialize(email_address) @grantee_type = GRANTEE_TYPES[:amazon_customer] @email_address = email_address end def to_s "Amazon customer with address '#{@email_address}" end end # An S3 user. class CanonicalUser < Grantee attr_accessor :user_id, :display_name # +owner_xml_doc+: XML::Document or Node instance, representing an node from # inside a ListBucketResult element # (see http://docs.amazonwebservices.com/AmazonS3/2006-03-01/) # or an ACL element. def initialize(user_id, display_name) @grantee_type = GRANTEE_TYPES[:canonical_user] @user_id = user_id @display_name = display_name end # Create a user object from an XML fragment with # ID and DisplayName elements. (Both ACL documents and # bucket listings represent users this way.) def self.from_xml(user_xml_doc) user_id = user_xml_doc.xget('ID') display_name = user_xml_doc.xget('DisplayName') new(user_id, display_name) end def to_s "Canonical user '#{@display_name}' (with user ID '#{@user_id}')" end end # One of the predefined S3 groups. # # A group must have a type (AllUsers or AuthenticatedUsers). class Group < Grantee attr_accessor :group_type # The type of group. A key from S3_GROUP_TYPES to # one of the pre-defined Amazon group types. def initialize(group_type) unless S3_GROUP_TYPES.has_key?(group_type) raise S3Exception::InvalidGroupType, 'No such group type #{group_type}' end @group_type = S3_GROUP_TYPES[group_type] @grantee_type = GRANTEE_TYPES[:group] end def to_s "Group '#{@group_type}'" end end end end