granity 0.1.0 → 0.1.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.
@@ -0,0 +1,96 @@
1
+ module Granity
2
+ # Simple in-memory cache with dependency tracking and TTL
3
+ class InMemoryCache
4
+ def initialize(max_size: 10_000, ttl: 600)
5
+ @data = {}
6
+ @dependencies = {}
7
+ @max_size = max_size
8
+ @default_ttl = ttl
9
+ @mutex = Mutex.new
10
+ end
11
+
12
+ # Read a value from cache
13
+ def read(key)
14
+ @mutex.synchronize do
15
+ entry = @data[key]
16
+ return nil unless entry
17
+
18
+ # Check if entry is expired
19
+ if entry[:expires_at] && entry[:expires_at] < Time.now
20
+ @data.delete(key)
21
+ return nil
22
+ end
23
+
24
+ entry[:value]
25
+ end
26
+ end
27
+
28
+ # Write a value to cache with dependencies
29
+ def write(key, value, dependencies: [], ttl: nil)
30
+ @mutex.synchronize do
31
+ # Set expiration time if ttl provided
32
+ expires_at = if ttl
33
+ Time.now + ttl
34
+ else
35
+ (@default_ttl ? Time.now + @default_ttl : nil)
36
+ end
37
+
38
+ # Store the value
39
+ @data[key] = {
40
+ value: value,
41
+ expires_at: expires_at
42
+ }
43
+
44
+ # Register dependencies
45
+ dependencies.each do |dependency|
46
+ @dependencies[dependency] ||= []
47
+ @dependencies[dependency] << key unless @dependencies[dependency].include?(key)
48
+ end
49
+
50
+ # Enforce max size by removing oldest entries if needed
51
+ if @data.size > @max_size
52
+ # Simple LRU - just remove oldest N/4 entries
53
+ keys_to_remove = @data.keys.take(@max_size / 4)
54
+ keys_to_remove.each { |k| @data.delete(k) }
55
+ end
56
+
57
+ value
58
+ end
59
+ end
60
+
61
+ # Invalidate cache entries by dependency key
62
+ def invalidate_dependencies(dependency_keys)
63
+ @mutex.synchronize do
64
+ keys_to_invalidate = []
65
+
66
+ dependency_keys.each do |dep_key|
67
+ # Get all cache keys dependent on this key
68
+ if @dependencies[dep_key]
69
+ keys_to_invalidate.concat(@dependencies[dep_key])
70
+ @dependencies.delete(dep_key)
71
+ end
72
+ end
73
+
74
+ # Remove the invalidated entries
75
+ keys_to_invalidate.uniq.each do |key|
76
+ @data.delete(key)
77
+ end
78
+
79
+ keys_to_invalidate.size
80
+ end
81
+ end
82
+
83
+ # Clear the entire cache
84
+ def clear
85
+ @mutex.synchronize do
86
+ @data.clear
87
+ @dependencies.clear
88
+ end
89
+ end
90
+
91
+ # Get current cache size
92
+ def size
93
+ @mutex.synchronize { @data.size }
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,37 @@
1
+ module Granity
2
+ # Definition of a permission in the authorization schema
3
+ class Permission
4
+ attr_reader :name, :resource_type, :description, :rules
5
+
6
+ def initialize(name:, resource_type:, description: nil)
7
+ @name = name
8
+ @resource_type = resource_type
9
+ @description = description
10
+ @rules = []
11
+ end
12
+
13
+ # Include a relation in the permission
14
+ def include_relation(relation, from: nil)
15
+ @rules << Rules::Relation.new(relation: relation, from: from)
16
+ end
17
+
18
+ # Include another permission in this permission
19
+ def include_permission(permission, from: nil)
20
+ @rules << Rules::Permission.new(permission: permission, from: from)
21
+ end
22
+
23
+ # Define a set of rules where ANY must match
24
+ def include_any(&block)
25
+ rule = Rules::Any.new
26
+ rule.instance_eval(&block)
27
+ @rules << rule
28
+ end
29
+
30
+ # Define a set of rules where ALL must match
31
+ def include_all(&block)
32
+ rule = Rules::All.new
33
+ rule.instance_eval(&block)
34
+ @rules << rule
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,290 @@
1
+ module Granity
2
+ # Evaluates permissions based on the defined schema and relation tuples
3
+ class PermissionEvaluator
4
+ class << self
5
+ # Evaluate if a subject has a permission on a resource
6
+ def evaluate(subject_type:, subject_id:, permission:, resource_type:, resource_id:)
7
+ # Get the schema definition
8
+ schema = Granity::Schema.current
9
+ resource_type_def = schema.resource_types[resource_type.to_sym]
10
+
11
+ # If resource type doesn't exist, deny permission
12
+ return false unless resource_type_def
13
+
14
+ # Get the permission definition
15
+ permission_def = resource_type_def.permissions[permission.to_sym]
16
+
17
+ # If permission doesn't exist, deny permission
18
+ return false unless permission_def
19
+
20
+ # Evaluate the permission rules
21
+ evaluate_rules(
22
+ rules: permission_def.rules,
23
+ subject_type: subject_type,
24
+ subject_id: subject_id,
25
+ resource_type: resource_type,
26
+ resource_id: resource_id
27
+ )
28
+ end
29
+
30
+ # Find subjects with a permission on a resource
31
+ def find_subjects(resource_type:, resource_id:, permission:)
32
+ # Get the schema definition
33
+ schema = Granity::Schema.current
34
+ resource_type_def = schema.resource_types[resource_type.to_sym]
35
+
36
+ # If resource type doesn't exist, return empty array
37
+ return [] unless resource_type_def
38
+
39
+ # Get the permission definition
40
+ permission_def = resource_type_def.permissions[permission.to_sym]
41
+
42
+ # If permission doesn't exist, return empty array
43
+ return [] unless permission_def
44
+
45
+ # Get all relation tuples for this resource
46
+ collect_subjects_for_permission(
47
+ rules: permission_def.rules,
48
+ resource_type: resource_type,
49
+ resource_id: resource_id
50
+ )
51
+ end
52
+
53
+ private
54
+
55
+ # Evaluate rules recursively
56
+ def evaluate_rules(rules:, subject_type:, subject_id:, resource_type:, resource_id:, depth: 0)
57
+ # Prevent infinite recursion
58
+ max_depth = Granity.configuration.max_traversal_depth
59
+ if depth > max_depth
60
+ raise "Maximum permission evaluation depth (#{max_depth}) exceeded"
61
+ end
62
+
63
+ rules.each do |rule|
64
+ case rule
65
+ when Granity::Rules::Relation
66
+ # Check if relation tuple exists, handling the "from" case
67
+ if rule.from
68
+ # Relation traversal is needed
69
+ if check_relation_traversal(
70
+ subject_type: subject_type,
71
+ subject_id: subject_id,
72
+ relation: rule.relation,
73
+ from_relation: rule.from,
74
+ object_type: resource_type,
75
+ object_id: resource_id
76
+ )
77
+ return true
78
+ end
79
+ elsif check_relation(
80
+ subject_type: subject_type,
81
+ subject_id: subject_id,
82
+ relation: rule.relation,
83
+ object_type: resource_type,
84
+ object_id: resource_id
85
+ )
86
+ # Direct relation check
87
+ return true
88
+ end
89
+ when Granity::Rules::Permission
90
+ # Check referenced permission, handling "from" case
91
+ if rule.from
92
+ # Permission check with traversal - check permission on the "from" related objects
93
+ if check_permission_traversal(
94
+ subject_type: subject_type,
95
+ subject_id: subject_id,
96
+ permission: rule.permission,
97
+ from_relation: rule.from,
98
+ object_type: resource_type,
99
+ object_id: resource_id,
100
+ depth: depth + 1
101
+ )
102
+ return true
103
+ end
104
+ elsif evaluate(
105
+ subject_type: subject_type,
106
+ subject_id: subject_id,
107
+ permission: rule.permission,
108
+ resource_type: resource_type,
109
+ resource_id: resource_id
110
+ )
111
+ # Direct permission check on the same resource
112
+ return true
113
+ end
114
+ when Granity::Rules::Any
115
+ # Check if any of the subrules match
116
+ if rule.rules.any? do |subrule|
117
+ evaluate_rules(
118
+ rules: [subrule],
119
+ subject_type: subject_type,
120
+ subject_id: subject_id,
121
+ resource_type: resource_type,
122
+ resource_id: resource_id,
123
+ depth: depth + 1
124
+ )
125
+ end
126
+ return true
127
+ end
128
+ when Granity::Rules::All
129
+ # Check if all of the subrules match
130
+ if rule.rules.all? do |subrule|
131
+ evaluate_rules(
132
+ rules: [subrule],
133
+ subject_type: subject_type,
134
+ subject_id: subject_id,
135
+ resource_type: resource_type,
136
+ resource_id: resource_id,
137
+ depth: depth + 1
138
+ )
139
+ end
140
+ return true
141
+ end
142
+ end
143
+ end
144
+
145
+ false
146
+ end
147
+
148
+ # Check if a direct relation tuple exists
149
+ def check_relation(subject_type:, subject_id:, relation:, object_type:, object_id:)
150
+ Granity::RelationTuple.exists?(
151
+ subject_type: subject_type,
152
+ subject_id: subject_id,
153
+ relation: relation,
154
+ object_type: object_type,
155
+ object_id: object_id
156
+ )
157
+ end
158
+
159
+ # Check relation with traversal (the "from" case)
160
+ def check_relation_traversal(subject_type:, subject_id:, relation:, from_relation:, object_type:, object_id:)
161
+ # First, find all intermediary objects through the 'from' relation
162
+ intermediaries = Granity::RelationTuple.where(
163
+ object_type: object_type,
164
+ object_id: object_id,
165
+ relation: from_relation
166
+ )
167
+
168
+ # For each intermediary, check if the subject has the relation to it
169
+ intermediaries.any? do |tuple|
170
+ Granity::RelationTuple.exists?(
171
+ subject_type: subject_type,
172
+ subject_id: subject_id,
173
+ relation: relation,
174
+ object_type: tuple.subject_type,
175
+ object_id: tuple.subject_id
176
+ )
177
+ end
178
+ end
179
+
180
+ # Check permission with traversal (the "from" case)
181
+ def check_permission_traversal(subject_type:, subject_id:, permission:, from_relation:, object_type:, object_id:, depth:)
182
+ # First, find all intermediary objects through the 'from' relation
183
+ intermediaries = Granity::RelationTuple.where(
184
+ object_type: object_type,
185
+ object_id: object_id,
186
+ relation: from_relation
187
+ )
188
+
189
+ # For each intermediary, check if the subject has the permission on it
190
+ intermediaries.any? do |tuple|
191
+ evaluate(
192
+ subject_type: subject_type,
193
+ subject_id: subject_id,
194
+ permission: permission,
195
+ resource_type: tuple.subject_type,
196
+ resource_id: tuple.subject_id
197
+ )
198
+ end
199
+ end
200
+
201
+ # Collect subjects that have a permission on a resource
202
+ def collect_subjects_for_permission(rules:, resource_type:, resource_id:)
203
+ subjects = []
204
+
205
+ rules.each do |rule|
206
+ case rule
207
+ when Granity::Rules::Relation
208
+ if rule.from
209
+ # Handle relation traversal for finding subjects
210
+ intermediaries = Granity::RelationTuple.where(
211
+ object_type: resource_type,
212
+ object_id: resource_id,
213
+ relation: rule.from
214
+ )
215
+
216
+ intermediaries.each do |tuple|
217
+ # For each intermediary, find subjects with the relation
218
+ relations = Granity::RelationTuple.where(
219
+ object_type: tuple.subject_type,
220
+ object_id: tuple.subject_id,
221
+ relation: rule.relation
222
+ )
223
+
224
+ relations.each do |rel|
225
+ subjects << {type: rel.subject_type, id: rel.subject_id}
226
+ end
227
+ end
228
+ else
229
+ # Direct relation - find all subjects with this relation
230
+ tuples = Granity::RelationTuple.where(
231
+ object_type: resource_type,
232
+ object_id: resource_id,
233
+ relation: rule.relation
234
+ )
235
+
236
+ tuples.each do |tuple|
237
+ subjects << {type: tuple.subject_type, id: tuple.subject_id}
238
+ end
239
+ end
240
+ when Granity::Rules::Permission
241
+ # Find subjects with referenced permission
242
+ if rule.from
243
+ # Permission with traversal - we would need to traverse the "from" relation
244
+ # and then find subjects with the permission on those objects
245
+ # This is a simplification - in a real implementation we would handle this more efficiently
246
+ intermediaries = Granity::RelationTuple.where(
247
+ object_type: resource_type,
248
+ object_id: resource_id,
249
+ relation: rule.from
250
+ )
251
+
252
+ intermediaries.each do |tuple|
253
+ perm_subjects = find_subjects(
254
+ resource_type: tuple.subject_type,
255
+ resource_id: tuple.subject_id,
256
+ permission: rule.permission
257
+ )
258
+
259
+ subjects.concat(perm_subjects)
260
+ end
261
+ else
262
+ # Direct permission check
263
+ referenced_subjects = find_subjects(
264
+ resource_type: resource_type,
265
+ resource_id: resource_id,
266
+ permission: rule.permission
267
+ )
268
+
269
+ subjects.concat(referenced_subjects)
270
+ end
271
+ when Granity::Rules::Any, Granity::Rules::All
272
+ # Recursively collect subjects for each subrule
273
+ rule.rules.each do |subrule|
274
+ subrule_subjects = collect_subjects_for_permission(
275
+ rules: [subrule],
276
+ resource_type: resource_type,
277
+ resource_id: resource_id
278
+ )
279
+
280
+ subjects.concat(subrule_subjects)
281
+ end
282
+ end
283
+ end
284
+
285
+ # Remove duplicates
286
+ subjects.uniq { |s| "#{s[:type]}:#{s[:id]}" }
287
+ end
288
+ end
289
+ end
290
+ end
@@ -0,0 +1,13 @@
1
+ module Granity
2
+ # Definition of a relation between resources
3
+ class Relation
4
+ attr_reader :name, :resource_type, :target_type, :description
5
+
6
+ def initialize(name:, resource_type:, target_type:, description: nil)
7
+ @name = name
8
+ @resource_type = resource_type
9
+ @target_type = target_type
10
+ @description = description
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,34 @@
1
+ module Granity
2
+ # Definition of a resource type in the authorization schema
3
+ class ResourceType
4
+ attr_reader :name, :relations, :permissions
5
+
6
+ def initialize(name)
7
+ @name = name.to_sym
8
+ @relations = {}
9
+ @permissions = {}
10
+ end
11
+
12
+ # DSL method to define a relation
13
+ def relation(name, type:, description: nil)
14
+ @relations[name.to_sym] = Relation.new(
15
+ name: name.to_sym,
16
+ resource_type: @name,
17
+ target_type: type.to_sym,
18
+ description: description
19
+ )
20
+ end
21
+
22
+ # DSL method to define a permission
23
+ def permission(name, description: nil, &block)
24
+ permission = Permission.new(
25
+ name: name.to_sym,
26
+ resource_type: @name,
27
+ description: description
28
+ )
29
+
30
+ permission.instance_eval(&block) if block_given?
31
+ @permissions[name.to_sym] = permission
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,89 @@
1
+ module Granity
2
+ # Rules namespace for permission evaluation rules
3
+ module Rules
4
+ # Base class for all rules
5
+ class Base
6
+ def initialize
7
+ # Base initialization
8
+ end
9
+ end
10
+
11
+ # Rule for checking relation existence
12
+ class Relation < Base
13
+ attr_reader :relation, :from
14
+
15
+ def initialize(relation:, from: nil)
16
+ @relation = relation.to_sym
17
+ @from = from.to_sym if from
18
+ end
19
+ end
20
+
21
+ # Rule for checking another permission
22
+ class Permission < Base
23
+ attr_reader :permission, :from
24
+
25
+ def initialize(permission:, from: nil)
26
+ @permission = permission.to_sym
27
+ @from = from.to_sym if from
28
+ end
29
+ end
30
+
31
+ # Rule container where ANY rule must match
32
+ class Any < Base
33
+ attr_reader :rules
34
+
35
+ def initialize
36
+ @rules = []
37
+ end
38
+
39
+ def include_relation(relation, from: nil)
40
+ @rules << Relation.new(relation: relation, from: from)
41
+ end
42
+
43
+ def include_permission(permission, from: nil)
44
+ @rules << Permission.new(permission: permission, from: from)
45
+ end
46
+
47
+ def include_any(&block)
48
+ rule = Any.new
49
+ rule.instance_eval(&block)
50
+ @rules << rule
51
+ end
52
+
53
+ def include_all(&block)
54
+ rule = All.new
55
+ rule.instance_eval(&block)
56
+ @rules << rule
57
+ end
58
+ end
59
+
60
+ # Rule container where ALL rules must match
61
+ class All < Base
62
+ attr_reader :rules
63
+
64
+ def initialize
65
+ @rules = []
66
+ end
67
+
68
+ def include_relation(relation, from: nil)
69
+ @rules << Relation.new(relation: relation, from: from)
70
+ end
71
+
72
+ def include_permission(permission, from: nil)
73
+ @rules << Permission.new(permission: permission, from: from)
74
+ end
75
+
76
+ def include_any(&block)
77
+ rule = Any.new
78
+ rule.instance_eval(&block)
79
+ @rules << rule
80
+ end
81
+
82
+ def include_all(&block)
83
+ rule = All.new
84
+ rule.instance_eval(&block)
85
+ @rules << rule
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,46 @@
1
+ module Granity
2
+ # Schema definition for Granity authorization model
3
+ class Schema
4
+ attr_reader :resource_types
5
+
6
+ class << self
7
+ # DSL entry point for defining schema
8
+ def define(&block)
9
+ @current = new
10
+ @current.instance_eval(&block)
11
+ @current
12
+ end
13
+
14
+ # Get current schema
15
+ def current
16
+ @current ||= new
17
+ end
18
+ end
19
+
20
+ def initialize
21
+ @resource_types = {}
22
+ end
23
+
24
+ # DSL method to define a resource type
25
+ def resource_type(name, &block)
26
+ resource_type = ResourceType.new(name)
27
+ resource_type.instance_eval(&block) if block_given?
28
+ @resource_types[name] = resource_type
29
+ end
30
+
31
+ def validate_schema
32
+ # Validate that all relation types reference valid resource types
33
+ @resource_types.each do |name, resource_type|
34
+ resource_type.relations.each do |relation_name, relation|
35
+ unless @resource_types.key?(relation.type.to_sym)
36
+ raise SchemaError, "Resource type '#{name}' has relation '#{relation_name}' with invalid type '#{relation.type}'"
37
+ end
38
+ end
39
+ end
40
+
41
+ # More validations could be added here
42
+ end
43
+ end
44
+
45
+ class SchemaError < StandardError; end
46
+ end
@@ -1,3 +1,3 @@
1
1
  module Granity
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
data/lib/granity.rb CHANGED
@@ -1,6 +1,76 @@
1
1
  require "granity/version"
