devise-security 0.12.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +63 -0
  3. data/.gitignore +2 -0
  4. data/.mdlrc +1 -0
  5. data/.rubocop.yml +2 -1
  6. data/.ruby-version +1 -1
  7. data/.travis.yml +9 -11
  8. data/Appraisals +2 -2
  9. data/README.md +72 -53
  10. data/app/controllers/devise/paranoid_verification_code_controller.rb +2 -0
  11. data/app/controllers/devise/password_expired_controller.rb +2 -0
  12. data/config/locales/de.yml +13 -1
  13. data/config/locales/en.yml +13 -1
  14. data/config/locales/es.yml +13 -1
  15. data/config/locales/fr.yml +29 -0
  16. data/config/locales/tr.yml +17 -0
  17. data/devise-security.gemspec +10 -10
  18. data/gemfiles/{rails_4.1_stable.gemfile → rails_5.2.0.gemfile} +1 -1
  19. data/lib/devise-security.rb +8 -4
  20. data/lib/devise-security/controllers/helpers.rb +2 -0
  21. data/lib/devise-security/hooks/expirable.rb +3 -1
  22. data/lib/devise-security/hooks/paranoid_verification.rb +2 -0
  23. data/lib/devise-security/hooks/password_expirable.rb +2 -0
  24. data/lib/devise-security/hooks/session_limitable.rb +2 -0
  25. data/lib/devise-security/models/compatibility.rb +2 -0
  26. data/lib/devise-security/models/database_authenticatable_patch.rb +2 -0
  27. data/lib/devise-security/models/expirable.rb +2 -0
  28. data/lib/devise-security/models/old_password.rb +2 -0
  29. data/lib/devise-security/models/paranoid_verification.rb +2 -0
  30. data/lib/devise-security/models/password_archivable.rb +2 -0
  31. data/lib/devise-security/models/password_expirable.rb +96 -50
  32. data/lib/devise-security/models/secure_validatable.rb +10 -4
  33. data/lib/devise-security/models/security_questionable.rb +2 -0
  34. data/lib/devise-security/models/session_limitable.rb +2 -0
  35. data/lib/devise-security/orm/active_record.rb +2 -0
  36. data/lib/devise-security/patches.rb +2 -0
  37. data/lib/devise-security/patches/confirmations_controller_captcha.rb +2 -0
  38. data/lib/devise-security/patches/confirmations_controller_security_question.rb +2 -0
  39. data/lib/devise-security/patches/controller_captcha.rb +2 -0
  40. data/lib/devise-security/patches/controller_security_question.rb +2 -0
  41. data/lib/devise-security/patches/passwords_controller_captcha.rb +2 -0
  42. data/lib/devise-security/patches/passwords_controller_security_question.rb +2 -0
  43. data/lib/devise-security/patches/registrations_controller_captcha.rb +2 -0
  44. data/lib/devise-security/patches/sessions_controller_captcha.rb +2 -0
  45. data/lib/devise-security/patches/unlocks_controller_captcha.rb +2 -0
  46. data/lib/devise-security/patches/unlocks_controller_security_question.rb +2 -0
  47. data/lib/devise-security/rails.rb +2 -0
  48. data/lib/devise-security/routes.rb +2 -0
  49. data/lib/devise-security/schema.rb +2 -0
  50. data/lib/devise-security/validators/password_complexity_validator.rb +33 -0
  51. data/lib/devise-security/version.rb +3 -1
  52. data/lib/generators/devise_security/install_generator.rb +3 -1
  53. data/lib/generators/templates/devise-security.rb +9 -3
  54. data/test/dummy/Rakefile +3 -1
  55. data/test/dummy/app/controllers/application_controller.rb +2 -0
  56. data/test/dummy/app/controllers/captcha/sessions_controller.rb +2 -0
  57. data/test/dummy/app/controllers/security_question/unlocks_controller.rb +2 -0
  58. data/test/dummy/app/models/application_record.rb +2 -0
  59. data/test/dummy/app/models/captcha_user.rb +3 -1
  60. data/test/dummy/app/models/secure_user.rb +3 -1
  61. data/test/dummy/app/models/security_question_user.rb +3 -1
  62. data/test/dummy/app/models/user.rb +2 -0
  63. data/test/dummy/app/models/widget.rb +2 -0
  64. data/test/dummy/config.ru +3 -1
  65. data/test/dummy/config/application.rb +2 -0
  66. data/test/dummy/config/boot.rb +2 -0
  67. data/test/dummy/config/environment.rb +2 -0
  68. data/test/dummy/config/environments/test.rb +2 -0
  69. data/test/dummy/config/initializers/devise.rb +8 -0
  70. data/test/dummy/config/initializers/migration_class.rb +2 -0
  71. data/test/dummy/config/routes.rb +2 -0
  72. data/test/dummy/db/migrate/20120508165529_create_tables.rb +2 -0
  73. data/test/dummy/db/migrate/20150402165590_add_verification_columns.rb +2 -0
  74. data/test/dummy/db/migrate/20150407162345_add_verification_attempt_column.rb +2 -0
  75. data/test/dummy/db/migrate/20160320162345_add_security_questions_fields.rb +2 -0
  76. data/test/dummy/db/migrate/20180318103603_add_expireable_columns.rb +2 -0
  77. data/test/dummy/db/migrate/20180318105329_add_confirmable_columns.rb +2 -0
  78. data/test/dummy/db/migrate/20180318105732_add_rememberable_columns.rb +2 -0
  79. data/test/dummy/db/migrate/20180318111336_add_recoverable_columns.rb +2 -0
  80. data/test/dummy/db/migrate/20180319114023_add_widget.rb +2 -0
  81. data/test/test_captcha_controller.rb +2 -0
  82. data/test/test_complexity_validator.rb +60 -0
  83. data/test/test_helper.rb +19 -8
  84. data/test/test_install_generator.rb +7 -1
  85. data/test/test_paranoid_verification.rb +2 -0
  86. data/test/test_password_archivable.rb +2 -0
  87. data/test/test_password_expirable.rb +68 -7
  88. data/test/test_password_expired_controller.rb +2 -0
  89. data/test/test_secure_validatable.rb +10 -11
  90. data/test/test_security_question_controller.rb +2 -0
  91. metadata +32 -39
  92. data/.circleci/config.yml +0 -41
  93. data/gemfiles/rails_5.2_rc1.gemfile +0 -8
