shieldify 0.1.2.pre.alpha

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 (44) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +375 -0
  4. data/Rakefile +3 -0
  5. data/app/controllers/users/access_controller.rb +16 -0
  6. data/app/controllers/users/emails/reset_passwords_controller.rb +30 -0
  7. data/app/controllers/users/emails_controller.rb +17 -0
  8. data/app/models/jwt_session.rb +5 -0
  9. data/lib/generators/shieldify/USAGE +8 -0
  10. data/lib/generators/shieldify/install_generator.rb +52 -0
  11. data/lib/generators/shieldify/templates/initializer.rb.tt +52 -0
  12. data/lib/generators/shieldify/templates/locales/en.shieldify.yml.tt +58 -0
  13. data/lib/generators/shieldify/templates/locales/es.shieldify.yml.tt +48 -0
  14. data/lib/generators/shieldify/templates/mailer_layouts/mailer.html.erb +10 -0
  15. data/lib/generators/shieldify/templates/mailer_layouts/mailer.text.erb +3 -0
  16. data/lib/generators/shieldify/templates/mailer_views/email_changed.html.erb +3 -0
  17. data/lib/generators/shieldify/templates/mailer_views/email_changed.text.erb +5 -0
  18. data/lib/generators/shieldify/templates/mailer_views/email_confirmation_instructions.html.erb +7 -0
  19. data/lib/generators/shieldify/templates/mailer_views/email_confirmation_instructions.text.erb +7 -0
  20. data/lib/generators/shieldify/templates/mailer_views/password_changed.html.erb +3 -0
  21. data/lib/generators/shieldify/templates/mailer_views/password_changed.text.erb +5 -0
  22. data/lib/generators/shieldify/templates/mailer_views/reset_email_password_instructions.html.erb +5 -0
  23. data/lib/generators/shieldify/templates/mailer_views/reset_email_password_instructions.text.erb +9 -0
  24. data/lib/generators/shieldify/templates/mailer_views/unlock_access_instructions.html.erb +4 -0
  25. data/lib/generators/shieldify/templates/mailer_views/unlock_access_instructions.text.erb +7 -0
  26. data/lib/generators/shieldify/templates/migration.rb.tt +28 -0
  27. data/lib/generators/shieldify/templates/model.rb.tt +2 -0
  28. data/lib/shieldify/controllers/helpers.rb +29 -0
  29. data/lib/shieldify/failure_app.rb +8 -0
  30. data/lib/shieldify/jwt_service.rb +158 -0
  31. data/lib/shieldify/mailer.rb +44 -0
  32. data/lib/shieldify/middleware/authentication.rb +27 -0
  33. data/lib/shieldify/middleware.rb +36 -0
  34. data/lib/shieldify/model_extensions.rb +73 -0
  35. data/lib/shieldify/models/email_authenticatable/confirmable.rb +159 -0
  36. data/lib/shieldify/models/email_authenticatable/registerable.rb +117 -0
  37. data/lib/shieldify/models/email_authenticatable.rb +41 -0
  38. data/lib/shieldify/railtie.rb +52 -0
  39. data/lib/shieldify/strategies/email.rb +48 -0
  40. data/lib/shieldify/strategies/jwt.rb +78 -0
  41. data/lib/shieldify/version.rb +3 -0
  42. data/lib/shieldify.rb +74 -0
  43. data/lib/tasks/shieldify_tasks.rake +4 -0
  44. metadata +163 -0
