hone-lockdown 1.2.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/.gitignore +6 -0
- data/History.txt +195 -0
- data/README.txt +36 -0
- data/Rakefile +14 -0
- data/VERSION +1 -0
- data/lib/lockdown.rb +73 -0
- data/lib/lockdown/context.rb +48 -0
- data/lib/lockdown/database.rb +117 -0
- data/lib/lockdown/frameworks/rails.rb +105 -0
- data/lib/lockdown/frameworks/rails/controller.rb +163 -0
- data/lib/lockdown/frameworks/rails/view.rb +50 -0
- data/lib/lockdown/helper.rb +101 -0
- data/lib/lockdown/orms/active_record.rb +68 -0
- data/lib/lockdown/permission.rb +240 -0
- data/lib/lockdown/rules.rb +378 -0
- data/lib/lockdown/session.rb +57 -0
- data/lib/lockdown/system.rb +52 -0
- data/rails_generators/lockdown/lockdown_generator.rb +273 -0
- data/rails_generators/lockdown/templates/app/controllers/permissions_controller.rb +22 -0
- data/rails_generators/lockdown/templates/app/controllers/sessions_controller.rb +39 -0
- data/rails_generators/lockdown/templates/app/controllers/user_groups_controller.rb +122 -0
- data/rails_generators/lockdown/templates/app/controllers/users_controller.rb +117 -0
- data/rails_generators/lockdown/templates/app/helpers/permissions_helper.rb +2 -0
- data/rails_generators/lockdown/templates/app/helpers/user_groups_helper.rb +2 -0
- data/rails_generators/lockdown/templates/app/helpers/users_helper.rb +2 -0
- data/rails_generators/lockdown/templates/app/models/permission.rb +13 -0
- data/rails_generators/lockdown/templates/app/models/profile.rb +10 -0
- data/rails_generators/lockdown/templates/app/models/user.rb +95 -0
- data/rails_generators/lockdown/templates/app/models/user_group.rb +15 -0
- data/rails_generators/lockdown/templates/app/views/permissions/index.html.erb +16 -0
- data/rails_generators/lockdown/templates/app/views/permissions/show.html.erb +26 -0
- data/rails_generators/lockdown/templates/app/views/sessions/new.html.erb +12 -0
- data/rails_generators/lockdown/templates/app/views/user_groups/edit.html.erb +33 -0
- data/rails_generators/lockdown/templates/app/views/user_groups/index.html.erb +20 -0
- data/rails_generators/lockdown/templates/app/views/user_groups/new.html.erb +31 -0
- data/rails_generators/lockdown/templates/app/views/user_groups/show.html.erb +29 -0
- data/rails_generators/lockdown/templates/app/views/users/edit.html.erb +51 -0
- data/rails_generators/lockdown/templates/app/views/users/index.html.erb +22 -0
- data/rails_generators/lockdown/templates/app/views/users/new.html.erb +50 -0
- data/rails_generators/lockdown/templates/app/views/users/show.html.erb +33 -0
- data/rails_generators/lockdown/templates/config/initializers/lockit.rb +1 -0
- data/rails_generators/lockdown/templates/db/migrate/create_admin_user.rb +17 -0
- data/rails_generators/lockdown/templates/db/migrate/create_permissions.rb +19 -0
- data/rails_generators/lockdown/templates/db/migrate/create_profiles.rb +26 -0
- data/rails_generators/lockdown/templates/db/migrate/create_user_groups.rb +19 -0
- data/rails_generators/lockdown/templates/db/migrate/create_users.rb +17 -0
- data/rails_generators/lockdown/templates/lib/lockdown/README +42 -0
- data/rails_generators/lockdown/templates/lib/lockdown/init.rb +131 -0
- data/spec/lockdown/database_spec.rb +158 -0
- data/spec/lockdown/frameworks/rails/controller_spec.rb +224 -0
- data/spec/lockdown/frameworks/rails/view_spec.rb +87 -0
- data/spec/lockdown/frameworks/rails_spec.rb +175 -0
- data/spec/lockdown/permission_spec.rb +166 -0
- data/spec/lockdown/rules_spec.rb +109 -0
- data/spec/lockdown/session_spec.rb +89 -0
- data/spec/lockdown/system_spec.rb +59 -0
- data/spec/lockdown_spec.rb +19 -0
- data/spec/rcov.opts +5 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +1 -0
- metadata +131 -0
@@ -0,0 +1,105 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "rails", "controller")
|
2
|
+
require File.join(File.dirname(__FILE__), "rails", "view")
|
3
|
+
|
4
|
+
module Lockdown
|
5
|
+
module Frameworks
|
6
|
+
module Rails
|
7
|
+
class << self
|
8
|
+
def use_me?
|
9
|
+
Object.const_defined?("ActionController") && ActionController.const_defined?("Base")
|
10
|
+
end
|
11
|
+
|
12
|
+
def included(mod)
|
13
|
+
mod.extend Lockdown::Frameworks::Rails::Environment
|
14
|
+
mixin
|
15
|
+
end
|
16
|
+
|
17
|
+
def mixin
|
18
|
+
mixin_controller
|
19
|
+
|
20
|
+
Lockdown.view_helper.class_eval do
|
21
|
+
include Lockdown::Frameworks::Rails::View
|
22
|
+
end
|
23
|
+
|
24
|
+
Lockdown::System.class_eval do
|
25
|
+
extend Lockdown::Frameworks::Rails::System
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def mixin_controller(klass = Lockdown.controller_parent)
|
30
|
+
klass.class_eval do
|
31
|
+
include Lockdown::Session
|
32
|
+
include Lockdown::Frameworks::Rails::Controller::Lock
|
33
|
+
end
|
34
|
+
|
35
|
+
klass.helper_method :authorized?
|
36
|
+
|
37
|
+
klass.hide_action(:set_current_user, :configure_lockdown, :check_request_authorization)
|
38
|
+
|
39
|
+
klass.before_filter do |c|
|
40
|
+
c.set_current_user
|
41
|
+
c.configure_lockdown
|
42
|
+
c.check_request_authorization
|
43
|
+
c.check_model_authorization
|
44
|
+
end
|
45
|
+
|
46
|
+
klass.filter_parameter_logging :password, :password_confirmation
|
47
|
+
|
48
|
+
klass.rescue_from SecurityError, :with => proc{|e| access_denied(e)}
|
49
|
+
end
|
50
|
+
end # class block
|
51
|
+
|
52
|
+
module Environment
|
53
|
+
|
54
|
+
def project_root
|
55
|
+
::RAILS_ROOT
|
56
|
+
end
|
57
|
+
|
58
|
+
def init_file
|
59
|
+
"#{project_root}/lib/lockdown/init.rb"
|
60
|
+
end
|
61
|
+
|
62
|
+
def view_helper
|
63
|
+
::ActionView::Base
|
64
|
+
end
|
65
|
+
|
66
|
+
# cache_classes is true in production and testing, need to
|
67
|
+
# modify the ApplicationController
|
68
|
+
def controller_parent
|
69
|
+
if ::Rails.configuration.cache_classes
|
70
|
+
ApplicationController
|
71
|
+
else
|
72
|
+
ActionController::Base
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# cache_classes is true in production and testing, need to
|
77
|
+
# do an instance eval instead
|
78
|
+
def add_controller_method(code)
|
79
|
+
Lockdown.controller_parent.class_eval code, __FILE__,__LINE__ +1
|
80
|
+
end
|
81
|
+
|
82
|
+
def controller_class_name(str)
|
83
|
+
str = "#{str}Controller"
|
84
|
+
if str.include?("__")
|
85
|
+
str.split("__").collect{|p| Lockdown.camelize(p)}.join("::")
|
86
|
+
else
|
87
|
+
Lockdown.camelize(str)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def fetch_controller_class(str)
|
92
|
+
eval("::#{controller_class_name(str)}")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
module System
|
97
|
+
include Lockdown::Frameworks::Rails::Controller
|
98
|
+
|
99
|
+
def skip_sync?
|
100
|
+
Lockdown::System.fetch(:skip_db_sync_in).include?(ENV['RAILS_ENV'])
|
101
|
+
end
|
102
|
+
end # System
|
103
|
+
end # Rails
|
104
|
+
end # Frameworks
|
105
|
+
end # Lockdown
|
@@ -0,0 +1,163 @@
|
|
1
|
+
module Lockdown
|
2
|
+
module Frameworks
|
3
|
+
module Rails
|
4
|
+
module Controller
|
5
|
+
|
6
|
+
def available_actions(klass)
|
7
|
+
klass.action_methods
|
8
|
+
end
|
9
|
+
|
10
|
+
def controller_name(klass)
|
11
|
+
klass.controller_name
|
12
|
+
end
|
13
|
+
|
14
|
+
# Locking methods
|
15
|
+
module Lock
|
16
|
+
|
17
|
+
def configure_lockdown
|
18
|
+
check_session_expiry
|
19
|
+
store_location
|
20
|
+
end
|
21
|
+
|
22
|
+
# Basic auth functionality needs to be reworked as
|
23
|
+
# Lockdown doesn't provide authentication functionality.
|
24
|
+
def set_current_user
|
25
|
+
#login_from_basic_auth? unless logged_in?
|
26
|
+
if logged_in?
|
27
|
+
Thread.current[:who_did_it] = Lockdown::System.
|
28
|
+
call(self, :who_did_it)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def check_request_authorization
|
33
|
+
unless authorized?(path_from_hash(params))
|
34
|
+
raise SecurityError, "Authorization failed! \nparams: #{params.inspect}\nsession: #{session.inspect}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def path_allowed?(url)
|
41
|
+
session[:access_rights] ||= Lockdown::System.public_access
|
42
|
+
#
|
43
|
+
# If it isn't in the list of non-nested url's,
|
44
|
+
# try matching the allowed path with /controller_name/nnn/ tacked onto the front
|
45
|
+
#
|
46
|
+
if ret = session[:access_rights].include?(url)
|
47
|
+
return ret
|
48
|
+
else
|
49
|
+
return !session[:access_rights].detect { |v| url =~ /\/\w+\/\d+\/#{v}/ }.nil?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def check_session_expiry
|
54
|
+
if session[:expiry_time] && session[:expiry_time] < Time.now
|
55
|
+
nil_lockdown_values
|
56
|
+
Lockdown::System.call(self, :session_timeout_method)
|
57
|
+
end
|
58
|
+
session[:expiry_time] = Time.now + Lockdown::System.fetch(:session_timeout)
|
59
|
+
end
|
60
|
+
|
61
|
+
def store_location
|
62
|
+
if (request.method == :get) && (session[:thispage] != sent_from_uri)
|
63
|
+
session[:prevpage] = session[:thispage] || ''
|
64
|
+
session[:thispage] = sent_from_uri
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def sent_from_uri
|
69
|
+
request.request_uri
|
70
|
+
end
|
71
|
+
|
72
|
+
def authorized?(url, method = nil)
|
73
|
+
return false unless url
|
74
|
+
|
75
|
+
return true if current_user_is_admin?
|
76
|
+
|
77
|
+
method ||= (params[:method] || request.method)
|
78
|
+
|
79
|
+
url_parts = URI::split(url.strip)
|
80
|
+
|
81
|
+
path = url_parts[5]
|
82
|
+
|
83
|
+
return true if path_allowed?(path)
|
84
|
+
|
85
|
+
begin
|
86
|
+
hash = ActionController::Routing::Routes.recognize_path(path, :method => method)
|
87
|
+
return path_allowed?(path_from_hash(hash)) if hash
|
88
|
+
rescue Exception => e
|
89
|
+
# continue on
|
90
|
+
end
|
91
|
+
|
92
|
+
# Mailto link
|
93
|
+
return true if url =~ /^mailto:/
|
94
|
+
|
95
|
+
# Public file
|
96
|
+
file = File.join(RAILS_ROOT, 'public', url)
|
97
|
+
return true if File.exists?(file)
|
98
|
+
|
99
|
+
# Passing in different domain
|
100
|
+
return remote_url?(url_parts[2])
|
101
|
+
end
|
102
|
+
|
103
|
+
def access_denied(e)
|
104
|
+
|
105
|
+
RAILS_DEFAULT_LOGGER.info "Access denied: #{e}"
|
106
|
+
|
107
|
+
if Lockdown::System.fetch(:logout_on_access_violation)
|
108
|
+
reset_session
|
109
|
+
end
|
110
|
+
respond_to do |format|
|
111
|
+
format.html do
|
112
|
+
store_location
|
113
|
+
redirect_to Lockdown::System.fetch(:access_denied_path)
|
114
|
+
return
|
115
|
+
end
|
116
|
+
format.xml do
|
117
|
+
headers["Status"] = "Unauthorized"
|
118
|
+
headers["WWW-Authenticate"] = %(Basic realm="Web Password")
|
119
|
+
render :text => e.message, :status => "401 Unauthorized"
|
120
|
+
return
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def path_from_hash(hash)
|
126
|
+
hash[:controller].to_s + "/" + hash[:action].to_s
|
127
|
+
end
|
128
|
+
|
129
|
+
def remote_url?(domain = nil)
|
130
|
+
return false if domain.nil? || domain.strip.length == 0
|
131
|
+
request.host.downcase != domain.downcase
|
132
|
+
end
|
133
|
+
|
134
|
+
def redirect_back_or_default(default)
|
135
|
+
if session[:prevpage].nil? || session[:prevpage].blank?
|
136
|
+
redirect_to(default)
|
137
|
+
else
|
138
|
+
redirect_to(session[:prevpage])
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Called from current_user. Now, attempt to login by
|
143
|
+
# basic authentication information.
|
144
|
+
def login_from_basic_auth?
|
145
|
+
username, passwd = get_auth_data
|
146
|
+
if username && passwd
|
147
|
+
set_session_user ::User.authenticate(username, passwd)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
@@http_auth_headers = %w(X-HTTP_AUTHORIZATION HTTP_AUTHORIZATION Authorization)
|
152
|
+
# gets BASIC auth info
|
153
|
+
def get_auth_data
|
154
|
+
auth_key = @@http_auth_headers.detect { |h| request.env.has_key?(h) }
|
155
|
+
auth_data = request.env[auth_key].to_s.split unless auth_key.blank?
|
156
|
+
return auth_data && auth_data[0] == 'Basic' ? Base64.decode64(auth_data[1]).split(':')[0..1] : [nil, nil]
|
157
|
+
end
|
158
|
+
end # Lock
|
159
|
+
end # Controller
|
160
|
+
end # Rails
|
161
|
+
end # Frameworks
|
162
|
+
end # Lockdown
|
163
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Lockdown
|
2
|
+
module Frameworks
|
3
|
+
module Rails
|
4
|
+
module View
|
5
|
+
def self.included(base)
|
6
|
+
base.class_eval do
|
7
|
+
alias_method :link_to_open, :link_to
|
8
|
+
alias_method :link_to, :link_to_secured
|
9
|
+
|
10
|
+
alias_method :button_to_open, :button_to
|
11
|
+
alias_method :button_to, :button_to_secured
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def link_to_secured(name, options = {}, html_options = nil)
|
16
|
+
url = url_for(options)
|
17
|
+
|
18
|
+
method = html_options ? html_options[:method] : :get
|
19
|
+
|
20
|
+
if authorized?(url, method)
|
21
|
+
return link_to_open(name, url, html_options)
|
22
|
+
end
|
23
|
+
return ""
|
24
|
+
end
|
25
|
+
|
26
|
+
def button_to_secured(name, options = {}, html_options = nil)
|
27
|
+
url = url_for(options)
|
28
|
+
|
29
|
+
method = html_options ? html_options[:method] : :get
|
30
|
+
|
31
|
+
if authorized?(url, method)
|
32
|
+
return button_to_open(name, url, html_options)
|
33
|
+
end
|
34
|
+
return ""
|
35
|
+
end
|
36
|
+
|
37
|
+
def link_to_or_show(name, options = {}, html_options = nil)
|
38
|
+
lnk = link_to(name, options, html_options)
|
39
|
+
lnk.length == 0 ? name : lnk
|
40
|
+
end
|
41
|
+
|
42
|
+
def links(*lis)
|
43
|
+
rvalue = []
|
44
|
+
lis.each{|link| rvalue << link if link.length > 0 }
|
45
|
+
rvalue.join( Lockdown::System.fetch(:link_separator) )
|
46
|
+
end
|
47
|
+
end # View
|
48
|
+
end # Rails
|
49
|
+
end # Frameworks
|
50
|
+
end # Lockdown
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Lockdown
|
2
|
+
module Helper
|
3
|
+
def class_name_from_file(str)
|
4
|
+
str.split(".")[0].split("/").collect{|s| camelize(s) }.join("::")
|
5
|
+
end
|
6
|
+
|
7
|
+
# If str_sym is a Symbol (:users), return "Users"
|
8
|
+
# If str_sym is a String ("Users"), return :users
|
9
|
+
def convert_reference_name(str_sym)
|
10
|
+
if str_sym.is_a?(Symbol)
|
11
|
+
titleize(str_sym)
|
12
|
+
else
|
13
|
+
underscore(str_sym).tr(' ','_').to_sym
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def user_group_class
|
18
|
+
eval(Lockdown::System.fetch(:user_group_model))
|
19
|
+
end
|
20
|
+
|
21
|
+
def user_groups_hbtm_reference
|
22
|
+
underscore(Lockdown::System.fetch(:user_group_model)).pluralize.to_sym
|
23
|
+
end
|
24
|
+
|
25
|
+
def user_group_id_reference
|
26
|
+
underscore(Lockdown::System.fetch(:user_group_model)) + "_id"
|
27
|
+
end
|
28
|
+
|
29
|
+
def user_class
|
30
|
+
eval(Lockdown::System.fetch(:user_model))
|
31
|
+
end
|
32
|
+
|
33
|
+
def users_hbtm_reference
|
34
|
+
underscore(Lockdown::System.fetch(:user_model)).pluralize.to_sym
|
35
|
+
end
|
36
|
+
|
37
|
+
def user_id_reference
|
38
|
+
underscore(Lockdown::System.fetch(:user_model)) + "_id"
|
39
|
+
end
|
40
|
+
|
41
|
+
def get_string(value)
|
42
|
+
if value.respond_to?(:name)
|
43
|
+
string_name(value.name)
|
44
|
+
else
|
45
|
+
string_name(value)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_symbol(value)
|
50
|
+
if value.respond_to?(:name)
|
51
|
+
symbol_name(value.name)
|
52
|
+
elsif value.is_a?(String)
|
53
|
+
symbol_name(value)
|
54
|
+
else
|
55
|
+
value
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def camelize(str)
|
60
|
+
str.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
|
61
|
+
end
|
62
|
+
|
63
|
+
def random_string(len = 10)
|
64
|
+
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
|
65
|
+
Array.new(len){||chars[rand(chars.size)]}.join
|
66
|
+
end
|
67
|
+
|
68
|
+
def administrator_group_string
|
69
|
+
string_name(administrator_group_symbol)
|
70
|
+
end
|
71
|
+
|
72
|
+
def administrator_group_symbol
|
73
|
+
:administrators
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def string_name(str_sym)
|
79
|
+
str_sym.is_a?(Symbol) ? convert_reference_name(str_sym) : str_sym
|
80
|
+
end
|
81
|
+
|
82
|
+
def symbol_name(str_sym)
|
83
|
+
str_sym.is_a?(String) ? convert_reference_name(str_sym) : str_sym
|
84
|
+
end
|
85
|
+
|
86
|
+
def titleize(str)
|
87
|
+
humanize(underscore(str)).gsub(/\b([a-z])/) { $1.capitalize }
|
88
|
+
end
|
89
|
+
|
90
|
+
def humanize(str)
|
91
|
+
str.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize
|
92
|
+
end
|
93
|
+
|
94
|
+
def underscore(str)
|
95
|
+
str.to_s.gsub(/::/, '/').
|
96
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
97
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
98
|
+
tr("-", "_").downcase
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Lockdown
|
2
|
+
module Orms
|
3
|
+
module ActiveRecord
|
4
|
+
class << self
|
5
|
+
def use_me?
|
6
|
+
Object.const_defined?("ActiveRecord") && ::ActiveRecord.const_defined?("Base")
|
7
|
+
end
|
8
|
+
|
9
|
+
def included(mod)
|
10
|
+
mod.extend Lockdown::Orms::ActiveRecord::Helper
|
11
|
+
mixin
|
12
|
+
end
|
13
|
+
|
14
|
+
def mixin
|
15
|
+
Lockdown.orm_parent.class_eval do
|
16
|
+
include Lockdown::Orms::ActiveRecord::Stamps
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end # class block
|
20
|
+
|
21
|
+
module Helper
|
22
|
+
def orm_parent
|
23
|
+
::ActiveRecord::Base
|
24
|
+
end
|
25
|
+
|
26
|
+
def database_execute(query)
|
27
|
+
orm_parent.connection.execute(query)
|
28
|
+
end
|
29
|
+
|
30
|
+
def database_query(query)
|
31
|
+
orm_parent.connection.execute(query)
|
32
|
+
end
|
33
|
+
|
34
|
+
def database_table_exists?(klass)
|
35
|
+
klass.table_exists?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module Stamps
|
40
|
+
def self.included(base)
|
41
|
+
base.class_eval do
|
42
|
+
alias_method :create_without_stamps, :create
|
43
|
+
alias_method :create, :create_with_stamps
|
44
|
+
alias_method :update_without_stamps, :update
|
45
|
+
alias_method :update, :update_with_stamps
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def current_who_did_it
|
50
|
+
Thread.current[:who_did_it]
|
51
|
+
end
|
52
|
+
|
53
|
+
def create_with_stamps
|
54
|
+
pid = current_who_did_it || Lockdown::System.fetch(:default_who_did_it)
|
55
|
+
self[:created_by] = pid if self.respond_to?(:created_by)
|
56
|
+
self[:updated_by] = pid if self.respond_to?(:updated_by)
|
57
|
+
create_without_stamps
|
58
|
+
end
|
59
|
+
|
60
|
+
def update_with_stamps
|
61
|
+
pid = current_who_did_it || Lockdown::System.fetch(:default_who_did_it)
|
62
|
+
self[:updated_by] = pid if self.respond_to?(:updated_by)
|
63
|
+
update_without_stamps
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|