@@ -3,7 +3,19 @@ es:
3
3
  messages:
4
4
  taken_in_past: 'la contraseña fue usada previamente, favor elegir otra.'
5
5
  equal_to_current_password: 'tiene que ser diferente a la contraseña actual.'
6
- password_format: 'tiene que contener mayúsculas, minúsculas y digitos '
6
+ password_complexity:
7
+ digit:
8
+ one: tiene que contener al menos un digito
9
+ other: tiene que contener al menos %{count} digitos
10
+ lower:
11
+ one: tiene que contener al menos un minúscula
12
+ other: tiene que contener al menos %{count} minúsculas
13
+ symbol:
14
+ one: tiene que contener al menos un signo de puntuación
15
+ other: tiene que contener al menos %{count} signos de puntuación
16
+ upper:
17
+ one: tiene que contener al menos un mayúscula
18
+ other: tiene que contener al menos %{count} mayúsculas
7
19
  devise:
8
20
  invalid_captcha: 'El captcha ingresado es inválido.'
9
21
  invalid_security_question: 'La respuesta a la pregunta de suguridad fue incorrecta.'
@@ -0,0 +1,29 @@
1
+ fr:
2
+ errors:
3
+ messages:
4
+ taken_in_past: a été utilisé trop récemment - s'il vous plaît, choisissez un autre
5
+ equal_to_current_password: doit être différent du l'actuel
6
+ password_complexity:
7
+ digit:
8
+ one: doit containir en moins d'un chiffre
9
+ other: doit containir en moins de %{count} chiffres
10
+ lower:
11
+ one: doit containir en moins d'un miniscule
12
+ other: doit containir en moins de %{count} miniscules
13
+ symbol:
14
+ one: doit containir en moins d'un signe de ponctuation
15
+ other: doit containir en moins de %{count} signes de ponctuation
16
+ upper:
17
+ one: doit containir en moins d'un majuscule
18
+ other: doit containir en moins de %{count} majuscules
19
+ devise:
20
+ invalid_captcha: Le captcha n'est pas valide
21
+ invalid_security_question: La réponse à la question de sécurité était invalide
22
+ paranoid_verify:
23
+ code_required: Veuillez entrer le code fourni par notre équipe de support
24
+ password_expired:
25
+ updated: Votre nouveau mot de passe est enregistré
26
+ change_required: Votre mot de passe a expiré - s'il vous plaît, choisissez un autre
27
+ failure:
28
+ session_limited: Vos identifiants de connexion ont été utilisés dans un autre navigateur. Veuillez vous reconnecter pour continuer dans ce navigateur
29
+ expired: Votre compte a expiré en raison de l'inactivité. Veuillez contacter l'administrateur du site
@@ -0,0 +1,17 @@
1
+ tr:
2
+ errors:
3
+ messages:
4
+ taken_in_past: "daha önce kullanıldı."
5
+ equal_to_current_password: "mevcut paroladan farklı olmalı."
6
+ password_format: "büyük, küçük harfler ve sayılar içermeli."
7
+ devise:
8
+ invalid_captcha: "Captcha hatalı."
9
+ invalid_security_question: "Güvenlik sorusunun cevabı yanlış."
10
+ paranoid_verify:
11
+ code_required: "Destek ekibimizden aldığınız kodu girin."
12
+ password_expired:
13
+ updated: "Yeni parolanız kaydedildi."
14
+ change_required: "Parolanızın geçerlilik süresi dolmuş. Lütfen parolanızı yenileyin."
15
+ failure:
16
+ session_limited: 'Hesabınıza başka bir tarayıcıdan giriş yapılmış. Lütfen devam etmek için yeniden giriş yapın.'
17
+ expired: 'Hesabınız aktif olarak kullanılmadığı için artık geçerli değil. Lütfen yönetici ile irtibata geçin.'
@@ -20,25 +20,25 @@ Gem::Specification.new do |s|
20
20
  s.files = `git ls-files`.split("\n")
