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 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ::Ditty::Components.seeders.each(&:call)
@@ -1,13 +1,16 @@
1
- require 'ditty/models/identity'
2
- require 'ditty/controllers/auth'
1
+ # frozen_string_literal: true
2
+
3
+ require 'ditty/controllers/application'
3
4
  require 'ditty/services/settings'
4
5
  require 'ditty/services/logger'
6
+ require 'backports/2.4.0/hash/compact'
5
7
 
6
8
  require 'omniauth'
7
9
  OmniAuth.config.logger = Ditty::Services::Logger.instance
8
10
  OmniAuth.config.path_prefix = "#{Ditty::Application.map_path}/auth"
9
11
  OmniAuth.config.on_failure = proc { |env|
10
12
  next [400, {}, []] if env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
13
+
11
14
  OmniAuth::FailureEndpoint.new(env).redirect_to_failure
12
15
  }
13
16
 
@@ -42,6 +45,8 @@ module Ditty
42
45
  end
43
46
 
44
47
  def default
48
+ require 'ditty/models/identity'
49
+ require 'ditty/controllers/auth'
45
50
  {
46
51
  identity: {
47
52
  arguments: [
@@ -11,6 +11,8 @@ module Ditty
11
11
  class << self
12
12
  include ActiveSupport::Inflector
13
13
 
14
+ attr_writer :config
15
+
14
16
  def config!
15
17
  cfg = config
16
18
  Mail.defaults do
@@ -21,6 +23,7 @@ module Ditty
21
23
  def deliver(email, to = nil, options = {})
22
24
  config!
23
25
  options[:to] ||= to unless to.nil?
26
+ options[:from] ||= config[:from] unless config[:from].nil?
24
27
  email = from_symbol(email, options) if email.is_a? Symbol
25
28
  email.deliver!
26
29
  end
@@ -28,11 +31,14 @@ module Ditty
28
31
  private
29
32
 
30
33
  def config
31
- default.merge Ditty::Services::Settings.values(:email) || {}
34
+ @config ||= default.merge Ditty::Services::Settings.values(:email) || {}
32
35
  end
33
36
 
34
37
  def default
35
- { delivery_method: :logger, logger: Ditty::Services::Logger.instance }
38
+ {
39
+ delivery_method: :logger,
40
+ logger: Ditty::Services::Logger.instance
41
+ }
36
42
  end
37
43
 
38
44
  def from_symbol(email, options)
@@ -20,6 +20,7 @@ module Ditty
20
20
  def initialize
21
21
  @loggers = []
22
22
  return if config[:loggers].blank?
23
+
23
24
  config[:loggers].each do |values|
24
25
  klass = values[:class].constantize
25
26
  opts = tr(values[:options]) || nil
@@ -53,6 +54,16 @@ module Ditty
53
54
  def default
54
55
  { loggers: [{ name: 'default', class: 'Logger' }] }
55
56
  end
57
+
58
+ class << self
59
+ def method_missing(method, *args, &block)
60
+ instance.send(method, *args, &block)
61
+ end
62
+
63
+ def respond_to_missing?(method, _include_private)
64
+ instance.respond_to? method
65
+ end
66
+ end
56
67
  end
57
68
  end
58
69
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Ditty
2
4
  module Services
3
5
  class PaginationWrapper
@@ -16,12 +16,19 @@ module Ditty
16
16
  # in separate files will be used in preference of those in the `settings.yml`
17
17
  # file.
18
18
  module Settings
19
- CONFIG_FOLDER = './config'.freeze
20
- CONFIG_FILE = "#{CONFIG_FOLDER}/settings.yml".freeze
19
+ CONFIG_FOLDER = './config'
20
+ CONFIG_FILE = "#{CONFIG_FOLDER}/settings.yml"
21
21
 
22
22
  class << self
23
23
  def [](key)
24
- values(key.to_sym)
24
+ keys = key.to_s.split('.').map(&:to_sym)
25
+ from = values
26
+ if keys.count > 1 && scope?(keys.first)
27
+ from = values(keys.first)
28
+ keys = keys[1..-1]
29
+ key = keys.join('.')
30
+ end
31
+ from[key.to_sym] || from.dig(*keys)
25
32
  end
26
33
 
27
34
  def values(scope = :settings)
@@ -40,6 +47,10 @@ module Ditty
40
47
  @values[scope]
41
48
  end
42
49
 
50
+ def scope?(name)
51
+ @values.key?(name.to_sym) || File.file?("#{CONFIG_FOLDER}/#{name}.yml")
52
+ end
53
+
43
54
  attr_writer :values
44
55
 
45
56
  def read(filename)
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :ditty do
4
+ desc 'Prepare Ditty'
5
+ task prep: ['generate_tokens', 'prep:folders', 'prep:public', 'prep:migrations']
6
+
7
+ desc 'Generate the needed tokens'
8
+ task :generate_tokens do
9
+ puts 'Generating the Ditty tokens'
10
+ require 'securerandom'
11
+ File.write('.session_secret', SecureRandom.random_bytes(40)) unless File.file?('.session_secret')
12
+ File.write('.token_secret', SecureRandom.random_bytes(40)) unless File.file?('.token_secret')
13
+ end
14
+
15
+ desc 'Seed the Ditty database'
16
+ task :seed do
17
+ puts 'Seeding the Ditty database'
18
+ require 'ditty/seed'
19
+ end
20
+
21
+ desc 'Dump the Ditty DB Schema'
22
+ task :dump_schema do
23
+ Ditty::Components.components.each do |_name, comp|
24
+ comp.load if comp.respond_to?(:load)
25
+ end.compact
26
+ DB.dump_schema_cache('./config/schema.dump')
27
+ end
28
+
29
+ namespace :prep do
30
+ desc 'Check that the required Ditty folders are present'
31
+ task :folders do
32
+ puts 'Prepare the Ditty folders'
33
+ Dir.mkdir 'pids' unless File.exist?('pids')
34
+ end
35
+
36
+ desc 'Check that the public folder is present and populated'
37
+ task :public do
38
+ puts 'Preparing the Ditty public folder'
39
+ Dir.mkdir 'public' unless File.exist?('public')
40
+ ::Ditty::Components.public_folder.each do |path|
41
+ puts "Checking #{path}"
42
+ path = "#{path}/."
43
+ FileUtils.cp_r path, 'public' unless File.expand_path(path).eql? File.expand_path('public')
44
+ end
45
+ end
46
+
47
+ desc 'Check that the migrations folder is present and populated'
48
+ task :migrations do
49
+ puts 'Preparing the Ditty migrations folder'
50
+ Dir.mkdir 'migrations' unless File.exist?('migrations')
51
+ ::Ditty::Components.migrations.each do |path|
52
+ path = "#{path}/."
53
+ FileUtils.cp_r path, 'migrations' unless File.expand_path(path).eql? File.expand_path('migrations')
54
+ end
55
+ puts 'Migrations added:'
56
+ Dir.foreach('migrations').sort.each { |x| puts x if File.file?("migrations/#{x}") && x[-3..-1] == '.rb' }
57
+ end
58
+ end
59
+
60
+ desc 'Migrate Ditty database to latest version'
61
+ task migrate: ['prep:migrations'] do
62
+ puts 'Running the Ditty migrations'
63
+ Rake::Task['ditty:migrate:up'].invoke
64
+ end
65
+
66
+ namespace :migrate do
67
+ require 'logger'
68
+
69
+ folder = 'migrations'
70
+
71
+ desc 'Check if the migration is current'
72
+ task :check do
73
+ ::DB.loggers << Logger.new($stdout) if ::DB.loggers.count.zero?
74
+ puts '** [ditty] Running Ditty Sequel Migrations check'
75
+ ::Sequel.extension :migration
76
+ begin
77
+ ::Sequel::Migrator.check_current(::DB, folder)
78
+ puts '** [ditty] Sequel Migrations up to date'
79
+ rescue Sequel::Migrator::Error => _e
80
+ raise 'Sequel Migrations NOT up to date'
81
+ end
82
+ end
83
+
84
+ desc 'Migrate Ditty database to latest version'
85
+ task :up do
86
+ ::DB.loggers << Logger.new($stdout) if ::DB.loggers.count.zero?
87
+ puts '** [ditty] Running Ditty Migrations up'
88
+ ::Sequel.extension :migration
89
+ ::Sequel::Migrator.apply(::DB, folder)
90
+ end
91
+
92
+ desc 'Remove the whole Ditty database. You WILL lose data'
93
+ task :down do
94
+ ::DB.loggers << Logger.new($stdout) if ::DB.loggers.count.zero?
95
+ puts '** [ditty] Running Ditty Migrations down'
96
+ ::Sequel.extension :migration
97
+ ::Sequel::Migrator.apply(::DB, folder, 0)
98
+ end
99
+
100
+ desc 'Reset the Ditty database. You WILL lose data'
101
+ task :bounce do
102
+ ::DB.loggers << Logger.new($stdout) if ::DB.loggers.count.zero?
103
+ puts '** [ditty] Running Ditty Migrations bounce'
104
+ ::Sequel.extension :migration
105
+ ::Sequel::Migrator.apply(::DB, folder, 0)
106
+ ::Sequel::Migrator.apply(::DB, folder)
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :ditty do
4
+ namespace :ldap do
5
+ desc 'Check the LDAP settings'
6
+ task :check do
7
+ settings = Ditty::Services::Settings[:authentication][:ldap][:arguments].first
8
+ ldap = Net::LDAP.new host: settings[:host], port: settings[:port]
9
+ ldap.authenticate settings[:bind_dn], settings[:password] if settings[:bind_dn]
10
+ raise 'Could not bind to LDAP server' unless ldap.bind
11
+
12
+ puts 'LDAP Binding Successful'
13
+ end
14
+
15
+ desc 'Add the AD / LDAP Groups to Ditty as Roles'
16
+ task :populate_groups, [:filter] do |_task, args|
17
+ puts 'Adding AD / LDAP Groups to Ditty as Roles'
18
+ require 'ditty/services/settings'
19
+ require 'ditty/models/role'
20
+
21
+ settings = Ditty::Services::Settings[:authentication][:ldap][:arguments].first
22
+ ldap = Net::LDAP.new host: settings[:host], port: settings[:port]
23
+ ldap.authenticate settings[:bind_dn], settings[:password] if settings[:bind_dn]
24
+ if ldap.bind
25
+ group_filter = Net::LDAP::Filter.construct(settings[:group_filter]) unless settings[:group_filter].blank?
26
+ group_filter ||= Net::LDAP::Filter.eq('ObjectClass', 'Group')
27
+ if args[:filter]
28
+ search_filter = Net::LDAP::Filter.eq(*args[:filter].split(':', 2))
29
+ filter = Net::LDAP::Filter.join(group_filter, search_filter)
30
+ else
31
+ filter = group_filter
32
+ end
33
+ ldap.search(base: settings[:base], filter: filter).each do |group|
34
+ Ditty::Role.find_or_create(name: group.name) do |role|
35
+ puts "Adding #{role.name}"
36
+ end
37
+ end
38
+ else
39
+ puts 'Could not connect to LDAP Server'
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ditty
4
- VERSION = '0.7.2'.freeze
4
+ VERSION = '0.8.0'
5
5
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+ require 'rubocop/ast/node'
5
+ require 'rubocop/cop/cop'
6
+
7
+ module RuboCop
8
+ module Cop
9
+ module Ditty
10
+ # This cop enforces the use of `Service.method` instead of
11
+ # `Service.instance.method`. Calling the singleton instance has been
12
+ # deprecated for services.
13
+ #
14
+ # @example
15
+ # # bad
16
+ # Ditty::Services::Logger.instance.info 'This is a log message'
17
+ #
18
+ # # good
19
+ # Ditty::Services::Logger.info 'This is a log message'
20
+ class CallServicesDirectly < RuboCop::Cop::Cop
21
+ MSG = 'Do not use `.instance` on services. Call the method directly instead'
22
+
23
+ def_node_matcher :service_instance_call?, <<-PATTERN
24
+ (send (const (const (const ... :Ditty) :Services) _) :instance)
25
+ PATTERN
26
+
27
+ def on_send(node)
28
+ return unless service_instance_call?(node)
29
+
30
+ add_offense(node)
31
+ end
32
+
33
+ def autocorrect(node)
34
+ lambda do |corrector|
35
+ internal = node.children.first.source
36
+ corrector.replace(node.loc.expression, internal)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ create_table :user_login_traits do
6
+ primary_key :id
7
+ foreign_key :user_id, :users
8
+ String :ip_address, nullable: true
9
+ String :platform, nullable: true
10
+ String :device, nullable: true
11
+ String :browser, nullable: true
12
+ DateTime :created_at
13
+ DateTime :updated_at
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ alter_table :audit_logs do
6
+ add_column :ip_address, String
7
+ add_column :platform, String
8
+ add_column :device, String
9
+ add_column :browser, String
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,2 @@
1
+ %p.lead
2
+ Your user is not allowed access to the requested URL.
@@ -9,6 +9,10 @@
9
9
  %th User email
10
10
  %th Action
11
11
  %th Details
12
+ %th IP Address
13
+ %th Browser
14
+ %th Device
15
+ %th Platform
12
16
  %th Created at
13
17
  %tbody
14
18
  - if list.count > 0
@@ -19,12 +23,13 @@
19
23
  %a{ href: "#{settings.map_path}/users/#{entity.user.id}" }= entity.user.email
20
24
  -else
21
25
  None
22
- %td
23
- = entity.action
24
- %td
25
- = entity.details
26
- %td
27
- = entity.created_at.strftime('%Y-%m-%d %H:%M:%S')
26
+ %td= entity.action
27
+ %td= entity.details
28
+ %td= entity.ip_address || 'Unknown'
29
+ %td= entity.browser || 'Unknown'
30
+ %td= entity.device || 'Unknown'
31
+ %td= entity.platform || 'Unknown'
32
+ %td= entity.created_at&.strftime('%Y-%m-%d %H:%M:%S') || 'Unknown'
28
33
  - else
29
34
  %tr
30
35
  %td.text-center{ colspan: 4 } No records
@@ -0,0 +1,17 @@
1
+ .row
2
+ .col-sm-2
3
+ .col-sm-8
4
+ .panel.panel-default
5
+ .panel-body
6
+ = form_tag("#{settings.map_path}/auth/ldap/callback", attributes: { class: '' }) do
7
+ .form-group
8
+ %label.control-label Username
9
+ %input.form-control.border-input{ name: 'username', tabindex: '1' }
10
+ .form-group
11
+ %label.control-label{ style: 'display: block' }
12
+ Password
13
+ %input.form-control.border-input{ name: 'password', type: 'password', tabindex: '2' }
14
+ %button.btn.btn-primary{ type: 'submit', tabindex: '3' }
15
+ %i.fa.fa-building
16
+ Log In
17
+ .col-sm-2
@@ -8,7 +8,7 @@
8
8
  This reset is only valid for 24 hours
9
9
 
10
10
  %p.text-center
11
- %a{ href: reset_url }
11
+ %a{ href: reset_url } Reset Password
12
12
 
13
13
  %p
14
14
  For security purposes, if you did not request this password reset,
@@ -3,7 +3,8 @@
3
3
  %head
4
4
  %meta{:content => "width=device-width", :name => "viewport"}/
5
5
  %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
6
- %title Actionable emails e.g. reset password
6
+ %title
7
+ = subject
7
8
  :css
8
9
  img {
9
10
  max-width: 100%;
@@ -59,10 +60,13 @@
59
60
  %td.container{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;", :valign => "top", :width => "600"}
60
61
  .content{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;"}
61
62
  = content
62
- - if defined? footer
63
- .footer{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;"}
64
- %table{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;", :width => "100%"}
65
- %tr{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"}
66
- %td.aligncenter.content-block{:align => "center", :style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;", :valign => "top"}
63
+ .footer{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;"}
64
+ %table{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;", :width => "100%"}
65
+ %tr{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"}
66
+ %td.aligncenter.content-block{:align => "center", :style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;", :valign => "top"}
67
+ - if defined? footer
67
68
  = footer
69
+ - else
70
+ This email was sent to
71
+ = to
68
72
  %td{:style => "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;", :valign => "top"}