├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── .rspec ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── access-granted.gemspec ├── benchmarks ├── README.md ├── config.rb └── permissions.rb ├── lib ├── access-granted.rb ├── access-granted │ ├── exceptions.rb │ ├── permission.rb │ ├── policy.rb │ ├── rails │ │ └── controller_methods.rb │ ├── railtie.rb │ └── role.rb └── generators │ ├── access_granted │ └── policy_generator.rb │ └── templates │ └── access_policy.rb └── spec ├── controller_methods_spec.rb ├── permission_spec.rb ├── policy_spec.rb ├── role_spec.rb └── spec_helper.rb /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake 6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby 7 | 8 | name: Ruby 9 | 10 | on: 11 | push: 12 | branches: [ master ] 13 | pull_request: 14 | branches: [ master ] 15 | 16 | jobs: 17 | test: 18 | 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | ruby-version: 23 | - 2.6 24 | - 2.7 25 | - 3.0 26 | - 3.1 27 | - 3.2 28 | - 3.3 29 | steps: 30 | - uses: actions/checkout@v4 31 | - name: Set up Ruby 32 | uses: ruby/setup-ruby@v1 33 | with: 34 | ruby-version: ${{ matrix.ruby-version }} 35 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 36 | - name: Run tests 37 | run: bundle exec rspec 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 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 18 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.3.3 2 | 3 | - Fix compatibility with Rails 6.0 and Zeitwerk ([PR #53](https://github.com/chaps-io/access-granted/pull/53)), thanks [dmorehouse](https://github.com/dmorehouse)! 4 | 5 | # 1.3.2 6 | 7 | - Expose `applicable_roles` method on the policy instance. This allows insight into what roles actually apply to a given user. 8 | 9 | # 1.3.1 10 | 11 | - Add information about action and subject when raising AccessDenied exception ([PR #46](https://github.com/chaps-io/access-granted/pull/46)), thanks [jraqula](https://github.com/jraqula)! 12 | 13 | # 1.3.0 14 | 15 | - Drop support for Ruby 1.9.3, it might still work but we are no longer testing against it. 16 | - Start testing against Rubies 2.3-2.5 in CI 17 | - Move Rails integration into Railties, this fixes some load order issues ([PR #45](https://github.com/chaps-io/access-granted/pull/45)), thanks [jraqula](https://github.com/jraqula)! 18 | 19 | # 1.2.0 20 | 21 | - Cache whole blocks of identical permissions when one of them is checked. 22 | For example, assuming we have a given permissions set: 23 | 24 | ```ruby 25 | can [:update, :destroy, :archive], Post do |post, user| 26 | post.user_id == user.id 27 | end 28 | ``` 29 | 30 | When resolving one of them like this: 31 | 32 | ```ruby 33 | can? :update, @post 34 | ``` 35 | 36 | Access Granted will cache the result for each of the remaining actions, too. 37 | So next time when checking permissions `:destroy` or `:archive`, AG will serve the result from cache instead of running the block again. 38 | 39 | 40 | # 1.1.2 41 | 42 | - Expose internal `block` instance variable in Permission class 43 | 44 | # 1.1.1 45 | 46 | - Return detailed information about which permission is duplicate when raising DuplicatePermission exception 47 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in access-granted.gemspec 4 | gemspec 5 | 6 | group :test, :development do 7 | gem 'rb-readline' 8 | gem 'rake' 9 | gem 'pry' 10 | gem 'cancan' 11 | gem 'benchmark-ips' 12 | end 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Chaps sp. z o.o. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AccessGranted [![Code Climate](https://codeclimate.com/github/pokonski/access-granted.png)](https://codeclimate.com/github/pokonski/access-granted) 2 | 3 | AccessGranted is a multi-role and whitelist based authorization gem for Rails. And it's lightweight (~300 lines of code)! 4 | 5 | 6 | ## Installation 7 | 8 | Add the gem to your gemfile: 9 | 10 | ```ruby 11 | gem 'access-granted', '~> 1.3' 12 | ``` 13 | Run the bundle command to install it. Then run the generator: 14 | 15 | rails generate access_granted:policy 16 | 17 | ### Supported Ruby versions 18 | 19 | Because it has **zero** runtime dependencies it is guaranteed to work on all supported MRI Ruby versions, see CI to check the up to date list. 20 | It might and probably is working on Rubinius and JRuby but we are no longer testing against those. 21 | 22 | ## Summary 23 | 24 | AccessGranted is meant as a replacement for CanCan to solve major problems: 25 | 26 | 1. Performance 27 | 28 | On average AccessGranted is **20 times faster** in resolving identical permissions and takes less memory. 29 | See [benchmarks](https://github.com/chaps-io/access-granted/blob/master/benchmarks). 30 | 31 | 2. Roles 32 | 33 | Adds support for roles, so no more `if`s and `else`s in your Policy file. This makes it extremely easy to maintain and read the code. 34 | 35 | 3. Whitelists 36 | 37 | This means that you define what the user can do, which results in clean, readable policies regardless of application complexity. 38 | You don't have to worry about juggling `can`s and `cannot`s in a very convoluted way! 39 | 40 | _Note_: `cannot` is still available, but has a very specifc use. See [Usage](#usage) below. 41 | 42 | 4. Framework agnostic 43 | 44 | Permissions can work on basically any object and AccessGranted is framework-agnostic, 45 | but it has Rails support out of the box. :) 46 | It does not depend on any libraries, pure and clean Ruby code. Guaranteed to always work, 47 | even when software around changes. 48 | 49 | ## Usage 50 | 51 | Roles are defined using blocks (or by passing custom classes to keep things tidy). 52 | 53 | **Order of the roles is VERY important**, because they are being traversed in top-to-bottom order. 54 | At the top you must have an admin or some other important role giving the user top permissions, and as you go down you define less-privileged roles. 55 | 56 | **I recommend starting your adventure by reading the [wiki page on how to start with Access Granted](https://github.com/chaps-io/access-granted/wiki/Role-based-authorization-in-Rails), where I demonstrate its abilities on a real life example.** 57 | 58 | ### Defining an access policy 59 | 60 | Let's start with a complete example of what can be achieved: 61 | 62 | ```ruby 63 | # app/policies/access_policy.rb 64 | 65 | class AccessPolicy 66 | include AccessGranted::Policy 67 | 68 | def configure 69 | # The most important admin role, gets checked first 70 | role :admin, { is_admin: true } do 71 | can :manage, Post 72 | can :manage, Comment 73 | end 74 | 75 | # Less privileged moderator role 76 | role :moderator, proc {|u| u.moderator? } do 77 | can [:update, :destroy], Post 78 | can :update, User 79 | end 80 | 81 | # The basic role. Applies to every user. 82 | role :member do 83 | can :create, Post 84 | 85 | can [:update, :destroy], Post do |post, user| 86 | post.author == user && post.comments.empty? 87 | end 88 | end 89 | end 90 | end 91 | ``` 92 | 93 | #### Defining roles 94 | 95 | Each `role` method accepts the name of the role you're creating and an optional matcher. 96 | Matchers are used to check if the user belongs to that role and if the permissions inside should be executed against it. 97 | 98 | The simplest role can be defined as follows: 99 | 100 | ```ruby 101 | role :member do 102 | can :read, Post 103 | can :create, Post 104 | end 105 | ``` 106 | 107 | This role will allow everyone (since we didn't supply a matcher) to read and create posts. 108 | 109 | But now we want to let admins delete those posts. 110 | In this case we can create a new role above the `:member` to add more permissions for the admin: 111 | 112 | ```ruby 113 | role :admin, { is_admin: true } do 114 | can :destroy, Post 115 | end 116 | 117 | role :member do 118 | can :read, Post 119 | can :create, Post 120 | end 121 | ``` 122 | 123 | The `{ is_admin: true }` hash is compared with the user's attributes to see if the role should be applied to it. 124 | So, if the user has an attribute `is_admin` set to `true`, then the role will be applied to it. 125 | 126 | **Note:** you can use more keys in the hash to check many attributes at once. 127 | 128 | #### Hash conditions 129 | 130 | Hashes can be used as matchers to check if an action is permitted. 131 | For example, we may allow users to only see published posts, like this: 132 | 133 | ```ruby 134 | role :member do 135 | can :read, Post, { published: true } 136 | end 137 | ``` 138 | 139 | #### Block conditions 140 | 141 | Sometimes you may need to dynamically check for ownership or other conditions, 142 | this can be done using a block condition in `can` method, like so: 143 | 144 | ```ruby 145 | role :member do 146 | can :update, Post do |post, user| 147 | post.author_id == user.id 148 | end 149 | end 150 | ``` 151 | 152 | When the given block evaluates to `true`, then `user` is allowed to update the post. 153 | 154 | #### Roles in order of importance 155 | 156 | Additionally, we can allow admins to update **all** posts despite them not being authors like so: 157 | 158 | 159 | ```ruby 160 | role :admin, { is_admin: true } do 161 | can :update, Post 162 | end 163 | 164 | role :member do 165 | can :update, Post do |post, user| 166 | post.author_id == user.id 167 | end 168 | end 169 | ``` 170 | 171 | As stated before: **`:admin` role takes precedence over `:member`** role, so when AccessGranted sees that admin can update all posts, it stops looking at the less important roles. 172 | 173 | That way you can keep a tidy and readable policy file which is basically human readable. 174 | 175 | ### Usage with Rails 176 | 177 | AccessGranted comes with a set of helpers available in Ruby on Rails apps: 178 | 179 | #### Authorizing controller actions 180 | 181 | ```ruby 182 | class PostsController 183 | def show 184 | @post = Post.find(params[:id]) 185 | authorize! :read, @post 186 | end 187 | 188 | def create 189 | authorize! :create, Post 190 | # (...) 191 | end 192 | end 193 | ``` 194 | 195 | `authorize!` throws an exception when `current_user` doesn't have a given permission. 196 | You can rescue from it using `rescue_from`: 197 | 198 | ```ruby 199 | class ApplicationController < ActionController::Base 200 | rescue_from "AccessGranted::AccessDenied" do |exception| 201 | redirect_to root_path, alert: "You don't have permission to access this page." 202 | end 203 | end 204 | ``` 205 | 206 | You can also extract the action and subject which raised the error, 207 | if you want to handle authorization errors differently for some cases: 208 | ```ruby 209 | rescue_from "AccessGranted::AccessDenied" do |exception| 210 | status = case exception.action 211 | when :read # invocation like `authorize! :read, @something` 212 | 403 213 | else 214 | 404 215 | end 216 | 217 | body = case exception.subject 218 | when Post # invocation like `authorize! @some_action, Post` 219 | "failed to access a post" 220 | else 221 | "failed to access something else" 222 | end 223 | end 224 | ``` 225 | 226 | You can also have a custom exception message while authorizing a request. 227 | This message will be associated with the exception object thrown. 228 | 229 | ```ruby 230 | class PostsController 231 | def show 232 | @post = Post.find(params[:id]) 233 | authorize! :read, @post, 'You do not have access to this post' 234 | render json: { post: @post } 235 | rescue AccessGranted::AccessDenied => e 236 | render json: { error: e.message }, status: :forbidden 237 | end 238 | end 239 | ``` 240 | 241 | #### Checking permissions in controllers 242 | 243 | To check if the user has a permission to perform an action, use the `can?` and `cannot?` methods. 244 | 245 | **Example:** 246 | 247 | ```ruby 248 | class UsersController 249 | def update 250 | # (...) 251 | 252 | # only admins can elevate users to moderator status 253 | 254 | if can? :make_moderator, @user 255 | @user.moderator = params[:user][:moderator] 256 | end 257 | 258 | # (...) 259 | end 260 | end 261 | ``` 262 | 263 | #### Checking permissions in views 264 | 265 | Usually you don't want to show "Create" buttons for people who can't create something. 266 | You can hide any part of the page from users without permissions like this: 267 | 268 | ```erb 269 | # app/views/categories/index.html.erb 270 | 271 | <% if can? :create, Category %> 272 | <%= link_to "Create new category", new_category_path %> 273 | <% end %> 274 | ``` 275 | 276 | #### Customizing policy 277 | 278 | By default, AccessGranted adds this method to your controllers: 279 | 280 | ```ruby 281 | def current_policy 282 | @current_policy ||= ::AccessPolicy.new(current_user) 283 | end 284 | ``` 285 | 286 | If you have a different policy class or if your user is not stored in the `current_user` variable, then you can override it in any controller and modify the logic as you please. 287 | 288 | You can even have different policies for different controllers! 289 | 290 | ### Usage with pure Ruby 291 | 292 | Initialize the Policy class: 293 | 294 | ```ruby 295 | policy = AccessPolicy.new(current_user) 296 | ``` 297 | 298 | Check the ability to do something: 299 | 300 | with `can?`: 301 | 302 | ```ruby 303 | policy.can?(:create, Post) #=> true 304 | policy.can?(:update, @post) #=> false 305 | ``` 306 | 307 | or with `cannot?`: 308 | 309 | ```ruby 310 | policy.cannot?(:create, Post) #=> false 311 | policy.cannot?(:update, @post) #=> true 312 | ``` 313 | 314 | ## Common examples 315 | 316 | ### Extracting roles to separate files 317 | 318 | Let's say your app is getting bigger and more complex. This means your policy file is also getting longer. 319 | 320 | Below you can see an extracted `:member` role: 321 | 322 | ```ruby 323 | class AccessPolicy 324 | include AccessGranted::Policy 325 | 326 | def configure 327 | role :administrator, is_admin: true do 328 | can :manage, User 329 | end 330 | 331 | role :member, MemberRole, -> { |user| !u.guest? } 332 | end 333 | end 334 | 335 | ``` 336 | 337 | And roles should look like this: 338 | 339 | ```ruby 340 | # app/roles/member_role.rb 341 | 342 | class MemberRole < AccessGranted::Role 343 | def configure 344 | can :create, Post 345 | can :destroy, Post do |post, user| 346 | post.author == user 347 | end 348 | end 349 | end 350 | ``` 351 | 352 | ## Compatibility with CanCan 353 | 354 | This gem has been created as a replacement for CanCan and therefore it requires minimum work to switch. 355 | 356 | ### Main differences 357 | 358 | 1. AccessGranted does not extend ActiveRecord in any way, so it does not have the `accessible_by?` 359 | method which could be used for querying objects available to current user. 360 | This was very complex and only worked with permissions defined using hash conditions, so 361 | I decided to not implement this functionality as it was mostly ignored by CanCan users. 362 | 363 | 2. Both `can?`/`cannot?` and `authorize!` methods work in Rails controllers and views, just like in CanCan. 364 | The only change you have to make is to replace all `can? :manage, Class` with the exact action to check against. 365 | `can :manage` is still available for **defining** permissions and serves as a shortcut for defining `:create`, `:read`, `:update`, `:destroy` all in one line. 366 | 367 | 3. Syntax for defining permissions in the AccessPolicy file (Ability in CanCan) is exactly the same, 368 | with roles added on top. See [Usage](#usage) above. 369 | 370 | 371 | ## Contributing 372 | 373 | 1. Fork it 374 | 2. Create your feature branch (`git checkout -b my-new-feature`) 375 | 3. Commit your changes (`git commit -am 'Add some feature'`) 376 | 4. Push to the branch (`git push origin my-new-feature`) 377 | 5. Create new pull request 378 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | task :default => [:spec] 4 | desc 'run Rspec specs' 5 | task :spec do 6 | sh 'rspec spec' 7 | end 8 | -------------------------------------------------------------------------------- /access-granted.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "access-granted" 7 | spec.version = "1.3.3" 8 | spec.authors = ["Piotrek Okoński"] 9 | spec.email = ["piotrek@okonski.org"] 10 | spec.description = %q{Role based authorization gem} 11 | spec.summary = %q{Elegant whitelist and role based authorization with ability to prioritize roles.} 12 | spec.homepage = "https://github.com/chaps-io/access-granted" 13 | spec.license = "MIT" 14 | 15 | spec.files = `git ls-files`.split($/) 16 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 17 | spec.test_files = spec.files.grep(%r{^spec/}) 18 | spec.require_paths = ["lib"] 19 | 20 | spec.add_development_dependency "bundler", "~> 2.3" 21 | spec.add_development_dependency "rspec", "~> 3.0" 22 | end 23 | -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # Benchmark results 2 | 3 | Benchmarks ran on MacBook Pro M1 Pro 2021, 32 GB RAM on Ruby 3.2. 4 | 5 | ## permissions.rb 6 | 7 | This benchmark runs `can?` method for the 3 user roles for 20 seconds each, for both CanCan and AccessGranted. 8 | 9 | ``` 10 | Warming up -------------------------------------- 11 | ag-admin 358.693k i/100ms 12 | ag-moderator 359.044k i/100ms 13 | ag-user 360.627k i/100ms 14 | cancan-admin 30.797k i/100ms 15 | cancan-moderator 26.825k i/100ms 16 | cancan-user 37.946k i/100ms 17 | Calculating ------------------------------------- 18 | ag-admin 3.640M (± 0.3%) i/s - 18.293M in 5.025691s 19 | ag-moderator 3.642M (± 0.4%) i/s - 18.311M in 5.027575s 20 | ag-user 3.643M (± 0.3%) i/s - 18.392M in 5.049271s 21 | cancan-admin 308.383k (± 0.7%) i/s - 1.571M in 5.093398s 22 | cancan-moderator 270.716k (± 0.8%) i/s - 1.368M in 5.053863s 23 | cancan-user 383.198k (± 0.7%) i/s - 1.935M in 5.050472s 24 | ``` 25 | -------------------------------------------------------------------------------- /benchmarks/config.rb: -------------------------------------------------------------------------------- 1 | class Ability 2 | include CanCan::Ability 3 | 4 | def initialize(user) 5 | if user.is_admin 6 | can :destroy, String 7 | can :foo, Integer 8 | end 9 | 10 | if user.is_moderator 11 | can :update, String 12 | can :bar, String 13 | end 14 | 15 | can :read, String 16 | can :zoom, Integer 17 | can :boom, Hash 18 | can :rub, File 19 | end 20 | end 21 | 22 | class AccessPolicy 23 | include AccessGranted::Policy 24 | 25 | def configure 26 | role :administrator, { is_admin: true } do 27 | can :destroy, String 28 | can :foo, Integer 29 | end 30 | 31 | role :moderator, { is_moderator: true } do 32 | can :update, String 33 | can :bar, String 34 | end 35 | 36 | role :member do 37 | can :read, String 38 | can :zoom, Integer 39 | can :boom, Hash 40 | can :rub, File 41 | end 42 | end 43 | end 44 | 45 | class User < Struct.new(:id, :is_admin, :is_moderator) 46 | end 47 | -------------------------------------------------------------------------------- /benchmarks/permissions.rb: -------------------------------------------------------------------------------- 1 | require 'benchmark/ips' 2 | require 'access-granted' 3 | require 'cancan' 4 | require_relative './config' 5 | 6 | admin = User.new(1, true, false) 7 | mod = User.new(2, false, true) 8 | user = User.new(3, false, false) 9 | 10 | user_policy = AccessPolicy.new(user) 11 | admin_policy = AccessPolicy.new(admin) 12 | mod_policy = AccessPolicy.new(mod) 13 | 14 | user_ability = Ability.new(user) 15 | admin_ability = Ability.new(admin) 16 | mod_ability = Ability.new(mod) 17 | 18 | Benchmark.ips do |x| 19 | x.config(time: 5, warmup: 1) 20 | 21 | x.report("ag-admin") do 22 | admin_policy.can?(:read, String) 23 | end 24 | 25 | x.report("ag-moderator") do 26 | mod_policy.can?(:bar, String) 27 | end 28 | 29 | x.report("ag-user") do 30 | user_policy.can?(:zoom, Integer) 31 | end 32 | 33 | x.report("cancan-admin") do 34 | admin_ability.can?(:read, String) 35 | end 36 | 37 | x.report("cancan-moderator") do 38 | mod_ability.can?(:bar, String) 39 | end 40 | 41 | x.report("cancan-user") do 42 | user_ability.can?(:zoom, Integer) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/access-granted.rb: -------------------------------------------------------------------------------- 1 | require "access-granted/exceptions" 2 | require "access-granted/policy" 3 | require "access-granted/permission" 4 | require "access-granted/role" 5 | require "access-granted/rails/controller_methods" 6 | require "access-granted/railtie" if defined?(Rails) 7 | 8 | module AccessGranted 9 | 10 | end 11 | 12 | -------------------------------------------------------------------------------- /lib/access-granted/exceptions.rb: -------------------------------------------------------------------------------- 1 | module AccessGranted 2 | class Error < StandardError; end 3 | 4 | class DuplicatePermission < Error; end; 5 | class DuplicateRole < Error; end; 6 | class AccessDenied < Error 7 | attr_reader :action, :subject, :message 8 | def initialize(action = nil, subject = nil, message = nil) 9 | @action = action 10 | @subject = subject 11 | @message = message 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/access-granted/permission.rb: -------------------------------------------------------------------------------- 1 | module AccessGranted 2 | class Permission 3 | attr_reader :action, :subject, :granted, :conditions, :actions, :block 4 | 5 | def initialize(granted, action, subject, user = nil, conditions = {}, actions = [], block = nil) 6 | @action = action 7 | @user = user 8 | @granted = granted 9 | @subject = subject 10 | @conditions = conditions 11 | @actions = actions 12 | @block = block 13 | end 14 | 15 | def matches_action?(action) 16 | @action == action 17 | end 18 | 19 | def matches_subject?(subject) 20 | subject == @subject || subject.class <= @subject 21 | end 22 | 23 | def matches_conditions?(subject) 24 | if @block 25 | @block.call(subject, @user) 26 | elsif !@conditions.empty? 27 | matches_hash_conditions?(subject) 28 | else 29 | true 30 | end 31 | end 32 | 33 | def matches_hash_conditions?(subject) 34 | @conditions.each_pair do |name, value| 35 | return false if subject.send(name) != value 36 | end 37 | true 38 | end 39 | 40 | def eql?(other) 41 | other.class == self.class && 42 | @action == other.action && 43 | @subject == other.subject && 44 | @granted == other.granted 45 | end 46 | 47 | def ==(other) 48 | eql?(other) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/access-granted/policy.rb: -------------------------------------------------------------------------------- 1 | module AccessGranted 2 | module Policy 3 | attr_accessor :roles, :cache 4 | attr_reader :user 5 | 6 | def initialize(user, cache_enabled = true) 7 | @user = user 8 | @roles = [] 9 | @cache = {} 10 | configure 11 | end 12 | 13 | def configure 14 | end 15 | 16 | def role(name, conditions_or_klass = nil, conditions = nil, &block) 17 | name = name.to_sym 18 | if roles.any? {|r| r.name == name } 19 | raise DuplicateRole, "Role '#{name}' already defined" 20 | end 21 | r = if conditions_or_klass.is_a?(Class) && conditions_or_klass <= AccessGranted::Role 22 | conditions_or_klass.new(name, conditions, user, block) 23 | else 24 | Role.new(name, conditions_or_klass, user, block) 25 | end 26 | roles << r 27 | r 28 | end 29 | 30 | def can?(action, subject = nil) 31 | cache[action] ||= {} 32 | 33 | if cache[action][subject] 34 | cache[action][subject] 35 | else 36 | granted, actions = check_permission(action, subject) 37 | actions.each do |a| 38 | cache[a] ||= {} 39 | cache[a][subject] ||= granted 40 | end 41 | 42 | granted 43 | end 44 | end 45 | 46 | def check_permission(action, subject) 47 | applicable_roles.each do |role| 48 | permission = role.find_permission(action, subject) 49 | return [permission.granted, permission.actions] if permission 50 | end 51 | 52 | [false, []] 53 | end 54 | 55 | def cannot?(*args) 56 | !can?(*args) 57 | end 58 | 59 | def authorize!(action, subject, message = 'Access Denied') 60 | if cannot?(action, subject) 61 | raise AccessDenied.new(action, subject, message) 62 | end 63 | subject 64 | end 65 | 66 | def applicable_roles 67 | @applicable_roles ||= roles.select do |role| 68 | role.applies_to?(user) 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/access-granted/rails/controller_methods.rb: -------------------------------------------------------------------------------- 1 | module AccessGranted 2 | module Rails 3 | module ControllerMethods 4 | def current_policy 5 | @current_policy ||= ::AccessPolicy.new(current_user) 6 | end 7 | 8 | def self.included(base) 9 | base.helper_method :can?, :cannot?, :current_policy if base.respond_to? :helper_method 10 | end 11 | 12 | def can?(*args) 13 | current_policy.can?(*args) 14 | end 15 | 16 | def cannot?(*args) 17 | current_policy.cannot?(*args) 18 | end 19 | 20 | def authorize!(*args) 21 | current_policy.authorize!(*args) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/access-granted/railtie.rb: -------------------------------------------------------------------------------- 1 | require 'rails/railtie' 2 | 3 | module AccessGranted 4 | class Railtie < ::Rails::Railtie 5 | initializer :access_granted do 6 | if ::Rails::VERSION::MAJOR >= 6 7 | ActiveSupport.on_load(:action_controller_base) do |base| 8 | base.include AccessGranted::Rails::ControllerMethods 9 | end 10 | 11 | ActiveSupport.on_load(:action_controller_api) do |base| 12 | base.include AccessGranted::Rails::ControllerMethods 13 | end 14 | else 15 | if defined? ActionController::Base 16 | ActionController::Base.class_eval do 17 | include AccessGranted::Rails::ControllerMethods 18 | end 19 | end 20 | 21 | if defined? ActionController::API 22 | ActionController::API.class_eval do 23 | include AccessGranted::Rails::ControllerMethods 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/access-granted/role.rb: -------------------------------------------------------------------------------- 1 | module AccessGranted 2 | class Role 3 | attr_reader :name, :user, :conditions, :permissions 4 | 5 | def initialize(name, conditions = nil, user = nil, block = nil) 6 | @user = user 7 | @name = name 8 | @conditions = conditions 9 | @block = block 10 | @permissions = [] 11 | 12 | if @block 13 | instance_eval(&@block) 14 | else 15 | configure 16 | end 17 | end 18 | 19 | def configure 20 | end 21 | 22 | def can(action, subject = nil, conditions = {}, &block) 23 | add_permission(true, action, subject, conditions, block) 24 | end 25 | 26 | def cannot(action, subject, conditions = {}, &block) 27 | add_permission(false, action, subject, conditions, block) 28 | end 29 | 30 | def find_permission(action, subject) 31 | permissions.detect do |permission| 32 | permission.action == action && 33 | permission.matches_subject?(subject) && 34 | permission.matches_conditions?(subject) 35 | end 36 | end 37 | 38 | def applies_to?(user) 39 | case @conditions 40 | when Hash 41 | matches_hash?(user, @conditions) 42 | when Proc 43 | @conditions.call(user) 44 | else 45 | true 46 | end 47 | end 48 | 49 | def matches_hash?(user, conditions = {}) 50 | conditions.all? do |name, value| 51 | user.send(name) == value 52 | end 53 | end 54 | 55 | def add_permission(granted, action, subject, conditions, block) 56 | prepared_actions = prepare_actions(action) 57 | prepared_actions.each do |a| 58 | raise DuplicatePermission, "Permission `#{a}` is already defined for #{subject} in role `#{name}`" if find_permission(a, subject) 59 | permissions << Permission.new(granted, a, subject, @user, conditions, prepared_actions, block) 60 | end 61 | end 62 | 63 | private 64 | 65 | def permission_exists?(action, subject) 66 | permissions.any? do |permission| 67 | permission.matches_subject?(subject) 68 | end 69 | end 70 | 71 | def prepare_actions(action) 72 | actions = Array(*[action]) 73 | actions.flat_map { |a| a == :manage ? [:create, :read, :update, :destroy ] : [a] } 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/generators/access_granted/policy_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/base' 2 | 3 | module Accessgranted 4 | module Generators 5 | class PolicyGenerator < Rails::Generators::Base 6 | source_root File.expand_path("../../templates", __FILE__) 7 | 8 | namespace "access_granted:policy" 9 | desc "Creates an Access Granted policy." 10 | 11 | def copy_policy 12 | template "access_policy.rb", "app/policies/access_policy.rb" 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/generators/templates/access_policy.rb: -------------------------------------------------------------------------------- 1 | class AccessPolicy 2 | include AccessGranted::Policy 3 | 4 | def configure 5 | # Example policy for AccessGranted. 6 | # For more details check the README at 7 | # 8 | # https://github.com/chaps-io/access-granted/blob/master/README.md 9 | # 10 | # Roles inherit from less important roles, so: 11 | # - :admin has permissions defined in :member, :guest and himself 12 | # - :member has permissions from :guest and himself 13 | # - :guest has only its own permissions since it's the first role. 14 | # 15 | # The most important role should be at the top. 16 | # In this case an administrator. 17 | # 18 | # role :admin, proc { |user| user.admin? } do 19 | # can :destroy, User 20 | # end 21 | 22 | # More privileged role, applies to registered users. 23 | # 24 | # role :member, proc { |user| user.registered? } do 25 | # can :create, Post 26 | # can :create, Comment 27 | # can [:update, :destroy], Post do |post, user| 28 | # post.author == user 29 | # end 30 | # end 31 | 32 | # The base role with no additional conditions. 33 | # Applies to every user. 34 | # 35 | # role :guest do 36 | # can :read, Post 37 | # can :read, Comment 38 | # end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/controller_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe AccessGranted::Rails::ControllerMethods do 4 | before(:each) do 5 | @current_user = double("User") 6 | @controller_class = Class.new 7 | @controller = @controller_class.new 8 | allow(@controller_class).to receive(:helper_method).with(:can?, :cannot?, :current_policy) 9 | @controller_class.send(:include, AccessGranted::Rails::ControllerMethods) 10 | allow(@controller).to receive(:current_user).and_return(@current_user) 11 | end 12 | 13 | it "should have current_policy method returning Policy instance" do 14 | expect(@controller.current_policy).to be_kind_of(AccessGranted::Policy) 15 | end 16 | 17 | it "provides can? and cannot? method delegated to current_policy" do 18 | expect(@controller.can?(:read, String)).to eq(false) 19 | expect(@controller.cannot?(:read, String)).to eq(true) 20 | end 21 | 22 | describe "#authorize!" do 23 | it "raises exception when authorization fails" do 24 | expect { @controller.authorize!(:read, String) }.to raise_error do |err| 25 | expect(err).to be_a(AccessGranted::AccessDenied) 26 | expect(err.action).to eq(:read) 27 | expect(err.subject).to eq(String) 28 | end 29 | end 30 | 31 | it "returns subject if authorization succeeds" do 32 | klass = Class.new do 33 | include AccessGranted::Policy 34 | 35 | def configure 36 | role :member, 1 do 37 | can :read, String 38 | end 39 | end 40 | end 41 | policy = klass.new(@current_user) 42 | allow(@controller).to receive(:current_policy).and_return(policy) 43 | expect(@controller.authorize!(:read, String)).to eq(String) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/permission_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe AccessGranted::Permission do 4 | subject { AccessGranted::Permission } 5 | 6 | describe "#matches_proc_conditions?" do 7 | 8 | it "matches proc conditions when true" do 9 | sub = double("Element", published?: true) 10 | perm = subject.new(true, :read, sub, nil, {}, [], proc {true}) 11 | expect(perm.matches_conditions?(sub)).to eq(true) 12 | end 13 | 14 | it "does not match proc conditions false" do 15 | sub = double("Element", published?: true) 16 | perm = subject.new(true, :read, sub, nil, {}, [], proc {false}) 17 | expect(perm.matches_conditions?(sub)).to eq(false) 18 | end 19 | 20 | end 21 | 22 | describe "#matches_hash_conditions?" do 23 | 24 | it "matches when conditions given" do 25 | sub = double("Element", published: true) 26 | perm = subject.new(true, :read, sub, nil, { published: true }) 27 | expect(perm.matches_hash_conditions?(sub)).to eq(true) 28 | end 29 | 30 | it "does not match if one of the conditions mismatches" do 31 | sub = double("Element", published: true, readable: false) 32 | perm = subject.new(true, :read, sub, nil, { published: true, readable: true }) 33 | expect(perm.matches_hash_conditions?(sub)).to eq(false) 34 | end 35 | 36 | end 37 | 38 | describe "#matches_action?" do 39 | it "matches if actions are identical" do 40 | perm = subject.new(true, :read, String) 41 | expect(perm.matches_action?(:read)).to_not be_nil 42 | end 43 | 44 | end 45 | 46 | describe "#matches_subject?" do 47 | it "matches if subjects are identical" do 48 | perm = subject.new(true, :read, String) 49 | expect(perm.matches_subject? String).to eq(true) 50 | end 51 | 52 | it "matches if class is equal to subject" do 53 | perm = subject.new(true, :read, String) 54 | expect(perm.matches_subject? "test").to eq(true) 55 | end 56 | 57 | it "matches if superclass is equal to subject" do 58 | perm = subject.new(true, :read, Object) 59 | expect(perm.matches_subject? "test").to eq(true) 60 | end 61 | 62 | it "matches if any ancestor is equal to subject" do 63 | perm = subject.new(true, :read, BasicObject) 64 | expect(perm.matches_subject? "test").to eq(true) 65 | end 66 | 67 | it "does not match if any descendant is equal to subject" do 68 | perm = subject.new(true, :read, String) 69 | expect(perm.matches_subject? Object.new).to eq(false) 70 | end 71 | 72 | end 73 | 74 | describe "#matches_empty_conditions?" do 75 | it "matches when no conditions given" do 76 | perm = subject.new(true, :read, String) 77 | expect(perm.matches_conditions?(String)).to eq(true) 78 | end 79 | 80 | end 81 | 82 | end 83 | -------------------------------------------------------------------------------- /spec/policy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe AccessGranted::Policy do 4 | let(:klass) { Class.new { include AccessGranted::Policy } } 5 | subject(:policy) { klass.new(nil) } 6 | 7 | describe "#configure" do 8 | before :each do 9 | @member = FakeUser.new(1, false, false, false) 10 | @mod = FakeUser.new(2, true, false, false) 11 | @admin = FakeUser.new(3, false, true, false) 12 | @banned = FakeUser.new(4, false, true, true) 13 | end 14 | 15 | it "passes user object to permission block" do 16 | post_owner = double(id: 123) 17 | other_user = double(id: 5) 18 | post = FakePost.new(1, post_owner.id) 19 | 20 | klass = Class.new do 21 | include AccessGranted::Policy 22 | 23 | def configure 24 | role :member do 25 | can :destroy, FakePost do |post, user| 26 | post.user_id == user.id 27 | end 28 | end 29 | end 30 | end 31 | 32 | expect(klass.new(post_owner).can?(:destroy, post)).to eq(true) 33 | expect(klass.new(other_user).can?(:destroy, post)).to eq(false) 34 | end 35 | 36 | it "selects permission based on role priority" do 37 | klass = Class.new do 38 | include AccessGranted::Policy 39 | 40 | def configure 41 | role :administrator, { is_admin: true } do 42 | can :destroy, String 43 | end 44 | 45 | role :moderator, { is_moderator: true } do 46 | can :update, String 47 | end 48 | 49 | role :member do 50 | can :read, String 51 | end 52 | end 53 | end 54 | expect(klass.new(@admin).can?(:destroy, String)).to eq(true) 55 | expect(klass.new(@admin).can?(:read, String)).to eq(true) 56 | 57 | expect(klass.new(@member).cannot?(:destroy, String)).to eq(true) 58 | expect(klass.new(@member).can?(:read, String)).to eq(true) 59 | 60 | expect(klass.new(@mod).can?(:read, String)).to eq(true) 61 | expect(klass.new(@mod).cannot?(:destroy, String)).to eq(true) 62 | end 63 | 64 | context "when multiple roles define the same permission" do 65 | it "checks all roles until conditions are met" do 66 | user_post = FakePost.new(1, @member.id) 67 | other_post = FakePost.new(2, 66) 68 | 69 | klass = Class.new do 70 | include AccessGranted::Policy 71 | 72 | def configure 73 | role :administrator, { is_admin: true } do 74 | can :destroy, FakePost 75 | end 76 | 77 | role :member do 78 | can :destroy, FakePost do |post, user| 79 | post.user_id == user.id 80 | end 81 | end 82 | end 83 | end 84 | 85 | expect(klass.new(@admin).can?(:destroy, user_post)).to eq(true) 86 | expect(klass.new(@admin).can?(:destroy, other_post)).to eq(true) 87 | 88 | expect(klass.new(@member).can?(:destroy, user_post)).to eq(true) 89 | expect(klass.new(@member).cannot?(:destroy, other_post)).to eq(true) 90 | end 91 | end 92 | 93 | it "resolves permissions without subject" do 94 | klass = Class.new do 95 | include AccessGranted::Policy 96 | 97 | def configure 98 | role :member do 99 | can :vague_action 100 | end 101 | end 102 | end 103 | 104 | expect(klass.new(@member).can?(:vague_action)).to eq(true) 105 | end 106 | 107 | describe "#cannot" do 108 | it "forbids action when used in superior role" do 109 | klass = Class.new do 110 | include AccessGranted::Policy 111 | 112 | def configure 113 | role :banned, { is_banned: true } do 114 | cannot :create, String 115 | end 116 | 117 | role :member do 118 | can :create, String 119 | end 120 | end 121 | end 122 | expect(klass.new(@member).can?(:create, String)).to eq(true) 123 | expect(klass.new(@banned).can?(:create, String)).to eq(false) 124 | end 125 | end 126 | 127 | describe "#authorize!" do 128 | let(:klass) do 129 | Class.new do 130 | include AccessGranted::Policy 131 | 132 | def configure 133 | role(:member) { can :create, String } 134 | end 135 | end 136 | end 137 | 138 | it "raises AccessDenied if action is not allowed" do 139 | expect { klass.new(@member).authorize!(:create, Integer) }.to raise_error do |err| 140 | expect(err).to be_a(AccessGranted::AccessDenied) 141 | expect(err.action).to eq(:create) 142 | expect(err.subject).to eq(Integer) 143 | end 144 | end 145 | 146 | it "raises AccessDenied with supplied message if action is not allowed" do 147 | message = 'You are not allowed to create Integer' 148 | expect { klass.new(@member).authorize!(:create, Integer, message) }.to raise_error do |err| 149 | expect(err).to be_a(AccessGranted::AccessDenied) 150 | expect(err.action).to eq(:create) 151 | expect(err.subject).to eq(Integer) 152 | expect(err.message).to eq(message) 153 | end 154 | end 155 | 156 | it "returns the subject if allowed" do 157 | expect(klass.new(@member).authorize!(:create, String)).to equal String 158 | end 159 | end 160 | end 161 | 162 | describe "#role" do 163 | it "allows passing role class" do 164 | klass_role = Class.new AccessGranted::Role do 165 | def configure 166 | can :read, String 167 | end 168 | end 169 | subject.role(:member, klass_role) 170 | expect(policy.roles.first.class).to eq(klass_role) 171 | end 172 | 173 | it "returns roles in the order of priority" do 174 | policy.role(:admin) 175 | policy.role(:moderator) 176 | policy.role(:user) 177 | policy.role(:guest) 178 | 179 | expect(policy.roles.map(&:name)).to eq([:admin, :moderator, :user, :guest]) 180 | end 181 | 182 | it "allows defining a default role" do 183 | policy.role(:member) 184 | expect(policy.roles.map(&:name)).to include(:member) 185 | end 186 | 187 | it "does not allow duplicate role names" do 188 | policy.role(:member) 189 | expect { policy.role(:member) }.to raise_error AccessGranted::DuplicateRole 190 | end 191 | 192 | it "allows nesting `can` calls inside a block" do 193 | role = policy.role(:member) do 194 | can :read, String 195 | end 196 | 197 | expect(role.find_permission(:read, String).granted).to eq(true) 198 | end 199 | end 200 | 201 | describe "#applicable_roles" do 202 | let(:user) { double("User", is_moderator: true, is_admin: true) } 203 | subject(:policy) { klass.new(user) } 204 | 205 | before do 206 | policy.role(:administrator, { is_admin: true }) 207 | policy.role(:moderator, { is_moderator: true }) 208 | policy.role(:member) 209 | end 210 | 211 | context "user matches all roles" do 212 | it "returns all matching roles in the order of priority" do 213 | expect(policy.applicable_roles.map(&:name)).to eq([:administrator, :moderator, :member]) 214 | end 215 | end 216 | 217 | context "user is just an admin" do 218 | let(:user) { double("User", is_moderator: false, is_admin: true) } 219 | 220 | it 'returns array with admin and member roles' do 221 | expect(policy.applicable_roles.map(&:name)).to eq([:administrator, :member]) 222 | end 223 | end 224 | end 225 | end 226 | -------------------------------------------------------------------------------- /spec/role_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe AccessGranted::Role do 4 | subject { AccessGranted::Role } 5 | 6 | it "requires a role name" do 7 | expect { subject.new }.to raise_error(ArgumentError) 8 | end 9 | 10 | it "creates a default role without conditions" do 11 | expect(subject.new(:member).conditions).to be_nil 12 | end 13 | 14 | describe "#applies_to?" do 15 | it "matches user when no conditions given" do 16 | role = subject.new(:member) 17 | user = double("User") 18 | expect(role.applies_to?(user)).to eq(true) 19 | end 20 | 21 | it "matches user by hash conditions" do 22 | role = subject.new(:moderator, { is_moderator: true }) 23 | user = double("User", is_moderator: true) 24 | expect(role.applies_to?(user)).to eq(true) 25 | end 26 | 27 | it "doesn't match user if any of hash conditions is not met" do 28 | role = subject.new(:moderator, { is_moderator: true, is_admin: true }) 29 | user = double("User", is_moderator: true, is_admin: false) 30 | expect(role.applies_to?(user)).to eq(false) 31 | end 32 | 33 | it "matches user by Proc conditions" do 34 | role = subject.new(:moderator, proc {|user| user.is_moderator? }) 35 | user = double("User", is_moderator?: true) 36 | expect(role.applies_to?(user)).to eq(true) 37 | end 38 | end 39 | 40 | describe "#can" do 41 | before :each do 42 | @role = AccessGranted::Role.new(:member) 43 | end 44 | 45 | it "allows adding permission without subject" do 46 | @role.can :vague_action 47 | expect(@role.find_permission(:vague_action, nil)).to_not be_nil 48 | end 49 | 50 | it "forbids creating actions with the same name" do 51 | @role.can :read, String 52 | expect { @role.can :read, String }.to raise_error AccessGranted::DuplicatePermission, "Permission `read` is already defined for String in role `member`" 53 | end 54 | 55 | it "accepts :manage shortcut for CRUD actions" do 56 | @role.can :manage, String 57 | expect(@role.permissions.map(&:action)).to include(:read, :create, :update, :destroy) 58 | end 59 | 60 | describe "when action is an Array" do 61 | it "creates multiple permissions" do 62 | @role.can [:read, :create], String 63 | expect(@role.permissions.size).to eq(2) 64 | end 65 | end 66 | 67 | describe "when no conditions given" do 68 | it "should be able to read a class" do 69 | @role.can :read, String 70 | expect(@role.find_permission(:read, String)).to_not be_nil 71 | end 72 | 73 | it "should be able to read instance of class" do 74 | @role.can :read, String 75 | expect(@role.find_permission(:read, "text")).to_not be_nil 76 | end 77 | end 78 | 79 | describe "when conditions given" do 80 | it "should be able to read when conditions match" do 81 | sub = double("Element", published: true) 82 | @role.can :read, sub.class, { published: true } 83 | expect(@role.find_permission(:read, sub)).to_not be_nil 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | if ENV["COV"] 5 | require 'simplecov' 6 | SimpleCov.start 7 | end 8 | require 'pry' 9 | 10 | RSpec.configure do |config| 11 | config.run_all_when_everything_filtered = true 12 | config.filter_run :focus 13 | 14 | config.order = 'random' 15 | end 16 | 17 | module ActionController 18 | class Base 19 | def self.helper_method(*args) 20 | end 21 | end 22 | end 23 | 24 | require 'access-granted' 25 | 26 | class FakeUser < Struct.new(:id, :is_moderator, :is_admin, :is_banned) 27 | end 28 | 29 | class FakePost < Struct.new(:id, :user_id) 30 | end 31 | 32 | class AccessPolicy 33 | include AccessGranted::Policy 34 | end 35 | --------------------------------------------------------------------------------