rbacan 0.4.0 → 0.5.0

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.
@@ -14,20 +14,23 @@ module Rbacan
14
14
  through: :user_roles
15
15
 
16
16
  # Returns users that have the given role (by name).
17
- scope :with_role, lambda { |role_name|
18
- joins(:roles).where(Rbacan.role_table => { name: role_name.to_s })
17
+ scope :with_role, lambda { |role_name|
18
+ Rbacan.ensure_legacy_mode!(:with_role)
19
+ joins(:roles).where(Rbacan.role_table => { Rbacan.role_key => role_name.to_s })
19
20
  }
20
21
 
21
22
  # Returns users that have the given permission via any of their roles.
22
- scope :with_permission, lambda { |permission_name|
23
- joins(roles: { role_permissions: :permission })
24
- .where(Rbacan.permission_table => { name: permission_name.to_s })
23
+ scope :with_permission, lambda { |permission_name|
24
+ Rbacan.ensure_legacy_mode!(:with_permission)
25
+ joins(roles: { role_permissions: :permission })
26
+ .where(Rbacan.permission_table => { Rbacan.permission_key => permission_name.to_s })
25
27
  }
26
28
 
27
29
  # Assigns a role to the user. Idempotent — safe to call multiple times.
28
30
  # When tenant_scoped is enabled, pass tenant_id: to scope the assignment.
29
- def assign_role(role_name, tenant_id: nil)
30
- assigned_role = Rbacan.role_class.constantize.find_by_name(role_name.to_s)
31
+ def assign_role(role_name, tenant_id: nil)
32
+ Rbacan.ensure_legacy_mode!(:assign_role)
33
+ assigned_role = Rbacan.role_class.constantize.find_by(Rbacan.role_key => role_name.to_s)
31
34
  raise ArgumentError, "Role '#{role_name}' not found" unless assigned_role
32
35
 
33
36
  attrs = { role_id: assigned_role.id }
@@ -37,8 +40,9 @@ module Rbacan
37
40
 
38
41
  # Removes a role from the user.
39
42
  # When tenant_scoped is enabled, pass tenant_id: to remove from a specific tenant.
40
- def remove_role(role_name, tenant_id: nil)
41
- removed_role = Rbacan.role_class.constantize.find_by_name(role_name.to_s)
43
+ def remove_role(role_name, tenant_id: nil)
44
+ Rbacan.ensure_legacy_mode!(:remove_role)
45
+ removed_role = Rbacan.role_class.constantize.find_by(Rbacan.role_key => role_name.to_s)
42
46
  return unless removed_role
43
47
 
44
48
  scope = user_roles.where(role_id: removed_role.id)
@@ -47,58 +51,65 @@ module Rbacan
47
51
  end
48
52
 
49
53
  # Returns true if the user has the named permission via any of their roles.
50
- def can?(permission_name)
51
- roles
54
+ def can?(permission_name)
55
+ Rbacan.ensure_legacy_mode!(:can?)
56
+ roles
52
57
  .joins(role_permissions: :permission)
53
- .where(Rbacan.permission_table => { name: permission_name.to_s })
58
+ .where(Rbacan.permission_table => { Rbacan.permission_key => permission_name.to_s })
54
59
  .exists?
55
60
  end
56
61
 
57
62
  # Returns true if the user has the named permission in a specific tenant context.
58
63
  # Checks both global role assignments (tenant_id NULL) and tenant-specific ones.
59
- def can_in_tenant?(permission_name, tenant_id)
60
- roles
64
+ def can_in_tenant?(permission_name, tenant_id)
65
+ Rbacan.ensure_legacy_mode!(:can_in_tenant?)
66
+ roles
61
67
  .joins(role_permissions: :permission)
62
- .where(Rbacan.permission_table => { name: permission_name.to_s })
68
+ .where(Rbacan.permission_table => { Rbacan.permission_key => permission_name.to_s })
63
69
  .where(Rbacan.user_role_table => { tenant_id: [tenant_id, nil] })
64
70
  .exists?
65
71
  end
66
72
 
67
73
  # Returns true if the user has the named role.
68
- def has_role?(role_name)
69
- roles.where(name: role_name.to_s).exists?
74
+ def has_role?(role_name)
75
+ Rbacan.ensure_legacy_mode!(:has_role?)
76
+ roles.where(Rbacan.role_key => role_name.to_s).exists?
70
77
  end
71
78
 
72
79
  # Returns true if the user has the named role in a specific tenant context.
73
80
  # Checks both global role assignments (tenant_id NULL) and tenant-specific ones.
74
- def has_role_in_tenant?(role_name, tenant_id)
75
- user_roles
81
+ def has_role_in_tenant?(role_name, tenant_id)
82
+ Rbacan.ensure_legacy_mode!(:has_role_in_tenant?)
83
+ user_roles
76
84
  .joins(:role)
77
- .where(Rbacan.role_table => { name: role_name.to_s })
85
+ .where(Rbacan.role_table => { Rbacan.role_key => role_name.to_s })
78
86
  .where(Rbacan.user_role_table => { tenant_id: [tenant_id, nil] })
79
87
  .exists?
80
88
  end
81
89
 
82
90
  # Returns true if the user has ANY of the listed roles.
83
- def has_any_role?(*role_names)
84
- roles.where(name: role_names.map(&:to_s)).exists?
91
+ def has_any_role?(*role_names)
92
+ Rbacan.ensure_legacy_mode!(:has_any_role?)
93
+ roles.where(Rbacan.role_key => role_names.map(&:to_s)).exists?
85
94
  end
86
95
 
87
96
  # Returns true if the user has ALL of the listed permissions.
88
- def can_all?(*permission_names)
89
- names = permission_names.map(&:to_s)
97
+ def can_all?(*permission_names)
98
+ Rbacan.ensure_legacy_mode!(:can_all?)
99
+ names = permission_names.map(&:to_s)
90
100
  roles
91
101
  .joins(role_permissions: :permission)
92
- .where(Rbacan.permission_table => { name: names })
93
- .select("#{Rbacan.permission_table}.name")
102
+ .where(Rbacan.permission_table => { Rbacan.permission_key => names })
103
+ .select("#{Rbacan.permission_table}.#{Rbacan.permission_key}")
94
104
  .distinct
95
105
  .count == names.uniq.size
96
106
  end
97
107
 
98
108
  # Returns roles assigned to this user for a specific tenant.
99
109
  # Includes global role assignments (tenant_id NULL).
100
- def roles_for_tenant(tenant_id)
101
- roles
110
+ def roles_for_tenant(tenant_id)
111
+ Rbacan.ensure_legacy_mode!(:roles_for_tenant)
112
+ roles
102
113
  .where(Rbacan.user_role_table => { tenant_id: [tenant_id, nil] })
103
114
  end
104
115
  end
@@ -3,25 +3,33 @@ module Rbacan
3
3
 
4
4
  # Creates roles by name. Idempotent — skips existing ones.
5
5
  def self.create_roles(roles)
6
+ Rbacan.ensure_legacy_mode!(:create_roles)
7
+
6
8
  roles.each do |role|
7
- Rbacan.role_class.constantize.find_or_create_by(name: role.to_s)
9
+ Rbacan.role_class.constantize.find_or_create_by(Rbacan.role_key => role.to_s)
8
10
  end
9
11
  end
10
12
 
11
13
  # Creates permissions by name. Idempotent — skips existing ones.
12
14
  def self.create_permissions(permissions)
15
+ Rbacan.ensure_legacy_mode!(:create_permissions)
16
+
13
17
  permissions.each do |permission|
14
- Rbacan.permission_class.constantize.find_or_create_by(name: permission.to_s)
18
+ Rbacan.permission_class.constantize.find_or_create_by(Rbacan.permission_key => permission.to_s)
15
19
  end
16
20
  end
17
21
 
18
22
  # Assigns a list of permissions to a role. Idempotent — skips duplicates.
19
23
  def self.assign_permissions_to_role(role_name, permissions)
20
- chosen_role = Rbacan.role_class.constantize.find_by_name(role_name.to_s)
24
+ Rbacan.ensure_legacy_mode!(:assign_permissions_to_role)
25
+
26
+ chosen_role = Rbacan.role_class.constantize.find_by(Rbacan.role_key => role_name.to_s)
21
27
  return unless chosen_role
22
28
 
23
29
  permissions.each do |permission|
24
- given_permission = Rbacan.permission_class.constantize.find_by_name(permission.to_s)
30
+ given_permission = Rbacan.permission_class.constantize.find_by(
31
+ Rbacan.permission_key => permission.to_s
32
+ )
25
33
  next unless given_permission
26
34
 
27
35
  Rbacan.role_permission_class.constantize.find_or_create_by(
@@ -1,30 +1,22 @@
1
1
  module Rbacan
2
2
  class RouteConstraint
3
- # Constrains routes to users that have a given role or permission.
4
- #
5
- # Usage in config/routes.rb:
6
- # constraints Rbacan::RouteConstraint.new(role: :admin) do
7
- # namespace :admin do
8
- # resources :users
9
- # end
10
- # end
11
- #
12
- # constraints Rbacan::RouteConstraint.new(permission: :access_admin_panel) do
13
- # ...
14
- # end
15
- def initialize(role: nil, permission: nil)
3
+ def initialize(role: nil, permission: nil, user_resolver: nil, context_resolver: nil)
16
4
  if role.nil? && permission.nil?
17
- raise ArgumentError, "Provide exactly one of role: or permission:"
5
+ raise ArgumentError, 'Provide exactly one role: or permission:'
18
6
  end
19
7
  if role && permission
20
- raise ArgumentError, "Provide exactly one of role: or permission:, not both"
8
+ raise ArgumentError, 'Provide exactly one role: or permission:, not both'
21
9
  end
22
10
 
23
- @role = role&.to_s
11
+ @role = role&.to_s
24
12
  @permission = permission&.to_s
13
+ @user_resolver = user_resolver
14
+ @context_resolver = context_resolver
25
15
  end
26
16
 
27
17
  def matches?(request)
18
+ return hardened_match?(request) unless Rbacan.legacy_mode
19
+
28
20
  user = current_user_from(request)
29
21
  return false unless user
30
22
 
@@ -33,12 +25,23 @@ module Rbacan
33
25
 
34
26
  private
35
27
 
28
+ def hardened_match?(request)
29
+ return false unless @user_resolver&.respond_to?(:call) && @context_resolver&.respond_to?(:call)
30
+
31
+ Rbacan.authorized?(
32
+ user: @user_resolver.call(request),
33
+ permission: @permission,
34
+ role: @role,
35
+ context: @context_resolver.call(request)
36
+ )
37
+ rescue Rbacan::ConfigurationError, ArgumentError
38
+ false
39
+ end
40
+
36
41
  def current_user_from(request)
37
- # Warden (Devise) path — most common, no extra query needed.
38
42
  warden = request.env['warden']
39
43
  return warden.user if warden&.authenticated?
40
44
 
41
- # Fallback: manual session lookup for apps not using Warden.
42
45
  user_id = request.session[:user_id] || request.session['user_id']
43
46
  return nil unless user_id
44
47
 
@@ -1,3 +1,3 @@
1
1
  module Rbacan
2
- VERSION = '0.4.0'
2
+ VERSION = '0.5.0'
3
3
  end
@@ -1,13 +1,18 @@
1
1
  module Rbacan
2
2
  module ViewHelpers
3
- # Renders the block only if the current user has the given permission.
4
- #
5
- # Usage in views:
6
- # <% authorized?(:publish_post) do %>
7
- # <%= link_to "Publish", publish_post_path(@post) %>
8
- # <% end %>
9
- def authorized?(permission, &block)
10
- return unless current_user&.can?(permission.to_s)
3
+ # Renders the block only when the host authorization adapter permits it.
4
+ def authorized?(permission, context: nil, &block)
5
+ allowed = if Rbacan.legacy_mode
6
+ current_user&.can?(permission.to_s)
7
+ else
8
+ Rbacan.authorized?(
9
+ user: current_user,
10
+ permission: permission,
11
+ context: context || rbacan_authorization_context
12
+ )
13
+ end
14
+ return unless allowed
15
+
11
16
  capture(&block) if block_given?
12
17
  end
13
18
  end
data/lib/rbacan.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  require 'rbacan/version'
2
2
  require 'rbacan/not_authorized'
3
+ require 'rbacan/host_authorization'
4
+ require 'rbacan/authorizable'
3
5
  require 'rbacan/permittable'
4
6
  require 'rbacan/engine'
5
7
  require 'rbacan/roles_and_permissions'
@@ -8,13 +10,25 @@ require 'rbacan/view_helpers'
8
10
  require 'rbacan/route_constraint'
9
11
 
10
12
  module Rbacan
13
+ mattr_accessor :authorization_adapter
14
+ @@authorization_adapter = nil
15
+
16
+ mattr_accessor :assignment_adapter
17
+ @@assignment_adapter = nil
18
+
19
+ # Legacy Active Record mutation/query APIs are opt-in in the hardened API.
20
+ mattr_accessor :legacy_mode
21
+ @@legacy_mode = false
22
+
11
23
  mattr_accessor :permittable_class
12
24
  @@permittable_class = 'User'
13
25
 
14
26
  mattr_accessor :role_class
15
27
  mattr_accessor :role_table
28
+ mattr_accessor :role_key
16
29
  @@role_class = 'Rbacan::Role'
17
30
  @@role_table = 'roles'
31
+ @@role_key = :name
18
32
 
19
33
  mattr_accessor :user_role_class
20
34
  mattr_accessor :user_role_table
@@ -23,8 +37,10 @@ module Rbacan
23
37
 
24
38
  mattr_accessor :permission_class
25
39
  mattr_accessor :permission_table
40
+ mattr_accessor :permission_key
26
41
  @@permission_class = 'Rbacan::Permission'
27
42
  @@permission_table = 'permissions'
43
+ @@permission_key = :name
28
44
 
29
45
  mattr_accessor :role_permission_class
30
46
  mattr_accessor :role_permission_table
@@ -67,16 +83,22 @@ module Rbacan
67
83
  end
68
84
 
69
85
  def self.create_role(role_name)
70
- @@role_class.constantize.create(name: role_name)
86
+ ensure_legacy_mode!(:create_role)
87
+
88
+ @@role_class.constantize.create(@@role_key => role_name.to_s)
71
89
  end
72
90
 
73
91
  def self.create_permission(permission_name)
74
- @@permission_class.constantize.create(name: permission_name)
92
+ ensure_legacy_mode!(:create_permission)
93
+
94
+ @@permission_class.constantize.create(@@permission_key => permission_name.to_s)
75
95
  end
76
96
 
77
97
  def self.assign_permission_to_role(role_name, permission_name)
78
- chosen_role = @@role_class.constantize.find_by_name(role_name)
79
- given_permission = @@permission_class.constantize.find_by_name(permission_name)
98
+ ensure_legacy_mode!(:assign_permission_to_role)
99
+
100
+ chosen_role = @@role_class.constantize.find_by(@@role_key => role_name.to_s)
101
+ given_permission = @@permission_class.constantize.find_by(@@permission_key => permission_name.to_s)
80
102
  @@role_permission_class.constantize.create(
81
103
  role_id: chosen_role.id,
82
104
  permission_id: given_permission.id
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rbacan
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - hamdi
@@ -147,24 +147,15 @@ executables: []
147
147
  extensions: []
148
148
  extra_rdoc_files: []
149
149
  files:
150
- - ".gitignore"
151
- - ".rspec"
152
- - ".travis.yml"
153
150
  - CHANGELOG.md
154
- - CLAUDE.md
155
151
  - CODE_OF_CONDUCT.md
156
- - Gemfile
157
- - Gemfile.lock
158
152
  - LICENSE.txt
159
153
  - README.md
160
- - Rakefile
161
154
  - app/models/rbacan/application_record.rb
162
155
  - app/models/rbacan/permission.rb
163
156
  - app/models/rbacan/role.rb
164
157
  - app/models/rbacan/role_permission.rb
165
158
  - app/models/rbacan/user_role.rb
166
- - bin/console
167
- - bin/setup
168
159
  - lib/generators/install/templates/copy_to_seeds.rb
169
160
  - lib/generators/install/templates/create_permissions.rb
170
161
  - lib/generators/install/templates/create_role_permissions.rb
@@ -173,15 +164,16 @@ files:
173
164
  - lib/generators/install/templates/rbacan.rb
174
165
  - lib/generators/rbacan/install_generator.rb
175
166
  - lib/rbacan.rb
167
+ - lib/rbacan/authorizable.rb
176
168
  - lib/rbacan/authorization.rb
177
169
  - lib/rbacan/engine.rb
170
+ - lib/rbacan/host_authorization.rb
178
171
  - lib/rbacan/not_authorized.rb
179
172
  - lib/rbacan/permittable.rb
180
173
  - lib/rbacan/roles_and_permissions.rb
181
174
  - lib/rbacan/route_constraint.rb
182
175
  - lib/rbacan/version.rb
183
176
  - lib/rbacan/view_helpers.rb
184
- - rbacan.gemspec
185
177
  homepage: https://github.com/hamdi777/RBACan
186
178
  licenses:
187
179
  - MIT
@@ -203,7 +195,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
203
195
  - !ruby/object:Gem::Version
204
196
  version: '0'
205
197
  requirements: []
206
- rubygems_version: 3.6.9
198
+ rubygems_version: 4.0.16
207
199
  specification_version: 4
208
200
  summary: A gem to give permission access to users based on their roles
209
201
  test_files: []
data/.gitignore DELETED
@@ -1,14 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /_yardoc/
4
- /coverage/
5
- /doc/
6
- /pkg/
7
- /spec/reports/
8
- /tmp/
9
-
10
- # rspec failure tracking
11
- .rspec_status
12
-
13
- # Built gem files
14
- *.gem
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper
data/.travis.yml DELETED
@@ -1,7 +0,0 @@
1
- ---
2
- sudo: false
3
- language: ruby
4
- cache: bundler
5
- rvm:
6
- - 2.4.2
7
- before_install: gem install bundler -v 2.0.1
data/CLAUDE.md DELETED
@@ -1,51 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Commands
6
-
7
- ```bash
8
- bin/setup # install dependencies
9
- rake spec # run all tests
10
- bundle exec rspec spec/rbacan_spec.rb # run a single spec file
11
- bundle exec rake install # install gem locally
12
- bundle exec rake release # tag, push commits/tags, push .gem to RubyGems
13
- gem build rbacan.gemspec # build .gem file manually
14
- gem push rbacan-<version>.gem # push to RubyGems manually
15
- ```
16
-
17
- ## Architecture
18
-
19
- This is a Rails engine gem that provides Role-Based Access Control (RBAC).
20
-
21
- **Data model** — four tables managed by the gem's migrations:
22
- - `roles` / `permissions` — named entities
23
- - `role_permissions` — join table linking roles to permissions
24
- - `user_roles` — join table linking the host app's user model to roles
25
-
26
- **Entry points:**
27
- - `lib/rbacan.rb` — main module; holds `mattr_accessor` config accessors and a `configure` block. All class names and table names are configurable here (defaults to `User`, `Rbacan::Role`, etc.).
28
- - `lib/rbacan/permittable.rb` — `ActiveSupport::Concern` included in the host app's user model via `include Rbacan::Permittable`. Adds `assign_role`, `remove_role`, and `can?` instance methods.
29
- - `lib/rbacan/roles_and_permissions.rb` — utility module (`RolesAndPermissions`) used in seeds to bulk-create roles/permissions and assign permissions to roles.
30
- - `lib/rbacan/engine.rb` — Rails engine that auto-loads `app/models`.
31
-
32
- **Generator** (`rails generate rbacan:install`) copies four migration files, `db/copy_to_seeds.rb`, and `config/initializers/rbacan.rb` into the host app.
33
-
34
- **Configuration** (host app's `config/initializers/rbacan.rb`):
35
- ```ruby
36
- Rbacan.configure do |config|
37
- config.permittable_class = "User" # change if your user model has a different name
38
- end
39
- ```
40
-
41
- ## Releasing a new version
42
-
43
- 1. Bump `lib/rbacan/version.rb`
44
- 2. `gem build rbacan.gemspec`
45
- 3. `gem push rbacan-<version>.gem`
46
-
47
- RubyGems push host is `https://rubygems.org`. GitHub repo: `https://github.com/hamdi777/RBACan`.
48
-
49
- ## Known state
50
-
51
- The spec suite has a placeholder test (`expect(false).to eq(true)`) in `spec/rbacan_spec.rb` that intentionally fails — it was never replaced with real tests.
data/Gemfile DELETED
@@ -1,4 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- # Specify your gem's dependencies in rbacan.gemspec
4
- gemspec