console1984 0.1.4 → 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +32 -10
  3. data/config/protections.yml +30 -0
  4. data/lib/console1984/command_executor.rb +90 -0
  5. data/lib/console1984/command_validator/forbidden_constant_reference_validation.rb +31 -0
  6. data/lib/console1984/command_validator/forbidden_reopening_validation.rb +29 -0
  7. data/lib/console1984/command_validator/parsed_command.rb +90 -0
  8. data/lib/console1984/command_validator/suspicious_terms_validation.rb +22 -0
  9. data/lib/console1984/command_validator.rb +71 -0
  10. data/lib/console1984/config.rb +21 -9
  11. data/lib/console1984/engine.rb +6 -8
  12. data/lib/console1984/errors.rb +10 -1
  13. data/lib/console1984/ext/active_record/protected_auditable_tables.rb +28 -0
  14. data/lib/console1984/ext/core/module.rb +15 -0
  15. data/lib/console1984/ext/core/object.rb +43 -0
  16. data/lib/console1984/ext/irb/commands.rb +16 -0
  17. data/lib/console1984/ext/irb/context.rb +20 -0
  18. data/lib/console1984/{protected_tcp_socket.rb → ext/socket/tcp_socket.rb} +10 -4
  19. data/lib/console1984/freezeable.rb +70 -0
  20. data/lib/console1984/{supervisor/input_output.rb → input_output.rb} +9 -3
  21. data/lib/console1984/messages.rb +0 -10
  22. data/lib/console1984/protections_config.rb +17 -0
  23. data/lib/console1984/refrigerator.rb +32 -0
  24. data/lib/console1984/sessions_logger/database.rb +3 -1
  25. data/lib/console1984/shield/method_invocation_shell.rb +52 -0
  26. data/lib/console1984/shield/modes/protected.rb +27 -0
  27. data/lib/console1984/shield/modes/unprotected.rb +8 -0
  28. data/lib/console1984/shield/modes.rb +60 -0
  29. data/lib/console1984/shield.rb +85 -0
  30. data/lib/console1984/supervisor.rb +27 -22
  31. data/lib/console1984/username/env_resolver.rb +2 -0
  32. data/lib/console1984/version.rb +1 -1
  33. data/lib/console1984.rb +43 -21
  34. metadata +66 -14
  35. data/config/routes.rb +0 -9
  36. data/lib/console1984/commands.rb +0 -16
  37. data/lib/console1984/frozen_methods.rb +0 -17
  38. data/lib/console1984/protected_auditable_tables.rb +0 -29
  39. data/lib/console1984/protected_context.rb +0 -18
  40. data/lib/console1984/supervisor/accesses/protected.rb +0 -10
  41. data/lib/console1984/supervisor/accesses/unprotected.rb +0 -5
  42. data/lib/console1984/supervisor/accesses.rb +0 -41
  43. data/lib/console1984/supervisor/executor.rb +0 -41
  44. data/lib/console1984/supervisor/protector.rb +0 -37
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 39d8147e2b8b8fa0a20c6aec1422a28633afe80567bac0ac0c98a7d6096ae430
4
- data.tar.gz: 760d761d4ced82752b89aa7bd1de44416d545a75bdb0f4266165718100d8a7a5
3
+ metadata.gz: bc64d037f2de5570292e0b09710b4543a68ba1af12759150cc68e7b7f4dd6e16
4
+ data.tar.gz: c5929af5061393a32c4df38022535d53c62aaaabe951727798e340322fb0d950
5
5
  SHA512:
6
- metadata.gz: 3eedbf3cc3436469edd615b397af1c5ad03195f7f1eebfcc1ed6da859a90e0fc213327f8d487841640ddec8c4d39ed1562239e9dda10360a622f59aa809b6f9a
7
- data.tar.gz: bdd11a8580f29fd700c954c486e01dc1f8189e452565f37b281d9ee96b78bd375c2f02728df3c4ce405c047fc9fe52b5eac8b5b90a62519bfeaff4aa1465a5d9
6
+ metadata.gz: b64f422bcdd421e7a874af965c9ee615f6d873e0d2a685d68667216fd1ee91d4e8e4815d4dd9e78ef7838a75763fb898be1f328c8466a137f1782a4121531da6
7
+ data.tar.gz: ce7e1f60bfdb666abe3c85a02ddaa1ac25344c994a1471ec64d07cd15f8aac5735bcd0608497e71d9813cf111cdb76f414a2373eb8dbef14b983757ba3876812
data/README.md CHANGED
@@ -10,10 +10,12 @@ A Rails console extension that protects sensitive accesses and makes them audita
10
10
 
