ditty 0.7.2 → 0.8.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -7
- data/.travis.yml +5 -5
- data/Gemfile.ci +2 -0
- data/Rakefile +4 -3
- data/Readme.md +24 -2
- data/ditty.gemspec +4 -3
- data/lib/ditty.rb +24 -0
- data/lib/ditty/cli.rb +6 -2
- data/lib/ditty/components/app.rb +10 -1
- data/lib/ditty/controllers/application.rb +72 -10
- data/lib/ditty/controllers/audit_logs.rb +1 -5
- data/lib/ditty/controllers/auth.rb +37 -17
- data/lib/ditty/controllers/component.rb +15 -5
- data/lib/ditty/controllers/main.rb +1 -5
- data/lib/ditty/controllers/roles.rb +2 -5
- data/lib/ditty/controllers/user_login_traits.rb +18 -0
- data/lib/ditty/controllers/users.rb +4 -9
- data/lib/ditty/db.rb +3 -1
- data/lib/ditty/emails/base.rb +13 -4
- data/lib/ditty/helpers/authentication.rb +6 -5
- data/lib/ditty/helpers/component.rb +9 -1
- data/lib/ditty/helpers/response.rb +24 -3
- data/lib/ditty/helpers/views.rb +20 -0
- data/lib/ditty/listener.rb +38 -10
- data/lib/ditty/middleware/accept_extension.rb +2 -0
- data/lib/ditty/middleware/error_catchall.rb +2 -0
- data/lib/ditty/models/audit_log.rb +1 -0
- data/lib/ditty/models/base.rb +4 -0
- data/lib/ditty/models/identity.rb +3 -0
- data/lib/ditty/models/role.rb +1 -0
- data/lib/ditty/models/user.rb +9 -1
- data/lib/ditty/models/user_login_trait.rb +17 -0
- data/lib/ditty/policies/audit_log_policy.rb +6 -6
- data/lib/ditty/policies/role_policy.rb +2 -2
- data/lib/ditty/policies/user_login_trait_policy.rb +45 -0
- data/lib/ditty/policies/user_policy.rb +2 -2
- data/lib/ditty/rubocop.rb +3 -0
- data/lib/ditty/seed.rb +2 -0
- data/lib/ditty/services/authentication.rb +7 -2
- data/lib/ditty/services/email.rb +8 -2
- data/lib/ditty/services/logger.rb +11 -0
- data/lib/ditty/services/pagination_wrapper.rb +2 -0
- data/lib/ditty/services/settings.rb +14 -3
- data/lib/ditty/tasks/ditty.rake +109 -0
- data/lib/ditty/tasks/omniauth-ldap.rake +43 -0
- data/lib/ditty/version.rb +1 -1
- data/lib/rubocop/cop/ditty/call_services_directly.rb +42 -0
- data/migrate/20181209_add_user_login_traits.rb +16 -0
- data/migrate/20181209_extend_audit_log.rb +12 -0
- data/views/403.haml +2 -0
- data/views/audit_logs/index.haml +11 -6
- data/views/auth/ldap.haml +17 -0
- data/views/emails/forgot_password.haml +1 -1
- data/views/emails/layouts/action.haml +10 -6
- data/views/emails/layouts/alert.haml +2 -1
- data/views/emails/layouts/billing.haml +2 -1
- data/views/error.haml +8 -3
- data/views/partials/form_control.haml +24 -20
- data/views/partials/navbar.haml +11 -12
- data/views/partials/sidebar.haml +1 -1
- data/views/roles/index.haml +2 -0
- data/views/user_login_traits/display.haml +32 -0
- data/views/user_login_traits/edit.haml +10 -0
- data/views/user_login_traits/form.haml +5 -0
- data/views/user_login_traits/index.haml +30 -0
- data/views/user_login_traits/new.haml +10 -0
- data/views/users/display.haml +1 -1
- data/views/users/login_traits.haml +27 -0
- data/views/users/profile.haml +2 -0
- metadata +50 -21
- data/lib/ditty/rake_tasks.rb +0 -102
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'csv'
|
4
|
+
|
3
5
|
module Ditty
|
4
6
|
module Helpers
|
5
7
|
module Response
|
@@ -22,6 +24,14 @@ module Ditty
|
|
22
24
|
'total' => total
|
23
25
|
)
|
24
26
|
end
|
27
|
+
format.csv do
|
28
|
+
CSV.generate do |csv|
|
29
|
+
csv << result.first.for_csv.keys
|
30
|
+
result.all.each do |r|
|
31
|
+
csv << r.for_csv.values
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
25
35
|
end
|
26
36
|
end
|
27
37
|
|
@@ -38,12 +48,17 @@ module Ditty
|
|
38
48
|
end
|
39
49
|
end
|
40
50
|
|
51
|
+
def actions(entity = nil)
|
52
|
+
actions = {}
|
53
|
+
actions["#{base_path}/#{entity.id}/edit"] = "Edit #{heading}" if entity && policy(entity).update?
|
54
|
+
actions["#{base_path}/new"] = "New #{heading}" if policy(settings.model_class).create?
|
55
|
+
actions
|
56
|
+
end
|
57
|
+
|
41
58
|
def read_response(entity)
|
59
|
+
actions = actions(entity)
|
42
60
|
respond_to do |format|
|
43
61
|
format.html do
|
44
|
-
actions = {}
|
45
|
-
actions["#{base_path}/#{entity.id}/edit"] = "Edit #{heading}" if policy(entity).update?
|
46
|
-
actions["#{base_path}/new"] = "New #{heading}" if policy(entity).create?
|
47
62
|
title = heading(:read) + (entity.respond_to?(:name) ? ": #{entity.name}" : '')
|
48
63
|
haml :"#{view_location}/display",
|
49
64
|
locals: { entity: entity, title: title, actions: actions },
|
@@ -53,6 +68,12 @@ module Ditty
|
|
53
68
|
# TODO: Add links defined by actions (Edit #{heading})
|
54
69
|
json entity.for_json
|
55
70
|
end
|
71
|
+
format.csv do
|
72
|
+
CSV.generate do |csv|
|
73
|
+
csv << entity.for_csv.keys
|
74
|
+
csv << entity.for_csv.values
|
75
|
+
end
|
76
|
+
end
|
56
77
|
end
|
57
78
|
end
|
58
79
|
|
data/lib/ditty/helpers/views.rb
CHANGED
@@ -7,6 +7,7 @@ module Ditty
|
|
7
7
|
module Views
|
8
8
|
def layout
|
9
9
|
return :embedded if request.params['layout'] == 'embedded'
|
10
|
+
|
10
11
|
:layout
|
11
12
|
end
|
12
13
|
|
@@ -14,6 +15,7 @@ module Ditty
|
|
14
15
|
uri = URI.parse(url)
|
15
16
|
# Don't set the layout if there's none. Don't set the layout for external URIs
|
16
17
|
return url if params['layout'].nil? || uri.host
|
18
|
+
|
17
19
|
qs = { 'layout' => params['layout'] }.merge(uri.query ? CGI.parse(uri.query) : {})
|
18
20
|
uri.query = Rack::Utils.build_query qs
|
19
21
|
uri.to_s
|
@@ -41,6 +43,7 @@ module Ditty
|
|
41
43
|
def filter_control(filter, opts = {})
|
42
44
|
meth = "#{filter[:name]}_options".to_sym
|
43
45
|
return unless respond_to? meth
|
46
|
+
|
44
47
|
haml :'partials/filter_control', locals: {
|
45
48
|
name: filter[:name],
|
46
49
|
label: opts[:label] || filter[:name].to_s.titlecase,
|
@@ -50,6 +53,7 @@ module Ditty
|
|
50
53
|
|
51
54
|
def flash_messages(key = :flash)
|
52
55
|
return '' if flash(key).empty?
|
56
|
+
|
53
57
|
id = (key == :flash ? 'flash' : "flash_#{key}")
|
54
58
|
messages = flash(key).collect do |message|
|
55
59
|
" <div class='alert alert-#{message[0]} alert-dismissable' role='alert'>#{message[1]}</div>\n"
|
@@ -93,6 +97,7 @@ module Ditty
|
|
93
97
|
|
94
98
|
def pagination(list, base_path, qp = {})
|
95
99
|
return unless list.respond_to?(:pagination_record_count) || list.respond_to?(:total_entries)
|
100
|
+
|
96
101
|
list = Ditty::Services::PaginationWrapper.new(list)
|
97
102
|
locals = {
|
98
103
|
first_link: "#{base_path}?" + query_string(qp.merge(page: 1)),
|
@@ -114,6 +119,21 @@ module Ditty
|
|
114
119
|
value
|
115
120
|
end
|
116
121
|
end
|
122
|
+
|
123
|
+
def url_for(options = nil)
|
124
|
+
return options if options.is_a? String
|
125
|
+
return request.env['HTTP_REFERER'] if options == :back && request.env['HTTP_REFERER']
|
126
|
+
|
127
|
+
raise 'Unimplemented'
|
128
|
+
end
|
129
|
+
|
130
|
+
def link_to(name = nil, options = nil, html_options = {})
|
131
|
+
html_options[:href] ||= url_for(options)
|
132
|
+
|
133
|
+
capture_haml do
|
134
|
+
haml_tag :a, name, html_options
|
135
|
+
end
|
136
|
+
end
|
117
137
|
end
|
118
138
|
end
|
119
139
|
end
|
data/lib/ditty/listener.rb
CHANGED
@@ -19,33 +19,51 @@ module Ditty
|
|
19
19
|
def method_missing(method, *args)
|
20
20
|
return unless args[0].is_a?(Hash) && args[0][:target].is_a?(Sinatra::Base) && args[0][:target].settings.track_actions
|
21
21
|
|
22
|
-
log_action(
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
22
|
+
log_action(
|
23
|
+
user_traits(args[0][:target]).merge(
|
24
|
+
action: action_from(args[0][:target], method),
|
25
|
+
details: args[0][:details]
|
26
|
+
).merge(args[0][:values] || {})
|
27
|
+
)
|
27
28
|
end
|
28
29
|
|
29
30
|
def respond_to_missing?(method, _include_private = false)
|
30
31
|
EVENTS.include? method
|
31
32
|
end
|
32
33
|
|
34
|
+
def user_login(event)
|
35
|
+
log_action(
|
36
|
+
user_traits(event[:target]).merge(
|
37
|
+
action: action_from(event[:target], :user_login),
|
38
|
+
details: event[:details]
|
39
|
+
).merge(event[:values] || {})
|
40
|
+
)
|
41
|
+
|
42
|
+
@mutex.synchronize do
|
43
|
+
UserLoginTrait.update_or_create(user_traits(event[:target]), updated_at: Time.now)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
33
47
|
def user_register(event)
|
34
48
|
user = event[:values][:user]
|
35
|
-
log_action(
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
49
|
+
log_action(
|
50
|
+
user_traits(event[:target]).merge(
|
51
|
+
user_id: user.id,
|
52
|
+
action: action_from(event[:target], :user_register),
|
53
|
+
details: event[:details]
|
54
|
+
).merge(event[:values] || {})
|
55
|
+
)
|
40
56
|
|
41
57
|
# Create the SA user if none is present
|
42
58
|
sa = Role.find_or_create(name: 'super_admin')
|
43
59
|
return if User.where(roles: sa).count.positive?
|
60
|
+
|
44
61
|
user.add_role sa
|
45
62
|
end
|
46
63
|
|
47
64
|
def action_from(target, method)
|
48
65
|
return method unless method.to_s.start_with? 'component_'
|
66
|
+
|
49
67
|
target.class.to_s.demodulize.underscore + '_' + method.to_s.gsub(/^component_/, '')
|
50
68
|
end
|
51
69
|
|
@@ -53,6 +71,16 @@ module Ditty
|
|
53
71
|
values[:user] ||= values[:target].current_user if values[:target]
|
54
72
|
@mutex.synchronize { Ditty::AuditLog.create values }
|
55
73
|
end
|
74
|
+
|
75
|
+
def user_traits(target)
|
76
|
+
{
|
77
|
+
user_id: target.current_user&.id,
|
78
|
+
platform: target.browser.platform.name,
|
79
|
+
device: target.browser.device.name,
|
80
|
+
browser: target.browser.name,
|
81
|
+
ip_address: target.request.ip
|
82
|
+
}
|
83
|
+
end
|
56
84
|
end
|
57
85
|
end
|
58
86
|
|
data/lib/ditty/models/base.rb
CHANGED
@@ -22,6 +22,7 @@ module Ditty
|
|
22
22
|
|
23
23
|
def authenticate(unencrypted)
|
24
24
|
return false if crypted_password.blank?
|
25
|
+
|
25
26
|
self if ::BCrypt::Password.new(crypted_password) == unencrypted
|
26
27
|
end
|
27
28
|
|
@@ -38,6 +39,7 @@ module Ditty
|
|
38
39
|
|
39
40
|
# Validation
|
40
41
|
def validate
|
42
|
+
super
|
41
43
|
validates_presence :username
|
42
44
|
unless username.blank?
|
43
45
|
validates_unique :username
|
@@ -64,6 +66,7 @@ module Ditty
|
|
64
66
|
|
65
67
|
# Callbacks
|
66
68
|
def before_save
|
69
|
+
super
|
67
70
|
encrypt_password unless password == '' || password.nil?
|
68
71
|
end
|
69
72
|
|
data/lib/ditty/models/role.rb
CHANGED
data/lib/ditty/models/user.rb
CHANGED
@@ -13,6 +13,7 @@ module Ditty
|
|
13
13
|
one_to_many :identity
|
14
14
|
many_to_many :roles
|
15
15
|
one_to_many :audit_logs
|
16
|
+
one_to_many :user_login_traits
|
16
17
|
|
17
18
|
def role?(check)
|
18
19
|
@roles ||= Hash.new do |h, k|
|
@@ -39,25 +40,32 @@ module Ditty
|
|
39
40
|
end
|
40
41
|
|
41
42
|
def validate
|
43
|
+
super
|
42
44
|
validates_presence :email
|
43
45
|
return if email.blank?
|
46
|
+
|
44
47
|
validates_unique :email
|
45
48
|
validates_format(/\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :email)
|
46
49
|
end
|
47
50
|
|
48
51
|
# Add the basic roles and identity
|
49
52
|
def after_create
|
53
|
+
super
|
50
54
|
check_roles
|
51
55
|
end
|
52
56
|
|
53
57
|
def check_roles
|
54
58
|
return if roles_dataset.first(name: 'anonymous')
|
55
59
|
return if roles_dataset.first(name: 'user')
|
60
|
+
|
56
61
|
add_role Role.find_or_create(name: 'user')
|
57
62
|
end
|
58
63
|
|
59
64
|
def username
|
60
|
-
identity_dataset.first
|
65
|
+
identity = identity_dataset.first
|
66
|
+
return identity.username if identity
|
67
|
+
|
68
|
+
email
|
61
69
|
end
|
62
70
|
|
63
71
|
class << self
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ditty/models/base'
|
4
|
+
|
5
|
+
# Why not store this in Elasticsearch?
|
6
|
+
module Ditty
|
7
|
+
class UserLoginTrait < ::Sequel::Model
|
8
|
+
include ::Ditty::Base
|
9
|
+
|
10
|
+
many_to_one :user
|
11
|
+
|
12
|
+
def validate
|
13
|
+
super
|
14
|
+
validates_presence :user_id
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -5,23 +5,23 @@ require 'ditty/policies/application_policy'
|
|
5
5
|
module Ditty
|
6
6
|
class AuditLogPolicy < ApplicationPolicy
|
7
7
|
def create?
|
8
|
-
|
8
|
+
false
|
9
9
|
end
|
10
10
|
|
11
11
|
def list?
|
12
|
-
|
12
|
+
user&.super_admin?
|
13
13
|
end
|
14
14
|
|
15
15
|
def read?
|
16
|
-
|
16
|
+
user&.super_admin?
|
17
17
|
end
|
18
18
|
|
19
19
|
def update?
|
20
|
-
|
20
|
+
false
|
21
21
|
end
|
22
22
|
|
23
23
|
def delete?
|
24
|
-
|
24
|
+
false
|
25
25
|
end
|
26
26
|
|
27
27
|
def permitted_attributes
|
@@ -30,7 +30,7 @@ module Ditty
|
|
30
30
|
|
31
31
|
class Scope < ApplicationPolicy::Scope
|
32
32
|
def resolve
|
33
|
-
if user
|
33
|
+
if user&.super_admin?
|
34
34
|
scope
|
35
35
|
else
|
36
36
|
scope.where(id: -1)
|
@@ -5,7 +5,7 @@ require 'ditty/policies/application_policy'
|
|
5
5
|
module Ditty
|
6
6
|
class RolePolicy < ApplicationPolicy
|
7
7
|
def create?
|
8
|
-
user
|
8
|
+
user&.super_admin?
|
9
9
|
end
|
10
10
|
|
11
11
|
def list?
|
@@ -30,7 +30,7 @@ module Ditty
|
|
30
30
|
|
31
31
|
class Scope < ApplicationPolicy::Scope
|
32
32
|
def resolve
|
33
|
-
if user
|
33
|
+
if user&.super_admin?
|
34
34
|
scope
|
35
35
|
else
|
36
36
|
scope.where(id: -1)
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ditty/policies/application_policy'
|
4
|
+
|
5
|
+
module Ditty
|
6
|
+
class UserLoginTraitPolicy < ApplicationPolicy
|
7
|
+
def create?
|
8
|
+
user&.super_admin?
|
9
|
+
end
|
10
|
+
|
11
|
+
def list?
|
12
|
+
!!user
|
13
|
+
end
|
14
|
+
|
15
|
+
def read?
|
16
|
+
user && (record.user_id || user.super_admin?)
|
17
|
+
end
|
18
|
+
|
19
|
+
def update?
|
20
|
+
user&.super_admin?
|
21
|
+
end
|
22
|
+
|
23
|
+
def delete?
|
24
|
+
user&.super_admin?
|
25
|
+
end
|
26
|
+
|
27
|
+
def permitted_attributes
|
28
|
+
attribs = %i[ip_address os browser]
|
29
|
+
attribs << :user_id if user.super_admin?
|
30
|
+
attribs
|
31
|
+
end
|
32
|
+
|
33
|
+
class Scope < ApplicationPolicy::Scope
|
34
|
+
def resolve
|
35
|
+
if user&.super_admin?
|
36
|
+
scope
|
37
|
+
elsif user
|
38
|
+
scope.where(user_id: user.id)
|
39
|
+
else
|
40
|
+
scope.where(id: -1)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -10,7 +10,7 @@ module Ditty
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def create?
|
13
|
-
user
|
13
|
+
user&.super_admin?
|
14
14
|
end
|
15
15
|
|
16
16
|
def list?
|
@@ -37,7 +37,7 @@ module Ditty
|
|
37
37
|
|
38
38
|
class Scope < ApplicationPolicy::Scope
|
39
39
|
def resolve
|
40
|
-
if user
|
40
|
+
if user&.super_admin?
|
41
41
|
scope
|
42
42
|
elsif user
|
43
43
|
scope.where(id: user.id)
|