rockoauth 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/History.txt +5 -0
  3. data/README.rdoc +422 -0
  4. data/example/README.rdoc +11 -0
  5. data/example/application.rb +158 -0
  6. data/example/config.ru +3 -0
  7. data/example/environment.rb +11 -0
  8. data/example/models/connection.rb +9 -0
  9. data/example/models/note.rb +4 -0
  10. data/example/models/user.rb +5 -0
  11. data/example/public/style.css +78 -0
  12. data/example/schema.rb +22 -0
  13. data/example/views/authorize.erb +28 -0
  14. data/example/views/create_user.erb +3 -0
  15. data/example/views/error.erb +6 -0
  16. data/example/views/home.erb +24 -0
  17. data/example/views/layout.erb +24 -0
  18. data/example/views/login.erb +20 -0
  19. data/example/views/new_client.erb +25 -0
  20. data/example/views/new_user.erb +22 -0
  21. data/example/views/show_client.erb +15 -0
  22. data/lib/rockoauth/model/authorization.rb +132 -0
  23. data/lib/rockoauth/model/client.rb +54 -0
  24. data/lib/rockoauth/model/client_owner.rb +13 -0
  25. data/lib/rockoauth/model/hashing.rb +26 -0
  26. data/lib/rockoauth/model/helpers.rb +14 -0
  27. data/lib/rockoauth/model/resource_owner.rb +22 -0
  28. data/lib/rockoauth/model.rb +38 -0
  29. data/lib/rockoauth/provider/access_token.rb +70 -0
  30. data/lib/rockoauth/provider/authorization.rb +185 -0
  31. data/lib/rockoauth/provider/error.rb +19 -0
  32. data/lib/rockoauth/provider/exchange.rb +225 -0
  33. data/lib/rockoauth/provider.rb +133 -0
  34. data/lib/rockoauth/router.rb +75 -0
  35. data/lib/rockoauth/schema/20120828112156_rockoauth_schema_original_schema.rb +35 -0
  36. data/lib/rockoauth/schema/20121024180930_rockoauth_schema_add_authorization_index.rb +13 -0
  37. data/lib/rockoauth/schema/20121025180447_rockoauth_schema_add_unique_indexes.rb +31 -0
  38. data/lib/rockoauth/schema.rb +25 -0
  39. data/lib/rockoauth.rb +1 -0
  40. data/spec/factories.rb +20 -0
  41. data/spec/request_helpers.rb +62 -0
  42. data/spec/rockoauth/model/authorization_spec.rb +237 -0
  43. data/spec/rockoauth/model/client_spec.rb +44 -0
  44. data/spec/rockoauth/model/helpers_spec.rb +25 -0
  45. data/spec/rockoauth/model/resource_owner_spec.rb +87 -0
  46. data/spec/rockoauth/provider/access_token_spec.rb +138 -0
  47. data/spec/rockoauth/provider/authorization_spec.rb +356 -0
  48. data/spec/rockoauth/provider/exchange_spec.rb +361 -0
  49. data/spec/rockoauth/provider_spec.rb +560 -0
  50. data/spec/spec_helper.rb +80 -0
  51. data/spec/test_app/helper.rb +36 -0
  52. data/spec/test_app/provider/application.rb +67 -0
  53. data/spec/test_app/provider/views/authorize.erb +19 -0
  54. metadata +238 -0