21
21
  s.test_files = `git ls-files -- test/*`.split("\n")
22
22
  s.require_paths = ['lib']
23
- s.required_ruby_version = '>= 2.2.9'
23
+ s.required_ruby_version = '>= 2.3.7'
24
24
 
25
25
  if RUBY_VERSION >= '2.4'
26
- s.add_runtime_dependency 'rails', '>= 4.1.0', '< 6.0'
26
+ s.add_runtime_dependency 'rails', '>= 4.2.0', '< 6.0'
27
27
  else
28
- s.add_runtime_dependency 'railties', '>= 4.1.0', '< 6.0'
28
+ s.add_runtime_dependency 'railties', '>= 4.2.0', '< 6.0'
29
29
  end
30
30
  s.add_runtime_dependency 'devise', '>= 4.2.0', '< 5.0'
31
31
 
32
32
  s.add_development_dependency 'appraisal'
33
- s.add_development_dependency 'bundler', '>= 1.3.0', '< 2.0'
34
- s.add_development_dependency 'coveralls', '~> 0.8'
35
- s.add_development_dependency 'easy_captcha', '~> 0'
33
+ s.add_development_dependency 'bundler'
34
+ s.add_development_dependency 'coveralls'
35
+ s.add_development_dependency 'easy_captcha'
36
36
  s.add_development_dependency 'm'
37
- s.add_development_dependency 'minitest', '5.10.3' # see https://github.com/seattlerb/minitest/issues/730
37
+ s.add_development_dependency 'minitest'
38
38
  s.add_development_dependency 'pry-byebug'
