stevequinlan-aws-s3 0.6.3
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +19 -0
- data/INSTALL +55 -0
- data/README.rdoc +545 -0
- data/Rakefile +120 -0
- data/bin/s3sh +6 -0
- data/bin/setup.rb +10 -0
- data/lib/aws/s3.rb +60 -0
- data/lib/aws/s3/acl.rb +636 -0
- data/lib/aws/s3/authentication.rb +221 -0
- data/lib/aws/s3/base.rb +240 -0
- data/lib/aws/s3/bittorrent.rb +58 -0
- data/lib/aws/s3/bucket.rb +319 -0
- data/lib/aws/s3/connection.rb +287 -0
- data/lib/aws/s3/error.rb +69 -0
- data/lib/aws/s3/exceptions.rb +133 -0
- data/lib/aws/s3/extensions.rb +356 -0
- data/lib/aws/s3/logging.rb +314 -0
- data/lib/aws/s3/object.rb +612 -0
- data/lib/aws/s3/owner.rb +44 -0
- data/lib/aws/s3/parsing.rb +99 -0
- data/lib/aws/s3/response.rb +180 -0
- data/lib/aws/s3/service.rb +51 -0
- data/lib/aws/s3/version.rb +12 -0
- data/support/faster-xml-simple/lib/faster_xml_simple.rb +187 -0
- data/support/faster-xml-simple/test/regression_test.rb +47 -0
- data/support/faster-xml-simple/test/test_helper.rb +17 -0
- data/support/faster-xml-simple/test/xml_simple_comparison_test.rb +46 -0
- data/support/rdoc/code_info.rb +211 -0
- data/test/acl_test.rb +254 -0
- data/test/authentication_test.rb +114 -0
- data/test/base_test.rb +136 -0
- data/test/bucket_test.rb +74 -0
- data/test/connection_test.rb +215 -0
- data/test/error_test.rb +70 -0
- data/test/extensions_test.rb +341 -0
- data/test/fixtures.rb +89 -0
- data/test/fixtures/buckets.yml +133 -0
- data/test/fixtures/errors.yml +34 -0
- data/test/fixtures/headers.yml +3 -0
- data/test/fixtures/logging.yml +15 -0
- data/test/fixtures/loglines.yml +5 -0
- data/test/fixtures/logs.yml +7 -0
- data/test/fixtures/policies.yml +16 -0
- data/test/logging_test.rb +89 -0
- data/test/mocks/fake_response.rb +26 -0
- data/test/object_test.rb +205 -0
- data/test/parsing_test.rb +66 -0
- data/test/remote/acl_test.rb +117 -0
- data/test/remote/bittorrent_test.rb +45 -0
- data/test/remote/bucket_test.rb +146 -0
- data/test/remote/logging_test.rb +82 -0
- data/test/remote/object_test.rb +371 -0
- data/test/remote/test_file.data +0 -0
- data/test/remote/test_helper.rb +33 -0
- data/test/response_test.rb +68 -0
- data/test/service_test.rb +23 -0
- data/test/test_helper.rb +110 -0
- metadata +159 -0
data/Rakefile
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'rake/packagetask'
|
6
|
+
require 'rake/gempackagetask'
|
7
|
+
|
8
|
+
require File.dirname(__FILE__) + '/lib/aws/s3'
|
9
|
+
|
10
|
+
def library_root
|
11
|
+
File.dirname(__FILE__)
|
12
|
+
end
|
13
|
+
|
14
|
+
task :default => :test
|
15
|
+
|
16
|
+
Rake::TestTask.new do |test|
|
17
|
+
test.pattern = 'test/*_test.rb'
|
18
|
+
test.verbose = true
|
19
|
+
end
|
20
|
+
|
21
|
+
namespace :doc do
|
22
|
+
Rake::RDocTask.new do |rdoc|
|
23
|
+
rdoc.rdoc_dir = 'doc'
|
24
|
+
rdoc.title = "AWS::S3 -- Support for Amazon S3's REST api"
|
25
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
26
|
+
rdoc.rdoc_files.include('README')
|
27
|
+
rdoc.rdoc_files.include('COPYING')
|
28
|
+
rdoc.rdoc_files.include('INSTALL')
|
29
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
30
|
+
end
|
31
|
+
|
32
|
+
task :rdoc => 'doc:readme'
|
33
|
+
|
34
|
+
task :refresh => :rerdoc do
|
35
|
+
system 'open doc/index.html'
|
36
|
+
end
|
37
|
+
|
38
|
+
task :readme do
|
39
|
+
require 'support/rdoc/code_info'
|
40
|
+
RDoc::CodeInfo.parse('lib/**/*.rb')
|
41
|
+
|
42
|
+
strip_comments = lambda {|comment| comment.gsub(/^# ?/, '')}
|
43
|
+
docs_for = lambda do |location|
|
44
|
+
info = RDoc::CodeInfo.for(location)
|
45
|
+
raise RuntimeError, "Couldn't find documentation for `#{location}'" unless info
|
46
|
+
strip_comments[info.comment]
|
47
|
+
end
|
48
|
+
|
49
|
+
open('README.rdoc', 'w') do |file|
|
50
|
+
file.write ERB.new(IO.read('README.erb')).result(binding)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
task :deploy => :rerdoc do
|
55
|
+
sh %(scp -r doc marcel@rubyforge.org:/var/www/gforge-projects/amazon/)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
namespace :test do
|
60
|
+
find_file = lambda do |name|
|
61
|
+
file_name = lambda {|path| File.join(path, "#{name}.rb")}
|
62
|
+
root = $:.detect do |path|
|
63
|
+
File.exist?(file_name[path])
|
64
|
+
end
|
65
|
+
file_name[root] if root
|
66
|
+
end
|
67
|
+
|
68
|
+
TEST_LOADER = find_file['rake/rake_test_loader']
|
69
|
+
multiruby = lambda do |glob|
|
70
|
+
system 'multiruby', TEST_LOADER, *Dir.glob(glob)
|
71
|
+
end
|
72
|
+
|
73
|
+
desc 'Check test coverage'
|
74
|
+
task :coverage do
|
75
|
+
system("rcov -x Library -x support --sort coverage #{File.join(library_root, 'test/*_test.rb')}")
|
76
|
+
show_test_coverage_results
|
77
|
+
end
|
78
|
+
|
79
|
+
Rake::TestTask.new(:remote) do |test|
|
80
|
+
test.pattern = 'test/remote/*_test.rb'
|
81
|
+
test.verbose = true
|
82
|
+
end
|
83
|
+
|
84
|
+
Rake::TestTask.new(:all) do |test|
|
85
|
+
test.pattern = 'test/**/*_test.rb'
|
86
|
+
test.verbose = true
|
87
|
+
end
|
88
|
+
|
89
|
+
desc 'Check test coverage of full stack remote tests'
|
90
|
+
task :full_coverage do
|
91
|
+
system("rcov -x Library -x support --sort coverage #{File.join(library_root, 'test/remote/*_test.rb')} #{File.join(library_root, 'test/*_test.rb')}")
|
92
|
+
show_test_coverage_results
|
93
|
+
end
|
94
|
+
|
95
|
+
desc 'Run local tests against multiple versions of Ruby'
|
96
|
+
task :version_audit do
|
97
|
+
multiruby['test/*_test.rb']
|
98
|
+
end
|
99
|
+
|
100
|
+
namespace :version_audit do
|
101
|
+
desc 'Run remote tests against multiple versions of Ruby'
|
102
|
+
task :remote do
|
103
|
+
multiruby['test/remote/*_test.rb']
|
104
|
+
end
|
105
|
+
|
106
|
+
desc 'Run all tests against multiple versions of Ruby'
|
107
|
+
task :all do
|
108
|
+
multiruby['test/**/*_test.rb']
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def show_test_coverage_results
|
113
|
+
system("open #{File.join(library_root, 'coverage/index.html')}") if PLATFORM['darwin']
|
114
|
+
end
|
115
|
+
|
116
|
+
desc 'Remove coverage products'
|
117
|
+
task :clobber_coverage do
|
118
|
+
rm_r 'coverage' rescue nil
|
119
|
+
end
|
120
|
+
end
|
data/bin/s3sh
ADDED
data/bin/setup.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
if ENV['AMAZON_ACCESS_KEY_ID'] && ENV['AMAZON_SECRET_ACCESS_KEY']
|
3
|
+
AWS::S3::Base.establish_connection!(
|
4
|
+
:access_key_id => ENV['AMAZON_ACCESS_KEY_ID'],
|
5
|
+
:secret_access_key => ENV['AMAZON_SECRET_ACCESS_KEY']
|
6
|
+
)
|
7
|
+
end
|
8
|
+
|
9
|
+
require File.dirname(__FILE__) + '/../test/fixtures'
|
10
|
+
include AWS::S3
|
data/lib/aws/s3.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'uri'
|
3
|
+
require 'openssl'
|
4
|
+
require 'digest/sha1'
|
5
|
+
require 'net/https'
|
6
|
+
require 'time'
|
7
|
+
require 'date'
|
8
|
+
require 'open-uri'
|
9
|
+
|
10
|
+
$:.unshift(File.dirname(__FILE__))
|
11
|
+
require 's3/extensions'
|
12
|
+
require_library_or_gem 'builder' unless defined? Builder
|
13
|
+
require_library_or_gem 'mime/types', 'mime-types' unless defined? MIME::Types
|
14
|
+
|
15
|
+
require 's3/base'
|
16
|
+
require 's3/version'
|
17
|
+
require 's3/parsing'
|
18
|
+
require 's3/acl'
|
19
|
+
require 's3/logging'
|
20
|
+
require 's3/bittorrent'
|
21
|
+
require 's3/service'
|
22
|
+
require 's3/owner'
|
23
|
+
require 's3/bucket'
|
24
|
+
require 's3/object'
|
25
|
+
require 's3/error'
|
26
|
+
require 's3/exceptions'
|
27
|
+
require 's3/connection'
|
28
|
+
require 's3/authentication'
|
29
|
+
require 's3/response'
|
30
|
+
|
31
|
+
AWS::S3::Base.class_eval do
|
32
|
+
include AWS::S3::Connection::Management
|
33
|
+
end
|
34
|
+
|
35
|
+
AWS::S3::Bucket.class_eval do
|
36
|
+
include AWS::S3::Logging::Management
|
37
|
+
include AWS::S3::ACL::Bucket
|
38
|
+
end
|
39
|
+
|
40
|
+
AWS::S3::S3Object.class_eval do
|
41
|
+
include AWS::S3::ACL::S3Object
|
42
|
+
include AWS::S3::BitTorrent
|
43
|
+
end
|
44
|
+
|
45
|
+
require_library_or_gem 'xmlsimple', 'xml-simple' unless defined? XmlSimple
|
46
|
+
# If libxml is installed, we use the FasterXmlSimple library, that provides most of the functionality of XmlSimple
|
47
|
+
# except it uses the xml/libxml library for xml parsing (rather than REXML). If libxml isn't installed, we just fall back on
|
48
|
+
# XmlSimple.
|
49
|
+
AWS::S3::Parsing.parser =
|
50
|
+
begin
|
51
|
+
require_library_or_gem 'xml/libxml'
|
52
|
+
# Older version of libxml aren't stable (bus error when requesting attributes that don't exist) so we
|
53
|
+
# have to use a version greater than '0.3.8.2'.
|
54
|
+
raise LoadError unless XML::Parser::VERSION > '0.3.8.2'
|
55
|
+
$:.push(File.join(File.dirname(__FILE__), '..', '..', 'support', 'faster-xml-simple', 'lib'))
|
56
|
+
require_library_or_gem 'faster_xml_simple'
|
57
|
+
FasterXmlSimple
|
58
|
+
rescue LoadError
|
59
|
+
XmlSimple
|
60
|
+
end
|
data/lib/aws/s3/acl.rb
ADDED
@@ -0,0 +1,636 @@
|
|
1
|
+
module AWS
|
2
|
+
module S3
|
3
|
+
# By default buckets are private. This means that only the owner has access rights to the bucket and its objects.
|
4
|
+
# Objects in that bucket inherit the permission of the bucket unless otherwise specified. When an object is private, the owner can
|
5
|
+
# generate a signed url that exposes the object to anyone who has that url. Alternatively, buckets and objects can be given other
|
6
|
+
# access levels. Several canned access levels are defined:
|
7
|
+
#
|
8
|
+
# * <tt>:private</tt> - Owner gets FULL_CONTROL. No one else has any access rights. This is the default.
|
9
|
+
# * <tt>:public_read</tt> - 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.
|
10
|
+
# * <tt>:public_read_write</tt> - 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.
|
11
|
+
# * <tt>:authenticated_read</tt> - Owner gets FULL_CONTROL, and any principal authenticated as a registered Amazon S3 user is granted READ access.
|
12
|
+
#
|
13
|
+
# You can set a canned access level when you create a bucket or an object by using the <tt>:access</tt> option:
|
14
|
+
#
|
15
|
+
# S3Object.store(
|
16
|
+
# 'kiss.jpg',
|
17
|
+
# data,
|
18
|
+
# 'marcel',
|
19
|
+
# :access => :public_read
|
20
|
+
# )
|
21
|
+
#
|
22
|
+
# Since the image we created is publicly readable, we can access it directly from a browser by going to the corresponding bucket name
|
23
|
+
# and specifying the object's key without a special authenticated url:
|
24
|
+
#
|
25
|
+
# http://s3.amazonaws.com/marcel/kiss.jpg
|
26
|
+
#
|
27
|
+
# ==== Building custum access policies
|
28
|
+
#
|
29
|
+
# For both buckets and objects, you can use the <tt>acl</tt> method to see its access control policy:
|
30
|
+
#
|
31
|
+
# policy = S3Object.acl('kiss.jpg', 'marcel')
|
32
|
+
# pp policy.grants
|
33
|
+
# [#<AWS::S3::ACL::Grant FULL_CONTROL to noradio>,
|
34
|
+
# #<AWS::S3::ACL::Grant READ to AllUsers Group>]
|
35
|
+
#
|
36
|
+
# 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
|
37
|
+
# 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.
|
38
|
+
#
|
39
|
+
# 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
|
40
|
+
# 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.
|
41
|
+
#
|
42
|
+
# First we'll create a new grant object:
|
43
|
+
#
|
44
|
+
# grant = ACL::Grant.new
|
45
|
+
# # => #<AWS::S3::ACL::Grant (permission) to (grantee)>
|
46
|
+
# grant.permission = 'READ_ACP'
|
47
|
+
#
|
48
|
+
# Now we need to indicate who this grant is for. In other words, who the grantee is:
|
49
|
+
#
|
50
|
+
# grantee = ACL::Grantee.new
|
51
|
+
# # => #<AWS::S3::ACL::Grantee (xsi not set yet)>
|
52
|
+
#
|
53
|
+
# There are three ways to specify a grantee: 1) by their internal amazon id, such as the one returned with an object's Owner,
|
54
|
+
# 2) by their Amazon account email address or 3) by specifying a group. As of this writing you can not create custom groups, but
|
55
|
+
# Amazon does provide three already: AllUsers, Authenticated and LogDelivery. In this case we want to provide the grant to all users.
|
56
|
+
# This effectively means "anyone".
|
57
|
+
#
|
58
|
+
# grantee.group = 'AllUsers'
|
59
|
+
#
|
60
|
+
# Now that our grantee is setup, we'll associate it with the grant:
|
61
|
+
#
|
62
|
+
# grant.grantee = grantee
|
63
|
+
# grant
|
64
|
+
# # => #<AWS::S3::ACL::Grant READ_ACP to AllUsers Group>
|
65
|
+
#
|
66
|
+
# 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:
|
67
|
+
#
|
68
|
+
# policy.grants << grant
|
69
|
+
# pp policy.grants
|
70
|
+
# [#<AWS::S3::ACL::Grant FULL_CONTROL to noradio>,
|
71
|
+
# #<AWS::S3::ACL::Grant READ to AllUsers Group>,
|
72
|
+
# #<AWS::S3::ACL::Grant READ_ACP to AllUsers Group>]
|
73
|
+
#
|
74
|
+
# Now that the policy has the new grant, we reuse the <tt>acl</tt> method to persist the policy change:
|
75
|
+
#
|
76
|
+
# S3Object.acl('kiss.jpg', 'marcel', policy)
|
77
|
+
#
|
78
|
+
# If we fetch the object's policy again, we see that the grant has been added:
|
79
|
+
#
|
80
|
+
# pp S3Object.acl('kiss.jpg', 'marcel').grants
|
81
|
+
# [#<AWS::S3::ACL::Grant FULL_CONTROL to noradio>,
|
82
|
+
# #<AWS::S3::ACL::Grant READ to AllUsers Group>,
|
83
|
+
# #<AWS::S3::ACL::Grant READ_ACP to AllUsers Group>]
|
84
|
+
#
|
85
|
+
# If we were to access this object's acl url from a browser:
|
86
|
+
#
|
87
|
+
# http://s3.amazonaws.com/marcel/kiss.jpg?acl
|
88
|
+
#
|
89
|
+
# we would be shown its access control policy.
|
90
|
+
#
|
91
|
+
# ==== Pre-prepared grants
|
92
|
+
#
|
93
|
+
# Alternatively, the ACL::Grant class defines a set of stock grant policies that you can fetch by name. In most cases, you can
|
94
|
+
# just use one of these pre-prepared grants rather than building grants by hand. Two of these stock policies are <tt>:public_read</tt>
|
95
|
+
# and <tt>:public_read_acp</tt>, which happen to be the two grants that we built by hand above. In this case we could have simply written:
|
96
|
+
#
|
97
|
+
# policy.grants << ACL::Grant.grant(:public_read)
|
98
|
+
# policy.grants << ACL::Grant.grant(:public_read_acp)
|
99
|
+
# S3Object.acl('kiss.jpg', 'marcel', policy)
|
100
|
+
#
|
101
|
+
# The full details can be found in ACL::Policy, ACL::Grant and ACL::Grantee.
|
102
|
+
module ACL
|
103
|
+
# The ACL::Policy class lets you inspect and modify access controls for buckets and objects.
|
104
|
+
# A policy is made up of one or more Grants which specify a permission and a Grantee to whom that permission is granted.
|
105
|
+
#
|
106
|
+
# Buckets and objects are given a default access policy which contains one grant permitting the owner of the bucket or object
|
107
|
+
# FULL_CONTROL over its contents. This means they can read the object, write to the object, as well as read and write its
|
108
|
+
# policy.
|
109
|
+
#
|
110
|
+
# The <tt>acl</tt> method for both buckets and objects returns the policy object for that entity:
|
111
|
+
#
|
112
|
+
# policy = Bucket.acl('some-bucket')
|
113
|
+
#
|
114
|
+
# The <tt>grants</tt> method of a policy exposes its grants. You can treat this collection as an array and push new grants onto it:
|
115
|
+
#
|
116
|
+
# policy.grants << grant
|
117
|
+
#
|
118
|
+
# Check the documentation for Grant and Grantee for more details on how to create new grants.
|
119
|
+
class Policy
|
120
|
+
include SelectiveAttributeProxy #:nodoc:
|
121
|
+
attr_accessor :owner, :grants
|
122
|
+
|
123
|
+
def initialize(attributes = {})
|
124
|
+
@attributes = attributes
|
125
|
+
@grants = [].extend(GrantListExtensions)
|
126
|
+
extract_owner! if owner?
|
127
|
+
extract_grants! if grants?
|
128
|
+
end
|
129
|
+
|
130
|
+
# The xml representation of the policy.
|
131
|
+
def to_xml
|
132
|
+
Builder.new(owner, grants).to_s
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def owner?
|
138
|
+
attributes.has_key?('owner') || !owner.nil?
|
139
|
+
end
|
140
|
+
|
141
|
+
def grants?
|
142
|
+
(attributes.has_key?('access_control_list') && attributes['access_control_list']['grant']) || !grants.empty?
|
143
|
+
end
|
144
|
+
|
145
|
+
def extract_owner!
|
146
|
+
@owner = Owner.new(attributes.delete('owner'))
|
147
|
+
end
|
148
|
+
|
149
|
+
def extract_grants!
|
150
|
+
attributes['access_control_list']['grant'].each do |grant|
|
151
|
+
grants << Grant.new(grant)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
module GrantListExtensions #:nodoc:
|
156
|
+
def include?(grant)
|
157
|
+
case grant
|
158
|
+
when Symbol
|
159
|
+
super(ACL::Grant.grant(grant))
|
160
|
+
else
|
161
|
+
super
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def delete(grant)
|
166
|
+
case grant
|
167
|
+
when Symbol
|
168
|
+
super(ACL::Grant.grant(grant))
|
169
|
+
else
|
170
|
+
super
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Two grant lists are equal if they have identical grants both in terms of permission and grantee.
|
175
|
+
def ==(grants)
|
176
|
+
size == grants.size && all? {|grant| grants.include?(grant)}
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
class Builder < XmlGenerator #:nodoc:
|
181
|
+
attr_reader :owner, :grants
|
182
|
+
def initialize(owner, grants)
|
183
|
+
@owner = owner
|
184
|
+
@grants = grants.uniq # There could be some duplicate grants
|
185
|
+
super()
|
186
|
+
end
|
187
|
+
|
188
|
+
def build
|
189
|
+
xml.tag!('AccessControlPolicy', 'xmlns' => 'http://s3.amazonaws.com/doc/2006-03-01/') do
|
190
|
+
xml.Owner do
|
191
|
+
xml.ID owner.id
|
192
|
+
xml.DisplayName owner.display_name
|
193
|
+
end
|
194
|
+
|
195
|
+
xml.AccessControlList do
|
196
|
+
xml << grants.map {|grant| grant.to_xml}.join("\n")
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# A Policy is made up of one or more Grant objects. A grant sets a specific permission and grants it to the associated grantee.
|
204
|
+
#
|
205
|
+
# When creating a new grant to add to a policy, you need only set its permission and then associate with a Grantee.
|
206
|
+
#
|
207
|
+
# grant = ACL::Grant.new
|
208
|
+
# => #<AWS::S3::ACL::Grant (permission) to (grantee)>
|
209
|
+
#
|
210
|
+
# Here we see that neither the permission nor the grantee have been set. Let's make this grant provide the READ permission.
|
211
|
+
#
|
212
|
+
# grant.permission = 'READ'
|
213
|
+
# grant
|
214
|
+
# => #<AWS::S3::ACL::Grant READ to (grantee)>
|
215
|
+
#
|
216
|
+
# Now let's assume we have a grantee to the AllUsers group already set up. Just associate that grantee with our grant.
|
217
|
+
#
|
218
|
+
# grant.grantee = all_users_group_grantee
|
219
|
+
# grant
|
220
|
+
# => #<AWS::S3::ACL::Grant READ to AllUsers Group>
|
221
|
+
#
|
222
|
+
# And now are grant is complete. It provides READ permission to the AllUsers group, effectively making this object publicly readable
|
223
|
+
# without any authorization.
|
224
|
+
#
|
225
|
+
# Assuming we have some object's policy available in a local variable called <tt>policy</tt>, we can now add this grant onto its
|
226
|
+
# collection of grants.
|
227
|
+
#
|
228
|
+
# policy.grants << grant
|
229
|
+
#
|
230
|
+
# And then we send the updated policy to the S3 servers.
|
231
|
+
#
|
232
|
+
# some_s3object.acl(policy)
|
233
|
+
class Grant
|
234
|
+
include SelectiveAttributeProxy #:nodoc:
|
235
|
+
constant :VALID_PERMISSIONS, %w(READ WRITE READ_ACP WRITE_ACP FULL_CONTROL)
|
236
|
+
attr_accessor :grantee
|
237
|
+
|
238
|
+
class << self
|
239
|
+
# Returns stock grants with name <tt>type</tt>.
|
240
|
+
#
|
241
|
+
# public_read_grant = ACL::Grant.grant :public_read
|
242
|
+
# => #<AWS::S3::ACL::Grant READ to AllUsers Group>
|
243
|
+
#
|
244
|
+
# Valid stock grant types are:
|
245
|
+
#
|
246
|
+
# * <tt>:authenticated_read</tt>
|
247
|
+
# * <tt>:authenticated_read_acp</tt>
|
248
|
+
# * <tt>:authenticated_write</tt>
|
249
|
+
# * <tt>:authenticated_write_acp</tt>
|
250
|
+
# * <tt>:logging_read</tt>
|
251
|
+
# * <tt>:logging_read_acp</tt>
|
252
|
+
# * <tt>:logging_write</tt>
|
253
|
+
# * <tt>:logging_write_acp</tt>
|
254
|
+
# * <tt>:public_read</tt>
|
255
|
+
# * <tt>:public_read_acp</tt>
|
256
|
+
# * <tt>:public_write</tt>
|
257
|
+
# * <tt>:public_write_acp</tt>
|
258
|
+
def grant(type)
|
259
|
+
case type
|
260
|
+
when *stock_grant_map.keys
|
261
|
+
build_stock_grant_for type
|
262
|
+
else
|
263
|
+
raise ArgumentError, "Unknown grant type `#{type}'"
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
private
|
268
|
+
def stock_grant_map
|
269
|
+
grant = lambda {|permission, group| {:permission => permission, :group => group}}
|
270
|
+
groups = {:public => 'AllUsers', :authenticated => 'Authenticated', :logging => 'LogDelivery'}
|
271
|
+
permissions = %w(READ WRITE READ_ACP WRITE_ACP)
|
272
|
+
stock_grants = {}
|
273
|
+
groups.each do |grant_group_name, group_name|
|
274
|
+
permissions.each do |permission|
|
275
|
+
stock_grants["#{grant_group_name}_#{permission.downcase}".to_sym] = grant[permission, group_name]
|
276
|
+
end
|
277
|
+
end
|
278
|
+
stock_grants
|
279
|
+
end
|
280
|
+
memoized :stock_grant_map
|
281
|
+
|
282
|
+
def build_stock_grant_for(type)
|
283
|
+
stock_grant = stock_grant_map[type]
|
284
|
+
grant = new do |g|
|
285
|
+
g.permission = stock_grant[:permission]
|
286
|
+
end
|
287
|
+
grant.grantee = Grantee.new do |gr|
|
288
|
+
gr.group = stock_grant[:group]
|
289
|
+
end
|
290
|
+
grant
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def initialize(attributes = {})
|
295
|
+
attributes = {'permission' => nil}.merge(attributes)
|
296
|
+
@attributes = attributes
|
297
|
+
extract_grantee!
|
298
|
+
yield self if block_given?
|
299
|
+
end
|
300
|
+
|
301
|
+
# Set the permission for this grant.
|
302
|
+
#
|
303
|
+
# grant.permission = 'READ'
|
304
|
+
# grant
|
305
|
+
# => #<AWS::S3::ACL::Grant READ to (grantee)>
|
306
|
+
#
|
307
|
+
# If the specified permisison level is not valid, an <tt>InvalidAccessControlLevel</tt> exception will be raised.
|
308
|
+
def permission=(permission_level)
|
309
|
+
unless self.class.valid_permissions.include?(permission_level)
|
310
|
+
raise InvalidAccessControlLevel.new(self.class.valid_permissions, permission_level)
|
311
|
+
end
|
312
|
+
attributes['permission'] = permission_level
|
313
|
+
end
|
314
|
+
|
315
|
+
# The xml representation of this grant.
|
316
|
+
def to_xml
|
317
|
+
Builder.new(permission, grantee).to_s
|
318
|
+
end
|
319
|
+
|
320
|
+
def inspect #:nodoc:
|
321
|
+
"#<%s:0x%s %s>" % [self.class, object_id, self]
|
322
|
+
end
|
323
|
+
|
324
|
+
def to_s #:nodoc:
|
325
|
+
[permission || '(permission)', 'to', grantee ? grantee.type_representation : '(grantee)'].join ' '
|
326
|
+
end
|
327
|
+
|
328
|
+
def eql?(grant) #:nodoc:
|
329
|
+
# This won't work for an unposted AmazonCustomerByEmail because of the normalization
|
330
|
+
# to CanonicalUser but it will work for groups.
|
331
|
+
to_s == grant.to_s
|
332
|
+
end
|
333
|
+
alias_method :==, :eql?
|
334
|
+
|
335
|
+
def hash #:nodoc:
|
336
|
+
to_s.hash
|
337
|
+
end
|
338
|
+
|
339
|
+
private
|
340
|
+
|
341
|
+
def extract_grantee!
|
342
|
+
@grantee = Grantee.new(attributes['grantee']) if attributes['grantee']
|
343
|
+
end
|
344
|
+
|
345
|
+
class Builder < XmlGenerator #:nodoc:
|
346
|
+
attr_reader :grantee, :permission
|
347
|
+
|
348
|
+
def initialize(permission, grantee)
|
349
|
+
@permission = permission
|
350
|
+
@grantee = grantee
|
351
|
+
super()
|
352
|
+
end
|
353
|
+
|
354
|
+
def build
|
355
|
+
xml.Grant do
|
356
|
+
xml << grantee.to_xml
|
357
|
+
xml.Permission permission
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
# Grants bestow a access permission to grantees. Each grant of some access control list Policy is associated with a grantee.
|
364
|
+
# There are three ways of specifying a grantee at the time of this writing.
|
365
|
+
#
|
366
|
+
# * By canonical user - This format uses the <tt>id</tt> of a given Amazon account. The id value for a given account is available in the
|
367
|
+
# Owner object of a bucket, object or policy.
|
368
|
+
#
|
369
|
+
# grantee.id = 'bb2041a25975c3d4ce9775fe9e93e5b77a6a9fad97dc7e00686191f3790b13f1'
|
370
|
+
#
|
371
|
+
# Often the id will just be fetched from some owner object.
|
372
|
+
#
|
373
|
+
# grantee.id = some_object.owner.id
|
374
|
+
#
|
375
|
+
# * 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.
|
376
|
+
# though it must be unique across the entire Amazon system. This email address is normalized into a canonical user representation once the grant
|
377
|
+
# has been sent back up to the S3 servers.
|
378
|
+
#
|
379
|
+
# grantee.email_address = 'joe@example.org'
|
380
|
+
#
|
381
|
+
# * 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
|
382
|
+
# Grantee.group= method for details.
|
383
|
+
#
|
384
|
+
# grantee.group = 'Authenticated'
|
385
|
+
class Grantee
|
386
|
+
include SelectiveAttributeProxy #:nodoc:
|
387
|
+
|
388
|
+
undef_method :id if method_defined?(:id) # Get rid of Object#id
|
389
|
+
|
390
|
+
def initialize(attributes = {})
|
391
|
+
# Set default values for attributes that may not be passed in but we still want the object
|
392
|
+
# to respond to
|
393
|
+
attributes = {'id' => nil, 'display_name' => nil, 'email_address' => nil, 'uri' => nil}.merge(attributes)
|
394
|
+
@attributes = attributes
|
395
|
+
extract_type!
|
396
|
+
yield self if block_given?
|
397
|
+
end
|
398
|
+
|
399
|
+
# The xml representation of the current grantee object.
|
400
|
+
def to_xml
|
401
|
+
Builder.new(self).to_s
|
402
|
+
end
|
403
|
+
|
404
|
+
# Returns the type of grantee. Will be one of <tt>CanonicalUser</tt>, <tt>AmazonCustomerByEmail</tt> or <tt>Group</tt>.
|
405
|
+
def type
|
406
|
+
return attributes['type'] if attributes['type']
|
407
|
+
|
408
|
+
# Lookups are in order of preference so if, for example, you set the uri but display_name and id are also
|
409
|
+
# set, we'd rather go with the canonical representation.
|
410
|
+
if display_name && id
|
411
|
+
'CanonicalUser'
|
412
|
+
elsif email_address
|
413
|
+
'AmazonCustomerByEmail'
|
414
|
+
elsif uri
|
415
|
+
'Group'
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
# Sets the grantee's group by name.
|
420
|
+
#
|
421
|
+
# grantee.group = 'AllUsers'
|
422
|
+
#
|
423
|
+
# Currently, valid groups defined by S3 are:
|
424
|
+
#
|
425
|
+
# * <tt>AllUsers</tt>: This group represents anyone. In other words, an anonymous request.
|
426
|
+
# * <tt>Authenticated</tt>: Any authenticated account on the S3 service.
|
427
|
+
# * <tt>LogDelivery</tt>: The entity that delivers bucket access logs.
|
428
|
+
def group=(group_name)
|
429
|
+
section = %w(AllUsers Authenticated).include?(group_name) ? 'global' : 's3'
|
430
|
+
self.uri = "http://acs.amazonaws.com/groups/#{section}/#{group_name}"
|
431
|
+
end
|
432
|
+
|
433
|
+
# Returns the grantee's group. If the grantee is not a group, <tt>nil</tt> is returned.
|
434
|
+
def group
|
435
|
+
return unless uri
|
436
|
+
uri[%r([^/]+$)]
|
437
|
+
end
|
438
|
+
|
439
|
+
def type_representation #:nodoc:
|
440
|
+
case type
|
441
|
+
when 'CanonicalUser' then display_name || id
|
442
|
+
when 'AmazonCustomerByEmail' then email_address
|
443
|
+
when 'Group' then "#{group} Group"
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
def inspect #:nodoc:
|
448
|
+
"#<%s:0x%s %s>" % [self.class, object_id, type_representation || '(type not set yet)']
|
449
|
+
end
|
450
|
+
|
451
|
+
private
|
452
|
+
def extract_type!
|
453
|
+
attributes['type'] = attributes.delete('xsi:type')
|
454
|
+
end
|
455
|
+
|
456
|
+
class Builder < XmlGenerator #:nodoc:
|
457
|
+
|
458
|
+
def initialize(grantee)
|
459
|
+
@grantee = grantee
|
460
|
+
super()
|
461
|
+
end
|
462
|
+
|
463
|
+
def build
|
464
|
+
xml.tag!('Grantee', attributes) do
|
465
|
+
representation
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
private
|
470
|
+
attr_reader :grantee
|
471
|
+
|
472
|
+
def representation
|
473
|
+
case grantee.type
|
474
|
+
when 'CanonicalUser'
|
475
|
+
xml.ID grantee.id
|
476
|
+
xml.DisplayName grantee.display_name
|
477
|
+
when 'AmazonCustomerByEmail'
|
478
|
+
xml.EmailAddress grantee.email_address
|
479
|
+
when 'Group'
|
480
|
+
xml.URI grantee.uri
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
def attributes
|
485
|
+
{'xsi:type' => grantee.type, 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'}
|
486
|
+
end
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
module Bucket
|
491
|
+
def self.included(klass) #:nodoc:
|
492
|
+
klass.extend(ClassMethods)
|
493
|
+
end
|
494
|
+
|
495
|
+
module ClassMethods
|
496
|
+
# The acl method is the single point of entry for reading and writing access control list policies for a given bucket.
|
497
|
+
#
|
498
|
+
# # Fetch the acl for the 'marcel' bucket
|
499
|
+
# policy = Bucket.acl 'marcel'
|
500
|
+
#
|
501
|
+
# # Modify the policy ...
|
502
|
+
# # ...
|
503
|
+
#
|
504
|
+
# # Send updated policy back to the S3 servers
|
505
|
+
# Bucket.acl 'marcel', policy
|
506
|
+
def acl(name = nil, policy = nil)
|
507
|
+
if name.is_a?(ACL::Policy)
|
508
|
+
policy = name
|
509
|
+
name = nil
|
510
|
+
end
|
511
|
+
|
512
|
+
path = path(name) << '?acl'
|
513
|
+
respond_with ACL::Policy::Response do
|
514
|
+
policy ? put(path, {}, policy.to_xml) : ACL::Policy.new(get(path(name) << '?acl').policy)
|
515
|
+
end
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
# The acl method returns and updates the acl for a given bucket.
|
520
|
+
#
|
521
|
+
# # Fetch a bucket
|
522
|
+
# bucket = Bucket.find 'marcel'
|
523
|
+
#
|
524
|
+
# # Add a grant to the bucket's policy
|
525
|
+
# bucket.acl.grants << some_grant
|
526
|
+
#
|
527
|
+
# # Write the changes to the policy
|
528
|
+
# bucket.acl(bucket.acl)
|
529
|
+
def acl(reload = false)
|
530
|
+
policy = reload.is_a?(ACL::Policy) ? reload : nil
|
531
|
+
expirable_memoize(reload) do
|
532
|
+
self.class.acl(name, policy) if policy
|
533
|
+
self.class.acl(name)
|
534
|
+
end
|
535
|
+
end
|
536
|
+
end
|
537
|
+
|
538
|
+
module S3Object
|
539
|
+
def self.included(klass) #:nodoc:
|
540
|
+
klass.extend(ClassMethods)
|
541
|
+
end
|
542
|
+
|
543
|
+
module ClassMethods
|
544
|
+
# The acl method is the single point of entry for reading and writing access control list policies for a given object.
|
545
|
+
#
|
546
|
+
# # Fetch the acl for the 'kiss.jpg' object in the 'marcel' bucket
|
547
|
+
# policy = S3Object.acl 'kiss.jpg', 'marcel'
|
548
|
+
#
|
549
|
+
# # Modify the policy ...
|
550
|
+
# # ...
|
551
|
+
#
|
552
|
+
# # Send updated policy back to the S3 servers
|
553
|
+
# S3Object.acl 'kiss.jpg', 'marcel', policy
|
554
|
+
def acl(name, bucket = nil, policy = nil)
|
555
|
+
# We're using the second argument as the ACL::Policy
|
556
|
+
if bucket.is_a?(ACL::Policy)
|
557
|
+
policy = bucket
|
558
|
+
bucket = nil
|
559
|
+
end
|
560
|
+
|
561
|
+
bucket = bucket_name(bucket)
|
562
|
+
path = path!(bucket, name) << '?acl'
|
563
|
+
|
564
|
+
respond_with ACL::Policy::Response do
|
565
|
+
policy ? put(path, {}, policy.to_xml) : ACL::Policy.new(get(path).policy)
|
566
|
+
end
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
570
|
+
# The acl method returns and updates the acl for a given s3 object.
|
571
|
+
#
|
572
|
+
# # Fetch a the object
|
573
|
+
# object = S3Object.find 'kiss.jpg', 'marcel'
|
574
|
+
#
|
575
|
+
# # Add a grant to the object's
|
576
|
+
# object.acl.grants << some_grant
|
577
|
+
#
|
578
|
+
# # Write the changes to the policy
|
579
|
+
# object.acl(object.acl)
|
580
|
+
def acl(reload = false)
|
581
|
+
policy = reload.is_a?(ACL::Policy) ? reload : nil
|
582
|
+
expirable_memoize(reload) do
|
583
|
+
self.class.acl(key, bucket.name, policy) if policy
|
584
|
+
self.class.acl(key, bucket.name)
|
585
|
+
end
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
class OptionProcessor #:nodoc:
|
590
|
+
attr_reader :options
|
591
|
+
class << self
|
592
|
+
def process!(options)
|
593
|
+
new(options).process!
|
594
|
+
end
|
595
|
+
end
|
596
|
+
|
597
|
+
def initialize(options)
|
598
|
+
options.to_normalized_options!
|
599
|
+
@options = options
|
600
|
+
@access_level = extract_access_level
|
601
|
+
end
|
602
|
+
|
603
|
+
def process!
|
604
|
+
return unless access_level_specified?
|
605
|
+
validate!
|
606
|
+
options['x-amz-acl'] = access_level
|
607
|
+
end
|
608
|
+
|
609
|
+
private
|
610
|
+
def extract_access_level
|
611
|
+
options.delete('access') || options.delete('x-amz-acl')
|
612
|
+
end
|
613
|
+
|
614
|
+
def validate!
|
615
|
+
raise InvalidAccessControlLevel.new(valid_levels, access_level) unless valid?
|
616
|
+
end
|
617
|
+
|
618
|
+
def valid?
|
619
|
+
valid_levels.include?(access_level)
|
620
|
+
end
|
621
|
+
|
622
|
+
def access_level_specified?
|
623
|
+
!@access_level.nil?
|
624
|
+
end
|
625
|
+
|
626
|
+
def valid_levels
|
627
|
+
%w(private public-read public-read-write authenticated-read)
|
628
|
+
end
|
629
|
+
|
630
|
+
def access_level
|
631
|
+
@normalized_access_level ||= @access_level.to_header
|
632
|
+
end
|
633
|
+
end
|
634
|
+
end
|
635
|
+
end
|
636
|
+
end
|