granity 0.1.0 → 0.1.2
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 +4 -4
- data/MIT-LICENSE +1 -1
- data/README.md +207 -11
- data/app/models/granity/relation_tuple.rb +12 -0
- data/db/migrate/20250317000000_create_granity_relation_tuples.rb +20 -0
- data/lib/generators/granity/install/install_generator.rb +16 -0
- data/lib/granity/authorization_engine.rb +155 -0
- data/lib/granity/configuration.rb +18 -0
- data/lib/granity/dependency_analyzer.rb +127 -0
- data/lib/granity/engine.rb +8 -0
- data/lib/granity/in_memory_cache.rb +96 -0
- data/lib/granity/permission.rb +37 -0
- data/lib/granity/permission_evaluator.rb +290 -0
- data/lib/granity/relation.rb +13 -0
- data/lib/granity/resource_type.rb +34 -0
- data/lib/granity/rules.rb +89 -0
- data/lib/granity/schema.rb +46 -0
- data/lib/granity/version.rb +1 -1
- data/lib/granity.rb +72 -2
- metadata +41 -43
- data/app/assets/stylesheets/granity/application.css +0 -15
- data/app/controllers/granity/application_controller.rb +0 -4
- data/app/helpers/granity/application_helper.rb +0 -4
- data/app/jobs/granity/application_job.rb +0 -4
- data/app/mailers/granity/application_mailer.rb +0 -6
- data/app/models/granity/application_record.rb +0 -5
- data/app/views/layouts/granity/application.html.erb +0 -17
@@ -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
|
data/lib/granity/version.rb
CHANGED
data/lib/granity.rb
CHANGED
@@ -1,6 +1,76 @@
|
|
1
1
|
require "granity/version"
|
2
|
-
require "granity/
|
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
|
-
|
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
|