sanction 0.0.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e14e098c1724519c24335f03af617abd5382bcb1
4
- data.tar.gz: aad98cfafac83ac05de0e46add2b9246e5507440
3
+ metadata.gz: 0d9b7c911e6b858db458af6c2a1a1a2576831c6c
4
+ data.tar.gz: dfc5c0aa9ea471972fb4897649e195195b8d4002
5
5
  SHA512:
6
- metadata.gz: c2b809a7fea3393d9791768d380f2aa5dd04e869ac72d356f6e97aab425bbd18deff762fbbeb438ddd1bf45de2b7347f714fc1da45bc49f08207b3295d7bdee6
7
- data.tar.gz: bc6699456115656c49c2e6ca8717f152d03a35595111165e55c2f8bab695e3ff8243c11768e9e31bf74961c6cb5a00abd4a96ab0669b4d56052660ba1c19a984
6
+ metadata.gz: 627ca53b8f5c5d2d162b4035e792f94af8560e6f66cb6ab4030db8a646ccb909a9078a28c3e7d16c6f59254a3ef03cffd61c2fd9d38f48db93ea4b2ee550e7ed
7
+ data.tar.gz: 31e827b77feb6d04d471d54e08f260b833bf845d930fbab5cf401ecd4f2ea18d0936455d78f7c94c17fda1e043c673a30af6c6c094ac9653bf904165627b32b5
data/.gitignore CHANGED
@@ -1,20 +1,12 @@
1
- *.gem
2
- *.rbc
3
- .bundle
4
- .config
5
- .yardoc
6
- Gemfile.lock
7
- InstalledFiles
8
- _yardoc
9
- coverage
10
- doc/
11
- lib/bundler/man
12
- pkg
13
- rdoc
14
- spec/reports
15
- test/tmp
16
- test/version_tmp
17
- tmp
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
18
10
  *.bundle
19
11
  *.so
20
12
  *.o
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.0
4
+ - 2.0.0
5
+ - jruby-19mode # JRuby in 1.9 mode
6
+ script: bundle exec rake test
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014 JGW Maxwell
1
+ Copyright (c) 2014 Adam Carlile
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # Sanction
2
2
 
3
- TODO: Write a gem description
3
+ Sanction is a permissions manager specifically for managing nested permission sets, with varying scopes or roles. Having found nothing that fit our specific problem domain. The idea is that object relationships are stored as a Hash, and persisted as JSON, and Sanction can then read that permission graph, and return you a true or false for your resource, or scope for that resource.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  Add this line to your application's Gemfile:
8
8
 
9
- gem 'sanction'
9
+ ```ruby
10
+ gem 'sanction'
11
+ ```
10
12
 
11
13
  And then execute:
12
14
 
@@ -16,13 +18,111 @@ Or install it yourself as:
16
18
 
17
19
  $ gem install sanction
18
20
 
19
- ## Usage
21
+ ## What can it give me?
20
22
 