@@ -0,0 +1,78 @@
1
+ body {
2
+ font: 16px/1.4 FreeSans, Helvetica, Arial, sans-serif;
3
+ background: #353e4b;
4
+ }
5
+
6
+ .sub {
7
+ width: 640px;
8
+ margin: 0 auto;
9
+ padding: 1em 2em;
10
+ }
11
+
12
+ .header {
13
+ text-shadow: #23282e 0px -2px 0px;
14
+ }
15
+
16
+ .header h1 {
17
+ color: #e5dec7;
18
+ font-size: 4em;
19
+ letter-spacing: -0.06em;
20
+ margin: 0;
21
+ }
22
+
23
+ .header h2 {
24
+ color: #b3a784;
25
+ font-size: 1.5em;
26
+ font-weight: normal;
27
+ letter-spacing: -0.06em;
28
+ margin: 0 0 0.5em;
29
+ }
30
+
31
+ .content .sub {
32
+ background: #fff;
33
+ color: #333;
34
+ -webkit-border-radius: 16px;
35
+ -moz-border-radius: 16px;
36
+ border-radius: 16px;
37
+ }
38
+
39
+ h3 {
40
+ color: #888;
41
+ font-size: 1.5em;
42
+ font-weight: normal;
43
+ margin: 0 0 1em;
44
+ }
45
+
46
+ fieldset {
47
+ border: none;
48
+ border-top: 1px solid #ccc;
49
+ padding: 12px 0 0 0;
50
+ margin: 12px 0 0 0;
51
+ }
52
+
53
+ table {
54
+ border-collapse: collapse;
55
+ }
56
+
57
+ table th, table td {
58
+ border-top: 1px solid #eee;
59
+ padding: 8px 16px;
60
+ }
61
+
62
+ table th {
63
+ border-right: 2px solid #ccc;
64
+ text-align: left;
65
+ }
66
+
67
+ a {
68
+ color: #9ba749;
69
+ font-weight: bold;
70
+ text-decoration: none;
71
+ }
72
+
73
+ .footer {
74
+ font-size: 0.8em;
75
+ color: #999;
76
+ text-shadow: #23282e 0px -1px 0px;
77
+ }
78
+
data/example/schema.rb ADDED
@@ -0,0 +1,22 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'rockoauth/provider'
5
+ require 'active_record'
6
+ require File.expand_path('../models/connection', __FILE__)
7
+
8
+ ActiveRecord::Schema.define do |version|
9
+ create_table :users, :force => true do |t|
10
+ t.timestamps
11
+ t.string :username
12
+ end
13
+
14
+ create_table :notes, :force => true do |t|
15
+ t.timestamps
16
+ t.belongs_to :user
17
+ t.string :title
18
+ t.text :body
19
+ end
20
+ end
21
+
22
+ RockOAuth::Model::Schema.up
@@ -0,0 +1,28 @@
1
+ <h3>Authorize OAuth client</h3>
2
+
3
+ <p>This application <b><%= @oauth2.client.name %></b> wants the following
4
+ permissions:</p>
5
+
6
+ <ul>
7
+ <% @oauth2.scopes.each do |scope| %>
8
+ <% next unless PERMISSIONS[scope] %>
9
+ <li><%= PERMISSIONS[scope] %></li>
10
+ <% end %>
11
+ </ul>
12
+
13
+ <form method="post" action="/oauth/allow">
14
+ <% @oauth2.params.each do |key, value| %>
15
+ <input type="hidden" name="<%= key %>" value="<%= value %>">
16
+ <% end %>
17
+ <input type="hidden" name="user_id" value="<%= @user.id %>">
18
+
19
+ <fieldset>
20
+ <input type="checkbox" name="allow" id="allow" value="1">
21
+ <label for="allow">Allow this application</label>
22
+ </fieldset>
23
+
24
+ <fieldset>
25
+ <input type="submit" value="Go!">
26
+ </fieldset>
27
+ </form>
28
+
@@ -0,0 +1,3 @@
1
+ <h3>New User Created</h3>
2
+
3
+ <p>Your username is: <%= @user.username %></p>
@@ -0,0 +1,6 @@
1
+ <h3>Oh noes, an error!</h3>
2
+
3
+ <p>The application made an invalid OAuth request.</p>
4
+
5
+ <pre><%= @oauth2.error_description %></pre>
6
+
@@ -0,0 +1,24 @@
1
+ <p>Welcome to the <b>RockOAuth demo</b>. The endpoint you should direct
2
+ users to is:</p>
3
+
4
+ <pre> <%= host %>/oauth/authorize</pre>
5
+
6
+ <p>This handles both user authorization and token exchange requests. Before you
7
+ can use this though, you&rsquo;ll need to register your application.</p>
8
+
9
+ <ul>
10
+ <li><a href="/oauth/apps/new">Register your application</a></li>
11
+ </ul>
12
+
13
+ <p>This application is a note-taking app. It exposes a JSON API for reading a
14
+ user&rsquo;s notes, but you need an access token for this. Use the OAuth
15
+ protocol to get one, or see if you can hack in without permission!</p>
16
+
17
+ <p>The following resources are available. You&rsquo;ll need to ask the user for
18
+ the <b><code>read_notes</code></b> permission scope to get at them.</p>
19
+
20
+ <ul>
21
+ <li><code>/me</code> &mdash; returns the current user&rsquo;s data</li>
22
+ <li><code>/users/:username/notes</code></li>
23
+ <li><code>/users/:username/notes/:note_id</code></li>
24
+ </ul>
@@ -0,0 +1,24 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
5
+ <title>OAuth 2.0 demo</title>
6
+ <link rel="stylesheet" href="/style.css">
7
+ </head>
8
+ <body>
9
+
10
+ <div class="header"><div class="sub">
11
+ <h1>OAuth 2.0 demo</h1>
12
+ <h2>Steal my notes, why don&rsquo;t you</h2>
13
+ </div></div>
14
+
15
+ <div class="content"><div class="sub">
16
+ <%= yield %>
17
+ </div></div>
18
+
19
+ <div class="footer"><div class="sub">
20
+ <p>Copyright &copy; 2010 Songkick.com, 2014 Rocketmade.com</p>
21
+ </div></div>
22
+
23
+ </body>
24
+ </html>
@@ -0,0 +1,20 @@
1
+ <h3>Sign in</h3>
2
+
3
+ <p>Who are you? We&rsquo;d ask for a password usually, but seeing as it&rsquo;s
4
+ <em>you</em>&hellip;</p>
5
+
6
+ <form method="post" action="/login">
7
+ <% @oauth2.params.each do |key, value| %>
8
+ <input type="hidden" name="<%= key %>" value="<%= value %>">
9
+ <% end %>
10
+
11
+ <fieldset>
12
+ <label for="username">Username</label>
13
+ <input type="text" name="username" id="username">
14
+ </fieldset>
15
+
16
+ <fieldset>
17
+ <input type="submit" value="Sign in">
18
+ </fieldset>
19
+ </form>
20
+
@@ -0,0 +1,25 @@
1
+ <h3>Register you application</h3>
2
+
3
+ <% if @client.errors.any? %>
4
+ <ul class="errors">
5
+ <% @client.errors.full_messages.each do |message| %>
6
+ <li><%= message %></li>
7
+ <% end %>
8
+ </ul>
9
+ <% end %>
10
+
11
+ <form method="post" action="/oauth/apps">
12
+ <fieldset>
13
+ <label for="name">Application name</label>
14
+ <input type="text" name="name" id="name">
15
+ </fieldset>
16
+ <fieldset>
17
+ <label for="redirect_uri">Callback URI</label>
18
+ <input type="text" name="redirect_uri" id="redirect_uri">
19
+ </fieldset>
20
+
21
+ <fieldset>
22
+ <input type="submit" value="Register">
23
+ </fieldset>
24
+ </form>
25
+
@@ -0,0 +1,22 @@
1
+ <h3>Register a User</h3>
2
+
3
+ <% if @user.errors.any? %>
4
+ <ul class="errors">
5
+ <% @user.errors.full_messages.each do |message| %>
6
+ <li><%= message %></li>
7
+ <% end %>
8
+ </ul>
9
+ <% end %>
10
+
11
+ <form method="post" action="/users/create">
12
+ <fieldset>
13
+ <label for="name">Username</label>
14
+ <input type="text" name="username" id="username">
15
+ </fieldset>
16
+
17
+ <fieldset>
18
+ <input type="submit" value="Register">
19
+ </fieldset>
20
+ </form>
21
+
22
+
@@ -0,0 +1,15 @@
1
+ <h3>Client app: <%= @client.name %></h3>
2
+
3
+ <table>
4
+ <tbody>
5
+ <tr>
6
+ <th scope="row">client_id</th>
7
+ <td><%= @client.client_id %></td>
8
+ </tr>
9
+ <tr>
10
+ <th scope="row">client_secret</th>
11
+ <td><%= @client_secret %></td>
12
+ </tr>
13
+ </tbody>
14
+ </table>
15
+
@@ -0,0 +1,132 @@
1
+ module RockOAuth
2
+ module Model
3
+
4
+ class Authorization < ActiveRecord::Base
5
+ self.table_name = :oauth2_authorizations
6
+
7
+ belongs_to :oauth2_resource_owner, :polymorphic => true
8
+ alias :owner :oauth2_resource_owner
9
+ alias :owner= :oauth2_resource_owner=
10
+
11
+ belongs_to :client, :class_name => 'RockOAuth::Model::Client'
12
+
13
+ validates_presence_of :client, :owner
14
+
15
+ validates_uniqueness_of :code, :scope => :client_id, :allow_nil => true
16
+ validates_uniqueness_of :refresh_token_hash, :scope => :client_id, :allow_nil => true
17
+ validates_uniqueness_of :access_token_hash, :allow_nil => true
18
+
19
+ class << self
20
+ private :create, :new
21
+ end
22
+
23
+ extend Hashing
24
+ hashes_attributes :access_token, :refresh_token
25
+
26
+ def self.create_code(client)
27
+ RockOAuth.generate_id do |code|
28
+ Helpers.count(client.authorizations, :code => code).zero?
29
+ end
30
+ end
31
+
32
+ def self.create_access_token
33
+ RockOAuth.generate_id do |token|
34
+ hash = RockOAuth.hashify(token)
35
+ Helpers.count(self, :access_token_hash => hash).zero?
36
+ end
37
+ end
38
+
39
+ def self.create_refresh_token(client)
40
+ RockOAuth.generate_id do |refresh_token|
41
+ hash = RockOAuth.hashify(refresh_token)
42
+ Helpers.count(client.authorizations, :refresh_token_hash => hash).zero?
43
+ end
44
+ end
45
+
46
+ def self.for(owner, client, attributes = {})
47
+ return nil unless owner and client
48
+
49
+ unless client.is_a?(Client)
50
+ raise ArgumentError, "The argument should be a #{Client}, instead it was a #{client.class}"
51
+ end
52
+
53
+ instance = owner.oauth2_authorization_for(client) ||
54
+ new do |authorization|
55
+ authorization.owner = owner
56
+ authorization.client = client
57
+ end
58
+
59
+ case attributes[:response_type]
60
+ when CODE
61
+ instance.code ||= create_code(client)
62
+ when TOKEN
63
+ instance.access_token ||= create_access_token
64
+ instance.refresh_token ||= create_refresh_token(client)
65
+ when CODE_AND_TOKEN
66
+ instance.code = create_code(client)
67
+ instance.access_token ||= create_access_token
68
+ instance.refresh_token ||= create_refresh_token(client)
69
+ end
70
+
71
+ if attributes[:duration]
72
+ instance.expires_at = Time.now + attributes[:duration].to_i
73
+ else
74
+ instance.expires_at = nil
75
+ end
76
+
77
+ scopes = instance.scopes + (attributes[:scopes] || [])
78
+ scopes += attributes[:scope].split(/\s+/) if attributes[:scope]
79
+ instance.scope = scopes.empty? ? nil : scopes.entries.join(' ')
80
+
81
+ instance.save && instance
82
+
83
+ rescue Object => error
84
+ if Model.duplicate_record_error?(error)
85
+ retry
86
+ else
87
+ raise error
88
+ end
89
+ end
90
+
91
+ def exchange!
92
+ self.code = nil
93
+ self.access_token = self.class.create_access_token
94
+ self.refresh_token = nil
95
+ save!
96
+ end
97
+
98
+ def expired?
99
+ return false unless expires_at
100
+ expires_at < Time.now
101
+ end
102
+
103
+ def expires_in
104
+ expires_at && (expires_at - Time.now).ceil
105
+ end
106
+
107
+ def generate_code
108
+ self.code ||= self.class.create_code(client)
109
+ save && code
110
+ end
111
+
112
+ def generate_access_token
113
+ self.access_token ||= self.class.create_access_token
114
+ save && access_token
115
+ end
116
+
117
+ def grants_access?(user, *scopes)
118
+ not expired? and user == owner and in_scope?(scopes)
119
+ end
120
+
121
+ def in_scope?(request_scope)
122
+ [*request_scope].all?(&scopes.method(:include?))
123
+ end
124
+
125
+ def scopes
126
+ scopes = scope ? scope.split(/\s+/) : []
127
+ Set.new(scopes)
128
+ end
129
+ end
130
+
131
+ end
132
+ end
@@ -0,0 +1,54 @@
1
+ module RockOAuth
2
+ module Model
3
+
4
+ class Client < ActiveRecord::Base
5
+ self.table_name = :oauth2_clients
6
+
7
+ belongs_to :oauth2_client_owner, :polymorphic => true
8
+ alias :owner :oauth2_client_owner
9
+ alias :owner= :oauth2_client_owner=
10
+
11
+ has_many :authorizations, :class_name => 'RockOAuth::Model::Authorization', :dependent => :destroy
12
+
13
+ validates_uniqueness_of :client_id, :name
14
+ validates_presence_of :name, :redirect_uri
15
+ validate :check_format_of_redirect_uri
16
+
17
+ before_create :generate_credentials
18
+
19
+ def self.create_client_id
20
+ RockOAuth.generate_id do |client_id|
21
+ Helpers.count(self, :client_id => client_id).zero?
22
+ end
23
+ end
24
+
25
+ attr_reader :client_secret
26
+
27
+ def client_secret=(secret)
28
+ @client_secret = secret
29
+ hash = BCrypt::Password.create(secret)
30
+ hash.force_encoding('UTF-8') if hash.respond_to?(:force_encoding)
31
+ self.client_secret_hash = hash
32
+ end
33
+
34
+ def valid_client_secret?(secret)
35
+ BCrypt::Password.new(client_secret_hash) == secret
36
+ end
37
+
38
+ private
39
+
40
+ def check_format_of_redirect_uri
41
+ uri = URI.parse(redirect_uri)
42
+ errors.add(:redirect_uri, 'must be an absolute URI') unless uri.absolute?
43
+ rescue
44
+ errors.add(:redirect_uri, 'must be a URI')
45
+ end
46
+
47
+ def generate_credentials
48
+ self.client_id = self.class.create_client_id
49
+ self.client_secret = RockOAuth.random_string
50
+ end
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,13 @@
1
+ module RockOAuth
2
+ module Model
3
+
4
+ module ClientOwner
5
+ def self.included(klass)
6
+ klass.has_many :oauth2_clients,
7
+ :class_name => 'RockOAuth::Model::Client',
8
+ :as => :oauth2_client_owner
9
+ end
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ module RockOAuth
2
+ module Model
3
+
4
+ module Hashing
5
+ def hashes_attributes(*attributes)
6
+ attributes.each do |attribute|
7
+ define_method("#{attribute}=") do |value|
8
+ instance_variable_set("@#{attribute}", value)
9
+ __send__("#{attribute}_hash=", value && RockOAuth.hashify(value))
10
+ end
11
+ attr_reader attribute
12
+ end
13
+
14
+ class_eval <<-RUBY
15
+ def reload(*args)
16
+ super
17
+ #{ attributes.inspect }.each do |attribute|
18
+ instance_variable_set('@' + attribute.to_s, nil)
19
+ end
20
+ end
21
+ RUBY
22
+ end
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,14 @@
1
+ module RockOAuth
2
+ module Model
3
+
4
+ module Helpers
5
+ def self.count(model, conditions={})
6
+ if model.respond_to?(:where)
7
+ model.where(conditions).count
8
+ else
9
+ model.count(:conditions => conditions)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ module RockOAuth
2
+ module Model
3
+
4
+ module ResourceOwner
5
+ def self.included(klass)
6
+ klass.has_many :oauth2_authorizations,
7
+ :class_name => Authorization.name,
8
+ :as => :oauth2_resource_owner,
9
+ :dependent => :destroy
10
+ end
11
+
12
+ def grant_access!(client, options = {})
13
+ Authorization.for(self, client, options)
14
+ end
15
+
16
+ def oauth2_authorization_for(client)
17
+ oauth2_authorizations.find_by_client_id(client.id)
18
+ end
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,38 @@
1
+ require 'active_record'
2
+
3
+ module RockOAuth
4
+ module Model
5
+ autoload :Helpers, ROOT + '/model/helpers'
6
+ autoload :ClientOwner, ROOT + '/model/client_owner'
7
+ autoload :ResourceOwner, ROOT + '/model/resource_owner'
8
+ autoload :Hashing, ROOT + '/model/hashing'
9
+ autoload :Authorization, ROOT + '/model/authorization'
10
+ autoload :Client, ROOT + '/model/client'
11
+
12
+ Schema = RockOAuth::Schema
13
+
14
+ DUPLICATE_RECORD_ERRORS = [
15
+ /^Mysql::Error:\s+Duplicate\s+entry\b/,
16
+ /^PG::Error:\s+ERROR:\s+duplicate\s+key\b/,
17
+ /\bConstraintException\b/
18
+ ]
19
+
20
+ # ActiveRecord::RecordNotUnique was introduced in Rails 3.0 so referring
21
+ # to it while running earlier versions will raise an error. The above
22
+ # error strings should match PostgreSQL, MySQL and SQLite errors on
23
+ # Rails 2. If you're running a different adapter, add a suitable regex to
24
+ # the list:
25
+ #
26
+ # RockOAuth::Model::DUPLICATE_RECORD_ERRORS << /DB2 found a dup/
27
+ #
28
+ def self.duplicate_record_error?(error)
29
+ error.class.name == 'ActiveRecord::RecordNotUnique' or
30
+ DUPLICATE_RECORD_ERRORS.any? { |re| re =~ error.message }
31
+ end
32
+
33
+ def self.find_access_token(access_token)
34
+ return nil if access_token.nil?
35
+ Authorization.find_by_access_token_hash(RockOAuth.hashify(access_token))
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,70 @@
1
+ module RockOAuth
2
+ class Provider
3
+
4
+ class AccessToken
5
+ attr_reader :authorization
6
+
7
+ def initialize(resource_owner = nil, scopes = [], access_token = nil, error = nil)
8
+ @resource_owner = resource_owner
9
+ @scopes = scopes
10
+ @access_token = access_token
11
+ @error = error && INVALID_REQUEST
12
+
13
+ authorize!(access_token, error)
14
+ validate!
15
+ end
16
+
17
+ def client
18
+ valid? ? @authorization.client : nil
19
+ end
20
+
21
+ def owner
22
+ valid? ? @authorization.owner : nil
23
+ end
24
+
25
+ def response_headers
26
+ return {} if valid?
27
+ error_message = "OAuth realm='#{ Provider.realm }'"
28
+ error_message << ", error='#{ @error }'" unless @error == ''
29
+ {'WWW-Authenticate' => error_message}
30
+ end
31
+
32
+ def response_status
33
+ case @error
34
+ when INVALID_REQUEST, INVALID_TOKEN, EXPIRED_TOKEN then 401
35
+ when INSUFFICIENT_SCOPE then 403
36
+ when '' then 401
37
+ else 200
38
+ end
39
+ end
40
+
41
+ def valid?
42
+ @error.nil?
43
+ end
44
+
45
+ private
46
+
47
+ def authorize!(access_token, error)
48
+ return unless @authorization = Model.find_access_token(access_token)
49
+ @authorization.update_attribute(:access_token, nil) if error
50
+ end
51
+
52
+ def validate!
53
+ return @error = '' unless @access_token
54
+ return @error = INVALID_TOKEN unless @authorization
55
+ return @error = EXPIRED_TOKEN if @authorization.expired?
56
+ return @error = INSUFFICIENT_SCOPE unless @authorization.in_scope?(@scopes)
57
+
58
+ case @resource_owner
59
+ when :implicit
60
+ # no error
61
+ when nil
62
+ @error = INVALID_TOKEN
63
+ else
64
+ @error = INSUFFICIENT_SCOPE if @authorization.owner != @resource_owner
65
+ end
66
+ end
67
+ end
68
+
69
+ end
70
+ end