autosign 0.1.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -13,14 +13,14 @@ raw_csr = $stdin.read
13
13
  @logger.add_appenders Logging.appenders.stdout
14
14
 
15
15
  # Load config and then add logfile as a log appender
16
- config = Autosign::Config.new
16
+ config_settings = Autosign::Config.new.settings
17
17
 
18
- unless config.settings['general']['logfile'].nil?
18
+ unless config_settings['general']['logfile'].nil?
19
19
  file_layout = Logging.layouts.pattern(:pattern => "%d %-5l -- %c : %m\n", :date_pattern => "%Y-%m-%dT%H:%M:%S.%s")
20
- @logger.add_appenders Logging.appenders.file(config.settings['general']['logfile'], :layout => file_layout)
20
+ @logger.add_appenders Logging.appenders.file(config_settings['general']['logfile'], :layout => file_layout)
21
21
  end
22
22
 
23
- @logger.level = config.settings['general']['loglevel'].to_sym unless config.settings['general']['loglevel'].nil?
23
+ @logger.level = config_settings['general']['loglevel'].to_sym unless config_settings['general']['loglevel'].nil?
24
24
 
25
25
  ### End logging initialization
26
26
 
@@ -41,7 +41,7 @@ exit 1 unless csr.is_a?(Hash)
41
41
  ### End Inputs
42
42
 
43
43
  ### validate token
44
- token_validation = Autosign::Validator.any_validator(csr[:challenge_password].to_s, certname.to_s, raw_csr)
44
+ token_validation = Autosign::Validator.any_validator(csr[:challenge_password].to_s, certname.to_s, raw_csr, config_settings)
45
45
  ### end validation
46
46
 
47
47
  ### Exit with correct exit status
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rbconfig'
2
4
  require 'securerandom'
3
5
  require 'deep_merge'
@@ -7,16 +9,16 @@ module Autosign
7
9
  # Exceptions namespace for Autosign class
8
10
  module Exceptions
9
11
  # Exception representing a general failure during validation
10
- class Validation < Exception
12
+ class Validation < RuntimeError
11
13
  end
12
14
  # Exception representing a missing file during config validation
13
- class NotFound < Exception
15
+ class NotFound < RuntimeError
14
16
  end
15
17
  # Exception representing a permissions error during config validation
16
- class Permissions < Exception
18
+ class Permissions < RuntimeError
17
19
  end
18
20
  # Exception representing errors that Autosign does not know how to handle
19
- class Error < Exception
21
+ class Error < RuntimeError
20
22
  end
21
23
  end
22
24
 
@@ -45,11 +47,16 @@ module Autosign
45
47
  @config_file_paths = ['/etc/puppetlabs/puppetserver/autosign.conf', '/etc/autosign.conf', '/usr/local/etc/autosign.conf']
46
48
 
47
49
  # HOME is unset when puppet runs, so we need to only use it if it's set
48
- @config_file_paths << File.join(Dir.home, '.autosign.conf') unless ENV['HOME'].nil?
49
- @config_file_paths = [ settings_param['config_file'] ] unless settings_param['config_file'].nil?
50
+ unless ENV['HOME'].nil?
51
+ @config_file_paths << File.join(Dir.home, '.autosign.conf')
52
+ end
53
+
54
+ unless settings_param['config_file'].nil?
55
+ @config_file_paths = [settings_param['config_file']]
56
+ end
50
57
 
51
58
  @settings = settings_param
52
- @log.debug "Using merged settings hash: " + @settings.to_s
59
+ @log.debug 'Using merged settings hash: ' + @settings.to_s
53
60
  end
54
61
 
55
62
  # Return a merged settings hash of defaults, config file settings
@@ -57,11 +64,11 @@ module Autosign
57
64
  #
58
65
  # @return [Hash] deep merged settings hash
59
66
  def settings
60
- @log.debug "merging settings"
67
+ @log.debug 'merging settings'
61
68
  setting_sources = [default_settings, configfile, @settings]
62
- merged_settings = setting_sources.inject({}) { |merged, hash| merged.deep_merge!(hash) }
63
- @log.debug "using merged settings: " + merged_settings.to_s
64
- return merged_settings
69
+ merged_settings = setting_sources.inject({}) { |merged, hash| merged.deep_merge!(hash, {:overwrite_arrays => true}) }
70
+ @log.debug 'using merged settings: ' + merged_settings.to_s
71
+ merged_settings
65
72
  end
66
73
 
