authority 0.0.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.
- data/.gitignore +17 -0
- data/.rvmrc +1 -0
- data/Gemfile +6 -0
- data/LICENSE +22 -0
- data/README.md +135 -0
- data/Rakefile +2 -0
- data/authority.gemspec +20 -0
- data/lib/authority.rb +11 -0
- data/lib/authority/abilities.rb +39 -0
- data/lib/authority/authorizer.rb +27 -0
- data/lib/authority/version.rb +3 -0
- data/spec/authority/abilities_spec.rb +88 -0
- data/spec/authority/authorizer_spec.rb +42 -0
- data/spec/authority_spec.rb +7 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/support/ability_model.rb +6 -0
- data/spec/support/actor.rb +17 -0
- metadata +93 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use --create default@authority
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Nathan Long
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
# Authority
|
2
|
+
|
3
|
+
## SUPER ALPHA VERSION
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
Authority gives you a clean and easy way to say, in your Rails app, **who** is allowed to do **what** with your models.
|
8
|
+
|
9
|
+
It assumes that you already have some kind of user object in your application.
|
10
|
+
|
11
|
+
The goals of Authority are:
|
12
|
+
|
13
|
+
- To allow broad, class-level rules. Examples:
|
14
|
+
- "Basic users cannot delete **any** Widget."
|
15
|
+
- "Only admin users can create Offices."
|
16
|
+
|
17
|
+
- To allow fine-grained, instance-level rules. Examples:
|
18
|
+
- "Management users can only edit schedules in their jurisdiction."
|
19
|
+
- "Users can't create playlists more than 20 songs long unless they've paid."
|
20
|
+
|
21
|
+
- To provide a clear syntax for permissions-based views. Example:
|
22
|
+
- `link_to 'Edit Widget', edit_widget_path(@widget) if current_user.can_edit?(@widget)`
|
23
|
+
|
24
|
+
- To gracefully handle any access violations: display a "you can't do that" screen and log the violation.
|
25
|
+
|
26
|
+
- To do all of this **without cluttering** either your controllers or your models. This is done by letting Authorizer classes do most of the work. More on that below.
|
27
|
+
|
28
|
+
## Installation
|
29
|
+
|
30
|
+
Add this line to your application's Gemfile:
|
31
|
+
|
32
|
+
gem 'authority'
|
33
|
+
|
34
|
+
And then execute:
|
35
|
+
|
36
|
+
$ bundle
|
37
|
+
|
38
|
+
Or install it yourself as:
|
39
|
+
|
40
|
+
$ gem install authority
|
41
|
+
|
42
|
+
## How it works
|
43
|
+
|
44
|
+
In broad terms, the authorization process flows like this:
|
45
|
+
|
46
|
+
- A request comes to a model, either the class or an instance, saying "can this user do this action to you?"
|
47
|
+
- The model passes that question to its Authorizer
|
48
|
+
- The Authorizer checks whatever user properties and business rules are relevant to answer that question.
|
49
|
+
- The answer is passed back up to the model, then back to the original caller
|
50
|
+
|
51
|
+
## Usage
|
52
|
+
|
53
|
+
### Users
|
54
|
+
|
55
|
+
Your user model (whatever you call it) should `include Authority::UserAbilities`. This defines methods like `can_edit?(resource)`, which are just nice shortcuts for `resource.editable_by?(user)`. (TODO: Add this module).
|
56
|
+
|
57
|
+
### Models
|
58
|
+
|
59
|
+
In your models, simply `include Authority::Abilities`. This sets up both class-level and instance-level methods like `creatable_by?(user)`, etc, all of which delegate to the model's corresponding authorizer. For example, the `Rabbit` model would delegate to `RabbitAuthorizer`.
|
60
|
+
|
61
|
+
### Controllers
|
62
|
+
|
63
|
+
#### Basic Usage
|
64
|
+
|
65
|
+
In your controllers, add this method call:
|
66
|
+
|
67
|
+
`check_authorization_on ModelName`
|
68
|
+
|
69
|
+
That sets up a `:before_filter` that calls your class-level methods before each action. For instance, before running the `update` action, it will check whether `ModelName` is `updatable_by?` the current user at a class level. A return value of false means "this user can never update models of this class."
|
70
|
+
|
71
|
+
If that's all you need, one line does it.
|
72
|
+
|
73
|
+
#### In-action usage
|
74
|
+
|
75
|
+
If you need to check some attributes of a model instance to decide if an action is permissible, you can use `check_authorization_for(:action, @model_instance, @user)` (TODO: determine this)
|
76
|
+
|
77
|
+
### Authorizers
|
78
|
+
|
79
|
+
Authorizers should be added under `app/authorizers`, one for each of your models. Each authorizer should correspond to a single model. So if you have `app/models/laser_cannon.rb`, you should have, at minimum:
|
80
|
+
|
81
|
+
# app/authorizers/laser_cannon_authorizer.rb
|
82
|
+
class LaserCannonAuthorizer < Authority::Authorizer
|
83
|
+
end
|
84
|
+
|
85
|
+
These are where your actual authorization logic goes. You do have to specify your own business rules, but Authority comes with the following baked in:
|
86
|
+
|
87
|
+
- All class-level methods defined on `Authority::Authorizer` return false by default; you must override them in your Authorizers to grant permissions. This whitelisting approach will keep you from accidentally allowing things you didn't intend.
|
88
|
+
- All instance-level methods defined on `Authority::Authorizer` call their corresponding class-level method by default.
|
89
|
+
|
90
|
+
This combination means that, with this code:
|
91
|
+
|
92
|
+
# app/authorizers/laser_cannon_authorizer.rb
|
93
|
+
class LaserCannonAuthorizer < Authority::Authorizer
|
94
|
+
end
|
95
|
+
|
96
|
+
... you can already do the following:
|
97
|
+
|
98
|
+
current_user.can_create?(LaserCannon) # false; all inherited class-level permissions are false
|
99
|
+
current_user.can_create?(@laser_cannon) # false; instance-level permissions check class-level ones by default
|
100
|
+
|
101
|
+
If you update your authorizer as follows:
|
102
|
+
|
103
|
+
# app/authorizers/laser_cannon_authorizer.rb
|
104
|
+
class LaserCannonAuthorizer < Authority::Authorizer
|
105
|
+
|
106
|
+
def self.creatable_by?(user) # class-level permission
|
107
|
+
true
|
108
|
+
end
|
109
|
+
|
110
|
+
def deletable_by?(user) # instance_level permission
|
111
|
+
user.first_name == 'Larry' && Date.today.friday?
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
... you can now do this following:
|
117
|
+
|
118
|
+
current_user.can_create?(LaserCannon) # true, per class method above
|
119
|
+
current_user.can_create?(@laser_cannon) # true; inherited instance method calls class method
|
120
|
+
current_user.can_delete?(@laser_cannon) # Only Larry, and only on Fridays
|
121
|
+
|
122
|
+
## TODO
|
123
|
+
|
124
|
+
- Add a module, to be included in whatever user class an app has, which defines all the `can_(verb)` methods.
|
125
|
+
- Determine exact syntax for checking rules during a controller action.
|
126
|
+
- Add ability to customize default authorization
|
127
|
+
- Add customizable logger for authorization violations
|
128
|
+
|
129
|
+
## Contributing
|
130
|
+
|
131
|
+
1. Fork it
|
132
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
133
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
134
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
135
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/authority.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/authority/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Nathan Long", "Adam Hunter"]
|
6
|
+
gem.email = ["nathanmlong@gmail.com", "adamhunter@me.com"]
|
7
|
+
gem.description = %q{Gem for managing authorization on model actions in Rails}
|
8
|
+
gem.summary = %q{Authority gives you a clean and easy way to say, in your Rails app, **who** is allowed to do **what** with your models.}
|
9
|
+
gem.homepage = "https://github.com/nathanl/authority"
|
10
|
+
|
11
|
+
gem.add_dependency "rails", ">= 3.0.0"
|
12
|
+
gem.add_development_dependency "bundler", ">= 1.0.0"
|
13
|
+
|
14
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
15
|
+
gem.files = `git ls-files`.split("\n")
|
16
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
gem.name = "authority"
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
gem.version = Authority::VERSION
|
20
|
+
end
|
data/lib/authority.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_support/core_ext/class/attribute'
|
3
|
+
require 'active_support/core_ext/string/inflections'
|
4
|
+
|
5
|
+
module Authority
|
6
|
+
ADJECTIVES = %w[creatable readable updatable deletable]
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'authority/abilities'
|
10
|
+
require 'authority/authorizer'
|
11
|
+
require 'authority/version'
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Authority
|
2
|
+
module Abilities
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
class_attribute :authorizer_name
|
7
|
+
|
8
|
+
self.authorizer_name = "#{name}Authorizer"
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
|
13
|
+
ADJECTIVES.each do |adjective|
|
14
|
+
|
15
|
+
# Metaprogram needed methods, allowing for nice backtraces
|
16
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
17
|
+
def #{adjective}_by?(actor)
|
18
|
+
authorizer.#{adjective}_by?(actor)
|
19
|
+
end
|
20
|
+
RUBY
|
21
|
+
end
|
22
|
+
|
23
|
+
def authorizer
|
24
|
+
@authorizer ||= authorizer_name.constantize
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
ADJECTIVES.each do |adjective|
|
29
|
+
|
30
|
+
# Metaprogram needed methods, allowing for nice backtraces
|
31
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
32
|
+
def #{adjective}_by?(actor)
|
33
|
+
self.class.authorizer.new(self).#{adjective}_by?(actor)
|
34
|
+
end
|
35
|
+
RUBY
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Authority
|
2
|
+
class Authorizer
|
3
|
+
|
4
|
+
attr_reader :resource
|
5
|
+
|
6
|
+
def initialize(resource)
|
7
|
+
@resource = resource
|
8
|
+
end
|
9
|
+
|
10
|
+
ADJECTIVES.each do |adjective|
|
11
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
12
|
+
def self.#{adjective}_by?(actor)
|
13
|
+
false
|
14
|
+
end
|
15
|
+
RUBY
|
16
|
+
end
|
17
|
+
|
18
|
+
ADJECTIVES.each do |adjective|
|
19
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
20
|
+
def #{adjective}_by?(actor)
|
21
|
+
false
|
22
|
+
end
|
23
|
+
RUBY
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'support/ability_model'
|
3
|
+
require 'support/actor'
|
4
|
+
|
5
|
+
describe Authority::Abilities do
|
6
|
+
|
7
|
+
before :each do
|
8
|
+
@actor = Actor.new
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "authorizer" do
|
12
|
+
|
13
|
+
it "should have a class attribute getter for authorizer_name" do
|
14
|
+
AbilityModel.should respond_to(:authorizer_name)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should have a class attribute setter for authorizer_name" do
|
18
|
+
AbilityModel.should respond_to(:authorizer_name=)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should have a default authorizer_name of '(ClassName)Authorizer'" do
|
22
|
+
AbilityModel.authorizer_name.should eq("AbilityModelAuthorizer")
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should constantize the authorizer name as the authorizer" do
|
26
|
+
AbilityModel.instance_variable_set(:@authorizer, nil)
|
27
|
+
AbilityModel.authorizer_name.should_receive(:constantize)
|
28
|
+
AbilityModel.authorizer
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should memoize the authorizer to avoid reconstantizing" do
|
32
|
+
AbilityModel.authorizer
|
33
|
+
AbilityModel.authorizer_name.should_not_receive(:constantize)
|
34
|
+
AbilityModel.authorizer
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "class methods" do
|
40
|
+
|
41
|
+
Authority::ADJECTIVES.each do |adjective|
|
42
|
+
method_name = "#{adjective}_by?"
|
43
|
+
|
44
|
+
it "should respond to `#{method_name}`" do
|
45
|
+
AbilityModel.should respond_to(method_name)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should delegate `#{method_name}` to its authorizer class" do
|
49
|
+
AbilityModel.authorizer.should_receive(method_name).with(@actor)
|
50
|
+
AbilityModel.send(method_name, @actor)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "instance methods" do
|
58
|
+
|
59
|
+
before :each do
|
60
|
+
@ability_model = AbilityModel.new
|
61
|
+
@authorizer = AbilityModel.authorizer.new(@ability_model)
|
62
|
+
end
|
63
|
+
|
64
|
+
Authority::ADJECTIVES.each do |adjective|
|
65
|
+
method_name = "#{adjective}_by?"
|
66
|
+
|
67
|
+
it "should respond to `#{method_name}`" do
|
68
|
+
@ability_model.should respond_to(method_name)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should delegate `#{method_name}` to a new authorizer instance" do
|
72
|
+
AbilityModel.authorizer.stub(:new).and_return(@authorizer)
|
73
|
+
@authorizer.should_receive(method_name).with(@actor)
|
74
|
+
@ability_model.send(method_name, @actor)
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should always create a new authorizer instance when checking `#{method_name}`" do
|
78
|
+
2.times do
|
79
|
+
@ability_model.class.authorizer.should_receive(:new).with(@ability_model).and_return(@authorizer)
|
80
|
+
@ability_model.send(method_name, @actor)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'support/ability_model'
|
3
|
+
|
4
|
+
describe Authority::Authorizer do
|
5
|
+
|
6
|
+
before :each do
|
7
|
+
@ability_model = AbilityModel.new
|
8
|
+
@authorizer = Authority::Authorizer.new(@ability_model)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should take a resource instance in its initializer" do
|
12
|
+
@authorizer.resource.should eq(@ability_model)
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "class methods" do
|
16
|
+
|
17
|
+
Authority::ADJECTIVES.each do |adjective|
|
18
|
+
method_name = "#{adjective}_by?"
|
19
|
+
|
20
|
+
it "should respond to `#{method_name}`" do
|
21
|
+
Authority::Authorizer.should respond_to(method_name)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "instance methods" do
|
29
|
+
|
30
|
+
Authority::ADJECTIVES.each do |adjective|
|
31
|
+
method_name = "#{adjective}_by?"
|
32
|
+
|
33
|
+
it "should respond to `#{method_name}`" do
|
34
|
+
@authorizer.should respond_to(method_name)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
class Actor
|
2
|
+
def can_create?(resource)
|
3
|
+
resource.creatable_by?(self)
|
4
|
+
end
|
5
|
+
|
6
|
+
def can_read?(resource)
|
7
|
+
resource.readable_by?(self)
|
8
|
+
end
|
9
|
+
|
10
|
+
def can_update?(resource)
|
11
|
+
resource.updatable_by?(self)
|
12
|
+
end
|
13
|
+
|
14
|
+
def can_delete?(resource)
|
15
|
+
resource.deletable_by?(self)
|
16
|
+
end
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: authority
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Nathan Long
|
9
|
+
- Adam Hunter
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-03-12 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rails
|
17
|
+
requirement: &2152357800 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 3.0.0
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *2152357800
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: bundler
|
28
|
+
requirement: &2152357040 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.0.0
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *2152357040
|
37
|
+
description: Gem for managing authorization on model actions in Rails
|
38
|
+
email:
|
39
|
+
- nathanmlong@gmail.com
|
40
|
+
- adamhunter@me.com
|
41
|
+
executables: []
|
42
|
+
extensions: []
|
43
|
+
extra_rdoc_files: []
|
44
|
+
files:
|
45
|
+
- .gitignore
|
46
|
+
- .rvmrc
|
47
|
+
- Gemfile
|
48
|
+
- LICENSE
|
49
|
+
- README.md
|
50
|
+
- Rakefile
|
51
|
+
- authority.gemspec
|
52
|
+
- lib/authority.rb
|
53
|
+
- lib/authority/abilities.rb
|
54
|
+
- lib/authority/authorizer.rb
|
55
|
+
- lib/authority/version.rb
|
56
|
+
- spec/authority/abilities_spec.rb
|
57
|
+
- spec/authority/authorizer_spec.rb
|
58
|
+
- spec/authority_spec.rb
|
59
|
+
- spec/spec_helper.rb
|
60
|
+
- spec/support/ability_model.rb
|
61
|
+
- spec/support/actor.rb
|
62
|
+
homepage: https://github.com/nathanl/authority
|
63
|
+
licenses: []
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options: []
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ! '>='
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ! '>='
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
requirements: []
|
81
|
+
rubyforge_project:
|
82
|
+
rubygems_version: 1.8.16
|
83
|
+
signing_key:
|
84
|
+
specification_version: 3
|
85
|
+
summary: Authority gives you a clean and easy way to say, in your Rails app, **who**
|
86
|
+
is allowed to do **what** with your models.
|
87
|
+
test_files:
|
88
|
+
- spec/authority/abilities_spec.rb
|
89
|
+
- spec/authority/authorizer_spec.rb
|
90
|
+
- spec/authority_spec.rb
|
91
|
+
- spec/spec_helper.rb
|
92
|
+
- spec/support/ability_model.rb
|
93
|
+
- spec/support/actor.rb
|