ditty 0.7.2 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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'