39
39
  s.add_development_dependency 'pry-rescue'
40
40
  s.add_development_dependency 'pry'
41
- s.add_development_dependency 'rails_email_validator', '~> 0'
42
- s.add_development_dependency 'rubocop', '~> 0'
43
- s.add_development_dependency 'sqlite3', '~> 1.3', '>= 1.3.10'
41
+ s.add_development_dependency 'rails_email_validator'
42
+ s.add_development_dependency 'rubocop', '~> 0.58.0'
43
+ s.add_development_dependency 'sqlite3'
44
44
  end
@@ -3,6 +3,6 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "omniauth"
6
- gem "rails", "~> 4.1.0"
6
+ gem "rails", "~> 5.2.0"
7
7
 
8
8
  gemspec path: "../"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record'
2
4
  require 'active_support/core_ext/integer'
3
5
  require 'active_support/ordered_hash'
@@ -6,13 +8,15 @@ require 'devise'
6
8
 
7
9
  module Devise
8
10
 
9
- # Should the password expire (e.g 3.months)
11
+ # Number of seconds that passwords are valid (e.g 3.months)
12
+ # Disable pasword expiration with +false+
13
+ # Expire only on demand with +true+
10
14
  mattr_accessor :expire_password_after
11
15
  @@expire_password_after = 3.months
12
16
 
13
17
  # Validate password for strongness
14
- mattr_accessor :password_regex
15
- @@password_regex = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/
18
+ mattr_accessor :password_complexity
19
+ @@password_complexity = { digit: 1, lower: 1, symbol: 1, upper: 1 }
16
20
 
17
21
  # Number of old passwords in archive
18
22
  mattr_accessor :password_archiving_count
@@ -23,7 +27,7 @@ module Devise
23
27
  @@deny_old_passwords = true
24
28
 
25
29
  # enable email validation for :secure_validatable. (true, false, validation_options)
26
- # dependency: need an email validator like rails_email_validator
30
+ # dependency: need an email validator, see https://github.com/devise-security/devise-security/blob/master/README.md#e-mail-validation
27
31
  mattr_accessor :email_validation
28
32
  @@email_validation = true
29
33
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DeviseSecurity
2
4
  module Controllers
3
5
  module Helpers
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Updates the last_activity_at fields from the record. Only when the user is active
2
4
  # for authentication and authenticated.
3
5
  # An expiry of the account is only checked on sign in OR on manually setting the
@@ -7,4 +9,4 @@ Warden::Manager.after_set_user do |record, warden, options|
7
9
  warden.authenticated?(options[:scope]) && record.respond_to?(:update_last_activity!)
8
10
  record.update_last_activity!
9
11
  end
10
- end
12
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Warden::Manager.after_set_user do |record, warden, options|
2
4
  if record.respond_to?(:need_paranoid_verification?)
3
5
  warden.session(options[:scope])['paranoid_verify'] = record.need_paranoid_verification?
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Warden::Manager.after_authentication do |record, warden, options|
2
4
  if record.respond_to?(:need_change_password?)
3
5
  warden.session(options[:scope])['password_expired'] = record.need_change_password?
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # After each sign in, update unique_session_id.
2
4
  # This is only triggered when the user is explicitly set (with set_user)
3
5
  # and on authentication. Retrieving the user from session (:fetch) does
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Devise
2
4
  module Models
3
5
  module Compatibility
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Devise
2
4
  module Models
3
5
  module DatabaseAuthenticatablePatch
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'devise-security/hooks/expirable'
2
4
 
3
5
  module Devise
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record'
2
4
  class OldPassword < ActiveRecord::Base
3
5
  belongs_to :password_archivable, polymorphic: true
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'devise-security/hooks/paranoid_verification'
2
4
 
3
5
  module Devise
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'compatibility'
2
4
 
3
5
  module Devise