21
- TODO: Write usage instructions here
23
+ Sanction is designed to be as flexible as possible, allowing various scopes to be applied to specific points in the resource graph, along with specific grant or deny to global resource types at specific levels. With either whitelisting or blacklisting, plus wildcarding for whitelists.
24
+
25
+ ### Object Structure
26
+ ```ruby
27
+ {
28
+ id: 1
29
+ type: 'bookcase'
30
+ mode: 'whitelist',
31
+ scope: ['manage', 'read'],
32
+ resources: ['shelf'],
33
+ subjects: [ ... ]
34
+ }
35
+ ```
36
+ * __ID__: The ID of the object that this permission applies to, is optional, root nodes will have a nil ID, in a whitelist node, this can be a wildcard("*") character to allow access to all
37
+ * __Type__: The Type of object in this graph, again it is optional, root nodes will have a nil type.
38
+ * __Mode__: The mode of operation for the subjects and resources arrays:
39
+ * Whitelist:
40
+ * Objects that exist in the subjects array are allowed, others are denied
41
+ * A blank array is an implicit *DENY ALL* subjects
42
+ * Blacklist:
43
+ * Objects that exist in the subjects array are denied, others are allowed
44
+ * A blank array is an implicit *ALLOW ALL* subjects
45
+ * __Scope__: An array of 'actions' that can be performed on the object, this can be anything, 'read', 'write', 'testing' etc.
46
+ * __Resources__: Resources are subject to the mode, blacklist is to exclude these resources from being accessed, whitelist being the opposite.
47
+ * Resources allows/denies will always take precedence over objects in the subjects array
48
+ * __Subjects__: An array that contains more of these objects, subject to the whitelist/blacklist mode of operation
49
+
50
+ ### Interface
51
+
52
+ To generate an interactive object graph from storage:
53
+ ```ruby
54
+ perms = Sanction.build(hash)
55
+ ```
56
+
57
+ This will return you the root node of the graph, and you can navigate it by using hash accessors, and then call interrogation methods on the returned objects
58
+
59
+ ```ruby
60
+ perms[:bookcase][1] # => Node in the graph
61
+ perms[:bookcase][1].permitted? # => true/false
62
+ perms[:bookcase][1].has_scope?(:read) # => true/false
63
+ perms[:bookcase][1].whitelist? # => true/false
64
+ perms[:bookcase][1].blacklist? # => true/false
65
+ ```
66
+
67
+ You can also get the state of the collection of objects too.
68
+
69
+ ```ruby
70
+ perms[:bookcase].allowed_ids # => Array of allowed ids
71
+ perms[:bookcase].denied_ids # => Array of allowed ids
72
+ perms[:bookcase].whitelist? # => true/false
73
+ perms[:bookcase].blacklist? # => true/false
74
+ perms[:bookcase].permitted? # => true/false
75
+ perms[:bookcase].has_scope?(:read) # => true/false
76
+ ```
77
+
78
+ And mutate the state of the graph
79
+
80
+ ```ruby
81
+ perms.whitelist? # => true
82
+ perms[:bookcase][1].allow!
83
+ perms[:bookcase][1].permitted? # => true
84
+
85
+ perms[:bookcase][1].deny!
86
+ perms[:bookcase][1].permitted? # => false
87
+
88
+ perms[:bookcase][1].scope << :testing
89
+ perms[:bookcase][1].has_scope? :testing # => true
90
+
91
+ perms = perms.change_type! :blacklist # => Returns a new root graph, blacklisted at root
92
+ perms[:bookcase].whitelist? # => false
93
+
94
+ perms[:bookcase][1].change_type! :blacklist # => Changes this to blacklist mode, applies to children
95
+ perms[:bookcase][1][:shelf].whitelist? # => false
96
+ ```
97
+
98
+ And of course add/remove objects.
99
+
100
+ ```ruby
101
+ perms.add_subject({
102
+ id: 1
103
+ type: 'user'
104
+ })
105
+
106
+ perms.whitelist? # => true
107
+ perms[:user][1].permitted? # => true
108
+
109
+
110
+ perms[:user][1].unlink
111
+ perms[:user][1].permitted? # => false
112
+ ```
113
+
114
+ One caveat is that for nodes that have a deny on a specific resource type, even if you add it in the graph it will still return false, ensure you either remove/add the resource type to the resources array, or ensure that a whitelisted node has a wildcard("*").
115
+
116
+ The best bit is that the objects don't have to exist in the graph to be queried against, it just relies on the last fragment it could find and applies the rule that was set there.
117
+
118
+ ```ruby
119
+ perms.whitelist? # => true
120
+ perms[:bookcase][1][:shelf][12][:book][3].permitted? # => false
121
+ ```
22
122
 
23
123
  ## Contributing
24
124
 