67
74
  private
@@ -78,12 +85,14 @@ module Autosign
78
85
  def default_settings
79
86
  { 'general' =>
80
87
  {
81
- 'loglevel' => 'INFO',
88
+ 'loglevel' => 'INFO',
89
+ 'validation_order' => %w[
90
+ jwt_token password_list multiplexer
91
+ ]
82
92
  },
83
93
  'jwt_token' => {
84
94
  'validity' => 7200
85
- }
86
- }
95
+ } }
87
96
  end
88
97
 
89
98
  # Locate the configuration file, parse it from INI-format, and return
@@ -91,21 +100,21 @@ module Autosign
91
100
  #
92
101
  # @return [Hash] configuration settings loaded from INI file
93
102
  def configfile
94
- @log.debug "Finding config file"
95
- @config_file_paths.each { |file|
103
+ @log.debug 'Finding config file'
104
+ @config_file_paths.each do |file|
96
105
  @log.debug "Checking if file '#{file}' exists"
97
106
  if File.file?(file)
98
- @log.debug "Reading config file from: " + file
107
+ @log.debug 'Reading config file from: ' + file
99
108
  config_file = File.read(file)
100
109
  parsed_config_file = YAML.load(config_file)
101
- #parsed_config_file = IniParse.parse(config_file).to_hash
102
- @log.debug "configuration read from config file: " + parsed_config_file.to_s
110
+ # parsed_config_file = IniParse.parse(config_file).to_hash
111
+ @log.debug 'configuration read from config file: ' + parsed_config_file.to_s
103
112
  return parsed_config_file if parsed_config_file.is_a?(Hash)
104
113
  else
105
114
  @log.debug "Configuration file '#{file}' not found"
106
115
  end
107
- }
108
- return {}
116
+ end
117
+ {}
109
118
  end
110
119
 
111
120
  # Validate configuration file
@@ -114,14 +123,14 @@ module Autosign
114
123
  # @param configfile [String] the absolute path of the config file to validate
115
124
  # @return [String] the absolute path of the config file
116
125
  def validate_config_file(configfile = location)
117
- @log.debug "validating config file"
126
+ @log.debug 'validating config file'
118
127
  unless File.file?(configfile)
119
128
  @log.error "configuration file not found at: #{configfile}"
120
129
  raise Autosign::Exceptions::NotFound
121
130
  end
122
131
 
123
132
  # check if file is world-readable
124
- if File.world_readable?(configfile) or File.world_writable?(configfile)
133
+ if File.world_readable?(configfile) || File.world_writable?(configfile)
125
134
  @log.error "configuration file #{configfile} is world-readable or world-writable, which is a security risk"
126
135
  raise Autosign::Exceptions::Permissions
127
136
  end
@@ -137,20 +146,20 @@ module Autosign
137
146
  case RbConfig::CONFIG['host_os']
138
147
  when /darwin|mac os/
139
148
  {
140
- 'logpath' => File.join(Dir.home, 'autosign.log'),
141
- 'confpath' => File.join(Dir.home, '.autosign.conf'),
149
+ 'logpath' => File.join(Dir.home, 'autosign.log'),
150
+ 'confpath' => File.join(Dir.home, '.autosign.conf'),
142
151
  'journalfile' => File.join(Dir.home, '.autosign.journal')
143
152
  }
144
153
  when /linux/
145
154
  {
146
- 'logpath' => '/var/log/autosign.log',
147
- 'confpath' => '/etc/autosign.conf',
155
+ 'logpath' => '/var/log/autosign.log',
156
+ 'confpath' => '/etc/autosign.conf',
148
157
  'journalfile' => File.join(Dir.home, '/var/autosign/autosign.journal')
149
158
  }
150
159
  when /bsd/
151
160
  {
152
- 'logpath' => '/var/log/autosign.log',
153
- 'confpath' => '/usr/local/etc/autosign.conf',
161
+ 'logpath' => '/var/log/autosign.log',
162
+ 'confpath' => '/usr/local/etc/autosign.conf',
154
163
  'journalfile' => File.join(Dir.home, '/var/autosign/autosign.journal')
155
164
  }
156
165
  else