2
- require "granity/engine"
2
+ require "granity/configuration"
3
+ require "granity/schema"
4
+ require "granity/resource_type"
5
+ require "granity/relation"
6
+ require "granity/permission"
7
+ require "granity/rules"
8
+ require "granity/in_memory_cache"
9
+ require "granity/dependency_analyzer"
10
+ require "granity/permission_evaluator"
11
+ require "granity/authorization_engine"
12
+ # Rails engine is loaded at the end
13
+ require "granity/engine" if defined?(Rails)
3
14
 
4
15
  module Granity
5
- # Your code goes here...
16
+ class Error < StandardError; end
17
+
18
+ class << self
19
+ def configuration
20
+ @configuration ||= Configuration.new
21
+ end
22
+
23
+ # Entry point for DSL schema definition
24
+ def define(&block)
25
+ Schema.define(&block)
26
+ end
27
+
28
+ # Configuration setup
29
+ def configure
30
+ yield(configuration) if block_given?
31
+ configuration
32
+ end
33
+
34
+ # Public API to check permissions
35
+ def check_permission(subject_type:, subject_id:, permission:, resource_type:, resource_id:)
36
+ AuthorizationEngine.check_permission(
37
+ subject_type: subject_type,
38
+ subject_id: subject_id,
39
+ permission: permission,
40
+ resource_type: resource_type,
41
+ resource_id: resource_id
42
+ )
43
+ end
44
+
45
+ # Public API to find subjects with a permission
46
+ def find_subjects(resource_type:, resource_id:, permission:)
47
+ AuthorizationEngine.find_subjects(
48
+ resource_type: resource_type,
49
+ resource_id: resource_id,
50
+ permission: permission
51
+ )
52
+ end
53
+
54
+ # Public API to create relation tuples
55
+ def create_relation(object_type:, object_id:, relation:, subject_type:, subject_id:)
56
+ AuthorizationEngine.create_relation(
57
+ object_type: object_type,
58
+ object_id: object_id,
59
+ relation: relation,
60
+ subject_type: subject_type,
61
+ subject_id: subject_id
62
+ )
63
+ end
64
+
65
+ # Public API to delete relation tuples
66
+ def delete_relation(object_type:, object_id:, relation:, subject_type:, subject_id:)
67
+ AuthorizationEngine.delete_relation(
68
+ object_type: object_type,
69
+ object_id: object_id,
70
+ relation: relation,
71
+ subject_type: subject_type,
72
+ subject_id: subject_id
73
+ )
74
+ end
75
+ end
6
76
  end