25
- 1. Fork it ( https://github.com/[my-github-username]/sanction/fork )
125
+ 1. Fork it ( https://github.com/boardiq/sanction/fork )
26
126
  2. Create your feature branch (`git checkout -b my-new-feature`)
27
127
  3. Commit your changes (`git commit -am 'Add some feature'`)
28
128
  4. Push to the branch (`git push origin my-new-feature`)
data/Rakefile CHANGED
@@ -1,2 +1,7 @@
1
1
  require "bundler/gem_tasks"
2
+ require 'rake/testtask'
2
3
 
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'spec'
6
+ t.pattern = "spec/*_spec.rb"
7
+ end
@@ -0,0 +1,57 @@
1
+ module Sanction
2
+ class AttachedList < SimpleDelegator
3
+
4
+ attr_accessor :key, :parent
5
+
6
+ def initialize(array = [])
7
+ super(array)
8
+ end
9
+
10
+ def [](index)
11
+ detect {|x| x.id == index} || wildcard_member || null_node_class.new({id: index, type: key, scope: []}, @parent)
12
+ end
13
+
14
+ def type
15
+ key
16
+ end
17
+
18
+ def ids_blank?
19
+ denied_ids.blank? && allowed_ids.blank?
20
+ end
21
+
22
+ def denied_ids
23
+ []
24
+ end
25
+
26
+ def allowed_ids
27
+ []
28
+ end
29
+
30
+ def has_scope? scope
31
+ @parent.has_scope? scope
32
+ end
33
+
34
+ def wildcard_member
35
+ detect {|x| x.wildcarded? }
36
+ end
37
+
38
+ def wildcarded?
39
+ !!wildcard_member
40
+ end
41
+
42
+ def wildcard_resource?
43
+ resources.include?(:*)
44
+ end
45
+
46
+ def resources
47
+ @parent.resources
48
+ end
49
+
50
+ private
51
+
52
+ def null_node_class
53
+ raise NotImplementedError
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,34 @@
1
+ module Sanction
2
+ module Blacklist
3
+ class List < Sanction::AttachedList
4
+
5
+ def allowed_ids
6
+ []
7
+ end
8
+
9
+ def permitted?
10
+ return false if wildcard_resource?
11
+ return false if resources.include?(@key)
12
+ return true if ids_blank?
13
+ true
14
+ end
15
+
16
+ def blacklist?
17
+ true
18
+ end
19
+
20
+ def whitelist?
21
+ false
22
+ end
23
+
24
+ def denied_ids
25
+ entries.map {|x| x.id}
26
+ end
27
+
28
+ def null_node_class
29
+ Sanction::Blacklist::NullNode
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,42 @@
1
+ module Sanction
2
+ module Blacklist
3
+ class Node < Sanction::Node
4
+
5
+ def permitted?
6
+ super
7
+ root? ? true : (@parent[type].permitted? && @parent[type].allowed_ids.include?(id))
8
+ end
9
+
10
+ def allow!
11
+ @parent.resources.reject! {|x| x == type } unless @parent[type].count > 1
12
+ unlink
13
+ true
14
+ end
15
+
16
+ def deny!
17
+ false
18
+ end
19
+
20
+ def whitelist?
21
+ false
22
+ end
23
+
24
+ def blacklist?
25
+ true
26
+ end
27
+
28
+ def mode
29
+ 'blacklist'
30
+ end
31
+
32
+ def array_class
33
+ Sanction::Blacklist::List
34
+ end
35
+
36
+ def null_array_class
37
+ Sanction::Blacklist::NullList
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,21 @@
1
+ module Sanction
2
+ module Blacklist
3
+ class NullList < Sanction::Blacklist::List
4
+
5
+ def permitted?
6
+ return false if wildcard_resource?
7
+ return false if resources.include?(@key) && ids_blank?
8
+ return true if ids_blank?
9
+ end
10
+
11
+ def persisted?
12
+ false
13
+ end
14
+
15
+ def null_node_class
16
+ Sanction::Blacklist::NullNode
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,37 @@
1
+ module Sanction
2
+ module Blacklist
3
+ class NullNode < Sanction::Blacklist::Node
4
+
5
+ def permitted?
6
+ a = ancestors.reject(&:root?).map(&:permitted?)
7
+ a << true
8
+ a.all?
9
+ end
10
+
11
+ def allow!
12
+ false
13
+ end
14
+
15
+ def deny!
16
+ ancestors.reject(&:persisted?).each(&:deny!)
17
+ @parent.resources << type
18
+ @parent.resources.uniq!
19
+ @parent.add_subject({
20
+ id: id,
21
+ type: type
22
+ })
23
+ end
24
+
25
+ def persisted?
26
+ false
27
+ end
28
+
29
+ def array_class
30
+ Sanction::Blacklist::NullList
31
+ end
32
+
33
+ alias :null_array_class :array_class
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,135 @@
1
+ module Sanction
2
+ class Node
3
+ include Tree
4
+
5
+ attr_reader :id, :type
6
+
7
+ def initialize(hash, parent=nil)
8
+ @parent = parent
9
+ process_hash(hash)
10
+ end
11
+
12
+ def permitted?
13
+ return false if wildcarded? && @parent.blacklist?
14
+ return true if wildcarded? && @parent.whitelist?
15
+ end
16
+
17
+ def to_hash
18
+ {
19
+ id: @id,
20
+ type: @type,
21
+ mode: mode,
22
+ scope: @scope,
23
+ subjects: subjects.map {|x| x.to_hash},
24
+ resources: @resources
25
+ }.reject { |k, v| v.blank? }
26
+ end
27
+
28
+ def persisted?
29
+ true
30
+ end
31
+
32
+ # Returns the new graph with the switched mode
33
+ def change_type! type
34
+ hash = to_hash
35
+ klass = "sanction/#{type}/node".classify.constantize
36
+ if root?
37
+ klass.new(hash)
38
+ else
39
+ node = klass.new(hash, parent)
40
+ parent.children << node
41
+ unlink
42
+ node.root
43
+ end
44
+ end
45
+
46
+ def add_subject(hash)
47
+ mode_class = hash[:mode] || mode
48
+ children << "sanction/#{mode_class}/node".classify.constantize.new(hash, self)
49
+ end
50
+
51
+ # Virtual
52
+ def array_class
53
+ raise NotImplementedError
54
+ end
55
+
56
+ def null_array_class
57
+ raise NotImplementedError
58
+ end
59
+
60
+ def [](key)
61
+ klass = subjects.select {|x| x.type?(key) }.any? ? array_class : null_array_class
62
+ klass.new(subjects.select {|x| x.type?(key) }).tap do |x|
63
+ x.key = key
64
+ x.parent = self
65
+ end
66
+ end
67
+
68
+ def find(type, id)
69
+ out = root
70
+ walk do |child|
71
+ out = child if (child.type?(type) && child.id?(id))
72
+ end
73
+ out
74
+ end
75
+
76
+ def has_scope? scope_symbol
77
+ scope.include? scope_symbol.to_sym
78
+ end
79
+
80
+ def type?(type)
81
+ @type == type.to_sym
82
+ end
83
+
84
+ def id?(id)
85
+ @id == id
86
+ end
87
+
88
+ def scope
89
+ @scope.blank? ? parent.scope : @scope
90
+ end
91
+
92
+ def scope=(scope)
93
+ @scope = [scope].flatten.compact.map(&:to_sym)
94
+ end
95
+
96
+ def resources
97
+ return [] if (@resources.blank? && root?)
98
+ @resources.blank? ? parent.resources : @resources
99
+ end
100
+
101
+ def resources=(resource)
102
+ @resources = [resource].flatten.compact.map(&:to_sym)
103
+ end
104
+
105
+ def mode
106
+ raise NotImplementedError
107
+ end
108
+
109
+ def children?
110
+ children.any?
111
+ end
112
+
113
+ def wildcarded?
114
+ @id == '*'
115
+ end
116
+
117
+ def children
118
+ @children ||= array_class.new
119
+ end
120
+
121
+ alias :subjects :children
122
+
123
+ private
124
+
125
+ def process_hash(hash)
126
+ @id = hash[:id]
127
+ @scope = hash[:scope].map(&:to_sym) unless hash[:scope].blank?
128
+ @type = hash[:type].to_sym if hash[:type]
129
+ @resources = []
130
+ @resources += hash[:resources].map(&:to_sym) unless hash[:resources].blank?
131
+ hash[:subjects].each { |subject| add_subject(subject) } unless hash[:subjects].blank?
132
+ end
133
+
134
+ end
135
+ end
@@ -0,0 +1,38 @@
1
+ module Sanction
2
+ class Permission
3
+
4
+ attr_reader :predicates
5
+
6
+ def initialize(permission_graph, *predicates)
7
+ @graph = permission_graph
8
+ @predicates = predicates
9
+ end
10
+
11
+ def path
12
+ @path ||= begin
13
+ path = @graph.root
14
+ @predicates.each do |predicate|
15
+ if predicate.is_a?(Class)
16
+ path = path[predicate.to_s.demodulize.underscore.to_sym]
17
+ else
18
+ path = path[predicate.class.to_s.demodulize.underscore.to_sym][predicate.id]
19
+ end
20
+ end
21
+ path
22
+ end
23
+ end
24
+
25
+ def persisted?
26
+ path.persisted?
27
+ end
28
+
29
+ def permitted?
30
+ path.permitted?
31
+ end
32
+
33
+ def permitted_with_scope?(scope)
34
+ permitted? && path.has_scope?(scope)
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,124 @@
1
+ module Sanction
2
+ module Tree
3
+
4
+ # This node's parent.
5
+ def parent
6
+ @parent
7
+ end
8
+
9
+ # Is this node the root of the tree?
10
+ def root?
11
+ parent.nil?
12
+ end
13
+
14
+ # Is this node a leaf node? Is this node childless?
15
+ def leaf?
16
+ children.nil? || children.empty?
17
+ end
18
+
19
+ # Get the root node in this tree.
20
+ def root
21
+ if root?
22
+ self
23
+ else
24
+ parent.root
25
+ end
26
+ end
27
+
28
+ # Get the children of this node.
29
+ def children
30
+ @children ||= []
31
+ end
32
+
33
+ # Get the siblings of this node. The other children belonging to this node's parent.
34
+ def siblings
35
+ if parent
36
+ parent.children.reject {|child| child == self }
37
+ end
38
+ end
39
+
40
+ # Is this node the only child of its parent. Does it have any siblings?
41
+ def only_child?
42
+ if parent
43
+ siblings.empty?
44
+ end
45
+ end
46
+
47
+ # Does this node have children? Is it not a leaf node?
48
+ def has_children?
49
+ !leaf?
50
+ end
51
+
52
+ # Get all of the ancestors for this node.
53
+ def ancestors
54
+ ancestors = []
55
+ if parent
56
+ ancestors << parent
57
+ parent.ancestors.each {|ancestor| ancestors << ancestor }
58
+ end
59
+ ancestors
60
+ end
61
+
62
+ # Get all of the descendants of this node.
63
+ # All of its children, and its childrens' children, and its childrens' childrens' children...
64
+ def descendants
65
+ descendants = []
66
+ if !children.empty?
67
+ (descendants << children).flatten!
68
+ children.each {|descendant| descendants << descendant.descendants }
69
+ descendants.flatten!
70
+ end
71
+ descendants
72
+ end
73
+
74
+ # An integer representation of how deep in the tree this node is.
75
+ # The root node has a depth of 1, its children have a depth of 2, etc.
76
+ def depth
77
+ ancestors.size + 1
78
+ end
79
+
80
+ # From Wikipedia: The height of a node is the length
81
+ # of the longest downward path to a leaf from that node.
82
+ # In other words, the length of this node to its furthest descendant.
83
+ def height
84
+ if !leaf?
85
+ descendants.collect {|child| child.depth }.uniq.size + 1
86
+ else
87
+ 1
88
+ end
89
+ end
90
+
91
+ # Orphan this node. Remove it from its parent node.
92
+ def unlink
93
+ if parent
94
+ parent.children.delete(self)
95
+ self.instance_variable_set(:@parent, nil)
96
+ return self
97
+ end
98
+ end
99
+
100
+ # Abandon all of this node's children.
101
+ def prune
102
+ if children
103
+ children.each {|child| child.unlink }
104
+ end
105
+ end
106
+
107
+ # Append a node to this node's children, and return the node.
108
+ def graft(node)
109
+ node.instance_variable_set(:@parent, self)
110
+ children << node
111
+ node
112
+ end
113
+
114
+ # Recursively yield every node in the tree.
115
+ def walk(&block)
116
+ if block_given?
117
+ yield self
118
+ children.each {|child| child.walk(&block) }
119
+ end
120
+ end
121
+
122
+
123
+ end
124
+ end
@@ -1,3 +1,3 @@
1
1
  module Sanction
2
- VERSION = "0.0.1"
2
+ VERSION = "2.1.0"
3
3
  end