module AWS module S3 # By default buckets are private. This means that only the owner has access rights to the bucket and its objects. # Objects in that bucket inherit the permission of the bucket unless otherwise specified. When an object is private, the owner can # generate a signed url that exposes the object to anyone who has that url. Alternatively, buckets and objects can be given other # access levels. Several canned access levels are defined: # # * :private - Owner gets FULL_CONTROL. No one else has any access rights. This is the default. # * :public_read - Owner gets FULL_CONTROL and the anonymous principal is granted READ access. If this policy is used on an object, it can be read from a browser with no authentication. # * :public_read_write - Owner gets FULL_CONTROL, the anonymous principal is granted READ and WRITE access. This is a useful policy to apply to a bucket, if you intend for any anonymous user to PUT objects into the bucket. # * :authenticated_read - Owner gets FULL_CONTROL, and any principal authenticated as a registered Amazon S3 user is granted READ access. # # You can set a canned access level when you create a bucket or an object by using the :access option: # # S3Object.store( # 'kiss.jpg', # data, # 'marcel', # :access => :public_read # ) # # Since the image we created is publicly readable, we can access it directly from a browser by going to the corresponding bucket name # and specifying the object's key without a special authenticated url: # # http://s3.amazonaws.com/marcel/kiss.jpg # # ==== Building custum access policies # # For both buckets and objects, you can use the acl method to see its access control policy: # # policy = S3Object.acl('kiss.jpg', 'marcel') # pp policy.grants # [#, # #] # # Policies are made up of one or more grants which grant a specific permission to some grantee. Here we see the default FULL_CONTROL grant # to the owner of this object. There is also READ permission granted to the Allusers Group, which means anyone has read access for the object. # # Say we wanted to grant access to anyone to read the access policy of this object. The current READ permission only grants them permission to read # the object itself (for example, from a browser) but it does not allow them to read the access policy. For that we will need to grant the AllUsers group the READ_ACP permission. # # First we'll create a new grant object: # # grant = ACL::Grant.new # # => # # grant.permission = 'READ_ACP' # # Now we need to indicate who this grant is for. In other words, who the grantee is: # # grantee = ACL::Grantee.new # # => # # # There are three ways to specify a grantee: 1) by their internal amazon id, such as the one returned with an object's Owner, # 2) by their Amazon account email address or 3) by specifying a group. As of this writing you can not create custom groups, but # Amazon does provide three already: AllUsers, Authenticated and LogDelivery. In this case we want to provide the grant to all users. # This effectively means "anyone". # # grantee.group = 'AllUsers' # # Now that our grantee is setup, we'll associate it with the grant: # # grant.grantee = grantee # grant # # => # # # Are grant has all the information we need. Now that it's ready, we'll add it on to the object's access control policy's list of grants: # # policy.grants << grant # pp policy.grants # [#, # #, # #] # # Now that the policy has the new grant, we reuse the acl method to persist the policy change: # # S3Object.acl('kiss.jpg', 'marcel', policy) # # If we fetch the object's policy again, we see that the grant has been added: # # pp S3Object.acl('kiss.jpg', 'marcel').grants # [#, # #, # #] # # If we were to access this object's acl url from a browser: # # http://s3.amazonaws.com/marcel/kiss.jpg?acl # # we would be shown its access control policy. # # ==== Pre-prepared grants # # Alternatively, the ACL::Grant class defines a set of stock grant policies that you can fetch by name. In most cases, you can # just use one of these pre-prepared grants rather than building grants by hand. Two of these stock policies are :public_read # and :public_read_acp, which happen to be the two grants that we built by hand above. In this case we could have simply written: # # policy.grants << ACL::Grant.grant(:public_read) # policy.grants << ACL::Grant.grant(:public_read_acp) # S3Object.acl('kiss.jpg', 'marcel', policy) # # The full details can be found in ACL::Policy, ACL::Grant and ACL::Grantee. module ACL # The ACL::Policy class lets you inspect and modify access controls for buckets and objects. # A policy is made up of one or more Grants which specify a permission and a Grantee to whom that permission is granted. # # Buckets and objects are given a default access policy which contains one grant permitting the owner of the bucket or object # FULL_CONTROL over its contents. This means they can read the object, write to the object, as well as read and write its # policy. # # The acl method for both buckets and objects returns the policy object for that entity: # # policy = Bucket.acl('some-bucket') # # The grants method of a policy exposes its grants. You can treat this collection as an array and push new grants onto it: # # policy.grants << grant # # Check the documentation for Grant and Grantee for more details on how to create new grants. class Policy include SelectiveAttributeProxy #:nodoc: attr_accessor :owner, :grants def initialize(attributes = {}) @attributes = attributes @grants = [].extend(GrantListExtensions) extract_owner! if owner? extract_grants! if grants? end # The xml representation of the policy. def to_xml Builder.new(owner, grants).to_s end private def owner? attributes.has_key?('owner') || !owner.nil? end def grants? (attributes.has_key?('access_control_list') && attributes['access_control_list']['grant']) || !grants.empty? end def extract_owner! @owner = Owner.new(attributes.delete('owner')) end def extract_grants! attributes['access_control_list']['grant'].each do |grant| grants << Grant.new(grant) end end module GrantListExtensions #:nodoc: def include?(grant) case grant when Symbol super(ACL::Grant.grant(grant)) else super end end def delete(grant) case grant when Symbol super(ACL::Grant.grant(grant)) else super end end # Two grant lists are equal if they have identical grants both in terms of permission and grantee. def ==(grants) size == grants.size && all? {|grant| grants.include?(grant)} end end class Builder < XmlGenerator #:nodoc: attr_reader :owner, :grants def initialize(owner, grants) @owner = owner @grants = grants.uniq # There could be some duplicate grants super() end def build xml.tag!('AccessControlPolicy', 'xmlns' => 'http://s3.amazonaws.com/doc/2006-03-01/') do xml.Owner do xml.ID owner.id xml.DisplayName owner.display_name end xml.AccessControlList do xml << grants.map {|grant| grant.to_xml}.join("\n") end end end end end # A Policy is made up of one or more Grant objects. A grant sets a specific permission and grants it to the associated grantee. # # When creating a new grant to add to a policy, you need only set its permission and then associate with a Grantee. # # grant = ACL::Grant.new # => # # # Here we see that neither the permission nor the grantee have been set. Let's make this grant provide the READ permission. # # grant.permission = 'READ' # grant # => # # # Now let's assume we have a grantee to the AllUsers group already set up. Just associate that grantee with our grant. # # grant.grantee = all_users_group_grantee # grant # => # # # And now are grant is complete. It provides READ permission to the AllUsers group, effectively making this object publicly readable # without any authorization. # # Assuming we have some object's policy available in a local variable called policy, we can now add this grant onto its # collection of grants. # # policy.grants << grant # # And then we send the updated policy to the S3 servers. # # some_s3object.acl(policy) class Grant include SelectiveAttributeProxy #:nodoc: constant :VALID_PERMISSIONS, %w(READ WRITE READ_ACP WRITE_ACP FULL_CONTROL) attr_accessor :grantee class << self # Returns stock grants with name type. # # public_read_grant = ACL::Grant.grant :public_read # => # # # Valid stock grant types are: # # * :authenticated_read # * :authenticated_read_acp # * :authenticated_write # * :authenticated_write_acp # * :logging_read # * :logging_read_acp # * :logging_write # * :logging_write_acp # * :public_read # * :public_read_acp # * :public_write # * :public_write_acp def grant(type) case type when *stock_grant_map.keys build_stock_grant_for type else raise ArgumentError, "Unknown grant type `#{type}'" end end private def stock_grant_map grant = lambda {|permission, group| {:permission => permission, :group => group}} groups = {:public => 'AllUsers', :authenticated => 'Authenticated', :logging => 'LogDelivery'} permissions = %w(READ WRITE READ_ACP WRITE_ACP) stock_grants = {} groups.each do |grant_group_name, group_name| permissions.each do |permission| stock_grants["#{grant_group_name}_#{permission.downcase}".to_sym] = grant[permission, group_name] end end stock_grants end memoized :stock_grant_map def build_stock_grant_for(type) stock_grant = stock_grant_map[type] grant = new do |g| g.permission = stock_grant[:permission] end grant.grantee = Grantee.new do |gr| gr.group = stock_grant[:group] end grant end end def initialize(attributes = {}) attributes = {'permission' => nil}.merge(attributes) @attributes = attributes extract_grantee! yield self if block_given? end # Set the permission for this grant. # # grant.permission = 'READ' # grant # => # # # If the specified permisison level is not valid, an InvalidAccessControlLevel exception will be raised. def permission=(permission_level) unless self.class.valid_permissions.include?(permission_level) raise InvalidAccessControlLevel.new(self.class.valid_permissions, permission_level) end attributes['permission'] = permission_level end # The xml representation of this grant. def to_xml Builder.new(permission, grantee).to_s end def inspect #:nodoc: "#<%s:0x%s %s>" % [self.class, object_id, self] end def to_s #:nodoc: [permission || '(permission)', 'to', grantee ? grantee.type_representation : '(grantee)'].join ' ' end def eql?(grant) #:nodoc: # This won't work for an unposted AmazonCustomerByEmail because of the normalization # to CanonicalUser but it will work for groups. to_s == grant.to_s end alias_method :==, :eql? def hash #:nodoc: to_s.hash end private def extract_grantee! @grantee = Grantee.new(attributes['grantee']) if attributes['grantee'] end class Builder < XmlGenerator #:nodoc: attr_reader :grantee, :permission def initialize(permission, grantee) @permission = permission @grantee = grantee super() end def build xml.Grant do xml << grantee.to_xml xml.Permission permission end end end end # Grants bestow a access permission to grantees. Each grant of some access control list Policy is associated with a grantee. # There are three ways of specifying a grantee at the time of this writing. # # * By canonical user - This format uses the id of a given Amazon account. The id value for a given account is available in the # Owner object of a bucket, object or policy. # # grantee.id = 'bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1' # # Often the id will just be fetched from some owner object. # # grantee.id = some_object.owner.id # # * By amazon email address - You can specify an email address for any Amazon account. The Amazon account need not be signed up with the S3 service. # though it must be unique across the entire Amazon system. This email address is normalized into a canonical user representation once the grant # has been sent back up to the S3 servers. # # grantee.email_address = 'joe@example.org' # # * By group - As of this writing you can not create custom groups, but Amazon provides three group that you can use. See the documentation for the # Grantee.group= method for details. # # grantee.group = 'Authenticated' class Grantee include SelectiveAttributeProxy #:nodoc: undef_method :id if method_defined?(:id) # Get rid of Object#id def initialize(attributes = {}) # Set default values for attributes that may not be passed in but we still want the object # to respond to attributes = {'id' => nil, 'display_name' => nil, 'email_address' => nil, 'uri' => nil}.merge(attributes) @attributes = attributes extract_type! yield self if block_given? end # The xml representation of the current grantee object. def to_xml Builder.new(self).to_s end # Returns the type of grantee. Will be one of CanonicalUser, AmazonCustomerByEmail or Group. def type return attributes['type'] if attributes['type'] # Lookups are in order of preference so if, for example, you set the uri but display_name and id are also # set, we'd rather go with the canonical representation. if display_name && id 'CanonicalUser' elsif email_address 'AmazonCustomerByEmail' elsif uri 'Group' end end # Sets the grantee's group by name. # # grantee.group = 'AllUsers' # # Currently, valid groups defined by S3 are: # # * AllUsers: This group represents anyone. In other words, an anonymous request. # * Authenticated: Any authenticated account on the S3 service. # * LogDelivery: The entity that delivers bucket access logs. def group=(group_name) section = %w(AllUsers Authenticated).include?(group_name) ? 'global' : 's3' self.uri = "http://acs.amazonaws.com/groups/#{section}/#{group_name}" end # Returns the grantee's group. If the grantee is not a group, nil is returned. def group return unless uri uri[%r([^/]+$)] end def type_representation #:nodoc: case type when 'CanonicalUser' then display_name || id when 'AmazonCustomerByEmail' then email_address when 'Group' then "#{group} Group" end end def inspect #:nodoc: "#<%s:0x%s %s>" % [self.class, object_id, type_representation || '(type not set yet)'] end private def extract_type! attributes['type'] = attributes.delete('xsi:type') end class Builder < XmlGenerator #:nodoc: def initialize(grantee) @grantee = grantee super() end def build xml.tag!('Grantee', attributes) do representation end end private attr_reader :grantee def representation case grantee.type when 'CanonicalUser' xml.ID grantee.id xml.DisplayName grantee.display_name when 'AmazonCustomerByEmail' xml.EmailAddress grantee.email_address when 'Group' xml.URI grantee.uri end end def attributes {'xsi:type' => grantee.type, 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'} end end end module Bucket def self.included(klass) #:nodoc: klass.extend(ClassMethods) end module ClassMethods # The acl method is the single point of entry for reading and writing access control list policies for a given bucket. # # # Fetch the acl for the 'marcel' bucket # policy = Bucket.acl 'marcel' # # # Modify the policy ... # # ... # # # Send updated policy back to the S3 servers # Bucket.acl 'marcel', policy def acl(name = nil, policy = nil) if name.is_a?(ACL::Policy) policy = name name = nil end path = path(name) << '?acl' respond_with ACL::Policy::Response do policy ? put(path, {}, policy.to_xml) : ACL::Policy.new(get(path(name) << '?acl').policy) end end end # The acl method returns and updates the acl for a given bucket. # # # Fetch a bucket # bucket = Bucket.find 'marcel' # # # Add a grant to the bucket's policy # bucket.acl.grants << some_grant # # # Write the changes to the policy # bucket.acl(bucket.acl) def acl(reload = false) policy = reload.is_a?(ACL::Policy) ? reload : nil memoize(reload) do self.class.acl(name, policy) if policy self.class.acl(name) end end end module S3Object def self.included(klass) #:nodoc: klass.extend(ClassMethods) end module ClassMethods # The acl method is the single point of entry for reading and writing access control list policies for a given object. # # # Fetch the acl for the 'kiss.jpg' object in the 'marcel' bucket # policy = S3Object.acl 'kiss.jpg', 'marcel' # # # Modify the policy ... # # ... # # # Send updated policy back to the S3 servers # S3Object.acl 'kiss.jpg', 'marcel', policy def acl(name, bucket = nil, policy = nil) # We're using the second argument as the ACL::Policy if bucket.is_a?(ACL::Policy) policy = bucket bucket = nil end bucket = bucket_name(bucket) path = path!(bucket, name) << '?acl' respond_with ACL::Policy::Response do policy ? put(path, {}, policy.to_xml) : ACL::Policy.new(get(path).policy) end end end # The acl method returns and updates the acl for a given s3 object. # # # Fetch a the object # object = S3Object.find 'kiss.jpg', 'marcel' # # # Add a grant to the object's # object.acl.grants << some_grant # # # Write the changes to the policy # object.acl(object.acl) def acl(reload = false) policy = reload.is_a?(ACL::Policy) ? reload : nil memoize(reload) do self.class.acl(key, bucket.name, policy) if policy self.class.acl(key, bucket.name) end end end class OptionProcessor #:nodoc: attr_reader :options class << self def process!(options) new(options).process! end end def initialize(options) options.to_normalized_options! @options = options @access_level = extract_access_level end def process! return unless access_level_specified? validate! options['x-amz-acl'] = access_level end private def extract_access_level options.delete('access') || options.delete('x-amz-acl') end def validate! raise InvalidAccessControlLevel.new(valid_levels, access_level) unless valid? end def valid? valid_levels.include?(access_level) end def access_level_specified? !@access_level.nil? end def valid_levels %w(private public-read public-read-write authenticated-read) end def access_level @normalized_access_level ||= @access_level.to_header end end end end end