iron_hide 0.2.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 43f6efb8400709edf09c958d29466af574d7bbeb
4
+ data.tar.gz: 122df1373619876ef2cd2c5e1d7c109839b59373
5
+ SHA512:
6
+ metadata.gz: 50020dee2524090274d7191e43bfb24a4271e8da4dfa6ddb4a73df560badfc4fc42f1090536171d9221ff352d0d72694cf3fda2a29b5b60249b0c68badc23574
7
+ data.tar.gz: 03112c2cee64e74ed11962aa6fcc2308b13686654726c617978df4700478c7bd15dc2cd67a907e2504afb11531be4dbae036582608880dac31fea47829c8ff26
@@ -0,0 +1,148 @@
1
+ require 'set'
2
+
3
+ module IronHide
4
+ class Condition
5
+ VALID_TYPES = {
6
+ 'equal'=> :EqualCondition,
7
+ 'not_equal'=> :NotEqualCondition
8
+ }.freeze
9
+
10
+ # @param params [Hash] It has a single key, which is the conditional operator
11
+ # type. The value is the set of conditionals that must be met.
12
+ #
13
+ # @example
14
+ # { :equal => {
15
+ # 'resource::manager_id' => ['user::manager_id'],
16
+ # 'user::user_role_ids' => ['8']
17
+ # }
18
+ # }
19
+ #
20
+ # @return [EqualCondition, NotEqualCondition]
21
+ # @raise [IronHide::InvalidConditional] for too many keys
22
+ #
23
+ def self.new(params)
24
+ if params.length > 1
25
+ raise InvalidConditional, "Expected #{params} to have one key"
26
+ end
27
+ type, conditionals = params.first
28
+ #=> :equal, { key: val, key: val }
29
+ #
30
+ # See: http://ruby-doc.org/core-1.9.3/Class.html#method-i-allocate
31
+ klass = VALID_TYPES.fetch(type){ raise InvalidConditional, "#{type} is not valid"}
32
+ cond = IronHide.const_get(klass).allocate
33
+ cond.send(:initialize, (conditionals))
34
+ cond
35
+ end
36
+
37
+ # @param conditionals [Hash]
38
+ # @example
39
+ # {
40
+ # 'resource::manager_id' => ['user::manager_id'],
41
+ # 'user::user_role_ids' => ['8']
42
+ # }
43
+ #
44
+ def initialize(conditionals)
45
+ @conditionals = conditionals
46
+ end
47
+
48
+ attr_reader :conditionals
49
+
50
+ # @param user [Object]
51
+ # @param resource [Object]
52
+ # return [Boolean] if is met
53
+ def met?(user, resource)
54
+ raise NotImplementedError
55
+ end
56
+
57
+ protected
58
+
59
+ EVALUATE_REGEX = /
60
+ (
61
+ \Auser\z| # 'user' or 'resource'
62
+ \Aresource\z
63
+ )
64
+ | # OR
65
+ \A\w+:{2}\w+ # "word::word"
66
+ (:{2}\w+)* # Followed by any number of "::word"
67
+ \z # End of string
68
+ /x
69
+
70
+ # *Safely* evaluate a conditional expression
71
+ #
72
+ # @note
73
+ # This does not guarantee that conditions are correctly specified.
74
+ # For example, 'user:::manager' will not resolve to anything, and
75
+ # and an exception will *not* be raised. The same goes for 'user:::' and
76
+ # 'user:id'.
77
+ #
78
+ # @param expressions [Array<String, Object>, String, Object] an array or
79
+ # a single expression. This represents either an immediate value (e.g.,
80
+ # '1', 99) or a valid expression that can be interpreted (see example)
81
+ #
82
+ # @example
83
+ # ['user::manager_id'] #=> [1]
84
+ # ['user::role_ids'] #=> [1,2,3,4]
85
+ # ['resource::manager_id'] #=> [1]
86
+ # [1,2,3,4] #=> [1,2,3,4]
87
+ # 'user::id' #=> [1]
88
+ # 'resource::id' #=> [2]
89
+ #
90
+ # @return [Array<Object>] a collection of 0 or more objects
91
+ # representing attributes on the user or resource
92
+ #
93
+ def evaluate(expression, user, resource)
94
+ Array(expression).flat_map do |el|
95
+ if expression?(el)
96
+ type, *ary = el.split('::')
97
+ if type == 'user'
98
+ Array(ary.inject(user) do |rval, attr|
99
+ rval.freeze.public_send(attr)
100
+ end)
101
+ elsif type == 'resource'
102
+ Array(ary.inject(resource) do |rval, attr|
103
+ rval.freeze.public_send(attr)
104
+ end)
105
+ else
106
+ raise "Expected #{type} to be 'resource' or 'user'"
107
+ end
108
+ else
109
+ el
110
+ end
111
+ end
112
+ end
113
+
114
+ def expression?(expression)
115
+ !!(expression =~ EVALUATE_REGEX)
116
+ end
117
+
118
+ def with_error_handling
119
+ yield
120
+ rescue => e
121
+ new_exception = InvalidConditional.new(e.to_s)
122
+ new_exception.set_backtrace(e.backtrace)
123
+ raise new_exception
124
+ end
125
+ end
126
+
127
+ # @api private
128
+ class EqualCondition < Condition
129
+ def met?(user, resource)
130
+ with_error_handling do
131
+ conditionals.all? do |left, right|
132
+ (evaluate(left, user, resource) & evaluate(right, user, resource)).size > 0
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ # @api private
139
+ class NotEqualCondition < Condition
140
+ def met?(user, resource)
141
+ with_error_handling do
142
+ conditionals.all? do |left, right|
143
+ !((evaluate(left, user, resource) & evaluate(right, user, resource)).size > 0)
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,18 @@
1
+ module IronHide
2
+ # All exceptions inherit from IronHideError to allow rescuing from all
3
+ # exceptions that occur in this gem.
4
+ #
5
+ class IronHideError < StandardError ; end
6
+
7
+ # Exception raised when an authorization failure occurs.
8
+ # Typically when IronHide::authorize! is invoked
9
+ #
10
+ class AuthorizationError < IronHideError ; end
11
+
12
+ # Exception raised when a conditional is incorrectly defined
13
+ # in the rules.
14
+ # @see IronHide::Condition
15
+ #
16
+ class InvalidConditional < IronHideError ; end
17
+
18
+ end
@@ -0,0 +1,69 @@
1
+ module IronHide
2
+ class Rule
3
+ ALLOW = 'allow'.freeze
4
+ DENY = 'deny'.freeze
5
+
6
+ attr_reader :description, :effect, :conditions, :user, :resource
7
+
8
+ def initialize(user, resource, params = {})
9
+ @user = user
10
+ @resource = resource
11
+ @description = params['description']
12
+ @effect = params.fetch('effect', DENY) # Default DENY
13
+ @conditions = Array(params['conditions']).map { |c| Condition.new(c) }
14
+ end
15
+
16
+ # Returns all applicable rules matching on resource and action
17
+ #
18
+ # @param user [Object]
19
+ # @param action [String]
20
+ # @param resource [Object]
21
+ # @return [Array<IronHide::Rule>]
22
+ def self.find(user, action, resource)
23
+ ns_resource = "#{IronHide.namespace}::#{resource.class.name}"
24
+ storage.where(resource: ns_resource, action: action).map do |json|
25
+ new(user, resource, json)
26
+ end
27
+ end
28
+
29
+ # NOTE: If any Rule is an explicit DENY, then an allow cannot override the Rule
30
+ # If any Rule is explicit ALLOW, and there is no explicit DENY, then ALLOW
31
+ # If no Rules match, then DENY
32
+ #
33
+ # @return [Boolean]
34
+ # @param user [Object]
35
+ # @param action [String]
36
+ # @param resource [String]
37
+ #
38
+ def self.allow?(user, action, resource)
39
+ find(user, action, resource).inject(false) do |rval, rule|
40
+ # For an explicit DENY, stop evaluating, and return false
41
+ rval = false and break if rule.explicit_deny?
42
+
43
+ # For an explicit ALLOW, true
44
+ rval = true if rule.allow?
45
+
46
+ rval
47
+ end
48
+ end
49
+
50
+ # An abstraction over the storage of the rules
51
+ # @see IronHide::Storage
52
+ # @return [IronHide::Storage]
53
+ def self.storage
54
+ IronHide.storage
55
+ end
56
+
57
+ # @return [Boolean]
58
+ def allow?
59
+ effect == ALLOW && conditions.all? { |c| c.met?(user,resource) }
60
+ end
61
+
62
+ # @return [Boolean]
63
+ def explicit_deny?
64
+ effect == DENY && conditions.all? { |c| c.met?(user,resource) }
65
+ end
66
+
67
+ alias_method :deny?, :explicit_deny?
68
+ end
69
+ end
@@ -0,0 +1,71 @@
1
+ module IronHide
2
+ class Storage
3
+ # @api private
4
+ class FileAdapter < AbstractAdapter
5
+ attr_reader :rules
6
+
7
+ def initialize
8
+ json = IronHide.json.each_with_object([]) do |files, ary|
9
+ Array(files).map do |file|
10
+ ary.concat(MultiJson.load(File.open(file).read, minify: true))
11
+ end
12
+ end
13
+ @rules = unfold(json)
14
+ rescue MultiJson::ParseError => e
15
+ raise IronHideError, "#{e.cause}: #{e.data}"
16
+ rescue => e
17
+ raise IronHideError, "Invalid or missing JSON file: #{e.to_s}"
18
+ end
19
+
20
+ def where(opts = {})
21
+ self[opts[:resource]][opts[:action]]
22
+ end
23
+
24
+ # Unfold the JSON definitions of the rules into a Hash with this structure:
25
+ # {
26
+ # "com::test::TestResource" => {
27
+ # "action" => [
28
+ # { ... }, { ... }, { ... }
29
+ # ]
30
+ # }
31
+ # }
32
+ #
33
+ # @param json [Array<Hash>]
34
+ # @return [Hash]
35
+ def unfold(json)
36
+ json.inject(hash_of_hashes) do |rules, json_rule|
37
+ resource, actions = json_rule["resource"], json_rule["action"]
38
+ actions.each { |act| rules[resource][act] << json_rule }
39
+ rules
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ # Return a Hash with default value that is a Hash with default value of Array
46
+ # @return [Hash<Hash, Array>]
47
+ def hash_of_hashes
48
+ Hash.new { |h1,k1|
49
+ h1[k1] = Hash.new { |h,k| h[k] = [] }
50
+ }
51
+ end
52
+
53
+ # Implements an interface that makes selecting rules look like a Hash:
54
+ # @example
55
+ # {
56
+ # 'com::test::TestResource' => {
57
+ # 'read' => [],
58
+ # ...
59
+ # }
60
+ # }
61
+ # adapter['com::test::TestResource']['read']
62
+ # #=> [Array<Hash>]
63
+ #
64
+ # @param [Symbol] val
65
+ # @return [Array<Hash>] array of canonical JSON representation of rules
66
+ def [](val)
67
+ rules[val]
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,37 @@
1
+ # IronHide::Storage provides a common interface regardless of storage type
2
+ # by implementing the Adapter pattern to decouple _how_ JSON
3
+ #
4
+ require 'multi_json'
5
+
6
+ module IronHide
7
+ # @api private
8
+ class Storage
9
+
10
+ ADAPTERS = {
11
+ file: :FileAdapter
12
+ }
13
+
14
+ attr_reader :adapter
15
+
16
+ def initialize(adapter_type)
17
+ @adapter = self.class.const_get(ADAPTERS[adapter_type]).new
18
+ end
19
+
20
+ # @see AbstractAdapter#where
21
+ def where(opts = {})
22
+ adapter.where(opts)
23
+ end
24
+ end
25
+
26
+ # @abstract Subclass and override {#where} to implement an Adapter class
27
+ class AbstractAdapter
28
+
29
+ # @option opts [String] :resource *required*
30
+ # @option opts [String] :action *required*
31
+ def where(opts = {})
32
+ raise NotImplementedError
33
+ end
34
+ end
35
+ end
36
+
37
+ require 'iron_hide/storage/file_adapter'
@@ -0,0 +1,6 @@
1
+ module IronHide
2
+ MAJOR = "0"
3
+ MINOR = "2"
4
+ BUILD = "1"
5
+ VERSION = [MAJOR, MINOR, BUILD].join(".")
6
+ end
data/lib/iron_hide.rb ADDED
@@ -0,0 +1,91 @@
1
+ module IronHide
2
+ # @raise [IronHide::AuthorizationError] if authorization fails
3
+ # @return [true] if authorization succeeds
4
+ #
5
+ def self.authorize!(user, action, resource)
6
+ unless can?(user, action, resource)
7
+ raise AuthorizationError
8
+ end
9
+ true
10
+ end
11
+
12
+ # @return [Boolean]
13
+ # @param user [Object]
14
+ # @param action [Symbol, String]
15
+ # @param resource [Object]
16
+ # @see IronHide::Rule::allow?
17
+ #
18
+ def self.can?(user, action, resource)
19
+ Rule.allow?(user, action.to_s, resource)
20
+ end
21
+
22
+ # Specify where to load rules from. This is specified in a config file
23
+ # @param type [:file] Specify the adapter type. Only json is supported
24
+ # for now
25
+ def self.adapter=(type)
26
+ @adapter_type = type
27
+ end
28
+
29
+ def self.adapter
30
+ @adapter_type
31
+ end
32
+
33
+ # Set the top-level namespace for the application's Rules
34
+ #
35
+ # @param val [String]
36
+ # @example
37
+ # 'com::myCompany::myProject'
38
+ def self.namespace=(val)
39
+ @namespace = val
40
+ end
41
+
42
+ # Default namespace is com::IronHide
43
+ #
44
+ # @return [String]
45
+ def self.namespace
46
+ @namespace || 'com::IronHide'
47
+ end
48
+
49
+ # Specify the file path for the JSON flat-file for rules
50
+ # Only applicable if using the JSON adapter
51
+ # @param files [String, Array<String>]
52
+ #
53
+ def self.json=(*files)
54
+ @json_files = files
55
+ end
56
+
57
+ # @return [Array<String>]
58
+ def self.json
59
+ @json_files
60
+ end
61
+
62
+ # @return [IronHide::Storage]
63
+ def self.storage
64
+ @storage ||= begin
65
+ if @adapter_type.nil?
66
+ raise IronHideError, "Storage adapter not defined"
67
+ end
68
+ IronHide::Storage.new(@adapter_type)
69
+ end
70
+ end
71
+
72
+ # Allow the module to be configurable from a config file
73
+ # See: {file:README.md}
74
+ # @yield [IronHide]
75
+ def self.config
76
+ yield self
77
+ end
78
+
79
+ # Resets internal state
80
+ #
81
+ # @return [void]
82
+ def self.reset
83
+ instance_variables.each { |i| instance_variable_set(i,nil) }
84
+ end
85
+ end
86
+
87
+ require "iron_hide/version"
88
+ require 'iron_hide/errors'
89
+ require 'iron_hide/rule'
90
+ require 'iron_hide/condition'
91
+ require 'iron_hide/storage'
metadata ADDED
@@ -0,0 +1,150 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: iron_hide
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Alan Cohen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: multi_json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: json_minify
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: yard
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: A Ruby authorization library
112
+ email:
113
+ - acohen@climate.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - lib/iron_hide.rb
119
+ - lib/iron_hide/condition.rb
120
+ - lib/iron_hide/errors.rb
121
+ - lib/iron_hide/rule.rb
122
+ - lib/iron_hide/storage.rb
123
+ - lib/iron_hide/storage/file_adapter.rb
124
+ - lib/iron_hide/version.rb
125
+ homepage: http://github.com/TheClimateCorporation/iron_hide
126
+ licenses:
127
+ - MIT
128
+ metadata: {}
129
+ post_install_message:
130
+ rdoc_options: []
131
+ require_paths:
132
+ - lib
133
+ required_ruby_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ requirements: []
144
+ rubyforge_project:
145
+ rubygems_version: 2.2.2
146
+ signing_key:
147
+ specification_version: 4
148
+ summary: Describe your authorization rules with JSON
149
+ test_files: []
150
+ has_rdoc: