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.
- 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 +39 -41
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d187a969f4aa61ad031d5eff41882fba68796b1455f8ea11011663d51fea4960
|
4
|
+
data.tar.gz: b11f5bf0eb6fa7576d505f4f168a4b4bc4c7353f87b0d4120484347616228629
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1f2ee84f563fb463429658e3d55125db8cbe70bad1369d01d8c5f3514a929f2d2127fc58b4133691abc1d3bfabc9d5dff028e73c32a077ae0e944e6f96f22c0c
|
7
|
+
data.tar.gz: db09edf1bc7eb3df8d97240f6e030782cc5fe9f9b17f92c44935b11ef2a3a0864113674dfbf5f1f3702285bd3b4a14e8ee576fe1abcbd0aca57aba910ca2bec8
|
data/MIT-LICENSE
CHANGED
data/README.md
CHANGED
@@ -1,28 +1,224 @@
|
|
1
1
|
# Granity
|
2
|
-
Short description and motivation.
|
3
2
|
|
4
|
-
|
5
|
-
How to use my plugin.
|
3
|
+
Granity is a fine-grained authorization engine for Ruby on Rails applications. It provides a flexible DSL for defining authorization rules and efficient permission checking.
|
6
4
|
|
7
5
|
## Installation
|
8
|
-
|
6
|
+
|
7
|
+
Add this gem to your application's Gemfile:
|
9
8
|
|
10
9
|
```ruby
|
11
|
-
gem
|
10
|
+
gem 'granity'
|
12
11
|
```
|
13
12
|
|
14
|
-
|
13
|
+
Then execute:
|
14
|
+
|
15
15
|
```bash
|
16
|
-
$ bundle
|
16
|
+
$ bundle install
|
17
17
|
```
|
18
18
|
|
19
|
-
|
19
|
+
Run the migrations:
|
20
|
+
|
20
21
|
```bash
|
21
|
-
$
|
22
|
+
$ rails granity:install:migrations
|
23
|
+
$ rails db:migrate
|
24
|
+
```
|
25
|
+
|
26
|
+
## Configuration
|
27
|
+
|
28
|
+
Create an initializer for Granity:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
# config/initializers/granity.rb
|
32
|
+
Granity.configure do |config|
|
33
|
+
config.cache_provider = Rails.cache # Optional: Uses Rails.cache if provided
|
34
|
+
config.cache_ttl = 10.minutes
|
35
|
+
config.max_cache_size = 10_000
|
36
|
+
config.enable_tracing = !Rails.env.production?
|
37
|
+
config.max_traversal_depth = 10
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
## Defining Authorization Schema
|
42
|
+
|
43
|
+
Use the Granity DSL to define your authorization schema:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
# config/initializers/granity.rb
|
47
|
+
Granity.define do
|
48
|
+
resource_type :user do
|
49
|
+
# User schema
|
50
|
+
end
|
51
|
+
|
52
|
+
resource_type :document do
|
53
|
+
relation :owner, type: :user
|
54
|
+
relation :viewer, type: :user
|
55
|
+
relation :team, type: :team
|
56
|
+
|
57
|
+
permission :view do
|
58
|
+
include_any do
|
59
|
+
include_relation :owner
|
60
|
+
include_relation :viewer
|
61
|
+
include_relation :admin from :team
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
permission :edit do
|
66
|
+
include_relation :owner
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
resource_type :team do
|
71
|
+
relation :member, type: :user
|
72
|
+
relation :admin, type: :user
|
73
|
+
end
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
## Usage
|
78
|
+
|
79
|
+
### Checking Permissions
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
# Check if a user has permission on a resource
|
83
|
+
if Granity.check_permission(
|
84
|
+
subject_type: 'user',
|
85
|
+
subject_id: current_user.id,
|
86
|
+
permission: 'view',
|
87
|
+
resource_type: 'document',
|
88
|
+
resource_id: document.id
|
89
|
+
)
|
90
|
+
# User can view the document
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
### Creating Relations
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
# Grant a user owner access to a document
|
98
|
+
Granity.create_relation(
|
99
|
+
object_type: 'document',
|
100
|
+
object_id: document.id,
|
101
|
+
relation: 'owner',
|
102
|
+
subject_type: 'user',
|
103
|
+
subject_id: user.id
|
104
|
+
)
|
105
|
+
```
|
106
|
+
|
107
|
+
### Finding Subjects
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
# Find all users who can view a document
|
111
|
+
viewers = Granity.find_subjects(
|
112
|
+
resource_type: 'document',
|
113
|
+
resource_id: document.id,
|
114
|
+
permission: 'view'
|
115
|
+
)
|
116
|
+
```
|
117
|
+
|
118
|
+
### Integration with Rails Controllers
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
class ApplicationController < ActionController::Base
|
122
|
+
def authorize!(resource, permission)
|
123
|
+
unless Granity.check_permission(
|
124
|
+
subject_type: 'user',
|
125
|
+
subject_id: current_user.id,
|
126
|
+
permission: permission,
|
127
|
+
resource_type: resource.model_name.singular,
|
128
|
+
resource_id: resource.id
|
129
|
+
)
|
130
|
+
raise Unauthorized, "Not authorized to #{permission} this #{resource.model_name.human}"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
class DocumentsController < ApplicationController
|
136
|
+
def show
|
137
|
+
@document = Document.find(params[:id])
|
138
|
+
authorize!(@document, :view)
|
139
|
+
# ...
|
140
|
+
end
|
141
|
+
|
142
|
+
def update
|
143
|
+
@document = Document.find(params[:id])
|
144
|
+
authorize!(@document, :edit)
|
145
|
+
# ...
|
146
|
+
end
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
150
|
+
## DSL Reference
|
151
|
+
|
152
|
+
### Resource Types
|
153
|
+
|
154
|
+
Define the entities in your authorization model:
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
resource_type :document do
|
158
|
+
# Resource definition
|
159
|
+
end
|
22
160
|
```
|
23
161
|
|
24
|
-
|
25
|
-
|
162
|
+
### Relations
|
163
|
+
|
164
|
+
Define relationships between resources:
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
relation :owner, type: :user
|
168
|
+
relation :parent_folder, type: :folder
|
169
|
+
```
|
170
|
+
|
171
|
+
### Permissions
|
172
|
+
|
173
|
+
Define access rules with Boolean logic:
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
permission :view do
|
177
|
+
include_any do
|
178
|
+
include_relation :owner
|
179
|
+
include_relation :viewer
|
180
|
+
include_relation :editor
|
181
|
+
end
|
182
|
+
end
|
183
|
+
```
|
184
|
+
|
185
|
+
### Boolean Logic
|
186
|
+
|
187
|
+
Combine relations with `include_any` (OR) and `include_all` (AND):
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
permission :publish do
|
191
|
+
include_all do
|
192
|
+
include_relation :editor
|
193
|
+
|
194
|
+
include_any do
|
195
|
+
include_relation :approved
|
196
|
+
include_relation :admin from :team
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
```
|
201
|
+
|
202
|
+
### Permission Composition
|
203
|
+
|
204
|
+
Reuse and compose permissions:
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
permission :manage do
|
208
|
+
include_permission :view
|
209
|
+
include_permission :edit
|
210
|
+
include_relation :owner
|
211
|
+
end
|
212
|
+
```
|
213
|
+
|
214
|
+
### Relation Traversal
|
215
|
+
|
216
|
+
Follow paths through related resources:
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
include_relation :member from :team
|
220
|
+
```
|
26
221
|
|
27
222
|
## License
|
223
|
+
|
28
224
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Granity
|
2
|
+
class RelationTuple < ActiveRecord::Base
|
3
|
+
self.table_name = "granity_relation_tuples"
|
4
|
+
|
5
|
+
validates :object_type, :object_id, :relation, :subject_type, :subject_id, presence: true
|
6
|
+
|
7
|
+
# Useful scopes for querying
|
8
|
+
scope :for_object, ->(type, id) { where(object_type: type, object_id: id) }
|
9
|
+
scope :for_subject, ->(type, id) { where(subject_type: type, subject_id: id) }
|
10
|
+
scope :with_relation, ->(relation) { where(relation: relation) }
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class CreateGranityRelationTuples < ActiveRecord::Migration[7.0]
|
2
|
+
def change
|
3
|
+
create_table :granity_relation_tuples do |t|
|
4
|
+
t.string :object_type, null: false
|
5
|
+
t.string :object_id, null: false
|
6
|
+
t.string :relation, null: false
|
7
|
+
t.string :subject_type, null: false
|
8
|
+
t.string :subject_id, null: false
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
|
13
|
+
add_index :granity_relation_tuples, [:object_type, :object_id, :relation],
|
14
|
+
name: "index_granity_tuples_on_object"
|
15
|
+
add_index :granity_relation_tuples, [:subject_type, :subject_id],
|
16
|
+
name: "index_granity_tuples_on_subject"
|
17
|
+
add_index :granity_relation_tuples, [:object_type, :object_id, :relation, :subject_type, :subject_id],
|
18
|
+
unique: true, name: "index_granity_tuples_unique"
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Granity
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path("templates", __dir__)
|
5
|
+
|
6
|
+
def create_migration
|
7
|
+
timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
|
8
|
+
copy_file "create_granity_tables.rb", "db/migrate/#{timestamp}_create_granity_tables.rb"
|
9
|
+
end
|
10
|
+
|
11
|
+
def create_initializer
|
12
|
+
copy_file "initializer.rb", "config/initializers/granity.rb"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
module Granity
|
2
|
+
class AuthorizationEngine
|
3
|
+
class << self
|
4
|
+
def check_permission(subject_type:, subject_id:, permission:, resource_type:, resource_id:)
|
5
|
+
cache_key = "granity:permission:#{subject_type}:#{subject_id}:#{permission}:#{resource_type}:#{resource_id}"
|
6
|
+
|
7
|
+
# Try fetching from cache first
|
8
|
+
cached_result = cache.read(cache_key)
|
9
|
+
if cached_result
|
10
|
+
trace("CACHE HIT: #{cache_key} -> #{cached_result}")
|
11
|
+
return cached_result
|
12
|
+
end
|
13
|
+
|
14
|
+
trace("CACHE MISS: #{cache_key}")
|
15
|
+
|
16
|
+
# Generate dependencies for this permission check
|
17
|
+
dependencies = DependencyAnalyzer.analyze_permission_check(
|
18
|
+
subject_type: subject_type,
|
19
|
+
subject_id: subject_id,
|
20
|
+
permission: permission,
|
21
|
+
resource_type: resource_type,
|
22
|
+
resource_id: resource_id
|
23
|
+
)
|
24
|
+
|
25
|
+
# Check the permission
|
26
|
+
result = PermissionEvaluator.evaluate(
|
27
|
+
subject_type: subject_type,
|
28
|
+
subject_id: subject_id,
|
29
|
+
permission: permission,
|
30
|
+
resource_type: resource_type,
|
31
|
+
resource_id: resource_id
|
32
|
+
)
|
33
|
+
|
34
|
+
# Store in cache with all dependency keys
|
35
|
+
cache.write(cache_key, result, dependencies: dependencies)
|
36
|
+
trace("CACHE WRITE: #{cache_key} -> #{result} with dependencies: #{dependencies}")
|
37
|
+
|
38
|
+
result
|
39
|
+
end
|
40
|
+
|
41
|
+
def find_subjects(resource_type:, resource_id:, permission:)
|
42
|
+
cache_key = "granity:subjects:#{permission}:#{resource_type}:#{resource_id}"
|
43
|
+
|
44
|
+
# Try fetching from cache first
|
45
|
+
cached_result = cache.read(cache_key)
|
46
|
+
if cached_result
|
47
|
+
trace("CACHE HIT: #{cache_key}")
|
48
|
+
return cached_result
|
49
|
+
end
|
50
|
+
|
51
|
+
trace("CACHE MISS: #{cache_key}")
|
52
|
+
|
53
|
+
# Generate dependencies for this subjects query
|
54
|
+
dependencies = DependencyAnalyzer.analyze_find_subjects(
|
55
|
+
resource_type: resource_type,
|
56
|
+
resource_id: resource_id,
|
57
|
+
permission: permission
|
58
|
+
)
|
59
|
+
|
60
|
+
# Get the subjects
|
61
|
+
subjects = PermissionEvaluator.find_subjects(
|
62
|
+
resource_type: resource_type,
|
63
|
+
resource_id: resource_id,
|
64
|
+
permission: permission
|
65
|
+
)
|
66
|
+
|
67
|
+
# Store in cache with all dependency keys
|
68
|
+
cache.write(cache_key, subjects, dependencies: dependencies)
|
69
|
+
trace("CACHE WRITE: #{cache_key} with dependencies: #{dependencies}")
|
70
|
+
|
71
|
+
subjects
|
72
|
+
end
|
73
|
+
|
74
|
+
def create_relation(object_type:, object_id:, relation:, subject_type:, subject_id:)
|
75
|
+
# Create the relation tuple in the database
|
76
|
+
tuple = Granity::RelationTuple.create!(
|
77
|
+
object_type: object_type,
|
78
|
+
object_id: object_id,
|
79
|
+
relation: relation,
|
80
|
+
subject_type: subject_type,
|
81
|
+
subject_id: subject_id
|
82
|
+
)
|
83
|
+
|
84
|
+
# Invalidate cache entries that depend on this relation
|
85
|
+
invalidate_cache_for_relation(object_type, object_id, relation)
|
86
|
+
|
87
|
+
tuple
|
88
|
+
rescue ActiveRecord::RecordNotUnique
|
89
|
+
# If the relation already exists, just return it
|
90
|
+
Granity::RelationTuple.find_by(
|
91
|
+
object_type: object_type,
|
92
|
+
object_id: object_id,
|
93
|
+
relation: relation,
|
94
|
+
subject_type: subject_type,
|
95
|
+
subject_id: subject_id
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
def delete_relation(object_type:, object_id:, relation:, subject_type:, subject_id:)
|
100
|
+
tuple = Granity::RelationTuple.find_by(
|
101
|
+
object_type: object_type,
|
102
|
+
object_id: object_id,
|
103
|
+
relation: relation,
|
104
|
+
subject_type: subject_type,
|
105
|
+
subject_id: subject_id
|
106
|
+
)
|
107
|
+
|
108
|
+
return false unless tuple
|
109
|
+
|
110
|
+
tuple.destroy
|
111
|
+
|
112
|
+
# Invalidate cache entries that depend on this relation
|
113
|
+
invalidate_cache_for_relation(object_type, object_id, relation)
|
114
|
+
|
115
|
+
true
|
116
|
+
end
|
117
|
+
|
118
|
+
def reset_cache
|
119
|
+
cache.clear
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def cache
|
125
|
+
@cache ||= begin
|
126
|
+
config = Granity.configuration
|
127
|
+
config.cache_provider || Granity::InMemoryCache.new(
|
128
|
+
max_size: config.max_cache_size,
|
129
|
+
ttl: config.cache_ttl
|
130
|
+
)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def invalidate_cache_for_relation(object_type, object_id, relation)
|
135
|
+
# This is a simplified approach. In a real implementation, we would use
|
136
|
+
# a more sophisticated approach to track which cache keys depend on which
|
137
|
+
# relations, possibly using Redis sets or a similar mechanism.
|
138
|
+
|
139
|
+
# For now, we take a conservative approach and invalidate any cache entry
|
140
|
+
# that might depend on this relation being changed
|
141
|
+
dependency_key = "granity:relation:#{object_type}:#{object_id}:#{relation}"
|
142
|
+
cache.invalidate_dependencies([dependency_key])
|
143
|
+
|
144
|
+
trace("CACHE INVALIDATE for dependency: #{dependency_key}")
|
145
|
+
end
|
146
|
+
|
147
|
+
def trace(message)
|
148
|
+
return unless Granity.configuration.enable_tracing
|
149
|
+
|
150
|
+
# In a real implementation, this would use a proper logging system
|
151
|
+
puts "[Granity] #{message}"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Granity
|
2
|
+
# Configuration class to hold Granity settings
|
3
|
+
class Configuration
|
4
|
+
attr_accessor :cache_provider
|
5
|
+
attr_accessor :cache_ttl
|
6
|
+
attr_accessor :max_cache_size
|
7
|
+
attr_accessor :enable_tracing
|
8
|
+
attr_accessor :max_traversal_depth
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@cache_provider = nil
|
12
|
+
@cache_ttl = 10.minutes
|
13
|
+
@max_cache_size = 10_000
|
14
|
+
@enable_tracing = !defined?(Rails) || !Rails.env.production?
|
15
|
+
@max_traversal_depth = 10
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Granity
|
2
|
+
# Analyzes dependencies between relations and permissions for proper cache invalidation
|
3
|
+
class DependencyAnalyzer
|
4
|
+
class << self
|
5
|
+
# Analyze dependencies for a permission check
|
6
|
+
# Returns an array of dependencies for cache invalidation
|
7
|
+
def analyze_permission_check(subject_type:, subject_id:, permission:, resource_type:, resource_id:)
|
8
|
+
deps = []
|
9
|
+
|
10
|
+
# Basic direct dependencies
|
11
|
+
deps << "granity:subject:#{subject_type}:#{subject_id}"
|
12
|
+
deps << "granity:resource:#{resource_type}:#{resource_id}"
|
13
|
+
|
14
|
+
# Get schema dependencies for this permission
|
15
|
+
schema_dependencies = analyze_permission_schema(resource_type, permission)
|
16
|
+
deps.concat(schema_dependencies)
|
17
|
+
|
18
|
+
deps
|
19
|
+
end
|
20
|
+
|
21
|
+
# Analyze dependencies for finding subjects with a permission
|
22
|
+
def analyze_find_subjects(resource_type:, resource_id:, permission:)
|
23
|
+
deps = []
|
24
|
+
|
25
|
+
# Basic resource dependency
|
26
|
+
deps << "granity:resource:#{resource_type}:#{resource_id}"
|
27
|
+
|
28
|
+
# Get schema dependencies for this permission
|
29
|
+
schema_dependencies = analyze_permission_schema(resource_type, permission)
|
30
|
+
deps.concat(schema_dependencies)
|
31
|
+
|
32
|
+
deps
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Analyze schema dependencies for a permission
|
38
|
+
def analyze_permission_schema(resource_type, permission)
|
39
|
+
deps = []
|
40
|
+
schema = Granity::Schema.current
|
41
|
+
|
42
|
+
# Add dependency on the permission definition itself
|
43
|
+
deps << "granity:schema:#{resource_type}:permission:#{permission}"
|
44
|
+
|
45
|
+
# Get the resource type from schema
|
46
|
+
resource_type_def = schema.resource_types[resource_type.to_sym]
|
47
|
+
return deps unless resource_type_def
|
48
|
+
|
49
|
+
# Get the permission definition
|
50
|
+
permission_def = resource_type_def.permissions[permission.to_sym]
|
51
|
+
return deps unless permission_def
|
52
|
+
|
53
|
+
# Track visited permissions to prevent cycles
|
54
|
+
visited = Set.new(["#{resource_type}:#{permission}"])
|
55
|
+
|
56
|
+
# Add all relations that this permission depends on
|
57
|
+
relations = extract_relations_from_permission(permission_def, [], visited)
|
58
|
+
relations.each do |relation|
|
59
|
+
deps << "granity:relation:#{resource_type}:#{relation}"
|
60
|
+
end
|
61
|
+
|
62
|
+
deps
|
63
|
+
end
|
64
|
+
|
65
|
+
# Extract all relations used in a permission definition
|
66
|
+
def extract_relations_from_permission(rule_or_permission, relations = [], visited = Set.new)
|
67
|
+
# Handle different types of input
|
68
|
+
if rule_or_permission.is_a?(Granity::Permission)
|
69
|
+
# It's a Permission object with rules
|
70
|
+
rule_or_permission.rules.each do |rule|
|
71
|
+
extract_relations_from_rule(rule, relations, visited, rule_or_permission.resource_type)
|
72
|
+
end
|
73
|
+
else
|
74
|
+
# It's a rule object directly
|
75
|
+
extract_relations_from_rule(rule_or_permission, relations, visited, nil)
|
76
|
+
end
|
77
|
+
|
78
|
+
relations.uniq
|
79
|
+
end
|
80
|
+
|
81
|
+
# Process a single rule to extract relations
|
82
|
+
def extract_relations_from_rule(rule, relations = [], visited = Set.new, resource_type = nil)
|
83
|
+
if rule.is_a?(Granity::Rules::Relation)
|
84
|
+
# Direct relation - add to results
|
85
|
+
relations << rule.relation
|
86
|
+
elsif rule.is_a?(Granity::Rules::Any) || rule.is_a?(Granity::Rules::All)
|
87
|
+
# Container rule - process each subrule
|
88
|
+
rule.rules.each do |subrule|
|
89
|
+
extract_relations_from_rule(subrule, relations, visited, resource_type)
|
90
|
+
end
|
91
|
+
elsif rule.is_a?(Granity::Rules::Permission)
|
92
|
+
# Referenced permission - get from schema and process
|
93
|
+
# Skip if we've already visited this permission to prevent cycles
|
94
|
+
permission_key = "#{resource_type}:#{rule.permission}"
|
95
|
+
return relations if visited.include?(permission_key)
|
96
|
+
|
97
|
+
# Mark as visited to prevent cycles
|
98
|
+
visited.add(permission_key)
|
99
|
+
|
100
|
+
# Get referenced permission from schema (if possible)
|
101
|
+
schema = Granity::Schema.current
|
102
|
+
|
103
|
+
if resource_type && schema.resource_types[resource_type.to_sym]
|
104
|
+
# If we know the resource type, look for the permission there
|
105
|
+
resource_type_def = schema.resource_types[resource_type.to_sym]
|
106
|
+
if resource_type_def.permissions.has_key?(rule.permission)
|
107
|
+
referenced_permission = resource_type_def.permissions[rule.permission]
|
108
|
+
extract_relations_from_permission(referenced_permission, relations, visited)
|
109
|
+
end
|
110
|
+
else
|
111
|
+
# If resource type is unknown, look in all resource types
|
112
|
+
resource_types = schema.resource_types.values
|
113
|
+
resource_types.each do |rt|
|
114
|
+
if rt.permissions.has_key?(rule.permission)
|
115
|
+
referenced_permission = rt.permissions[rule.permission]
|
116
|
+
extract_relations_from_permission(referenced_permission, relations, visited)
|
117
|
+
break
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
relations
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
data/lib/granity/engine.rb
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
module Granity
|
2
2
|
class Engine < ::Rails::Engine
|
3
3
|
isolate_namespace Granity
|
4
|
+
|
5
|
+
initializer "granity.load_migrations" do |app|
|
6
|
+
unless app.root.to_s.match root.to_s
|
7
|
+
config.paths["db/migrate"].expanded.each do |expanded_path|
|
8
|
+
app.config.paths["db/migrate"] << expanded_path
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
4
12
|
end
|
5
13
|
end
|