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 ACLDoc is oblivious to the resource it is going # to be applied to. class ACLDoc # 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 ACLDoc 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 ACLDoc.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) if @grants.include?(grant) return false else @grants << grant return true end 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 } 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 # Does the ACL make the associated resource available as a log target? def log_targetable? log_target_grants = Grant.log_target_grants log_target_grants.each { |g| return false if !grants.include?(g) } return true end # Add permissions to an instances which give READ_ACL # and WRITE permissions to the LogDelivery group. Used # to enable a bucket as a logging destination. # # Returns true if grants added, false otherwise # (if already a log target). def add_log_target_grants if log_targetable? return false else Grant.log_target_grants.each { |g| add_grant(g) } return true end end # Remove log target ACLs from the document. # # Returns true if all log target grants were removed; # false otherwise. # # NB even if this method returns false, that doesn't mean # the bucket is still a log target. Use log_targetable? to check # whether a bucket can be used as a log target. def remove_log_target_grants ok = true Grant.log_target_grants.each { |g| ok = ok and remove_grant(g) } ok 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 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 # Generator for a grant which gives the LogDelivery group # write and read_acl permissions on a bucket. # # Returns an array with the two required Grant instances. def Grant.log_target_grants log_delivery_group = Group.new(:log_delivery) [Grant.new(log_delivery_group, :read_acl), Grant.new(log_delivery_group, :write)] 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 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 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 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 InvalidS3GroupType, 'No such group type #{group_type}' end @group_type = S3_GROUP_TYPES[group_type] @grantee_type = GRANTEE_TYPES[:group] end end end end