@@ -0,0 +1,48 @@
1
+ es:
2
+ shieldify:
3
+ models:
4
+ email_authenticatable:
5
+ confirmable:
6
+ email_confirmation_token:
7
+ errors:
8
+ invalid: "inválido"
9
+ expired: "ha expirado"
10
+ unconfirmed_email:
11
+ errors:
12
+ not_found: "no encontrado"
13
+ email_confirmation_token_generated_at:
14
+ mailer:
15
+ email_confirmation_instructions:
16
+ subject: "Instrucciones de Confirmación de Email"
17
+ title: "Instrucciones de Confirmación de Email"
18
+ greeting: "Hola %{email},"
19
+ thanks: "Por favor confirma tu email haciendo clic en el siguiente enlace:"
20
+ confirm_account: "Confirmar email"
21
+ ignore: "Si no has solicitado esta confirmación, por favor ignora este correo."
22
+ reset_password_instructions:
23
+ subject: "Instrucciones para Restablecer Contraseña"
24
+ title: "Instrucciones para Restablecer Contraseña"
25
+ greeting: "Hola %{email},"
26
+ instructions: "Alguien ha solicitado un enlace para cambiar tu contraseña. Puedes hacerlo a través del enlace de abajo:"
27
+ change_password: "Cambiar mi contraseña"
28
+ ignore: "Si no solicitaste esto, por favor ignora este correo. Tu contraseña no cambiará."
29
+ link_expiration: "Este enlace expirará en %{expiration_hours} horas."
30
+ unlock_instructions:
31
+ subject: "Instrucciones para Desbloquear Cuenta"
32
+ title: "Instrucciones para Desbloquear Cuenta"
33
+ greeting: "Hola %{email},"
34
+ instructions: "Tu cuenta ha sido bloqueada debido a un número excesivo de intentos de inicio de sesión fallidos. Por favor, desbloquea tu cuenta haciendo clic en el siguiente enlace:"
35
+ unlock_account: "Desbloquear Cuenta"
36
+ ignore: "Si no has solicitado esto, por favor ignora este correo. Tu cuenta permanecerá segura."
37
+ email_changed:
38
+ subject: "Correo Electrónico Actualizado"
39
+ title: "Correo Electrónico Actualizado"
40
+ greeting: "Hola %{email},"
41
+ message: "Recibimos una solicitud para cambiar la dirección de correo electrónico asociada a tu cuenta a %{unconfirmed_email}."
42
+ ignore: "Si no hiciste este cambio, por favor contacta a soporte inmediatamente."
43
+ password_changed:
44
+ subject: "Contraseña Actualizada"
45
+ title: "Contraseña Actualizada"
46
+ greeting: "Hola %{email},"
47
+ message: "Esta es una notificación de que tu contraseña ha sido cambiada exitosamente."
48
+ advice: "Si no realizaste este cambio, por favor contacta a nuestro equipo de soporte inmediatamente."
@@ -0,0 +1,10 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= t('shieldify.mailer.confirmation_instructions.title') %></title>
5
+ <meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
6
+ </head>
7
+ <body>
8
+ <%= yield %>
9
+ </body>
10
+ </html>
@@ -0,0 +1,3 @@
1
+ <%= t('shieldify.mailer.email_changed.title') %>
2
+
3
+ <%= yield %>
@@ -0,0 +1,3 @@
1
+ <h1><%= t('shieldify.mailer.email_changed.greeting', email: @user.email) %></h1>
2
+ <p><%= t('shieldify.mailer.email_changed.message', unconfirmed_email: @user.unconfirmed_email) %></p>
3
+ <p><%= t('shieldify.mailer.email_changed.ignore') %></p>
@@ -0,0 +1,5 @@
1
+ <%= t('shieldify.mailer.email_changed.greeting', email: @user.email) %>
2
+
3
+ <%= t('shieldify.mailer.email_changed.message', unconfirmed_email: @user.unconfirmed_email) %>
4
+
5
+ <%= t('shieldify.mailer.email_changed.ignore') %>
@@ -0,0 +1,7 @@
1
+ <h1><%= t('shieldify.mailer.email_confirmation_instructions.greeting', email: @user.unconfirmed_email) %></h1>
2
+
3
+ <p><%= t('shieldify.mailer.email_confirmation_instructions.thanks') %></p>
4
+
5
+ <p><%= link_to t('shieldify.mailer.email_confirmation_instructions.confirm_account'), users_email_confirmation_url(token: @token) %></p>
6
+
7
+ <p><%= t('shieldify.mailer.email_confirmation_instructions.ignore') %></p>
@@ -0,0 +1,7 @@
1
+ <%= t('shieldify.mailer.email_confirmation_instructions.greeting', email: @user.unconfirmed_email) %>
2
+
3
+ <%= t('shieldify.mailer.email_confirmation_instructions.thanks') %>
4
+
5
+ <%= users_email_confirmation_url(token: @token) %>
6
+
7
+ <%= t('shieldify.mailer.email_confirmation_instructions.ignore') %>
@@ -0,0 +1,3 @@
1
+ <h1><%= t('shieldify.mailer.password_changed.greeting', email: @user.email) %></h1>
2
+ <p><%= t('shieldify.mailer.password_changed.message') %></p>
3
+ <p><%= t('shieldify.mailer.password_changed.advice') %></p>
@@ -0,0 +1,5 @@
1
+ <%= t('shieldify.mailer.password_changed.greeting', email: @user.email) %>
2
+
3
+ <%= t('shieldify.mailer.password_changed.message') %>
4
+
5
+ <%= t('shieldify.mailer.password_changed.advice') %>
@@ -0,0 +1,5 @@
1
+ <h1><%= t('shieldify.mailer.reset_password_instructions.greeting', name: @user.name) %></h1>
2
+ <p><%= t('shieldify.mailer.reset_password_instructions.instructions') %></p>
3
+ <p><%= link_to t('shieldify.mailer.reset_password_instructions.change_password'), edit_password_url(@user.reset_password_token) %></p>
4
+ <p><%= t('shieldify.mailer.reset_password_instructions.link_expiration', expiration_hours: 24) %></p>
5
+ <p><%= t('shieldify.mailer.reset_password_instructions.ignore') %></p>
@@ -0,0 +1,9 @@
1
+ <%= t('shieldify.mailer.reset_password_instructions.greeting', name: @user.name) %>
2
+
3
+ <%= t('shieldify.mailer.reset_password_instructions.instructions') %>
4
+
5
+ <%= edit_password_url(@user.reset_password_token) %>
6
+
7
+ <%= t('shieldify.mailer.reset_password_instructions.link_expiration', expiration_hours: 24) %>
8
+
9
+ <%= t('shieldify.mailer.reset_password_instructions.ignore') %>
@@ -0,0 +1,4 @@
1
+ <h1><%= t('shieldify.mailer.unlock_instructions.greeting', name: @user.name) %></h1>
2
+ <p><%= t('shieldify.mailer.unlock_instructions.instructions') %></p>
3
+ <p><%= link_to t('shieldify.mailer.unlock_instructions.unlock_account'), unlock_url(@user.unlock_token) %></p>
4
+ <p><%= t('shieldify.mailer.unlock_instructions.ignore') %></p>
@@ -0,0 +1,7 @@
1
+ <%= t('shieldify.mailer.unlock_instructions.greeting', name: @user.name) %>
2
+
3
+ <%= t('shieldify.mailer.unlock_instructions.instructions') %>
4
+
5
+ <%= unlock_url(@user.unlock_token) %>
6
+
7
+ <%= t('shieldify.mailer.unlock_instructions.ignore') %>
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ShieldifyCreateUsers < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :users do |t|
6
+ ## Email registerable
7
+ t.string :email, default: ""
8
+ t.string :password_digest, default: ""
9
+
10
+ ## Email confirmable
11
+ t.string :unconfirmed_email
12
+ t.string :email_confirmation_token
13
+ t.string :email_confirmation_token_generated_at
14
+
15
+ t.timestamps null: false
16
+ end
17
+
18
+ create_table :jwt_sessions do |t|
19
+ t.string :jti, null: false
20
+ t.references :user, null: false, foreign_key: true
21
+
22
+ t.timestamps
23
+ end
24
+
25
+ add_index :users, :email, unique: true
26
+ add_index :jwt_sessions, :jti, unique: true
27
+ end
28
+ end
@@ -0,0 +1,2 @@
1
+ class User < ApplicationRecord
2
+ end
@@ -0,0 +1,29 @@
1
+ module Shieldify
2
+ module Controllers
3
+ module Helpers
4
+ def current_user
5
+ warden.user
6
+ end
7
+
8
+ def user_signed_in?
9
+ !!current_user
10
+ end
11
+
12
+ def authenticate_user!
13
+ unless user_signed_in?
14
+ respond_to_unauthorized
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def warden
21
+ request.env['warden']
22
+ end
23
+
24
+ def respond_to_unauthorized
25
+ render json: { error: 'No autorizado' }, status: :unauthorized
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,8 @@
1
+ module Shieldify
2
+ class FailureApp
3
+ def self.call(env)
4
+ error_message = env['warden.options'][:message] || 'Unauthorized'
5
+ [401, { 'Content-Type' => 'application/json' }, [{ error: error_message }.to_json]]
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,158 @@
1
+ require 'jwt'
2
+
3
+ class JwtService
4
+ @secret_key = Shieldify::Configuration.jwt_secret
5
+ @issuer = Shieldify::Configuration.jwt_issuer
6
+ @jwt_exp = Shieldify::Configuration.jwt_exp
7
+
8
+ class << self
9
+ # Generates a new JWT token for a user using their unique identifier.
10
+ #
11
+ # @param user_id [Integer] The unique identifier for the user for whom the token is being generated.
12
+ #
13
+ # This method constructs a JWT payload containing several fields including the user's ID, the token's
14
+ # expiration time, and other standard JWT claims. The JWT is then encoded using the HS256 algorithm
15
+ # with a secret key stored in the service's configuration. The method handles encoding issues and returns
16
+ # a structured array containing the results of the token generation process.
17
+ #
18
+ # The method can be called with or without a block:
19
+ #
20
+ # Without a block, it returns an array with four elements:
21
+ # - [Boolean] `result`: true if the token is successfully generated, false if an error occurred.
22
+ # - [String, nil] `token`: the generated JWT token if successful, otherwise nil.
23
+ # - [String, nil] `jti`: the unique JWT ID if successful, otherwise nil.
24
+ # - [String, nil] `errors`: description of the error if token generation failed, otherwise nil.
25
+ #
26
+ # With a block, the block is yielded with the same four elements, allowing for inline handling:
27
+ #
28
+ # Usage Examples:
29
+ #
30
+ # Without a block:
31
+ # success, token, jti, error = JwtService.encode(user_id)
32
+ # if success
33
+ # puts "JWT generated successfully: #{token}, JTI: #{jti}"
34
+ # else
35
+ # puts "Error: #{error}"
36
+ # end
37
+ #
38
+ # With a block:
39
+ # JwtService.encode(user_id) do |success, token, jti, error|
40
+ # if success
41
+ # puts "JWT generated successfully: #{token}, JTI: #{jti}"
42
+ # else
43
+ # puts "Error: #{error}"
44
+ # end
45
+ # end
46
+ #
47
+ # This method provides a reliable way to generate JWTs with a standard set of claims and handles
48
+ # any exceptions that may occur during the encoding process.
49
+ def encode(user_id)
50
+ begin
51
+ payload = jwt_payload(user_id)
52
+ jti = payload[:jti]
53
+ token = JWT.encode(payload, secret_key, 'HS256')
54
+ result = [true, token, jti, nil]
55
+ rescue StandardError => e
56
+ result = [false, nil, nil, e.message]
57
+ end
58
+
59
+ if block_given?
60
+ yield(*result)
61
+ else
62
+ result
63
+ end
64
+ end
65
+
66
+ # Decodes a JWT token to verify its authenticity and check if it is still valid.
67
+ #
68
+ # @param token [String] The JWT token to be decoded.
69
+ #
70
+ # This method uses JWT.decode to attempt to decode the token using a predefined secret key and issuer.
71
+ # It handles various errors that might occur during the decoding process, such as expiration or incorrect
72
+ # formatting of the token. It encapsulates the results and errors into a structured array format.
73
+ #
74
+ # The method can be called with or without a block:
75
+ #
76
+ # Without a block, it returns an array with three elements:
77
+ # - [Boolean] `result`: true if the token is successfully decoded, false if an error occurred.
78
+ # - [Hash, nil] `payload`: the decoded payload of the token if successful, otherwise nil.
79
+ # - [String, nil] `errors`: description of the error if decoding failed, otherwise nil.
80
+ #
81
+ # With a block, the block is yielded with the same three elements, allowing for inline handling:
82
+ #
83
+ # Usage Examples:
84
+ #
85
+ # Without a block:
86
+ # success, payload, error = JwtService.decode(token)
87
+ # if success
88
+ # puts "Decoded Payload: #{payload}"
89
+ # else
90
+ # puts "Error: #{error}"
91
+ # end
92
+ #
93
+ # With a block:
94
+ # JwtService.decode(token) do |success, payload, error|
95
+ # if success
96
+ # puts "Decoded Payload: #{payload}"
97
+ # else
98
+ # puts "Error: #{error}"
99
+ # end
100
+ # end
101
+ #
102
+ # This method ensures robust handling of JWTs by validating their integrity and relevance,
103
+ # adhering to the security settings defined by the secret_key and issuer configuration.
104
+ def decode(token)
105
+ decoded_token = JWT.decode(token, secret_key, true, decode_options).first
106
+ result = [true, decoded_token, nil] # result: true (success), payload: decoded_token, errors: nil
107
+ rescue *jwt_exceptions => e
108
+ result = [false, nil, e.message]
109
+ rescue => e
110
+ result = [false, nil, "Unexpected error: #{e.message}"]
111
+ ensure
112
+ if block_given?
113
+ yield(*result)
114
+ else
115
+ result
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ def decode_options
122
+ { algorithm: 'HS256', verify_iss: true, iss: issuer, verify_expiration: true }
123
+ end
124
+
125
+ def jwt_payload(user_id)
126
+ {
127
+ sub: user_id,
128
+ exp: jwt_exp,
129
+ nbf: Time.now.to_i,
130
+ iss: issuer,
131
+ jti: SecureRandom.hex,
132
+ iat: Time.now.to_i
133
+ }
134
+ end
135
+
136
+ def jwt_exceptions
137
+ [
138
+ JWT::ExpiredSignature,
139
+ JWT::InvalidIssuerError,
140
+ JWT::DecodeError,
141
+ JWT::VerificationError,
142
+ JWT::InvalidIatError
143
+ ]
144
+ end
145
+
146
+ def secret_key
147
+ @secret_key
148
+ end
149
+
150
+ def issuer
151
+ @issuer
152
+ end
153
+
154
+ def jwt_exp
155
+ @jwt_exp
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shieldify
4
+ class Mailer < Shieldify::Configuration.parent_mailer.constantize
5
+ layout 'layouts/shieldify/mailer'
6
+
7
+ default(
8
+ from: Shieldify::Configuration.mailer_sender,
9
+ reply_to: Shieldify::Configuration.reply_to
10
+ )
11
+
12
+ def base_mailer
13
+ initialize_email_resources(params)
14
+
15
+ mail(define_headers)
16
+ end
17
+
18
+ private
19
+
20
+ def define_headers
21
+ headers = {
22
+ to: email_to,
23
+ subject: define_subject,
24
+ template_path: "shieldify/mailer",
25
+ template_name: action
26
+ }
27
+
28
+ headers.store(:reply_to, reply_to) if instance_variable_defined?(:@reply_to)
29
+ headers.store(:from, from) if instance_variable_defined?(:@from)
30
+ headers
31
+ end
32
+
33
+ def initialize_email_resources(options)
34
+ options.each do |key, value|
35
+ self.class.send(:attr_accessor, key)
36
+ instance_variable_set("@#{key}", value)
37
+ end
38
+ end
39
+
40
+ def define_subject
41
+ I18n.t("shieldify.mailer.#{action}.subject")
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,27 @@
1
+ module Shieldify
2
+ class Middleware
3
+ class Authentication
4
+ attr_reader :app
5
+
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ env['warden'].authenticate!(:email, :jwt)
12
+
13
+ status, headers, response = app.call(env)
14
+ headers = headers_with_token(env, headers)
15
+ [status, headers, response]
16
+ end
17
+
18
+ private
19
+
20
+ def headers_with_token(env, headers)
21
+ token = env["auth.jwt"]
22
+ headers['Authorization'] = "Bearer #{token}"
23
+ headers
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,36 @@
1
+ module Shieldify
2
+ class Middleware
3
+ attr_reader :app, :request
4
+
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ @request = Rack::Request.new(env)
11
+
12
+ if should_authenticate?
13
+ builder = Rack::Builder.new
14
+ builder.use(Shieldify::Middleware::Authentication)
15
+ builder.run(app)
16
+ builder.call(env)
17
+ else
18
+ app.call(env)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def should_authenticate?
25
+ should_authenticate_by_email? || should_authenticate_by_jwt?
26
+ end
27
+
28
+ def should_authenticate_by_email?
29
+ request.path.end_with?('/shfy/login')
30
+ end
31
+
32
+ def should_authenticate_by_jwt?
33
+ request.env['HTTP_AUTHORIZATION'].present?
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shieldify
4
+ # This module provides extensions for models to include Shieldify functionality.
5
+ # It allows dynamic inclusion of modules and submodules for various authentication features.
6
+ #
7
+ # The primary purpose of this module is to simplify the process of adding authentication
8
+ # capabilities to models in a Rails application. By including this module,
9
+ # developers can easily integrate functionalities such as email-based authentication,
10
+ # user registration, email confirmation, and more, depending on the modules and submodules
11
+ # specified.
12
+ #
13
+ # This module uses ActiveSupport::Concern to extend the including model with class methods
14
+ # and associations necessary for managing JWT sessions and other authentication-related tasks.
15
+ #
16
+ # @example Including the module in a User model
17
+ # # To use this module, include it in your model (typically a User model).
18
+ # # Then, use the `shieldify` method to specify the desired modules and submodules.
19
+ # # In this example, the User model is being extended with email authentication,
20
+ # # and two submodules: registerable and confirmable.
21
+ # class User < ApplicationRecord
22
+ # include Shieldify::ModelExtensions
23
+ #
24
+ # # The `shieldify` method is called with a hash where the key is the main module
25
+ # # (:email_authenticatable) and the value is an array of submodules
26
+ # # (%i[registerable confirmable]).
27
+ # # This will dynamically include the corresponding modules and submodules
28
+ # # from the Shieldify::Models namespace into the User model.
29
+ # shieldify email_authenticatable: %i[registerable confirmable]
30
+ # end
31
+ #
32
+ # @see Shieldify::ModelExtensions#shieldify
33
+ module ModelExtensions
34
+ extend ActiveSupport::Concern
35
+
36
+ class_methods do
37
+ # Dynamically includes modules. Accepts a hash where the keys are symbols
38
+ # of main modules and the values are arrays of submodules to include.
39
+ #
40
+ # @example
41
+ # shieldify email_authenticatable: %i[registerable confirmable]
42
+ #
43
+ # This method is intended to be used within a user class or any other class
44
+ # that acts as a user.
45
+ #
46
+ # @param modules [Hash<Symbol, Array<Symbol>>] A hash where the keys are main modules and the values are arrays of submodules to include.
47
+ # @return [void]
48
+ def shieldify(modules)
49
+ modules.each do |parent, submodules|
50
+ include_parent_module(parent)
51
+
52
+ submodules.each do |submodule|
53
+ include_submodule(parent, submodule)
54
+ end
55
+ end
56
+
57
+ has_many :jwt_sessions, dependent: :destroy
58
+ end
59
+
60
+ private
61
+
62
+ # Includes the parent module based on the provided symbol
63
+ def include_parent_module(parent_module)
64
+ include "Shieldify::Models::#{parent_module.to_s.camelize}".constantize
65
+ end
66
+
67
+ # Includes a specific submodule within a parent module
68
+ def include_submodule(parent_module, submodule)
69
+ include "Shieldify::Models::#{parent_module.to_s.camelize}::#{submodule.to_s.camelize}".constantize
70
+ end
71
+ end
72
+ end
73
+ end