@@ -1,67 +1,113 @@
1
- require 'devise-security/hooks/password_expirable'
1
+ # frozen_string_literal: true
2
2
 
3
- module Devise
4
- module Models
3
+ require 'devise-security/hooks/password_expirable'
5
4
 
6
- # PasswordExpirable takes care of change password after
7
- module PasswordExpirable
8
- extend ActiveSupport::Concern
5
+ module Devise::Models
6
+ # PasswordExpirable makes passwords expire after a configurable amount of
7
+ # time, or on demand.
8
+ #
9
+ # == Configuration
10
+ # Set +expire_password_after+ to the number of seconds a password is valid for
11
+ # (example: +3.months+). Setting it to +true+ will allow passwords to be expired
12
+ # on-demand only, and +false+ disables this feature.
13
+ #
14
+ # == Expire On-Demand
15
+ # This is useful to force users to change passwords for complex business reasons.
16
+ # Call +need_change_password+ to indicate a record needs a new password.
17
+ module PasswordExpirable
18
+ extend ActiveSupport::Concern
9
19
 
10
- included do
11
- before_save :update_password_changed
12
- end
20
+ included do
21
+ scope :with_password_change_requested, -> { where(password_changed_at: nil) }
22
+ scope :without_password_change_requested, -> { where.not(password_changed_at: nil) }
23
+ scope :with_expired_password, -> { where('password_changed_at is NULL OR password_changed_at < ?', expire_password_after.seconds.ago) }
24
+ scope :without_expired_password, -> { without_password_change_requested.where('password_changed_at >= ?', expire_password_after.seconds.ago) }
25
+ before_save :update_password_changed
26
+ end
13
27
 
14
- # is an password change required?
15
- def need_change_password?
16
- if expired_password_after_numeric?
17
- self.password_changed_at.nil? || self.password_changed_at < self.expire_password_after.seconds.ago
18
- else
19
- false
20
- end
21
- end
28
+ # Is a password change required?
29
+ # @return [Boolean]
30
+ # @return [true] if +password_changed_at+ has not been set or if it is old
31
+ # enough based on +expire_password_after+ configuration.
32
+ def need_change_password?
33
+ password_change_requested? || password_too_old?
34
+ end
22
35
 
23
- # set a fake datetime so a password change is needed and save the record
24
- def need_change_password!
25
- if expired_password_after_numeric?
26
- need_change_password
27
- self.save(validate: false)
28
- end
29
- end
36
+ # Clear the +password_changed_at+ field so that the user will be required to
37
+ # update their password.
38
+ # @note Saves the record (without validations)
39
+ # @return [Boolean]
40
+ def need_change_password!
41
+ return unless password_expiration_enabled?
42
+ need_change_password
43
+ save(validate: false)
44
+ end
45
+ alias expire_password! need_change_password!
46
+ alias request_password_change! need_change_password!
30
47
 
31
- # set a fake datetime so a password change is needed
32
- def need_change_password
33
- if expired_password_after_numeric?
34
- self.password_changed_at = self.expire_password_after.seconds.ago
35
- end
48
+ # Clear the +password_changed_at+ field so that the user will be required to
49
+ # update their password.
50
+ # @note Does not save the record
51
+ # @return [void]
52
+ def need_change_password
53
+ return unless password_expiration_enabled?
54
+ self.password_changed_at = nil
55
+ end
56
+ alias expire_password need_change_password
57
+ alias request_password_change need_change_password
36
58
 
37
- # is date not set it will set default to need set new password next login
38
- need_change_password if self.password_changed_at.nil?
59
+ # @return [Integer] number of seconds passwords are valid for
60
+ # @return [true] passwords are expired 'on demand' only.
61
+ # @return [false] passwords never expire (this feature is disabled)
62
+ def expire_password_after
63
+ self.class.expire_password_after
64
+ end
39
65
 
