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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -7
  3. data/.travis.yml +5 -5
  4. data/Gemfile.ci +2 -0
  5. data/Rakefile +4 -3
  6. data/Readme.md +24 -2
  7. data/ditty.gemspec +4 -3
  8. data/lib/ditty.rb +24 -0
  9. data/lib/ditty/cli.rb +6 -2
  10. data/lib/ditty/components/app.rb +10 -1
  11. data/lib/ditty/controllers/application.rb +72 -10
  12. data/lib/ditty/controllers/audit_logs.rb +1 -5
  13. data/lib/ditty/controllers/auth.rb +37 -17
  14. data/lib/ditty/controllers/component.rb +15 -5
  15. data/lib/ditty/controllers/main.rb +1 -5
  16. data/lib/ditty/controllers/roles.rb +2 -5
  17. data/lib/ditty/controllers/user_login_traits.rb +18 -0
  18. data/lib/ditty/controllers/users.rb +4 -9
  19. data/lib/ditty/db.rb +3 -1
  20. data/lib/ditty/emails/base.rb +13 -4
  21. data/lib/ditty/helpers/authentication.rb +6 -5
  22. data/lib/ditty/helpers/component.rb +9 -1
  23. data/lib/ditty/helpers/response.rb +24 -3
  24. data/lib/ditty/helpers/views.rb +20 -0
  25. data/lib/ditty/listener.rb +38 -10
  26. data/lib/ditty/middleware/accept_extension.rb +2 -0
  27. data/lib/ditty/middleware/error_catchall.rb +2 -0
  28. data/lib/ditty/models/audit_log.rb +1 -0
  29. data/lib/ditty/models/base.rb +4 -0
  30. data/lib/ditty/models/identity.rb +3 -0
  31. data/lib/ditty/models/role.rb +1 -0
  32. data/lib/ditty/models/user.rb +9 -1
  33. data/lib/ditty/models/user_login_trait.rb +17 -0
  34. data/lib/ditty/policies/audit_log_policy.rb +6 -6
  35. data/lib/ditty/policies/role_policy.rb +2 -2
  36. data/lib/ditty/policies/user_login_trait_policy.rb +45 -0
  37. data/lib/ditty/policies/user_policy.rb +2 -2
  38. data/lib/ditty/rubocop.rb +3 -0
  39. data/lib/ditty/seed.rb +2 -0
  40. data/lib/ditty/services/authentication.rb +7 -2
  41. data/lib/ditty/services/email.rb +8 -2
  42. data/lib/ditty/services/logger.rb +11 -0
  43. data/lib/ditty/services/pagination_wrapper.rb +2 -0
  44. data/lib/ditty/services/settings.rb +14 -3
  45. data/lib/ditty/tasks/ditty.rake +109 -0
  46. data/lib/ditty/tasks/omniauth-ldap.rake +43 -0
  47. data/lib/ditty/version.rb +1 -1
  48. data/lib/rubocop/cop/ditty/call_services_directly.rb +42 -0
  49. data/migrate/20181209_add_user_login_traits.rb +16 -0
  50. data/migrate/20181209_extend_audit_log.rb +12 -0
  51. data/views/403.haml +2 -0
  52. data/views/audit_logs/index.haml +11 -6
  53. data/views/auth/ldap.haml +17 -0
  54. data/views/emails/forgot_password.haml +1 -1
  55. data/views/emails/layouts/action.haml +10 -6
  56. data/views/emails/layouts/alert.haml +2 -1
  57. data/views/emails/layouts/billing.haml +2 -1
  58. data/views/error.haml +8 -3
  59. data/views/partials/form_control.haml +24 -20
  60. data/views/partials/navbar.haml +11 -12
  61. data/views/partials/sidebar.haml +1 -1
  62. data/views/roles/index.haml +2 -0
  63. data/views/user_login_traits/display.haml +32 -0
  64. data/views/user_login_traits/edit.haml +10 -0
  65. data/views/user_login_traits/form.haml +5 -0
  66. data/views/user_login_traits/index.haml +30 -0
  67. data/views/user_login_traits/new.haml +10 -0
  68. data/views/users/display.haml +1 -1
  69. data/views/users/login_traits.haml +27 -0
  70. data/views/users/profile.haml +2 -0
  71. metadata +50 -21
  72. 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
 
@@ -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
@@ -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
- user: args[0][:target].current_user,
24
- action: action_from(args[0][:target], method),
25
- details: args[0][:details]
26
- }.merge(args[0][:values] || {}))
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
- user: user,
37
- action: action_from(event[:target], :user_register),
38
- details: event[:details]
39
- }.merge(event[:values] || {}))
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
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Ditty
2
4
  module Middleware
3
5
  # Allow requests to be responded to in JSON if the URL has .json at the end.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ditty/services/logger'
2
4
 
3
5
  module Ditty
@@ -8,6 +8,7 @@ module Ditty
8
8
  many_to_one :user
9
9
 
10
10
  def validate
11
+ super
11
12
  validates_presence [:action]
12
13
  end
13
14
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'sequel'
2
4
 
3
5
  module Ditty
@@ -5,5 +7,7 @@ module Ditty
5
7
  def for_json
6
8
  values
7
9
  end
10
+
11
+ alias for_csv for_json
8
12
  end
9
13
  end
@@ -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
 
@@ -9,6 +9,7 @@ module Ditty
9
9
  many_to_many :users
10
10
 
11
11
  def validate
12
+ super
12
13
  validates_presence [:name]
13
14
  validates_unique [:name]
14
15
  end
@@ -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.username
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
- user && user.super_admin?
8
+ false
9
9
  end
10
10
 
11
11
  def list?
12
- create?
12
+ user&.super_admin?
13
13
  end
14
14
 
15
15
  def read?
16
- create?
16
+ user&.super_admin?
17
17
  end
18
18
 
19
19
  def update?
20
- read?
20
+ false
21
21
  end
22
22
 
23
23
  def delete?
24
- create?
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 && user.super_admin?
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 && user.super_admin?
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 && user.super_admin?
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 && user.super_admin?
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 && user.super_admin?
40
+ if user&.super_admin?
41
41
  scope
42
42
  elsif user
43
43
  scope.where(id: user.id)
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop/cop/ditty/call_services_directly'