11
11
  If you are looking for the auditing tool, check [`audits1984`](https://github.com/basecamp/audits1984).
12
12
 
13
- ![console-session-reason](docs/images/console-session-reason.png)
13
+ ![Terminal screenshot showing console1984 asking for a reason for the session](docs/images/console-session-reason.png)
14
14
 
15
15
  ## Installation
16
16
 
17
+ **Important:** `console1984` depends on [Active Record encryption](https://edgeguides.rubyonrails.org/active_record_encryption.html) which is a Rails 7 feature. Since no gem for Rails 7 has been released yet, you need to run Rails edge in your project (point the gem to latest `main` in the [repo](https://github.com/rails/rails)).
18
+
17
19
  Add it to your `Gemfile`:
18
20
 
19
21
  ```ruby
@@ -69,7 +71,7 @@ To decrypt data, enter the command `decrypt!`. It will ask for a justification,
69
71
  irb(main)> Topic.last.name
70
72
  Topic Load (1.4ms) SELECT `topics`.* FROM `topics` ORDER BY `topics`.`id` DESC LIMIT 1
71
73
  => "{\"p\":\"iu6+LfnNlurC6sL++JyOIDvedjNSz/AvnZQ=\",\"h\":{\"iv\":\"BYa86+JNM/LdkC18\",\"at\":\"r4sQNoSyIlAjJdZEKHVMow==\",\"k\":{\"p\":\"7L1l/5UiYsFQqqo4jfMZtLwp90KqcrIgS7HqgteVjuM=\",\"h\":{\"iv\":\"ItwRYxZAerKIoSZ8\",\"at\":\"ZUSNVfvtm4wAYWLBKRAx/g==\",\"e\":\"QVNDSUktOEJJVA==\"}},\"i\":\"OTdiOQ==\"}}"
72
- irb(main):002:0> decrypt!
74
+ irb(main)> decrypt!
73
75
  ```
74
76
 
75
77
  ```
@@ -104,15 +106,11 @@ irb(main)> Topic.last.name
104
106
  => "{\"p\":\"iu6+LfnNlurC6sL++JyOIDvedjNSz/AvnZQ=\",\"h\":{\"iv\":\"BYa86+JNM/LdkC18\",\"at\":\"r4sQNoSyIlAjJdZEKHVMow==\",\"k\":{\"p\":\"7L1l/5UiYsFQqqo4jfMZtLwp90KqcrIgS7HqgteVjuM=\",\"h\":{\"iv\":\"ItwRYxZAerKIoSZ8\",\"at\":\"ZUSNVfvtm4wAYWLBKRAx/g==\",\"e\":\"QVNDSUktOEJJVA==\"}},\"i\":\"OTdiOQ==\"}}"
105
107
  ```
106
108
 
107
- While in protected mode, you can't modify encrypted data, but can save unencrypted attributes normally. If you try to modify an encrypted column it will raise an error:
108
-
109
- ```ruby
110
- irb(main)> Rails.cache.read("some key") # raises Console1984::Errors::ProtectedConnection
111
- ```
109
+ While in protected mode, you can't modify encrypted data, but can save unencrypted attributes normally. If you try to modify an encrypted column it will raise an error.
112
110
 
113
111
  ### Access to external systems
114
112
 
115
- While Active Record encryption can protect personal information in the database, are other systems can contain very sensitive information. For example: Elasticsearch indexing user information or Redis caching template fragments.
113
+ While Active Record encryption can protect personal information in the database, there are other systems can contain very sensitive information. For example: Elasticsearch indexing user information or Redis caching template fragments.
116
114
 
117
115
  To protect the access to such systems, you can add their URLs to `config.console1984.protected_urls` in the corresponding environment config file (e.g: `production.rb`):
118
116
 
@@ -120,7 +118,13 @@ To protect the access to such systems, you can add their URLs to `config.console
120
118
  config.console1984.protected_urls = [ "https://my-app-us-east-1-whatever.us-east-1.es.amazonaws.com", "redis://my-app-cache-1.whatever.cache.amazonaws.com:6379" ]
121
119
  ```
122
120
 
123
- As with encryption data, running `decrypt!` will let you access these systems normally. The system will ask for a justfication and will flag those accesses as sensitive.
121
+ In the default protected mode, trying to read data from a protected system will be aborted with an error:
122
+
123
+ ```ruby
124
+ irb(main)> Rails.cache.read("some key") # raises Console1984::Errors::ProtectedConnection
125
+ ```
126
+
127
+ Running `decrypt!` will switch you to unprotected mode and let you access these systems normally. The system will ask for a justfication and will flag those accesses as sensitive.
124
128
 
125
129
  This will work for systems that use Ruby sockets as the underlying communication mechanism.
126
130
 
@@ -128,6 +132,10 @@ This will work for systems that use Ruby sockets as the underlying communication
128
132
 
129
133
  By default, sessions will be incinerated with a job 30 days after they are created. You can configure this period by setting `config.console1984.incinerate_after = 1.year` and you can disable incineration completely by setting `config.console1984.incinerate = false`.
130
134
 
135
+ ### Eager loading
136
+
137
+ When starting a console session, `console1984` will eager load all the application classes if necessary. In practice, production environments already load classes eagerly, so this won't represent any change for those.
138
+
131
139
  ## Configuration
132
140
 
133
141
  These config options are namespaced in `config.console1984`:
@@ -137,7 +145,7 @@ These config options are namespaced in `config.console1984`:
137
145
  | `protected_environments` | The list of environments where `console1984` will act on. Defaults to `%i[ production ]`. |
138
146
  | `protected_urls` | The list of URLs corresponding with external systems to protect. |
139
147
  | `session_logger` | The system used to record session data. The default logger is `Console1984::SessionsLogger::Database`. |
140
- | `username_resolver` | Configure an object responsible of resolving the current database username. The default is `Console1984::Username::EnvResolver.new("CONSOLE_USER")`, which returns the value of the environment variable `CONSOLE_USER`. |
148
+ | `username_resolver` | Configure how the current user is determined for a given console session. The default is `Console1984::Username::EnvResolver.new("CONSOLE_USER")`, which returns the value of the environment variable `CONSOLE_USER`. |
141
149
  | `production_data_warning` | The text to show when a console session starts. |
142
150
  | `enter_unprotected_encryption_mode_warning` | The text to show when user enters into unprotected mode. |
143
151
  | `enter_protected_mode_warning` | The text to show when user go backs to protected mode. |
@@ -149,3 +157,17 @@ These config options are namespaced in `config.console1984`:
149
157
 
150
158
  `console1984` uses Ruby to add several protection mechanisms. However, because Ruby is highly dynamic, it's technically possible to circumvent most of these controls if you know what you are doing. We have made an effort to prevent such attempts, but if your organization needs bullet-proof protection against malicious actors using the console, you should consider additional security measures.
151
159
 
160
+ The current version includes protection mechanisms to avoid tampering the tables that store console sessions. A bullet-proof mechanism would be using a read only connection when user commands are evaluated. Implementing such scheme is possible by writing a custom session logger and leveraging Rails' multi-database support. We would like that future versions of `console1984` supported this scheme directly as a configuration option.
161
+
162
+ ## Running the test suite
163
+
164
+ The test suite runs against SQLite by default, but can be run against Postgres and MySQL too. It will run against the three in the CI server.
165
+
166
+ To run the suite in your computer, first, run `bin/setup` to create the docker containers for MySQL/PostgreSQL and create the databases. Then run:
167
+
168
+ ```bash
169
+ bin/rails test # against SQLite (default)
170
+ bin/rails test TARGET_DB=mysql
171
+ bin/rails test TARGET_DB=postgres
172
+ bin/rails test TARGET_DB=sqlite
173
+ ```
@@ -0,0 +1,30 @@
1
+ static_validations:
2
+ forbidden_reopening:
3
+ - ActiveRecord
4
+ - Console1984
5
+ - PG
6
+ - Mysql2
7
+ forbidden_constant_reference:
8
+ always:
9
+ - Console1984
10
+ protected:
11
+ - PG
12
+ - Mysql2
13
+ - ActiveRecord::ActiveRecordEncryption
14
+ suspicious_terms:
15
+ - console_1984
16
+ - Console1984
17
+ - secret
18
+ - credentials
19
+ forbidden_methods:
20
+ always:
21
+ user:
22
+ Kernel:
23
+ - eval
24
+ Object:
25
+ - eval
26
+ BasicObject:
27
+ - eval
28
+ - instance_eval
29
+ Module:
30
+ - class_eval
@@ -0,0 +1,90 @@
1
+ # Supervise execution of console commands:
2
+ #
3
+ # * It will {validate commands}[rdoc-ref:Console1984::CommandValidator] before running
4
+ # them.
5
+ # * It will execute the commands in {protected mode}[rdoc-ref:Console1984::Shield#with_protected_mode]
6
+ # if needed.
7
+ # * It will log the command execution, and flag suspicious attempts and forbidden commands
8
+ # appropriately.
9
+ class Console1984::CommandExecutor
10
+ include Console1984::Freezeable
11
+
12
+ delegate :username_resolver, :session_logger, :shield, to: Console1984
13
+
14
+ # Logs and validates +commands+, and executes the passed block in a protected environment.
15
+ #
16
+ # Suspicious commands will be executed but flagged as suspicious. Forbidden commands will
17
+ # be prevented and flagged too.
18
+ def execute(commands, &block)
19
+ run_as_system { session_logger.before_executing commands }
20
+ validate_command commands
21
+ execute_in_protected_mode(&block)
22
+ rescue Console1984::Errors::ForbiddenCommand, FrozenError => e
23
+ flag_suspicious(commands)
24
+ rescue Console1984::Errors::SuspiciousCommand
25
+ flag_suspicious(commands)
26
+ execute_in_protected_mode(&block)
27
+ ensure
28
+ run_as_system { session_logger.after_executing commands }
29
+ end
30
+
31
+ # Executes the passed block in protected mode.
32
+ #
33
+ # See Console1984::Shield::Modes.
34
+ def execute_in_protected_mode(&block)
35
+ run_as_user do
36
+ shield.with_protected_mode(&block)
37
+ end
38
+ end
39
+
40
+ # Executes the passed block as a user.
41
+ #
42
+ # While the block is being executed, #executing_user_command? will return true.
43
+ # This method helps implementing certain protection mechanisms that should only act with
44
+ # user commands.
45
+ def run_as_user(&block)
46
+ run_command true, &block
47
+ end
48
+
49
+ # Executes the passed block as the system.
50
+ #
51
+ # While the block is being executed, #executing_user_command? will return false.
52
+ def run_as_system(&block)
53
+ run_command false, &block
54
+ end
55
+
56
+ # Returns whether the system is currently executing a user command.
57
+ def executing_user_command?
58
+ @executing_user_command
59
+ end
60
+
61
+ # Validates the command.
62
+ #
63
+ # See Console1984::CommandValidator.
64
+ def validate_command(command)
65
+ command_validator.validate(command)
66
+ end
67
+
68
+ private
69
+ def command_validator
70
+ @command_validator ||= build_command_validator
71
+ end
72
+
73
+ def build_command_validator
74
+ Console1984::CommandValidator.from_config(Console1984.protections_config.static_validations)
75
+ end
76
+
77
+ def flag_suspicious(commands)
78
+ puts "Forbidden command attempted: #{commands.join("\n")}"
79
+ run_as_system { session_logger.suspicious_commands_attempted commands }
80
+ nil
81
+ end
82
+
83
+ def run_command(run_by_user, &block)
84
+ original_value = @executing_user_command
85
+ @executing_user_command = run_by_user
86
+ block.call
87
+ ensure
88
+ @executing_user_command = original_value
89
+ end
90
+ end
@@ -0,0 +1,31 @@
1
+ # Validates references to a configured set of constants.
2
+ class Console1984::CommandValidator::ForbiddenConstantReferenceValidation
3
+ include Console1984::Freezeable
4
+
5
+ # +config+ will be a hash like:
6
+ #
7
+ # { always: [ Console1984 ], protected: [ PG, Mysql2 ] }
8
+ def initialize(shield = Console1984.shield, config)
9
+ # We make shield an injectable dependency for testing purposes. Everything is frozen
10
+ # for security purposes, so stubbing won't work.
11
+ @shield = shield
12
+
13
+ @forbidden_constants_names = config[:always] || []
14
+ @constant_names_forbidden_in_protected_mode = config[:protected] || []
15
+ end
16
+
17
+ # Raises a Console1984::Errors::ForbiddenCommand if a banned constant is referenced.
18
+ def validate(parsed_command)
19
+ if contains_invalid_const_reference?(parsed_command, @forbidden_constants_names) ||
20
+ (@shield.protected_mode? && contains_invalid_const_reference?(parsed_command, @constant_names_forbidden_in_protected_mode))
21
+ raise Console1984::Errors::ForbiddenCommand
22
+ end
23
+ end
24
+
25
+ private
26
+ def contains_invalid_const_reference?(parsed_command, banned_constants)
27
+ (parsed_command.constants + parsed_command.constant_assignments).find do |constant_name|
28
+ banned_constants.find { |banned_constant| "#{constant_name}::".start_with?("#{banned_constant}::") }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,29 @@
1
+ # Validates attempts to reopen classes and modules based on a configured set.
2
+ class Console1984::CommandValidator::ForbiddenReopeningValidation
3
+ include Console1984::Freezeable
4
+
5
+ attr_reader :banned_class_or_module_names
6
+
7
+ def initialize(banned_classes_or_modules)
8
+ @banned_class_or_module_names = banned_classes_or_modules.collect(&:to_s)
9
+ end
10
+
11
+ # Raises a Console1984::Errors::ForbiddenCommand if an banned class or module reopening
12
+ # is detected.
13
+ def validate(parsed_command)
14
+ if contains_invalid_class_or_module_declaration?(parsed_command)
15
+ raise Console1984::Errors::ForbiddenCommand
16
+ end
17
+ end
18
+
19
+ private
20
+ def contains_invalid_class_or_module_declaration?(parsed_command)
21
+ (parsed_command.declared_classes_or_modules + parsed_command.constant_assignments).find { |class_or_module_name| banned?(class_or_module_name) }
22
+ end
23
+
24
+ def banned?(class_or_module_name)
25
+ @banned_class_or_module_names.find do |banned_class_or_module_name|
26
+ "#{class_or_module_name}::".start_with?("#{banned_class_or_module_name}::")
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,90 @@
1
+ # Parses a command string and exposes different constructs to be used by validations.
2
+ #
3
+ # Internally, it uses the {parser}[https://github.com/whitequark/parser] gem to perform the parsing.
4
+ class Console1984::CommandValidator::ParsedCommand
5
+ include Console1984::Freezeable
6
+
7
+ attr_reader :raw_command
8
+
9
+ delegate :declared_classes_or_modules, :constants, :constant_assignments, to: :processed_ast
10
+
11
+ def initialize(raw_command)
12
+ @raw_command = Array(raw_command).join("\n")
13
+ end
14
+
15
+ private
16
+ def processed_ast
17
+ @processed_ast ||= CommandProcessor.new.tap do |processor|
18
+ ast = Parser::CurrentRuby.parse(raw_command)
19
+ processor.process(ast)
20
+ rescue Parser::SyntaxError
21
+ # Fail open with syntax errors
22
+ end
23
+ end
24
+
25
+ class CommandProcessor < ::Parser::AST::Processor
26
+ include AST::Processor::Mixin
27
+ include Console1984::Freezeable
28
+
29
+ def initialize
30
+ @constants = []
31
+ @declared_classes_or_modules = []
32
+ @constant_assignments = []
33
+ end
34
+
35
+ # We define accessors to define lists without duplicates. We are not using a +SortedSet+ because we want
36
+ # to mutate strings in the list while the processing is happening. And we don't use metapgroamming to define the
37
+ # accessors to prevent having problems with freezable and its instance_variable* protection.
38
+
39
+ def constants
40
+ @constants.uniq
41
+ end
42
+
43
+ def declared_classes_or_modules
44
+ @declared_classes_or_modules.uniq
45
+ end
46
+
47
+ def constant_assignments
48
+ @constant_assignments.uniq
49
+ end
50
+
51
+ def on_class(node)
52
+ super
53
+ const_declaration, _, _ = *node
54
+ constant = extract_constants(const_declaration).first
55
+ @declared_classes_or_modules << constant if constant.present?
56
+ end
57
+
58
+ alias_method :on_module, :on_class
59
+
60
+ def on_const(node)
61
+ super
62
+ name, const_name = *node
63
+ const_name = const_name.to_s
64
+ last_constant = @constants.last
65
+
66
+ if name.nil? || (name && name.type == :cbase) # cbase = leading ::
67
+ if last_constant&.end_with?("::")
68
+ last_constant << const_name
69
+ else
70
+ @constants << const_name
71
+ end
72
+ elsif last_constant
73
+ last_constant << "::#{const_name}"
74
+ end
75
+ end
76
+
77
+ def on_casgn(node)
78
+ super
79
+ scope_node, name, value_node = *node
80
+ @constant_assignments.push(*extract_constants(value_node))
81
+ end
82
+
83
+ private
84
+ def extract_constants(node)
85
+ self.class.new.tap do |processor|
86
+ processor.process(node)
87
+ end.constants
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,22 @@
1
+ # Validates that the command doesn't include a term based on a configured list.
2
+ class Console1984::CommandValidator::SuspiciousTermsValidation
3
+ include Console1984::Freezeable
4
+
5
+ def initialize(suspicious_terms)
6
+ @suspicious_terms = suspicious_terms
7
+ end
8
+
9
+ # Raises a Console1984::Errors::SuspiciousCommand if the term is referenced.
10
+ def validate(parsed_command)
11
+ if contains_suspicious_term?(parsed_command)
12
+ raise Console1984::Errors::SuspiciousCommand
13
+ end
14
+ end
15
+
16
+ private
17
+ def contains_suspicious_term?(parsed_command)
18
+ @suspicious_terms.find do |term|
19
+ parsed_command.raw_command.include?(term)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,71 @@
1
+ # Validates console commands.
2
+ #
3
+ # This performs an static analysis of console commands. The analysis is meant to happen
4
+ # *before* commands are executed, so that they can prevent the execution if needed.
5
+ #
6
+ # The validation itself happens as a chain of validation objects. The system will invoke
7
+ # each validation in order. Validations will raise an error if the validation fails (typically
8
+ # a Console1984::Errors::ForbiddenCommand or Console1984::Errors::SuspiciousCommands).
9
+ #
10
+ # Internally, validations will receive a Console1984::CommandValidator::ParsedCommand object. This
11
+ # exposes parsed constructs in addition to the raw strings so that validations can use those.
12
+ #
13
+ # There is a convenience method .from_config that lets you instantiate a validation setup from
14
+ # a config hash (e.g to customize validations via YAML).
15
+ #
16
+ # See +config/command_protections.yml+ and the validations in +lib/console1984/command_validator+.
17
+ class Console1984::CommandValidator
18
+ include Console1984::Freezeable
19
+
20
+ def initialize
21
+ @validations_by_name = HashWithIndifferentAccess.new
22
+ end
23
+
24
+ class << self
25
+ # Instantiates a command validator that will configure the validations based on the config passed.
26
+ #
27
+ # For each key in +config+, it will derive the class Console1984::CommandValidator::#{key.camelize}Validation
28
+ # and will instantiate the validation passed the values as params.
29
+ #
30
+ # For example for this config:
31
+ #
32
+ # { forbidden_reopening: [ActiveRecord, Console1984] }
33
+ #
34
+ # It will instantiate Console1984::CommandValidator::ForbiddenReopeningValidation passing
35
+ # +["ActiveRecord", "Console1984"]+ in the constructor.
36
+ #
37
+ # # See +config/command_protections.yml+ as an example.
38
+ def from_config(config)
39
+ Console1984::CommandValidator.new.tap do |validator|
40
+ config.each do |validator_name, validator_config|
41
+ validator_class = "Console1984::CommandValidator::#{validator_name.to_s.camelize}Validation".constantize
42
+ validator_config.try(:symbolize_keys!)
43
+ validator.add_validation validator_name, validator_class.new(validator_config)
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ # Adds a +validation+ to the chain indexed by the provided +name+
50
+ #
51
+ # Validations are executed in the order they are added.
52
+ def add_validation(name, validation)
53
+ validations_by_name[name] = validation
54
+ end
55
+
56
+ # Executes the chain of validations passing a {parsed command}[rdoc-ref:Console1984::CommandValidator::ParsedCommand]
57
+ # created with the +command+ string passed by parameter.
58
+ #
59
+ # The validations are executed in the order they were added. If one validation raises an error, the error will
60
+ # raise and the rest of validations won't get checked.
61
+ def validate(command)
62
+ parsed_command = ParsedCommand.new(command)
63
+
64
+ validations_by_name.values.each do |validation|
65
+ validation.validate(parsed_command)
66
+ end
67
+ end
68
+
69
+ private
70
+ attr_reader :validations_by_name
71
+ end