aws-s3-instructure 0.6.2.1319222580

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/COPYING +19 -0
  2. data/INSTALL +55 -0
  3. data/README +545 -0
  4. data/Rakefile +300 -0
  5. data/bin/s3sh +6 -0
  6. data/bin/setup.rb +10 -0
  7. data/lib/aws/s3.rb +60 -0
  8. data/lib/aws/s3/acl.rb +636 -0
  9. data/lib/aws/s3/authentication.rb +266 -0
  10. data/lib/aws/s3/base.rb +240 -0
  11. data/lib/aws/s3/bittorrent.rb +58 -0
  12. data/lib/aws/s3/bucket.rb +319 -0
  13. data/lib/aws/s3/connection.rb +295 -0
  14. data/lib/aws/s3/error.rb +69 -0
  15. data/lib/aws/s3/exceptions.rb +133 -0
  16. data/lib/aws/s3/extensions.rb +356 -0
  17. data/lib/aws/s3/logging.rb +314 -0
  18. data/lib/aws/s3/object.rb +612 -0
  19. data/lib/aws/s3/owner.rb +44 -0
  20. data/lib/aws/s3/parsing.rb +99 -0
  21. data/lib/aws/s3/response.rb +180 -0
  22. data/lib/aws/s3/service.rb +51 -0
  23. data/lib/aws/s3/version.rb +12 -0
  24. data/support/faster-xml-simple/lib/faster_xml_simple.rb +187 -0
  25. data/support/faster-xml-simple/test/regression_test.rb +47 -0
  26. data/support/faster-xml-simple/test/test_helper.rb +17 -0
  27. data/support/faster-xml-simple/test/xml_simple_comparison_test.rb +46 -0
  28. data/support/rdoc/code_info.rb +211 -0
  29. data/test/acl_test.rb +254 -0
  30. data/test/authentication_test.rb +156 -0
  31. data/test/base_test.rb +136 -0
  32. data/test/bucket_test.rb +74 -0
  33. data/test/connection_test.rb +256 -0
  34. data/test/error_test.rb +70 -0
  35. data/test/extensions_test.rb +340 -0
  36. data/test/fixtures.rb +89 -0
  37. data/test/fixtures/buckets.yml +133 -0
  38. data/test/fixtures/errors.yml +34 -0
  39. data/test/fixtures/headers.yml +3 -0
  40. data/test/fixtures/logging.yml +15 -0
  41. data/test/fixtures/loglines.yml +5 -0
  42. data/test/fixtures/logs.yml +7 -0
  43. data/test/fixtures/policies.yml +16 -0
  44. data/test/logging_test.rb +89 -0
  45. data/test/mocks/fake_response.rb +26 -0
  46. data/test/object_test.rb +205 -0
  47. data/test/parsing_test.rb +66 -0
  48. data/test/remote/acl_test.rb +117 -0
  49. data/test/remote/bittorrent_test.rb +45 -0
  50. data/test/remote/bucket_test.rb +146 -0
  51. data/test/remote/logging_test.rb +82 -0
  52. data/test/remote/object_test.rb +371 -0
  53. data/test/remote/test_file.data +0 -0
  54. data/test/remote/test_helper.rb +33 -0
  55. data/test/response_test.rb +68 -0
  56. data/test/service_test.rb +23 -0
  57. data/test/test_helper.rb +110 -0
  58. metadata +198 -0
