rom-auth 0.0.4
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.
- checksums.yaml +7 -0
- data/README.md +63 -0
- data/lib/rom-auth.rb +6 -0
- data/lib/rom/auth.rb +34 -0
- data/lib/rom/auth/authenticators/authenticator.rb +11 -0
- data/lib/rom/auth/authenticators/password_authenticator.rb +14 -0
- data/lib/rom/auth/configuration.rb +45 -0
- data/lib/rom/auth/digest.rb +34 -0
- data/lib/rom/auth/migration.rb +25 -0
- data/lib/rom/auth/password_verifiers/password_verifier.rb +69 -0
- data/lib/rom/auth/password_verifiers/pbkdf2_verifier.rb +24 -0
- data/lib/rom/auth/plugins/authentication_credentials_plugin.rb +115 -0
- data/lib/rom/auth/plugins/authentication_events_plugin.rb +92 -0
- data/lib/rom/auth/plugins/lockdown_plugin.rb +114 -0
- data/lib/rom/auth/plugins/plugin.rb +22 -0
- data/lib/rom/auth/support/shorthand_symbol.rb +22 -0
- data/lib/rom/auth/system.rb +146 -0
- data/lib/rom/auth/version.rb +5 -0
- data/spec/integration/common_plugins_spec.rb +162 -0
- data/spec/integration/system_spec.rb +68 -0
- data/spec/unit/rom/auth/configuration_spec.rb +27 -0
- data/spec/unit/rom/auth/password_verifiers/pbkdf2_verifier_spec.rb +30 -0
- data/spec/unit/rom/auth/plugins/authentication_events_plugin_spec.rb +109 -0
- data/spec/unit/rom/auth_spec.rb +3 -0
- metadata +242 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
module ROM::Auth
|
2
|
+
module Plugins
|
3
|
+
class AuthenticationEventsPlugin < Plugin
|
4
|
+
|
5
|
+
class Configuration
|
6
|
+
include Virtus.model
|
7
|
+
|
8
|
+
attribute :table_name, Symbol, default: :authentication_events
|
9
|
+
end
|
10
|
+
|
11
|
+
def install
|
12
|
+
system.extend(CallbackOverrides)
|
13
|
+
|
14
|
+
config = configuration
|
15
|
+
|
16
|
+
@mapper = Class.new(ROM::Mapper) do
|
17
|
+
relation(config.table_name)
|
18
|
+
model(AuthenticationEvent)
|
19
|
+
register_as :rom_auth_event
|
20
|
+
end
|
21
|
+
|
22
|
+
@relation = Class.new(ROM::Relation[:sql]) do
|
23
|
+
dataset(config.table_name)
|
24
|
+
end
|
25
|
+
|
26
|
+
@command = Class.new(ROM::Commands::Create[:sql]) do
|
27
|
+
register_as :create
|
28
|
+
relation(config.table_name)
|
29
|
+
result :one
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def migrate(setup)
|
34
|
+
AuthenticationEventsMigration.new(system, setup, configuration).run
|
35
|
+
end
|
36
|
+
|
37
|
+
class AuthenticationEvent
|
38
|
+
include Virtus.value_object(coerce: false)
|
39
|
+
|
40
|
+
values do
|
41
|
+
attribute :user_id, Integer # FIXME should be dynamic and could be account_id
|
42
|
+
attribute :success, Boolean
|
43
|
+
attribute :started_at, DateTime
|
44
|
+
attribute :ended_at, DateTime
|
45
|
+
attribute :identifier, String
|
46
|
+
attribute :type, String
|
47
|
+
attribute :authenticated, Boolean
|
48
|
+
attribute :data, String
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class AuthenticationEventsMigration < Migration
|
53
|
+
def run
|
54
|
+
auth_config = system.configuration
|
55
|
+
config = self.config
|
56
|
+
user_fk_name = auth_config.user_fk_name
|
57
|
+
|
58
|
+
database.create_table(config.table_name) do
|
59
|
+
primary_key :id
|
60
|
+
foreign_key(user_fk_name, auth_config.users_table_name.to_sym)
|
61
|
+
DateTime :started_at
|
62
|
+
DateTime :ended_at
|
63
|
+
String :identifier
|
64
|
+
String :type
|
65
|
+
Boolean :authenticated
|
66
|
+
Boolean :success
|
67
|
+
String :data
|
68
|
+
# TODO login_ip, failure reason
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
module CallbackOverrides
|
74
|
+
def on_authentication_completed(data)
|
75
|
+
super
|
76
|
+
|
77
|
+
# ROM.env.command(plugins[AuthenticationEventsPlugin].configuration.table_name).try{
|
78
|
+
# create(data)
|
79
|
+
# }
|
80
|
+
ROM.env.command(plugins[AuthenticationEventsPlugin].configuration.table_name).create.call(data)
|
81
|
+
|
82
|
+
# TODO
|
83
|
+
# - log origin/details & warn user
|
84
|
+
# - compute failed logins per minute and alert if above threshold
|
85
|
+
# - throttling
|
86
|
+
#
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module ROM::Auth
|
2
|
+
module Plugins
|
3
|
+
class LockdownPlugin < Plugin
|
4
|
+
|
5
|
+
class Configuration
|
6
|
+
include Virtus.model
|
7
|
+
|
8
|
+
attribute :table_name, Symbol, default: nil
|
9
|
+
attribute :lock_strategy, Object
|
10
|
+
attribute :unlock_strategy, Object
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(*args)
|
14
|
+
super
|
15
|
+
|
16
|
+
# FIXME move to configuration?
|
17
|
+
configuration.table_name ||= system.configuration.users_table_name
|
18
|
+
end
|
19
|
+
|
20
|
+
def install
|
21
|
+
system.extend(CallbackOverrides)
|
22
|
+
|
23
|
+
config = configuration
|
24
|
+
|
25
|
+
@relation = Class.new(ROM::Relation[:sql]) do
|
26
|
+
register_as :rom_auth_lockdowns
|
27
|
+
dataset(config.table_name)
|
28
|
+
|
29
|
+
def by_user_id(user_id)
|
30
|
+
where(id: user_id)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
@mapper = Class.new(ROM::Mapper) do
|
35
|
+
relation(:rom_auth_lockdowns)
|
36
|
+
model(Lockdown)
|
37
|
+
register_as :rom_auth_lockdown
|
38
|
+
end
|
39
|
+
|
40
|
+
@command = Class.new(ROM::Commands::Update[:sql]) do
|
41
|
+
register_as :update
|
42
|
+
relation(:rom_auth_lockdowns)
|
43
|
+
result :one
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def migrate(setup)
|
48
|
+
LockdownMigration.new(system, setup, configuration).run
|
49
|
+
end
|
50
|
+
|
51
|
+
def lock_strategy
|
52
|
+
configuration.lock_strategy
|
53
|
+
end
|
54
|
+
|
55
|
+
def unlock_strategy
|
56
|
+
configuration.unlock_strategy
|
57
|
+
end
|
58
|
+
|
59
|
+
def is_locked?(user_id)
|
60
|
+
#user.locked_at.present?
|
61
|
+
ROM.env.relation(:rom_auth_lockdowns).by_user_id(user_id).as(:rom_auth_lockdown).first.try(:locked_at).present?
|
62
|
+
end
|
63
|
+
|
64
|
+
def lock(user_id, reason)
|
65
|
+
raise ArgumentError unless user_id.is_a?(Integer)
|
66
|
+
ROM.env.command(:rom_auth_lockdowns).update.where(id: user_id).call(locked_at: Time.now, lock_reason: reason)
|
67
|
+
end
|
68
|
+
|
69
|
+
def unlock(user_id)
|
70
|
+
ROM.env.command(:rom_auth_lockdowns).update.where(id: user_id).call(locked_at: nil, lock_reason: nil)
|
71
|
+
end
|
72
|
+
|
73
|
+
class LockdownMigration < Migration
|
74
|
+
def run
|
75
|
+
config = self.config
|
76
|
+
auth_config = system.configuration
|
77
|
+
|
78
|
+
database.alter_table(config.table_name) do
|
79
|
+
add_column :locked_at, DateTime, default: nil, null: true
|
80
|
+
add_column :lock_reason, String, null: true
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class Lockdown
|
86
|
+
include Virtus.value_object(coerce: false)
|
87
|
+
|
88
|
+
values do
|
89
|
+
attribute :id, Integer
|
90
|
+
attribute :locked_at, DateTime
|
91
|
+
attribute :lock_reason, String
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
module CallbackOverrides
|
96
|
+
def authentication_authorized?(user, credentials)
|
97
|
+
plugin = plugins[LockdownPlugin]
|
98
|
+
|
99
|
+
plugin.unlock_strategy.call(plugin, user, credentials) if plugin.unlock_strategy
|
100
|
+
super && !plugin.is_locked?(user[:id])
|
101
|
+
end
|
102
|
+
|
103
|
+
def on_authentication_completed(data)
|
104
|
+
super
|
105
|
+
|
106
|
+
plugin = plugins[LockdownPlugin]
|
107
|
+
|
108
|
+
plugin.lock_strategy.call(plugin, data[:user_id], data) if plugin.lock_strategy
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ROM::Auth
|
2
|
+
module Plugins
|
3
|
+
class Plugin
|
4
|
+
|
5
|
+
attr_reader :system, :configuration
|
6
|
+
|
7
|
+
def initialize(system, config)
|
8
|
+
raise ArgumentError unless system.kind_of?(System)
|
9
|
+
@system = system
|
10
|
+
@configuration = config
|
11
|
+
end
|
12
|
+
|
13
|
+
def migrate(*args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def install(*args)
|
17
|
+
raise NotImplementedError
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ROM::Auth
|
2
|
+
module Support
|
3
|
+
module ShorthandSymbol
|
4
|
+
|
5
|
+
def self.strip(suffix)
|
6
|
+
mod = Module.new
|
7
|
+
|
8
|
+
mod.instance_eval <<-RUBY
|
9
|
+
def included(cls)
|
10
|
+
def cls.shorthand_symbol
|
11
|
+
#name.split('::').last.gsub(/#{suffix.source}/,'').underscore.to_sym
|
12
|
+
name.demodulize.gsub(/#{suffix.source}/,'').underscore.to_sym
|
13
|
+
end
|
14
|
+
end
|
15
|
+
RUBY
|
16
|
+
|
17
|
+
mod
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
module ROM
|
2
|
+
module Auth
|
3
|
+
class System
|
4
|
+
|
5
|
+
attr_reader :configuration, :plugins
|
6
|
+
|
7
|
+
def initialize(config)
|
8
|
+
raise ArgumentError unless config.is_a?(Configuration)
|
9
|
+
@configuration = config
|
10
|
+
|
11
|
+
load_plugins!
|
12
|
+
end
|
13
|
+
|
14
|
+
def migrate!(setup)
|
15
|
+
plugins.each do |_type, plugin|
|
16
|
+
plugin.migrate(setup)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# def authenticators
|
21
|
+
# Authenticators::Authenticator.descendants.inject({}) do |acc, desc|
|
22
|
+
# acc.merge(desc.shorthand_symbol => desc)
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
|
26
|
+
def authenticate(credentials)
|
27
|
+
raise ArgumentError unless credentials
|
28
|
+
|
29
|
+
success = false
|
30
|
+
now = Time.now
|
31
|
+
user = identify_user(credentials)
|
32
|
+
|
33
|
+
on_authentication_attempt(credentials)
|
34
|
+
|
35
|
+
if user
|
36
|
+
authenticated = run_authentication_check(credentials)
|
37
|
+
|
38
|
+
if authenticated
|
39
|
+
on_authentication_success(credentials)
|
40
|
+
|
41
|
+
if authentication_authorized?(user, credentials)
|
42
|
+
on_authorized_authentication(credentials)
|
43
|
+
success = true
|
44
|
+
else
|
45
|
+
on_unauthorized_authentication(credentials)
|
46
|
+
end
|
47
|
+
else
|
48
|
+
on_authentication_failure(credentials)
|
49
|
+
end
|
50
|
+
else
|
51
|
+
on_identification_failure(credentials)
|
52
|
+
end
|
53
|
+
|
54
|
+
on_authentication_completed(
|
55
|
+
identifier: credentials.identifier,
|
56
|
+
user_id: (user[:id] if user),
|
57
|
+
started_at: now,
|
58
|
+
ended_at: Time.now,
|
59
|
+
type: credentials.type,
|
60
|
+
authenticated: authenticated,
|
61
|
+
success: success
|
62
|
+
)
|
63
|
+
|
64
|
+
user if success
|
65
|
+
end
|
66
|
+
|
67
|
+
def logger
|
68
|
+
configuration.logger
|
69
|
+
end
|
70
|
+
|
71
|
+
def inspect
|
72
|
+
"#<ROM::Auth::AuthenticationSystem plugins: #{plugins.keys}>"
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def load_plugins!
|
78
|
+
@plugins = configuration.plugins.inject({}) do |acc, (plugin, args)|
|
79
|
+
acc.merge(plugin => plugin.new(self, *args))
|
80
|
+
end
|
81
|
+
|
82
|
+
@plugins.each do |_type, plugin|
|
83
|
+
plugin.install
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def identify_user(credentials)
|
88
|
+
raise NotImplementedError
|
89
|
+
end
|
90
|
+
|
91
|
+
def run_authentication_check(credentials)
|
92
|
+
#authenticators[type].new.authenticate(user, data)
|
93
|
+
raise NotImplementedError
|
94
|
+
end
|
95
|
+
|
96
|
+
def authentication_authorized?(user, credentials)
|
97
|
+
true # FIXME
|
98
|
+
end
|
99
|
+
|
100
|
+
def on_authentication_attempt(credentials)
|
101
|
+
logger.info("Attempt: #{credentials.identifier}")
|
102
|
+
end
|
103
|
+
|
104
|
+
def on_identification_failure(credentials)
|
105
|
+
logger.info("Identification failed: #{credentials.identifier}")
|
106
|
+
end
|
107
|
+
|
108
|
+
def on_authentication_completed(data)
|
109
|
+
raise ArgumentError unless data.is_a?(Hash)
|
110
|
+
#raise ArgumentError unless data.keys.to_set == [:identifier, :user_id, :started_at, :ended_at, :authenticated, :authenticator, :success].to_set
|
111
|
+
# TODO :reason ?
|
112
|
+
|
113
|
+
instrument_authentication_event(data)
|
114
|
+
|
115
|
+
# TODO
|
116
|
+
# - log origin/details & warn user
|
117
|
+
# - compute failed logins per minute and alert if above threshold
|
118
|
+
# - throttling
|
119
|
+
#
|
120
|
+
end
|
121
|
+
|
122
|
+
def on_authentication_failure(credentials)
|
123
|
+
logger.info("Authentication failed: #{credentials.identifier}")
|
124
|
+
end
|
125
|
+
|
126
|
+
def on_authentication_success(credentials)
|
127
|
+
logger.info("Authentication succeeded: #{credentials.identifier}")
|
128
|
+
end
|
129
|
+
|
130
|
+
def on_unauthorized_authentication(credentials)
|
131
|
+
logger.info("Unauthorized: #{credentials.identifier}")
|
132
|
+
end
|
133
|
+
|
134
|
+
def on_authorized_authentication(credentials)
|
135
|
+
logger.info("Authorized: #{credentials.identifier}")
|
136
|
+
end
|
137
|
+
|
138
|
+
def instrument_authentication_event(auth_data)
|
139
|
+
configuration.instrumentation.instrument(
|
140
|
+
'rom:auth:authentication_attempt',
|
141
|
+
auth_data
|
142
|
+
)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
describe 'CommonPlugins' do
|
2
|
+
let(:setup) { ROM.setup(:sql, "sqlite::memory") }
|
3
|
+
let(:connection) { setup.default.connection }
|
4
|
+
|
5
|
+
def password
|
6
|
+
'somepassword'
|
7
|
+
end
|
8
|
+
|
9
|
+
def password_verifier
|
10
|
+
ROM::Auth::PasswordVerifiers::PBKDF2Verifier.for_password(password)
|
11
|
+
end
|
12
|
+
|
13
|
+
before do
|
14
|
+
connection.create_table(:users) do
|
15
|
+
primary_key :id
|
16
|
+
end
|
17
|
+
|
18
|
+
class User
|
19
|
+
include Virtus.value_object(coerce: false)
|
20
|
+
|
21
|
+
values do
|
22
|
+
attribute :id, Integer
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
@users = Class.new(ROM::Relation[:sql]) do
|
27
|
+
register_as :users
|
28
|
+
dataset :users
|
29
|
+
|
30
|
+
def by_id(id)
|
31
|
+
where(id: id)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
@mapper = Class.new(ROM::Mapper) do
|
36
|
+
relation(:users)
|
37
|
+
model(User) # FIXME
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
it '#authenticate with AuthenticationCredentialsPlugin' do
|
42
|
+
config = ROM::Auth::Configuration.new do |c|
|
43
|
+
c.plugin(ROM::Auth::Plugins::AuthenticationCredentialsPlugin) do
|
44
|
+
end
|
45
|
+
|
46
|
+
c.plugin(ROM::Auth::Plugins::AuthenticationEventsPlugin) do
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
system = ROM::Auth::System.new(config)
|
51
|
+
|
52
|
+
system.migrate!(setup)
|
53
|
+
|
54
|
+
rom = ROM.finalize.env
|
55
|
+
|
56
|
+
connection[:users].insert(id: 1)
|
57
|
+
connection[:authentication_credentials].insert(
|
58
|
+
user_id: 1,
|
59
|
+
type: 'email',
|
60
|
+
identifier: 'a@b.c.de',
|
61
|
+
verifier_data: password_verifier.to_s
|
62
|
+
)
|
63
|
+
|
64
|
+
credentials = double(type: 'email', identifier: 'a@b.c.de', password: password)
|
65
|
+
user = rom.relation(:users).first
|
66
|
+
|
67
|
+
assert{ system.authenticate(credentials) == user }
|
68
|
+
end
|
69
|
+
|
70
|
+
it '#authenticate with AuthenticationEventsPlugin' do
|
71
|
+
config = ROM::Auth::Configuration.new do |c|
|
72
|
+
c.plugin(ROM::Auth::Plugins::AuthenticationCredentialsPlugin) do
|
73
|
+
end
|
74
|
+
|
75
|
+
c.plugin(ROM::Auth::Plugins::AuthenticationEventsPlugin) do
|
76
|
+
end
|
77
|
+
|
78
|
+
c.plugin(ROM::Auth::Plugins::LockdownPlugin) do
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
system = ROM::Auth::System.new(config)
|
83
|
+
|
84
|
+
system.migrate!(setup)
|
85
|
+
|
86
|
+
rom = ROM.finalize.env
|
87
|
+
|
88
|
+
connection[:users].insert(id: 1)
|
89
|
+
connection[:authentication_credentials].insert(
|
90
|
+
user_id: 1,
|
91
|
+
type: 'email',
|
92
|
+
identifier: 'a@b.c.de',
|
93
|
+
verifier_data: password_verifier.to_s
|
94
|
+
)
|
95
|
+
|
96
|
+
credentials = double(type: 'email', identifier: 'a@b.c.de', password: password)
|
97
|
+
user = rom.relation(:users).first
|
98
|
+
|
99
|
+
auths = rom.relation(:authentication_events)
|
100
|
+
|
101
|
+
assert{ system.authenticate(credentials) == user }
|
102
|
+
assert{ auths.relation.count == 1 }
|
103
|
+
|
104
|
+
event = auths.as(:rom_auth_event).first
|
105
|
+
assert{ event.type == 'email' }
|
106
|
+
assert{ event.authenticated == true }
|
107
|
+
assert{ event.success == true }
|
108
|
+
assert{ event.user_id == 1 }
|
109
|
+
end
|
110
|
+
|
111
|
+
it '#authenticate with LockdownPlugin' do
|
112
|
+
config = ROM::Auth::Configuration.new do |c|
|
113
|
+
c.plugin(ROM::Auth::Plugins::AuthenticationCredentialsPlugin) do
|
114
|
+
end
|
115
|
+
|
116
|
+
c.plugin(ROM::Auth::Plugins::AuthenticationEventsPlugin) do
|
117
|
+
end
|
118
|
+
|
119
|
+
c.plugin(ROM::Auth::Plugins::LockdownPlugin) do |c|
|
120
|
+
c.lock_strategy = ->(plugin, user_id, credentials){
|
121
|
+
#plugin.system.logger.info('LOCKING')
|
122
|
+
plugin.lock(user_id, 'Login failed')
|
123
|
+
}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
system = ROM::Auth::System.new(config)
|
128
|
+
|
129
|
+
system.migrate!(setup)
|
130
|
+
|
131
|
+
rom = ROM.finalize.env
|
132
|
+
|
133
|
+
connection[:users].insert(id: 1)
|
134
|
+
connection[:authentication_credentials].insert(
|
135
|
+
user_id: 1,
|
136
|
+
type: 'email',
|
137
|
+
identifier: 'a@b.c.de',
|
138
|
+
verifier_data: password_verifier.to_s
|
139
|
+
)
|
140
|
+
|
141
|
+
credentials = double(type: 'email', identifier: 'a@b.c.de', password: 'incorrect')
|
142
|
+
|
143
|
+
user = rom.relation(:users).first
|
144
|
+
|
145
|
+
assert{ system.authenticate(credentials) == nil }
|
146
|
+
|
147
|
+
lock = rom.relation(:rom_auth_lockdowns).as(:rom_auth_lockdown).first
|
148
|
+
|
149
|
+
assert{ lock.locked_at.close_to?(Time.now, 1) }
|
150
|
+
assert{ lock.lock_reason == 'Login failed' }
|
151
|
+
|
152
|
+
plugin = system.plugins[ROM::Auth::Plugins::LockdownPlugin]
|
153
|
+
assert{ plugin.is_locked?(lock.id) == true }
|
154
|
+
|
155
|
+
plugin.unlock(lock.id)
|
156
|
+
assert{ plugin.is_locked?(lock.id) == false }
|
157
|
+
|
158
|
+
lock = rom.relation(:rom_auth_lockdowns).as(:rom_auth_lockdown).first
|
159
|
+
assert{ lock.locked_at == nil }
|
160
|
+
assert{ lock.lock_reason == nil }
|
161
|
+
end
|
162
|
+
end
|