iron_hide 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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: