refinerycms-authentication 0.9.9.1
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.
- data/app/controllers/admin/users_controller.rb +90 -0
- data/app/controllers/passwords_controller.rb +43 -0
- data/app/controllers/registrations_controller.rb +67 -0
- data/app/controllers/sessions_controller.rb +23 -0
- data/app/helpers/sessions_helper.rb +2 -0
- data/app/helpers/users_helper.rb +2 -0
- data/app/mailers/user_mailer.rb +20 -0
- data/app/models/role.rb +16 -0
- data/app/models/roles_users.rb +6 -0
- data/app/models/user.rb +60 -0
- data/app/models/user_plugin.rb +5 -0
- data/app/views/admin/users/_form.html.erb +92 -0
- data/app/views/admin/users/_user.html.erb +19 -0
- data/app/views/admin/users/_users.html.erb +4 -0
- data/app/views/admin/users/edit.html.erb +1 -0
- data/app/views/admin/users/index.html.erb +12 -0
- data/app/views/admin/users/new.html.erb +1 -0
- data/app/views/layouts/login.html.erb +21 -0
- data/app/views/passwords/edit.html.erb +31 -0
- data/app/views/passwords/new.html.erb +18 -0
- data/app/views/registrations/new.html.erb +41 -0
- data/app/views/sessions/new.html.erb +29 -0
- data/app/views/user_mailer/reset_notification.html.erb +12 -0
- data/app/views/user_mailer/reset_notification.text.plain.erb +7 -0
- data/config/locales/cs.yml +75 -0
- data/config/locales/da.yml +72 -0
- data/config/locales/de.yml +72 -0
- data/config/locales/el.yml +72 -0
- data/config/locales/en.yml +72 -0
- data/config/locales/es.yml +100 -0
- data/config/locales/fr.yml +72 -0
- data/config/locales/it.yml +97 -0
- data/config/locales/lolcat.yml +55 -0
- data/config/locales/lt.yml +55 -0
- data/config/locales/lv.yml +72 -0
- data/config/locales/nb.yml +72 -0
- data/config/locales/nl.yml +70 -0
- data/config/locales/pl.yml +100 -0
- data/config/locales/pt-BR.yml +68 -0
- data/config/locales/rs.yml +72 -0
- data/config/locales/ru.yml +97 -0
- data/config/locales/sl.yml +61 -0
- data/config/locales/sv.yml +64 -0
- data/config/locales/vi.yml +72 -0
- data/config/locales/zh-CN.yml +72 -0
- data/config/locales/zh-TW.yml +72 -0
- data/config/routes.rb +31 -0
- data/db/migrate/20100913234705_create_refinerycms_authentication_schema.rb +43 -0
- data/db/migrate/20100929035252_add_missing_indexes_to_roles_users.rb +11 -0
- data/db/migrate/20101206013505_change_to_devise_users_table.rb +27 -0
- data/db/migrate/20110106184757_add_remember_created_at_to_users.rb +9 -0
- data/features/lost_password.feature +49 -0
- data/features/manage_users.feature +61 -0
- data/features/step_definitions/lost_password.rb +8 -0
- data/features/step_definitions/user_steps.rb +36 -0
- data/features/support/factories.rb +18 -0
- data/features/support/paths.rb +24 -0
- data/lib/authenticated_system.rb +29 -0
- data/lib/gemspec.rb +34 -0
- data/lib/generators/refinerycms_authentication_generator.rb +8 -0
- data/lib/refinerycms-authentication.rb +47 -0
- data/license.md +21 -0
- data/readme.md +17 -0
- data/refinerycms-authentication.gemspec +112 -0
- data/spec/models/user_spec.rb +159 -0
- metadata +144 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
class CreateRefinerycmsAuthenticationSchema < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
# Postgres apparently requires the roles_users table to exist before creating the roles table.
|
4
|
+
create_table ::RolesUsers.table_name, :id => false, :force => true do |t|
|
5
|
+
t.integer "user_id"
|
6
|
+
t.integer "role_id"
|
7
|
+
end
|
8
|
+
|
9
|
+
create_table ::Role.table_name, :force => true do |t|
|
10
|
+
t.string "title"
|
11
|
+
end
|
12
|
+
|
13
|
+
create_table ::UserPlugin.table_name, :force => true do |t|
|
14
|
+
t.integer "user_id"
|
15
|
+
t.string "name"
|
16
|
+
t.integer "position"
|
17
|
+
end
|
18
|
+
|
19
|
+
add_index ::UserPlugin.table_name, ["name"], :name => "index_#{::UserPlugin.table_name}_on_title"
|
20
|
+
add_index ::UserPlugin.table_name, ["user_id", "name"], :name => "index_unique_#{::UserPlugin.table_name}", :unique => true
|
21
|
+
|
22
|
+
create_table ::User.table_name, :force => true do |t|
|
23
|
+
t.string "login", :null => false
|
24
|
+
t.string "email", :null => false
|
25
|
+
t.string "crypted_password", :null => false
|
26
|
+
t.string "password_salt", :null => false
|
27
|
+
t.string "persistence_token"
|
28
|
+
t.datetime "created_at"
|
29
|
+
t.datetime "updated_at"
|
30
|
+
t.string "perishable_token"
|
31
|
+
end
|
32
|
+
|
33
|
+
add_index ::User.table_name, ["id"], :name => "index_#{::User.table_name}_on_id"
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.down
|
37
|
+
[::User].reject{|m|
|
38
|
+
!(defined?(m) and m.respond_to?(:table_name))
|
39
|
+
}.each do |model|
|
40
|
+
drop_table model.table_name
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class AddMissingIndexesToRolesUsers < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
add_index ::RolesUsers.table_name, [:role_id, :user_id]
|
4
|
+
add_index ::RolesUsers.table_name, [:user_id, :role_id]
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.down
|
8
|
+
remove_index ::RolesUsers.table_name, :column => [:role_id, :user_id]
|
9
|
+
remove_index ::RolesUsers.table_name, :column => [:user_id, :role_id]
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class ChangeToDeviseUsersTable < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
add_column ::User.table_name, :current_sign_in_at, :datetime
|
4
|
+
add_column ::User.table_name, :last_sign_in_at, :datetime
|
5
|
+
add_column ::User.table_name, :current_sign_in_ip, :string
|
6
|
+
add_column ::User.table_name, :last_sign_in_ip, :string
|
7
|
+
add_column ::User.table_name, :sign_in_count, :integer
|
8
|
+
add_column ::User.table_name, :remember_token, :string
|
9
|
+
add_column ::User.table_name, :reset_password_token, :string
|
10
|
+
|
11
|
+
rename_column ::User.table_name, :crypted_password, :encrypted_password
|
12
|
+
rename_column ::User.table_name, :login, :username
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.down
|
16
|
+
remove_column ::User.table_name, :current_sign_in_at
|
17
|
+
remove_column ::User.table_name, :last_sign_in_at
|
18
|
+
remove_column ::User.table_name, :current_sign_in_ip
|
19
|
+
remove_column ::User.table_name, :last_sign_in_ip
|
20
|
+
remove_column ::User.table_name, :sign_in_count
|
21
|
+
remove_column ::User.table_name, :remember_token
|
22
|
+
remove_column ::User.table_name, :reset_password_token
|
23
|
+
|
24
|
+
rename_column ::User.table_name, :encrypted_password, :crypted_password
|
25
|
+
rename_column ::User.table_name, :username, :login
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
@refinerycms @users @users-password
|
2
|
+
Feature: Lost Password
|
3
|
+
In order to restore my password
|
4
|
+
As a lost soul
|
5
|
+
I want to reset my password
|
6
|
+
|
7
|
+
Background:
|
8
|
+
Given A Refinery user exists
|
9
|
+
|
10
|
+
@users-password-forgot
|
11
|
+
Scenario: Forgot Password page (no email entered)
|
12
|
+
And I am on the forgot password page
|
13
|
+
When I press "Reset password"
|
14
|
+
Then I should see "You did not enter an email address."
|
15
|
+
|
16
|
+
@users-password-forgot
|
17
|
+
Scenario: Forgot Password page (non existing email entered)
|
18
|
+
Given I am on the forgot password page
|
19
|
+
And I have a user with email "green@cukes.com"
|
20
|
+
When I fill in "user_email" with "none@cukes.com"
|
21
|
+
And I press "Reset password"
|
22
|
+
Then I should see "Sorry, 'none@cukes.com' isn't associated with any accounts."
|
23
|
+
And I should see "Are you sure you typed the correct email address?"
|
24
|
+
|
25
|
+
@users-password-forgot
|
26
|
+
Scenario: Forgot Password page (existing email entered)
|
27
|
+
Given I am on the forgot password page
|
28
|
+
And I have a user with email "green@cukes.com"
|
29
|
+
When I fill in "user_email" with "green@cukes.com"
|
30
|
+
And I press "Reset password"
|
31
|
+
Then I should see "An email has been sent to you with a link to reset your password."
|
32
|
+
|
33
|
+
@users-password-reset
|
34
|
+
Scenario: Reset password page (invalid reset_code)
|
35
|
+
Given I am not requesting password reset
|
36
|
+
When I go to the reset password page
|
37
|
+
Then I should be on the forgot password page
|
38
|
+
And I should see "We're sorry, but this reset code has expired or is invalid."
|
39
|
+
And I should see "If you are having issues try copying and pasting the URL from your email into your browser or restarting the reset password process."
|
40
|
+
|
41
|
+
@users-password-reset
|
42
|
+
Scenario: Reset password page (valid reset_code)
|
43
|
+
Given I am requesting password reset
|
44
|
+
When I go to the reset password page
|
45
|
+
And I fill in "Password" with "cukes"
|
46
|
+
And I fill in "Password confirmation" with "cukes"
|
47
|
+
And I press "Reset password"
|
48
|
+
Then I should be on the admin root
|
49
|
+
And I should see "Password reset successfully for"
|
@@ -0,0 +1,61 @@
|
|
1
|
+
@refinerycms @users @users-manage
|
2
|
+
Feature: Manage Users
|
3
|
+
In order to control who can access my website's backend
|
4
|
+
As an administrator
|
5
|
+
I want to create and manage users
|
6
|
+
|
7
|
+
Background:
|
8
|
+
Given I have no users
|
9
|
+
|
10
|
+
Scenario: When there are no users, you are invited to create a user
|
11
|
+
When I go to the home page
|
12
|
+
Then I should see "There are no users yet, so we'll set you up first."
|
13
|
+
|
14
|
+
@users-add @add
|
15
|
+
Scenario: When there are no users, you can create a user
|
16
|
+
When I go to the home page
|
17
|
+
And I follow "Continue..."
|
18
|
+
And I should see "Fill out your details below so that we can get you started."
|
19
|
+
And I fill in "Username" with "cucumber"
|
20
|
+
And I fill in "Email" with "green@cucumber.com"
|
21
|
+
And I fill in "Password" with "greenandjuicy"
|
22
|
+
And I fill in "Password confirmation" with "greenandjuicy"
|
23
|
+
And I press "Sign up"
|
24
|
+
Then I should see "Welcome to Refinery, cucumber."
|
25
|
+
And I should see "Latest Activity"
|
26
|
+
And I should have 1 user
|
27
|
+
|
28
|
+
@users-list @list
|
29
|
+
Scenario: User List
|
30
|
+
Given I have a user named "steven"
|
31
|
+
And I am a logged in refinery user
|
32
|
+
When I go to the list of users
|
33
|
+
Then I should see "steven"
|
34
|
+
|
35
|
+
@users-add @add
|
36
|
+
Scenario: Create User
|
37
|
+
Given I have a user named "steven"
|
38
|
+
And I am a logged in refinery user
|
39
|
+
When I go to the list of users
|
40
|
+
And I follow "Add new user"
|
41
|
+
And I fill in "Username" with "cucumber"
|
42
|
+
And I fill in "Email" with "green@cucumber.com"
|
43
|
+
And I fill in "Password" with "greenandjuicy"
|
44
|
+
And I fill in "Password confirmation" with "greenandjuicy"
|
45
|
+
And I press "Save"
|
46
|
+
Then I should be on the list of users
|
47
|
+
And I should see "cucumber was successfully added."
|
48
|
+
And I should see "cucumber (green@cucumber.com)"
|
49
|
+
|
50
|
+
@users-edit @edit
|
51
|
+
Scenario: Edit User
|
52
|
+
Given I have a user named "steven"
|
53
|
+
And I am a logged in refinery user
|
54
|
+
When I go to the list of users
|
55
|
+
And I follow "Edit this user"
|
56
|
+
And I fill in "Username" with "cucumber"
|
57
|
+
And I fill in "Email" with "green@cucumber.com"
|
58
|
+
And I press "Save"
|
59
|
+
Then I should be on the list of users
|
60
|
+
And I should see "cucumber was successfully updated."
|
61
|
+
And I should see "cucumber (green@cucumber.com)"
|
@@ -0,0 +1,8 @@
|
|
1
|
+
Given /^I have a user with email "(.*)"$/ do |email|
|
2
|
+
Factory(:refinery_user, :email => email)
|
3
|
+
end
|
4
|
+
|
5
|
+
Given /^I am (not )?requesting password reset$/ do |action|
|
6
|
+
@user = Factory(:refinery_user, :updated_at => 11.minutes.ago)
|
7
|
+
@user.send(:generate_reset_password_token!) if action.nil?
|
8
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
def login
|
2
|
+
visit new_user_session_path
|
3
|
+
fill_in("user_login", :with => @user.email)
|
4
|
+
fill_in("user_password", :with => 'greenandjuicy')
|
5
|
+
click_button("submit_button")
|
6
|
+
end
|
7
|
+
|
8
|
+
Given /^I am a logged in refinery user$/ do
|
9
|
+
@user ||= Factory(:refinery_user)
|
10
|
+
login
|
11
|
+
end
|
12
|
+
|
13
|
+
Given /^I am a logged in customer$/ do
|
14
|
+
@user ||= Factory(:user)
|
15
|
+
login
|
16
|
+
end
|
17
|
+
|
18
|
+
Given /^A Refinery user exists$/ do
|
19
|
+
@refinery_user ||= Factory(:refinery_user)
|
20
|
+
end
|
21
|
+
|
22
|
+
Given /^I have a user named "(.*)"$/ do |name|
|
23
|
+
Factory(:user, :username => name)
|
24
|
+
end
|
25
|
+
|
26
|
+
Given /^I have a R|refinery user named "(.*)"$/ do |name|
|
27
|
+
Factory(:refinery_user, :username => name)
|
28
|
+
end
|
29
|
+
|
30
|
+
Given /^I have no users$/ do
|
31
|
+
User.delete_all
|
32
|
+
end
|
33
|
+
|
34
|
+
Then /^I should have ([0-9]+) users?$/ do |count|
|
35
|
+
User.count.should == count.to_i
|
36
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'factory_girl'
|
2
|
+
|
3
|
+
Factory.define :user do |u|
|
4
|
+
u.sequence(:username) { |n| "person#{n}" }
|
5
|
+
u.sequence(:email) { |n| "person#{n}@cucumber.com" }
|
6
|
+
u.password "greenandjuicy"
|
7
|
+
u.password_confirmation "greenandjuicy"
|
8
|
+
end
|
9
|
+
|
10
|
+
Factory.define :refinery_user, :parent => :user do |u|
|
11
|
+
u.roles { [ Role[:refinery] ] }
|
12
|
+
|
13
|
+
u.after_create do |user|
|
14
|
+
Refinery::Plugins.registered.each_with_index do |plugin, index|
|
15
|
+
user.plugins.create(:name => plugin.name, :position => index)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module NavigationHelpers
|
2
|
+
module Refinery
|
3
|
+
module Authentication
|
4
|
+
def path_to(page_name)
|
5
|
+
case page_name
|
6
|
+
|
7
|
+
when /the list of users/
|
8
|
+
admin_users_path
|
9
|
+
|
10
|
+
when /the login page/
|
11
|
+
new_user_session_path
|
12
|
+
|
13
|
+
when /the forgot password page/
|
14
|
+
new_user_password_path
|
15
|
+
|
16
|
+
when /the reset password page/
|
17
|
+
edit_user_password_path(:reset_password_token => @user.reset_password_token)
|
18
|
+
else
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module AuthenticatedSystem
|
2
|
+
protected
|
3
|
+
# Store the URI of the current request in the session.
|
4
|
+
#
|
5
|
+
# We can return to this location by calling #redirect_back_or_default.
|
6
|
+
def store_location
|
7
|
+
session[:return_to] = request.fullpath
|
8
|
+
end
|
9
|
+
|
10
|
+
# Redirect to the URI stored by the most recent store_location call or
|
11
|
+
# to the passed default.
|
12
|
+
def redirect_back_or_default(default)
|
13
|
+
redirect_to(session[:return_to] || default)
|
14
|
+
session[:return_to] = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
#def current_user
|
18
|
+
#current_user
|
19
|
+
#end
|
20
|
+
|
21
|
+
def refinery_user?
|
22
|
+
user_signed_in? && current_user.has_role?(:refinery)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.included(base)
|
26
|
+
base.send :helper_method, :current_user, :current_user_session, :user_signed_in?, :refinery_user? if base.respond_to? :helper_method
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
data/lib/gemspec.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
gempath = Pathname.new(File.expand_path('../../', __FILE__))
|
3
|
+
require gempath.join('..', 'base', 'lib', 'base', 'refinery')
|
4
|
+
|
5
|
+
gemspec = <<EOF
|
6
|
+
# DO NOT EDIT THIS FILE DIRECTLY! Instead, use lib/gemspec.rb to generate it.
|
7
|
+
|
8
|
+
Gem::Specification.new do |s|
|
9
|
+
s.name = %q{#{gemname = 'refinerycms-authentication'}}
|
10
|
+
s.version = %q{#{::Refinery.version}}
|
11
|
+
s.summary = %q{Authentication engine for Refinery CMS}
|
12
|
+
s.description = %q{The default authentication engine for Refinery CMS}
|
13
|
+
s.date = %q{#{Time.now.strftime('%Y-%m-%d')}}
|
14
|
+
s.email = %q{info@refinerycms.com}
|
15
|
+
s.homepage = %q{http://refinerycms.com}
|
16
|
+
s.rubyforge_project = %q{refinerycms}
|
17
|
+
s.authors = ['Resolve Digital', 'Philip Arndt', 'David Jones', 'Steven Heidel']
|
18
|
+
s.license = %q{MIT}
|
19
|
+
s.require_paths = %w(lib)
|
20
|
+
s.executables = %w(#{Pathname.glob(gempath.join('bin/*')).map{|d| d.relative_path_from(gempath)}.sort.join(" ")})
|
21
|
+
|
22
|
+
s.add_dependency 'refinerycms-core', '~> #{::Refinery::Version}'
|
23
|
+
s.add_dependency 'devise', '~> 1.1'
|
24
|
+
|
25
|
+
s.files = [
|
26
|
+
'#{%w( **/{*,.rspec,.gitignore,.yardopts} ).map { |file| Pathname.glob(gempath.join(file)) }.flatten.reject{|f|
|
27
|
+
!f.exist? or f.to_s =~ /\.gem$/ or (f.directory? and f.children.empty?)
|
28
|
+
}.map{|d| d.relative_path_from(gempath)}.uniq.sort.join("',\n '")}'
|
29
|
+
]
|
30
|
+
end
|
31
|
+
EOF
|
32
|
+
|
33
|
+
(gemfile = gempath.join("#{gemname}.gemspec")).open('w') {|f| f.puts(gemspec)}
|
34
|
+
puts `cd #{gempath} && gem build #{gemfile}` if ARGV.any?{|a| a == "BUILD=true"}
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'devise'
|
2
|
+
require 'refinerycms-core'
|
3
|
+
# Attach authenticated system methods to the ::Refinery::ApplicationController
|
4
|
+
require File.expand_path('../authenticated_system', __FILE__)
|
5
|
+
[::Refinery::ApplicationController, ::Refinery::ApplicationHelper].each do |c|
|
6
|
+
c.class_eval {
|
7
|
+
include AuthenticatedSystem
|
8
|
+
}
|
9
|
+
end
|
10
|
+
|
11
|
+
module Refinery
|
12
|
+
module Authentication
|
13
|
+
|
14
|
+
class Engine < ::Rails::Engine
|
15
|
+
config.autoload_paths += %W( #{config.root}/lib )
|
16
|
+
|
17
|
+
config.after_initialize do
|
18
|
+
::Refinery::Plugin.register do |plugin|
|
19
|
+
plugin.name = "refinery_users"
|
20
|
+
plugin.version = %q{0.9.9}
|
21
|
+
plugin.menu_match = /(refinery|admin)\/users$/
|
22
|
+
plugin.activity = {
|
23
|
+
:class => User,
|
24
|
+
:title => 'login'
|
25
|
+
}
|
26
|
+
plugin.url = {:controller => "/admin/users"}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class << self
|
32
|
+
attr_accessor :root
|
33
|
+
def root
|
34
|
+
@root ||= Pathname.new(File.expand_path('../../', __FILE__))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class << self
|
40
|
+
attr_accessor :authentication_login_field
|
41
|
+
def authentication_login_field
|
42
|
+
@authentication_login_field ||= 'login'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
::Refinery.engines << 'authentication'
|
data/license.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2005-2010 [Resolve Digital](http://www.resolvedigital.com)
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|