sekisyo 0.1.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.
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sekisyo
4
+ module Validators
5
+ #
6
+ # Sekisyo Validators ArrayValidator is a validator that accepts only array value.
7
+ #
8
+ class ArrayValidator
9
+ def initialize(key, options = {})
10
+ @key = key
11
+ @options = options
12
+
13
+ @items_options = @options.fetch('items', {})
14
+ items_validator_class = Sekisyo::Validator::VALIDATORS[@items_options.delete('type')]
15
+ items_validator_class ||= Sekisyo::Validators::AnyValidator
16
+ @items_validator = items_validator_class.new(nil, @items_options)
17
+ end
18
+
19
+ attr_reader :key
20
+
21
+ def valid?(value)
22
+ type_validate(value) &&
23
+ presence_validate(value) &&
24
+ bytesize_validate(value) &&
25
+ min_size_validate(value) &&
26
+ max_size_validate(value) &&
27
+ items_validate(value)
28
+ end
29
+
30
+ private
31
+
32
+ def type_validate(value)
33
+ value.is_a? Array
34
+ end
35
+
36
+ def presence_validate(value)
37
+ !@options['presence'] || !(value.respond_to?(:empty?) ? !!value.empty? : !value)
38
+ end
39
+
40
+ def bytesize_validate(value)
41
+ !@options['max_bytesize'] || value.to_s.bytesize <= @options['max_bytesize'].to_i
42
+ end
43
+
44
+ def min_size_validate(value)
45
+ !@options['min_size'] || value.size >= @options['min_size'].to_i
46
+ end
47
+
48
+ def max_size_validate(value)
49
+ !@options['max_size'] || value.size <= @options['max_size'].to_i
50
+ end
51
+
52
+ def items_validate(value)
53
+ value.all? { |v| @items_validator.valid?(v) }
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sekisyo
4
+ module Validators
5
+ #
6
+ # Sekisyo Validators BooleanValidator is a validator that accepts only boolean value.
7
+ #
8
+ class BooleanValidator
9
+ def initialize(key, options = {})
10
+ @key = key
11
+ @options = options
12
+ end
13
+
14
+ attr_reader :key
15
+
16
+ def valid?(value)
17
+ type_validate(value) &&
18
+ presence_validate(value)
19
+ end
20
+
21
+ private
22
+
23
+ def type_validate(value)
24
+ ['true', 'false', ''].include?(value)
25
+ end
26
+
27
+ def presence_validate(value)
28
+ !@options['presence'] || !(value.respond_to?(:empty?) ? !!value.empty? : !value)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sekisyo
4
+ module Validators
5
+ #
6
+ # Sekisyo Validators FileValidator is a validator that accepts only file params value.
7
+ #
8
+ class FileValidator
9
+ FILE_CONTENT_TYPES = %w[
10
+ text/plain
11
+ text/csv
12
+ text/html
13
+ text/css
14
+ text/javascript
15
+ application/octet-stream
16
+ application/json
17
+ application/pdf
18
+ application/vnd.ms-excel
19
+ application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
20
+ application/vnd.ms-powerpoint
21
+ application/vnd.openxmlformats-officedocument.presentationml.presentation
22
+ application/msword
23
+ application/vnd.openxmlformats-officedocument.wordprocessingml.document
24
+ image/jpeg
25
+ image/png
26
+ image/gif
27
+ image/bmp
28
+ image/svg+xml
29
+ application/zip
30
+ application/x-lzh
31
+ application/x-tar
32
+ audio/mpeg
33
+ video/mp4
34
+ video/mpeg
35
+ ].freeze
36
+
37
+ def initialize(key, options = {})
38
+ @key = key
39
+ @options = options
40
+ end
41
+
42
+ attr_reader :key
43
+
44
+ def valid?(value)
45
+ type_validate(value) &&
46
+ presence_validate(value) &&
47
+ file_size_validate(value)
48
+ end
49
+
50
+ private
51
+
52
+ def type_validate(value)
53
+ return false unless value.is_a? Hash
54
+
55
+ allow_file_types = [@options['allow_file_types']].flatten.compact
56
+ content_types = if allow_file_types.empty?
57
+ FILE_CONTENT_TYPES
58
+ else
59
+ FILE_CONTENT_TYPES & allow_file_types
60
+ end
61
+ content_types.any? { |type| type == value[:type] }
62
+ end
63
+
64
+ def presence_validate(value)
65
+ !@options['presence'] || value[:tempfile]&.then { |file| file.size.positive? }
66
+ end
67
+
68
+ def file_size_validate(value)
69
+ !@options['max_bytesize'] || value[:tempfile]&.then { |file| file.size <= @options['max_bytesize'].to_i }
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sekisyo
4
+ module Validators
5
+ #
6
+ # Sekisyo Validators FloatValidator is a validator that accepts only float value.
7
+ #
8
+ class FloatValidator
9
+ def initialize(key, options = {})
10
+ @key = key
11
+ @options = options
12
+ end
13
+
14
+ attr_reader :key
15
+
16
+ def valid?(value)
17
+ type_validate(value) &&
18
+ presence_validate(value) &&
19
+ bytesize_validate(value) &&
20
+ enum_validate(value)
21
+ end
22
+
23
+ private
24
+
25
+ def type_validate(value)
26
+ /^\d+\.\d+$/.match?(value.to_s) || value == ''
27
+ end
28
+
29
+ def presence_validate(value)
30
+ !@options['presence'] || !(value.respond_to?(:empty?) ? !!value.empty? : !value)
31
+ end
32
+
33
+ def bytesize_validate(value)
34
+ !@options['max_bytesize'] || value.to_s.bytesize <= @options['max_bytesize'].to_i
35
+ end
36
+
37
+ def enum_validate(value)
38
+ !@options['enum'] || @options['enum'].any? { |e| e.to_s == value.to_s }
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sekisyo
4
+ module Validators
5
+ #
6
+ # Sekisyo Validators IntegerValidator is a validator that accepts only integer value.
7
+ #
8
+ class IntegerValidator
9
+ def initialize(key, options = {})
10
+ @key = key
11
+ @options = options
12
+ end
13
+
14
+ attr_reader :key
15
+
16
+ def valid?(value)
17
+ type_validate(value) &&
18
+ presence_validate(value) &&
19
+ bytesize_validate(value) &&
20
+ enum_validate(value)
21
+ end
22
+
23
+ private
24
+
25
+ def type_validate(value)
26
+ /^\d+$/.match?(value.to_s) || value == ''
27
+ end
28
+
29
+ def presence_validate(value)
30
+ !@options['presence'] || !(value.respond_to?(:empty?) ? !!value.empty? : !value)
31
+ end
32
+
33
+ def bytesize_validate(value)
34
+ !@options['max_bytesize'] || value.to_s.bytesize <= @options['max_bytesize'].to_i
35
+ end
36
+
37
+ def enum_validate(value)
38
+ !@options['enum'] || @options['enum'].any? { |e| e.to_s == value.to_s }
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sekisyo
4
+ module Validators
5
+ #
6
+ # Sekisyo Validators NumericValidator is a validator that accepts only numeric value.
7
+ #
8
+ class NumericValidator
9
+ def initialize(key, options = {})
10
+ @key = key
11
+ @options = options
12
+ end
13
+
14
+ attr_reader :key
15
+
16
+ def valid?(value)
17
+ type_validate(value) &&
18
+ presence_validate(value) &&
19
+ bytesize_validate(value) &&
20
+ enum_validate(value)
21
+ end
22
+
23
+ private
24
+
25
+ def type_validate(value)
26
+ /^\d+(\.\d+)?$/.match?(value.to_s) || value == ''
27
+ end
28
+
29
+ def presence_validate(value)
30
+ !@options['presence'] || !(value.respond_to?(:empty?) ? !!value.empty? : !value)
31
+ end
32
+
33
+ def bytesize_validate(value)
34
+ !@options['max_bytesize'] || value.to_s.bytesize <= @options['max_bytesize'].to_i
35
+ end
36
+
37
+ def enum_validate(value)
38
+ !@options['enum'] || @options['enum'].any? { |e| e.to_s == value.to_s }
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sekisyo
4
+ module Validators
5
+ #
6
+ # Sekisyo Validators ObjectValidator is a validator that accepts only hash value.
7
+ #
8
+ class ObjectValidator
9
+ extend Forwardable
10
+ delegate :[] => :@properties
11
+
12
+ def initialize(key, properties = {})
13
+ @key = key
14
+ @properties = Sekisyo::WhitelistDetails::Properties.new(properties)
15
+ end
16
+
17
+ attr_reader :key
18
+
19
+ def valid?(value)
20
+ @properties.valid?(value)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sekisyo
4
+ module Validators
5
+ #
6
+ # Sekisyo Validators StringValidator is a validator that accepts only string value.
7
+ #
8
+ class StringValidator
9
+ def initialize(key, options = {})
10
+ @key = key
11
+ @options = options
12
+ end
13
+
14
+ attr_reader :key
15
+
16
+ def valid?(value)
17
+ type_validate(value) &&
18
+ presence_validate(value) &&
19
+ bytesize_validate(value) &&
20
+ enum_validate(value) &&
21
+ match_validate(value)
22
+ end
23
+
24
+ private
25
+
26
+ def type_validate(value)
27
+ value.is_a? String
28
+ end
29
+
30
+ def presence_validate(value)
31
+ !@options['presence'] || !(value.respond_to?(:empty?) ? !!value.empty? : !value)
32
+ end
33
+
34
+ def bytesize_validate(value)
35
+ !@options['max_bytesize'] || value.to_s.bytesize <= @options['max_bytesize'].to_i
36
+ end
37
+
38
+ def enum_validate(value)
39
+ !@options['enum'] || @options['enum'].any? { |e| e == value }
40
+ end
41
+
42
+ def match_validate(value)
43
+ !@options['match'] || /#{@options['match']}/.match?(value)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sekisyo
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'whitelist_details/path'
4
+ require_relative 'whitelist_details/method'
5
+ require_relative 'whitelist_details/properties'
6
+
7
+ module Sekisyo
8
+ #
9
+ # Sekisyo Whitelist parses the yaml file and assists in finding the corresponding whitelist.
10
+ #
11
+ class Whitelist
12
+ #
13
+ # Reads the yaml file containing the whitelist definition and converts it to a Ruby object.
14
+ #
15
+ # @param [Array<string>] file_paths Paths of the yaml file where the whitelist is defined.
16
+ #
17
+ def parse(*file_paths)
18
+ @hash = Hashie::Mash.new.deep_merge(*file_paths.map(&Hashie::Mash.method(:load)))
19
+ @paths = @hash['paths'].map do |path, object|
20
+ Sekisyo::WhitelistDetails::Path.new(path, object)
21
+ end
22
+ self
23
+ end
24
+
25
+ #
26
+ # @param [String] path URL string.
27
+ #
28
+ # @return [Sekisyo::WhitelistDetails::Path]
29
+ #
30
+ def find(path)
31
+ @paths.find { |pattern| pattern.match?(path) }
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'properties'
4
+
5
+ module Sekisyo
6
+ module WhitelistDetails
7
+ #
8
+ # Sekisyo WhitelistDetails Method is a definition object for each HTTP method of a specific path in the whitelist.
9
+ #
10
+ class Method
11
+ #
12
+ # @param [Hash] object Hash object with array values with :required key and Hash values with :properties key.
13
+ #
14
+ def initialize(object = {})
15
+ @required = object.fetch('required', []).flat_map do |attr|
16
+ attr.is_a?(Hash) ? transform_required_keys(attr) : [[attr]]
17
+ end
18
+ @properties = Sekisyo::WhitelistDetails::Properties.new(object['properties'])
19
+ end
20
+
21
+ #
22
+ # @param [Hash] params Request parameters.
23
+ #
24
+ # @return [true, false]
25
+ #
26
+ def valid?(params)
27
+ required_validate(params) && @properties.valid?(params)
28
+ end
29
+
30
+ private
31
+
32
+ def required_validate(params)
33
+ @required.all? do |keys|
34
+ dig_keys = keys.dup
35
+ key = dig_keys.pop
36
+
37
+ if dig_keys.empty?
38
+ params.has_key?(key)
39
+ else
40
+ dig_params = params.dig(*dig_keys)
41
+ dig_params.is_a?(Hash) && dig_params.has_key?(key)
42
+ end
43
+ end
44
+ end
45
+
46
+ def transform_required_keys(hash)
47
+ hash.flat_map do |key, values|
48
+ values.map do |value|
49
+ value.is_a?(Hash) ? [key, *transform_required_keys(value)].flatten : [key, value]
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'method'
4
+
5
+ module Sekisyo
6
+ module WhitelistDetails
7
+ #
8
+ # Sekisyo WhitelistDetails Path is a definition object for each path in the whitelist.
9
+ #
10
+ class Path
11
+ VALID_METHODS = {
12
+ GET: 'get',
13
+ POST: 'post',
14
+ PATCH: 'patch',
15
+ PUT: 'put',
16
+ DELETE: 'delete'
17
+ }.freeze
18
+
19
+ extend Forwardable
20
+ delegate match?: :@path_pattern
21
+
22
+ #
23
+ # @param [String] path Dynamic URL string. Example "/categories/{category_id}/pets/{id}".
24
+ # @param [hash] object Hash object with HTTP method keys.
25
+ #
26
+ # e.g.
27
+ #
28
+ # object = {
29
+ # 'get' => {
30
+ # 'required' => ['category_id', 'id'],
31
+ # 'properties' => {
32
+ # 'category_id' => { 'type' => 'integer', 'presence' => true }
33
+ # 'id' => { 'type' => 'integer', 'presence' => true }
34
+ # }
35
+ # },
36
+ # 'post' => { ... }
37
+ # }
38
+ #
39
+ #
40
+ def initialize(path, object)
41
+ @path = path
42
+ @path_pattern = /^#{path.gsub(/{(.+?)}/, '(?<\\1>.+?)')}(\..*)?$/
43
+ @methods = object.slice(*VALID_METHODS.values).transform_values do |value|
44
+ Sekisyo::WhitelistDetails::Method.new(value)
45
+ end
46
+ end
47
+
48
+ #
49
+ # Obtain the Path parameter from the dynamic URL according to the definition of the dynamic URL in the whitelist.
50
+ #
51
+ # Example
52
+ #
53
+ # @path = "/categories/{category_id}/pets/{id}"
54
+ # url = "/categories/1/pets/2"
55
+ # path_params(url) #=> { 'category_id' => '1', 'id' => '2' }
56
+ #
57
+ # @param [String] url URL string.
58
+ #
59
+ # @return [Hash] Path parameters.
60
+ #
61
+ def path_params(url)
62
+ param_keys = @path.scan(/(?<=\{).*?(?=\})/)
63
+ @path_pattern =~ url
64
+ param_keys.to_h do |key|
65
+ [key, Regexp.last_match(key)]
66
+ end
67
+ end
68
+
69
+ def [](method)
70
+ raise 'Failed to determine HTTP method.' if VALID_METHODS[method.to_sym].nil?
71
+
72
+ @methods[VALID_METHODS[method.to_sym]]
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../validator'
4
+
5
+ module Sekisyo
6
+ module WhitelistDetails
7
+ #
8
+ # Sekisyo WhitelistDetails Path is a definition object for each properties in the whitelist.
9
+ #
10
+ class Properties
11
+ def initialize(properties = {})
12
+ @permit_keys = properties.keys
13
+ @properties = properties.map do |k, v|
14
+ Sekisyo::Validator.new(k, v || {})
15
+ end
16
+ end
17
+
18
+ #
19
+ # @param [Hash] params Request parameter.
20
+ #
21
+ # @return [true, false]
22
+ #
23
+ def valid?(params)
24
+ return false unless params.is_a? Hash
25
+ return false unless params == params.slice(*@permit_keys)
26
+
27
+ params.all? do |k, v|
28
+ @properties.find { |validator| validator.key == k }&.valid?(v)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
data/lib/sekisyo.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'hashie/mash'
5
+ require 'hashie/extensions/parsers/yaml_erb_parser'
6
+ require_relative 'sekisyo/version'
7
+ require_relative 'sekisyo/configuration'
8
+ require_relative 'sekisyo/whitelist'
9
+ require_relative 'sekisyo/middleware'
10
+
11
+ module Sekisyo
12
+ class WhitelistDefinitionError < StandardError; end
13
+ end
data/sekisyo.gemspec ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/sekisyo/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'sekisyo'
7
+ spec.version = Sekisyo::VERSION
8
+ spec.authors = ['rhiroe']
9
+ spec.email = ['ride.poke@gmail.com']
10
+
11
+ spec.summary = 'Rack middleware that blocks HTTP requests that do not follow the Whitelist.'
12
+ spec.description = 'Whitelists can be defined in Yaml files.'
13
+ spec.homepage = 'https://github.com/rhiroe/sekisyo'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 2.6.0'
16
+
17
+ # spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
18
+
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = 'https://github.com/rhiroe/sekisyo'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/rhiroe/sekisyo/blob/master/CHANGELOG.md'
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ `git ls-files -z`.split("\x0").reject do |f|
27
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
28
+ end
29
+ end
30
+ spec.bindir = 'exe'
31
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ['lib']
33
+
34
+ # Uncomment to register a new dependency of your gem
35
+ spec.add_dependency 'hashie', '~> 5.0'
36
+ spec.add_dependency 'rake', '~> 13.0'
37
+
38
+ # For more information and examples about making a new gem, check out our
39
+ # guide at: https://bundler.io/guides/creating_gem.html
40
+ end
data/sig/sekisyo.rbs ADDED
@@ -0,0 +1,5 @@
1
+ module Sekisyo
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ # TODO: add signature
5
+ end