@@ -161,36 +170,41 @@ module Autosign
161
170
  config = {
162
171
  'general' => {
163
172
  'loglevel' => 'warn',
164
- 'logfile' => os_defaults['logpath']
173
+ 'logfile' => os_defaults['logpath'],
174
+ 'validation_order' => %w[
175
+ jwt_token password_list multiplexer
176
+ ]
165
177
  },
166
178
  'jwt_token' => {
167
- 'secret' => SecureRandom.base64(20),
179
+ 'secret' => SecureRandom.base64(20),
168
180
  'validity' => '7200',
169
181
  'journalfile' => os_defaults['journalfile']
170
182
  }
171
183
  }
172
184
 
173
- # config = IniParse.gen do |doc|
174
- # doc.section("general") do |general|
175
- # general.option("loglevel", "warn")
176
- # general.option("logfile", os_defaults['logpath'])
177
- # end
178
- # doc.section("jwt_token") do |jwt_token|
179
- # jwt_token.option("secret", SecureRandom.base64(15))
180
- # jwt_token.option("validity", 7200)
181
- # jwt_token.option("journalfile", os_defaults['journalfile'])
182
- # end
183
- # doc.section("multiplexer") do |jwt_token|
184
- # jwt_token.option(";external_policy_executable", '/usr/local/bin/some_autosign_executable')
185
- # jwt_token.option(";external_policy_executable", '/usr/local/bin/another_autosign_executable')
186
- # end
187
- # doc.section("password_list") do |jwt_token|
188
- # jwt_token.option(";password", 'static_autosign_password_here')
189
- # jwt_token.option(";password", 'another_static_autosign_password')
190
- # end
191
- # end.to_ini
192
- config_file=settings_param['config_file'] || os_defaults['confpath']
193
- raise Autosign::Exceptions::Error, "file #{config_file} already exists, aborting" if File.file?(config_file)
185
+ # config = IniParse.gen do |doc|
186
+ # doc.section("general") do |general|
187
+ # general.option("loglevel", "warn")
188
+ # general.option("logfile", os_defaults['logpath'])
189
+ # end
190
+ # doc.section("jwt_token") do |jwt_token|
191
+ # jwt_token.option("secret", SecureRandom.base64(15))
192
+ # jwt_token.option("validity", 7200)
193
+ # jwt_token.option("journalfile", os_defaults['journalfile'])
194
+ # end
195
+ # doc.section("multiplexer") do |jwt_token|
196
+ # jwt_token.option(";external_policy_executable", '/usr/local/bin/some_autosign_executable')
197
+ # jwt_token.option(";external_policy_executable", '/usr/local/bin/another_autosign_executable')
198
+ # end
199
+ # doc.section("password_list") do |jwt_token|
200
+ # jwt_token.option(";password", 'static_autosign_password_here')
201
+ # jwt_token.option(";password", 'another_static_autosign_password')
202
+ # end
203
+ # end.to_ini
204
+ config_file = settings_param['config_file'] || os_defaults['confpath']
205
+ if File.file?(config_file)
206
+ raise Autosign::Exceptions::Error, "file #{config_file} already exists, aborting"
207
+ end
194
208
  return config_file if File.write(config_file, config.to_yaml)
195
209
  end
196
210
  end
@@ -1,221 +1,58 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'logging'
2
- require 'require_all'
3
4
 
4
5
  module Autosign