data/Rakefile ADDED
@@ -0,0 +1,300 @@
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', '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 :dist do
60
+ spec = Gem::Specification.new do |s|
61
+ s.name = 'aws-s3-instructure'
62
+ s.version = Gem::Version.new(AWS::S3::Version)
63
+ s.summary = "Client library for Amazon's Simple Storage Service's REST API"
64
+ s.description = "Client library for Amazon's Simple Storage Service's REST API, forked by Instructure pending action on pull request https://github.com/marcel/aws-s3/pull/40"
65
+ s.email = 'jacob@instructure.com'
66
+ s.author = 'Jacob Fugal'
67
+ s.has_rdoc = true
68
+ s.extra_rdoc_files = %w(README COPYING INSTALL)
69
+ s.homepage = 'http://amazon.rubyforge.org'
70
+ s.files = FileList['Rakefile', 'lib/**/*.rb', 'bin/*', 'support/**/*.rb']
71
+ s.executables << 's3sh'
72
+ s.test_files = Dir['test/**/*']
73
+
74
+ s.add_dependency 'xml-simple'
75
+ s.add_dependency 'builder'
76
+ s.add_dependency 'mime-types'
77
+ s.rdoc_options = ['--title', "AWS::S3 -- Support for Amazon S3's REST api",
78
+ '--main', 'README',
79
+ '--line-numbers', '--inline-source']
80
+ end
81
+
82
+ # Regenerate README before packaging
83
+ task :package => 'doc:readme'
84
+ Rake::GemPackageTask.new(spec) do |pkg|
85
+ pkg.need_tar_gz = true
86
+ pkg.package_files.include('{lib,script,test,support}/**/*')
87
+ pkg.package_files.include('README')
88
+ pkg.package_files.include('COPYING')
89
+ pkg.package_files.include('INSTALL')
90
+ pkg.package_files.include('Rakefile')
91
+ end
92
+
93
+ desc 'Install with gems'
94
+ task :install => :repackage do
95
+ sh "sudo gem i pkg/#{spec.name}-#{spec.version}.gem"
96
+ end
97
+
98
+ desc 'Uninstall gem'
99
+ task :uninstall do
100
+ sh "sudo gem uninstall #{spec.name} -x"
101
+ end
102
+
103
+ desc 'Reinstall gem'
104
+ task :reinstall => [:uninstall, :install]
105
+
106
+ task :confirm_release do
107
+ print "Releasing version #{spec.version}. Are you sure you want to proceed? [Yn] "
108
+ abort if STDIN.getc == ?n
109
+ end
110
+
111
+ desc 'Tag release'
112
+ task :tag do
113
+ sh %(git tag -a '#{spec.version}-release' -m 'Tagging #{spec.version} release')
114
+ sh 'git push --tags'
115
+ end
116
+
117
+ desc 'Update changelog to include a release marker'
118
+ task :add_release_marker_to_changelog do
119
+ changelog = IO.read('CHANGELOG')
120
+ changelog.sub!(/^head:/, "#{spec.version}:")
121
+
122
+ open('CHANGELOG', 'w') do |file|
123
+ file.write "head:\n\n#{changelog}"
124
+ end
125
+ end
126
+
127
+ task :commit_changelog do
128
+ sh %(git commit CHANGELOG -m "Bump changelog version marker for release")
129
+ sh 'git push'
130
+ end
131
+
132
+ package_name = lambda {|specification| File.join('pkg', "#{specification.name}-#{specification.version}")}
133
+
134
+ task :spec do
135
+ puts spec.to_ruby
136
+ end
137
+ end
138
+
139
+ desc 'Check code to test ratio'
140
+ task :stats do
141
+ library_files = FileList["#{library_root}/lib/**/*.rb"]
142
+ test_files = FileList["#{library_root}/test/**/*_test.rb"]
143
+ count_code_lines = Proc.new do |lines|
144
+ lines.inject(0) do |code_lines, line|
145
+ next code_lines if [/^\s*$/, /^\s*#/].any? {|non_code_line| non_code_line === line}
146
+ code_lines + 1
147
+ end
148
+ end
149
+
150
+ count_code_lines_for_files = Proc.new do |files|
151
+ files.inject(0) {|code_lines, file| code_lines + count_code_lines[IO.read(file)]}
152
+ end
153
+
154
+ library_code_lines = count_code_lines_for_files[library_files]
155
+ test_code_lines = count_code_lines_for_files[test_files]
156
+ ratio = Proc.new { sprintf('%.2f', test_code_lines.to_f / library_code_lines)}
157
+
158
+ puts "Code LOC: #{library_code_lines} Test LOC: #{test_code_lines} Code to Test Ratio: 1:#{ratio.call}"
159
+ end
160
+
161
+ namespace :test do
162
+ find_file = lambda do |name|
163
+ file_name = lambda {|path| File.join(path, "#{name}.rb")}
164
+ root = $:.detect do |path|
165
+ File.exist?(file_name[path])
166
+ end
167
+ file_name[root] if root
168
+ end
169
+
170
+ TEST_LOADER = find_file['rake/rake_test_loader']
171
+ multiruby = lambda do |glob|
172
+ system 'multiruby', TEST_LOADER, *Dir.glob(glob)
173
+ end
174
+
175
+ desc 'Check test coverage'
176
+ task :coverage do
177
+ system("rcov -x Library -x support --sort coverage #{File.join(library_root, 'test/*_test.rb')}")
178
+ show_test_coverage_results
179
+ end
180
+
181
+ Rake::TestTask.new(:remote) do |test|
182
+ test.pattern = 'test/remote/*_test.rb'
183
+ test.verbose = true
184
+ end
185
+
186
+ Rake::TestTask.new(:all) do |test|
187
+ test.pattern = 'test/**/*_test.rb'
188
+ test.verbose = true
189
+ end
190
+
191
+ desc 'Check test coverage of full stack remote tests'
192
+ task :full_coverage do
193
+ system("rcov -x Library -x support --sort coverage #{File.join(library_root, 'test/remote/*_test.rb')} #{File.join(library_root, 'test/*_test.rb')}")
194
+ show_test_coverage_results
195
+ end
196
+
197
+ desc 'Run local tests against multiple versions of Ruby'
198
+ task :version_audit do
199
+ multiruby['test/*_test.rb']
200
+ end
201
+
202
+ namespace :version_audit do
203
+ desc 'Run remote tests against multiple versions of Ruby'
204
+ task :remote do
205
+ multiruby['test/remote/*_test.rb']
206
+ end
207
+
208
+ desc 'Run all tests against multiple versions of Ruby'
209
+ task :all do
210
+ multiruby['test/**/*_test.rb']
211
+ end
212
+ end
213
+
214
+ def show_test_coverage_results
215
+ system("open #{File.join(library_root, 'coverage/index.html')}") if PLATFORM['darwin']
216
+ end
217
+
218
+ desc 'Remove coverage products'
219
+ task :clobber_coverage do
220
+ rm_r 'coverage' rescue nil
221
+ end
222
+ end
223
+
224
+ namespace :todo do
225
+ class << TODOS = IO.read(File.join(library_root, 'TODO'))
226
+ def items
227
+ split("\n").grep(/^\[\s|X\]/)
228
+ end
229
+
230
+ def completed
231
+ find_items_matching(/^\[X\]/)
232
+ end
233
+
234
+ def uncompleted
235
+ find_items_matching(/^\[\s\]/)
236
+ end
237
+
238
+ def find_items_matching(regexp)
239
+ items.grep(regexp).instance_eval do
240
+ def display
241
+ puts map {|item| "* #{item.sub(/^\[[^\]]\]\s/, '')}"}
242
+ end
243
+ self
244
+ end
245
+ end
246
+ end
247
+
248
+ desc 'Completed todo items'
249
+ task :completed do
250
+ TODOS.completed.display
251
+ end
252
+
253
+ desc 'Incomplete todo items'
254
+ task :uncompleted do
255
+ TODOS.uncompleted.display
256
+ end
257
+ end if File.exists?(File.join(library_root, 'TODO'))
258
+
259
+ namespace :site do
260
+ require 'erb'
261
+ require 'rdoc/markup/simple_markup'
262
+ require 'rdoc/markup/simple_markup/to_html'
263
+
264
+ readme = lambda { IO.read('README')[/^== Getting started\n(.*)/m, 1] }
265
+
266
+ readme_to_html = lambda do
267
+ handler = SM::ToHtml.new
268
+ handler.instance_eval do
269
+ require 'syntax'
270
+ require 'syntax/convertors/html'
271
+ def accept_verbatim(am, fragment)
272
+ syntax = Syntax::Convertors::HTML.for_syntax('ruby')
273
+ @res << %(<div class="ruby">#{syntax.convert(fragment.txt, true)}</div>)
274
+ end
275
+ end
276
+ SM::SimpleMarkup.new.convert(readme.call, handler)
277
+ end
278
+
279
+ desc 'Regenerate the public website page'
280
+ task :build => 'doc:readme' do
281
+ open('site/public/index.html', 'w') do |file|
282
+ erb_data = {}
283
+ erb_data[:readme] = readme_to_html.call
284
+ file.write ERB.new(IO.read('site/index.erb')).result(binding)
285
+ end
286
+ end
287
+
288
+ task :refresh => :build do
289
+ system 'open site/public/index.html'
290
+ end
291
+
292
+ desc 'Update the live website'
293
+ task :deploy => :build do
294
+ site_files = FileList['site/public/*']
295
+ site_files.delete_if {|file| File.directory?(file)}
296
+ sh %(scp #{site_files.join ' '} marcel@rubyforge.org:/var/www/gforge-projects/amazon/)
297
+ end
298
+ end
299
+
300
+ task :clean => ['dist:clobber_package', 'doc:clobber_rdoc', 'test:clobber_coverage']
data/bin/s3sh ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ s3_lib = File.dirname(__FILE__) + '/../lib/aws/s3'
3
+ setup = File.dirname(__FILE__) + '/setup'
4
+ irb_name = RUBY_PLATFORM =~ /mswin32/ ? 'irb.bat' : 'irb'
5
+
6
+ exec "#{irb_name} -r #{s3_lib} -r #{setup} --simple-prompt"
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