40
- self.password_changed_at
41
- end
66
+ # When +password_changed_at+ is set to +NULL+ in the database
67
+ # the user is required to change their password. This only happens
68
+ # on demand or when the column is first added to the table.
69
+ # @return [Boolean]
70
+ def password_change_requested?
71
+ return false unless password_expiration_enabled?
72
+ return false if new_record?
73
+ password_changed_at.nil?
74
+ end
42
75
 
43
- def expire_password_after
44
- self.class.expire_password_after
45
- end
76
+ # Is this password older than the configured expiration timeout?
77
+ # @return [Boolean]
78
+ def password_too_old?
79
+ return false if new_record?
80
+ return false unless password_expiration_enabled?
81
+ return false if expire_password_on_demand?
82
+ password_changed_at < expire_password_after.seconds.ago
83
+ end
84
+ alias password_expired? password_too_old?
46
85
 
47
- private
86
+ private
48
87
 
49
- # is password changed then update password_cahanged_at
50
- def update_password_changed
51
- self.password_changed_at = Time.now if (self.new_record? || self.encrypted_password_changed?) && !self.password_changed_at_changed?
52
- end
88
+ # Update +password_changed_at+ for new records and changed passwords.
89
+ # @note called as a +before_save+ hook
90
+ def update_password_changed
91
+ return unless (new_record? || encrypted_password_changed?) && !password_changed_at_changed?
92
+ self.password_changed_at = Time.zone.now
93
+ end
53
94
 
54
- def expired_password_after_numeric?
55
- return @_numeric if defined?(@_numeric)
56
- @_numeric ||= self.expire_password_after.is_a?(1.class) ||
57
- self.expire_password_after.is_a?(Float)
58
- end
95
+ # Enabled if configuration +expire_password_after+ is set to an {Integer},
96
+ # {Float}, or {true}
97
+ def password_expiration_enabled?
98
+ expire_password_after.is_a?(1.class) ||
99
+ expire_password_after.is_a?(Float) ||
100
+ expire_password_on_demand?
101
+ end
59
102
 
60
- module ClassMethods
61
- ::Devise::Models.config(self, :expire_password_after)
62
- end
103
+ # When +expire_password_after+ is set to +true+ then only expire passwords
104
+ # on demand.
105
+ def expire_password_on_demand?
106
+ expire_password_after.present? && expire_password_after == true
63
107
  end
64
108
 
109
+ module ClassMethods
110
+ ::Devise::Models.config(self, :expire_password_after)
111
+ end
65
112
  end
66
-
67
113
  end
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'compatibility'
4
+ require_relative '../validators/password_complexity_validator'
2
5
 
3
6
  module Devise
4
7
  module Models
@@ -45,8 +48,10 @@ module Devise
45
48
  end
46
49
 
47
50
  # extra validations
48
- validates :email, email: email_validation if email_validation # use rails_email_validator or similar
49
- validates :password, format: { with: password_regex, message: :password_format }, if: :password_required?
51
+ validates :email, email: email_validation if email_validation # see https://github.com/devise-security/devise-security/blob/master/README.md#e-mail-validation
52
+ validates :password,
53
+ 'devise_security/password_complexity': password_complexity,
54
+ if: :password_required?
50
55
 
51
56
  # don't allow use same password
52
57
  validate :current_equal_password_validation
@@ -79,9 +84,10 @@ module Devise
79
84
  end
80
85
 
81
86
  module ClassMethods
82
- Devise::Models.config(self, :password_regex, :password_length, :email_validation)
87
+ Devise::Models.config(self, :password_complexity, :password_length, :email_validation)
88
+
89
+ private
83
90
 
84
- private
85
91
  def has_uniqueness_validation_of_login?
86
92
  validators.any? do |validator|
87
93
  validator.kind_of?(ActiveRecord::Validations::UniquenessValidator) &&
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Devise
2
4
  module Models
3
5
  # SecurityQuestionable is an accessible add-on for visually handicapped people,
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'devise-security/hooks/session_limitable'
2
4
 
3
5
  module Devise
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DeviseSecurity
2
4
  module Orm
3
5
  # This module contains some helpers and handle schema (migrations):