5
- # Parent class for validation backends. Validators take the
6
- # challenge_password and common name from a certificate signing request,
7
- # and perform some action to determine whether the request is valid.
8
- #
9
- # Validators also get the raw X509 CSR in case the extracted information
10
- # is insufficient for future, more powerful validators.
11
- #
12
- # All validators must inherit from this class, and must override several
13
- # methods in order to function. At a minimum, the name and perform_validation
14
- # methods must be implemented by child classes.
15
- #
16
- # @return [Autosign::Validator] instance of the Autosign::Validator class
17
- class Validator
18
- def initialize()
19
- start_logging()
20
- settings() # just run to validate settings
21
- setup()
22
- # call name to ensure that the class fails immediately if child classes
23
- # do not implement it.
24
- name()
25
- end
26
-
27
- # Name of the validator. This must be implemented by validators which
28
- # inherit from the Autosign::Validator class. The name is used to identify
29
- # the validator in friendly messages and to determine which configuration
30
- # file section settings will be loaded from.
31
- #
32
- # @example set the name of a child class validator to "example"
33
- # module Autosign
34
- # module Validators
35
- # class Example < Autosign::Validator
36
- # def name
37
- # "example"
38
- # end
39
- # end
40
- # end
41
- # end
42
- # @return [String] name of the validator. Do not use special characters.
43
- def name
44
- # override this after inheriting
45
- # should return a string with no spaces
46
- # this is the name used to reference the validator in config files
47
- raise NotImplementedError
48
- end
49
-
50
- # define how a validator actually validates the request.
51
- # This must be implemented by validators which inherit from the
52
- # Autosign::Validator class.
53
- #
54
- # @param challenge_password [String] the challenge_password OID from the certificate signing request. The challenge_password field is the same setting as the "challengePassword" field in a `csr_attributes.yaml` file when the CSR is generated. In a request using a JSON web token, this would be the serialized token.
55
- # @param certname [String] the common name being requested in the certificate signing request. Treat the certname as untrusted. This is user-submitted data that you must validate.
56
- # @param raw_csr [String] the encoded X509 certificate signing request, as received by the autosign policy executable. This is provided as an optional extension point, but your validator may not need to use it.
57
- # @return [True, False] return true if the certificate should be signed, and false if you cannot validate the request successfully.
58
- def perform_validation(challenge_password, certname, raw_csr)
59
- # override this after inheriting
60
- # should return true to indicate success validating
61
- # or false to indicate that the validator was unable to validate
62
- raise NotImplementedError
63
- end
64
-
65
- # wrapper method that wraps input validation and logging around the perform_validation method.
66
- # Do not override or use this class in child classes. This is the class that gets called
67
- # on validator objects.
68
- def validate(challenge_password, certname, raw_csr)
69
- @log.debug "running validate"
70
- fail unless challenge_password.is_a?(String)
71
- fail unless certname.is_a?(String)
72
-
73
- case perform_validation(challenge_password, certname, raw_csr)
74
- when true
75
- @log.debug "validated successfully"
76
- @log.info "Validated '#{certname}' using '#{name}' validator"
77
- return true
78
- when false
79
- @log.debug "validation failed"
80
- @log.debug "Unable to validate '#{certname}' using '#{name}' validator"
81
- return false
82
- else
83
- @log.error "perform_validation returned a non-boolean result"
84
- raise "perform_validation returned a non-boolean result"
6
+ module Validator
7
+
8
+ # @return [Array] - A list of all the validator classes
9
+ # @param list [Array] - a list of validators to use, uses the settings list by default
10
+ # This returns a list of validators that were specified by the user and the exact
11
+ # order they want the validation to procede.
12
+ def self.validation_order(settings = Autosign::Config.new.settings, list = nil)
13
+ validation_order = list || settings['general']['validation_order']
14
+ # create a key pair where the key is the name of the validator and value is the class
15
+ validator_list = validator_classes.each_with_object({}) do |klass, acc|
16
+ acc[klass::NAME] = klass
17
+ acc
85
18
  end
19
+ # filter out validators that do not exist
20
+ order = validation_order.map { |v| validator_list.fetch(v, nil) }.compact
21
+ @log = Logging.logger[self.class]
22
+ @log.debug("Validator order: #{order.inspect}")
23
+ order
86
24
  end
87
25
 
26
+ # @summary
88
27
  # Class method to attempt validation of a request against all validators which inherit from this class.
89
28
  # The request is considered to be validated if any one validator succeeds.
29
+ # The first validator to pass shorts the validation process so other validators are not called.
90
30
  # @param challenge_password [String] the challenge_password OID from the certificate signing request
91
31
  # @param certname [String] the common name being requested in the certificate signing request
92
32
  # @param raw_csr [String] the encoded X509 certificate signing request, as received by the autosign policy executable
93
- # @return [True, False] return true if the certificate should be signed, and false if it cannot be validated
94
- def self.any_validator(challenge_password, certname, raw_csr)
33
+ # @return [Boolean] return true if the certificate should be signed, and false if it cannot be validated
34
+ def self.any_validator(challenge_password, certname, raw_csr, settings = Autosign::Config.new.settings)
95
35
  @log = Logging.logger[self.class]
96
- # iterate over all known validators and attempt to validate using them
97
- results_by_validator = {}
98
- results = self.descendants.map {|c|
99
- validator = c.new()
100
- @log.debug "attempting to validate using #{validator.name}"
101
- result = validator.validate(challenge_password, certname, raw_csr)
102
- results_by_validator[validator.name] = result
103
- @log.debug "result: #{result.to_s}"
104
- result
105
- }
106
- @log.debug "validator results: " + results.to_s
107
- @log.info "results by validator: " + results_by_validator.to_s
108
- success = results.any?{|result| result == true}
109
- if success
110
- @log.info "successfully validated using one or more validators"
111
- return true
36
+ # find the first validator that passes and return the class
37
+ validator = validation_order(settings).find { |c| c.new(settings).validate(challenge_password, certname, raw_csr) }
38
+ if validator
39
+ @log.info "Successfully validated using #{validator::NAME}"
40
+ true
112
41
  else
113
- @log.info "unable to validate using any validator"
114
- return false
42
+ @log.info 'unable to validate using any validator'
43
+ false
115
44
  end
116
45
  end
117
46
 
118
47
  private
119
48
 
120
- # this is automatically called when the class is initialized; do not
121
- # override it in child classes.
122
- def start_logging
123
- @log = Logging.logger[self.class]
124
- @log.debug "starting autosign validator: " + self.name.to_s
125
- end
126
-
127
- # (optionally) override this method in validator child classes to perform any additional
128
- # setup during class initialization prior to beginning validation.
129
- # If you need to create a database connection, this would be a good place to do it.
130
- # @return [True, False] return true if setup succeeded, or false if setup failed and the validation should not continue
131
- def setup
132
- true
133
- end
134
-
135
49
  # Find other classes that inherit from this class.
136
50
  # Used to discover autosign validators. There is probably no reason to use
137
51
  # this directly.
138
52
  # @return [Array] of classes inheriting from Autosign::Validator
139
- def self.descendants
140
- ObjectSpace.each_object(Class).select { |klass| klass < self }
53
+ def self.validator_classes
54
+ validators = Dir.glob(File.join(__dir__, 'validator', '*')).sort.each {|k| require k }
55
+ ObjectSpace.each_object(Class).select { |klass| klass < Autosign::Validator::ValidatorBase }
141
56
  end
142
-
143
- # provide a merged settings hash of default settings for a validator,
144
- # config file settings for the validator, and override settings defined in
145
- # the validator.
146
- #
147
- # Do not override this in child classes. If you need to set
148
- # custom config settings, override the get_override_settings method.
149
- # The section of the config file this reads from is the same as the name
150
- # method returns.
151
- #
152
- # @return [Hash] of config settings
153
- def settings
154
- @log.debug "merging settings"
155
- setting_sources = [get_override_settings, load_config, default_settings]
156
- merged_settings = setting_sources.inject({}) { |merged, hash| merged.deep_merge(hash) }
157
- @log.debug "using merged settings: " + merged_settings.to_s
158
- @log.debug "validating merged settings"
159
- if validate_settings(merged_settings)
160
- @log.debug "successfully validated merged settings"
161
- return merged_settings
162
- else
163
- @log.warn "validation of merged settings failed"
164
- @log.warn "unable to validate settings in #{self.name} validator"
165
- raise "settings validation error"
166
- end
167
- end
168
-
169
- # (optionally) override this from a child class to set config defaults.
170
- # These will be overridden by config file settings.
171
- #
172
- # Override this when inheriting if you need to set config defaults.
173
- # For example, if you want to pull settings from zookeeper, this would
174
- # be a good place to do that.
175
- #
176
- # @return [Hash] of config settings
177
- def default_settings
178
- {}
179
- end
180
-
181
-
182
- # (optionally) override this to perform validation checks on the merged
183
- # config hash of default settings, config file settings, and override
184
- # settings.
185
- # @return [True, False]
186
- def validate_settings(settings)
187
- settings.is_a?(Hash)
188
- end
189
-
190
- # load any required configuration from the config file.
191
- # Do not override this in child classes.
192
- # @return [Hash] configuration settings from the validator's section of the config file
193
- def load_config
194
- @log.debug "loading validator-specific configuration"
195
- config = Autosign::Config.new
196
-
197
- if config.settings.to_hash[self.name].nil?
198
- @log.warn "Unable to load validator-specific configuration"
199
- @log.warn "Cannot load configuration section named '#{self.name}'"
200
- return {}
201
- else
202
- @log.debug "Set validator-specific settings from config file: " + config.settings.to_hash[self.name].to_s
203
- return config.settings.to_hash[self.name]
204
- end
205
- end
206
-
207
- # (optionally) override this from child classes to get custom configuration
208
- # from a validator.
209
- #
210
- # This is how you override defaults and config file settings.
211
- # @return [Hash] configuration settings
212
- def get_override_settings
213
- {}
214
- end
215
-
216
57
  end
217
58
  end
218
-
219
- # must run at the end because the validators inherit this class
220
- # this loads all validators
221
- require_rel 'validators'