action_access 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/CONTRIBUTING.md +23 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +87 -0
- data/LICENSE.txt +20 -0
- data/README.md +283 -0
- data/Rakefile +28 -0
- data/action_access.gemspec +24 -0
- data/lib/action_access.rb +14 -0
- data/lib/action_access/controller_additions.rb +75 -0
- data/lib/action_access/keeper.rb +90 -0
- data/lib/action_access/model_additions.rb +14 -0
- data/lib/action_access/railtie.rb +19 -0
- data/lib/action_access/user_utilities.rb +39 -0
- data/lib/action_access/version.rb +3 -0
- data/test/action_access_test.rb +21 -0
- data/test/controllers/articles_controller_test.rb +41 -0
- data/test/controllers/secrets_controller_test.rb +10 -0
- data/test/controllers/static_controller_test.rb +15 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/images/.keep +0 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +15 -0
- data/test/dummy/app/controllers/articles_controller.rb +36 -0
- data/test/dummy/app/controllers/concerns/.keep +0 -0
- data/test/dummy/app/controllers/secrets_controller.rb +4 -0
- data/test/dummy/app/controllers/static_controller.rb +6 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/mailers/.keep +0 -0
- data/test/dummy/app/models/.keep +0 -0
- data/test/dummy/app/models/concerns/.keep +0 -0
- data/test/dummy/app/models/user.rb +7 -0
- data/test/dummy/app/views/articles/edit.html.erb +1 -0
- data/test/dummy/app/views/articles/index.html.erb +1 -0
- data/test/dummy/app/views/articles/new.html.erb +1 -0
- data/test/dummy/app/views/articles/show.html.erb +3 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/app/views/static/home.html.erb +1 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +23 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +78 -0
- data/test/dummy/config/environments/test.rb +39 -0
- data/test/dummy/config/initializers/assets.rb +8 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +6 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/db/migrate/20140926071026_create_users.rb +10 -0
- data/test/dummy/db/schema.rb +23 -0
- data/test/dummy/lib/assets/.keep +0 -0
- data/test/dummy/log/.keep +0 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/models/user_test.rb +22 -0
- data/test/support/compact_environment.rb +9 -0
- data/test/test_helper.rb +8 -0
- metadata +203 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c3a922a62dc47428db03349b5ab4381c32d669ea
|
4
|
+
data.tar.gz: 639f7cf2ce63e3783f7d33970ea874624f8c7981
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 10c2835e378462860b56b636431ebb26ac196acd179c82703a72bb77cb61745352b97ed668d28f2999dc88a4deec16f0ac32d3c10794a42be1489dd103b37d94
|
7
|
+
data.tar.gz: 8a1b64c08c10887c41e55a1e8f16d2ed8677cd68a7f7ef42f050f8e04a7d517727e2651e7841c0cc8bfa84b195768ae7c43d99c168eaa0f6a6d61e2b96bf797a
|
data/.gitignore
ADDED
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
## Please read before contributing
|
2
|
+
|
3
|
+
1. If you have questions about Action Access, use the
|
4
|
+
[Mailing List](https://groups.google.com/d/forum/action_access) or
|
5
|
+
[Stack Overflow](https://stackoverflow.com/questions/tagged/action_access).
|
6
|
+
Do not post questions here.
|
7
|
+
|
8
|
+
2. If you find a security bug, **DO NOT** submit an issue here.
|
9
|
+
Please send an e-mail to [mgag.issues@gmail.com](mailto:mgag.issues@gmail.com)
|
10
|
+
instead.
|
11
|
+
|
12
|
+
3. Do a small search on the issues tracker before submitting your issue to
|
13
|
+
see if it was already reported or fixed. In case it was not, create your report
|
14
|
+
with a **clear description** of the issue and a **code sample** that
|
15
|
+
demonstrates it. Include Rails and Action Access versions, and as much relevant
|
16
|
+
information as possible. Tests or a sample application showing how the
|
17
|
+
expected behavior is not occurring will be much appreciated.
|
18
|
+
|
19
|
+
The more information you give, the easier it becomes to track the issue and fix
|
20
|
+
it. Your goal should be to make it easy for yourself and others to replicate
|
21
|
+
the bug and figure out a fix.
|
22
|
+
|
23
|
+
Thanks for your time and support!
|
data/Gemfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
# Declare your gem's dependencies in action_access.gemspec.
|
4
|
+
# Bundler will treat runtime dependencies like base dependencies, and
|
5
|
+
# development dependencies will be added by default to the :development group.
|
6
|
+
gemspec
|
7
|
+
|
8
|
+
# Declare any dependencies that are still in development here instead of in
|
9
|
+
# your gemspec. These might include edge Rails or gems from your path or
|
10
|
+
# Git. Remember to move these dependencies to your gemspec before releasing
|
11
|
+
# your gem to rubygems.org.
|
12
|
+
|
13
|
+
# To use debugger
|
14
|
+
# gem 'debugger'
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
action_access (0.0.1)
|
5
|
+
rails (~> 4.1)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
actionmailer (4.1.6)
|
11
|
+
actionpack (= 4.1.6)
|
12
|
+
actionview (= 4.1.6)
|
13
|
+
mail (~> 2.5, >= 2.5.4)
|
14
|
+
actionpack (4.1.6)
|
15
|
+
actionview (= 4.1.6)
|
16
|
+
activesupport (= 4.1.6)
|
17
|
+
rack (~> 1.5.2)
|
18
|
+
rack-test (~> 0.6.2)
|
19
|
+
actionview (4.1.6)
|
20
|
+
activesupport (= 4.1.6)
|
21
|
+
builder (~> 3.1)
|
22
|
+
erubis (~> 2.7.0)
|
23
|
+
activemodel (4.1.6)
|
24
|
+
activesupport (= 4.1.6)
|
25
|
+
builder (~> 3.1)
|
26
|
+
activerecord (4.1.6)
|
27
|
+
activemodel (= 4.1.6)
|
28
|
+
activesupport (= 4.1.6)
|
29
|
+
arel (~> 5.0.0)
|
30
|
+
activesupport (4.1.6)
|
31
|
+
i18n (~> 0.6, >= 0.6.9)
|
32
|
+
json (~> 1.7, >= 1.7.7)
|
33
|
+
minitest (~> 5.1)
|
34
|
+
thread_safe (~> 0.1)
|
35
|
+
tzinfo (~> 1.1)
|
36
|
+
arel (5.0.1.20140414130214)
|
37
|
+
builder (3.2.2)
|
38
|
+
erubis (2.7.0)
|
39
|
+
hike (1.2.3)
|
40
|
+
i18n (0.6.11)
|
41
|
+
json (1.8.1)
|
42
|
+
mail (2.6.1)
|
43
|
+
mime-types (>= 1.16, < 3)
|
44
|
+
mime-types (2.3)
|
45
|
+
minitest (5.4.1)
|
46
|
+
multi_json (1.10.1)
|
47
|
+
rack (1.5.2)
|
48
|
+
rack-test (0.6.2)
|
49
|
+
rack (>= 1.0)
|
50
|
+
rails (4.1.6)
|
51
|
+
actionmailer (= 4.1.6)
|
52
|
+
actionpack (= 4.1.6)
|
53
|
+
actionview (= 4.1.6)
|
54
|
+
activemodel (= 4.1.6)
|
55
|
+
activerecord (= 4.1.6)
|
56
|
+
activesupport (= 4.1.6)
|
57
|
+
bundler (>= 1.3.0, < 2.0)
|
58
|
+
railties (= 4.1.6)
|
59
|
+
sprockets-rails (~> 2.0)
|
60
|
+
railties (4.1.6)
|
61
|
+
actionpack (= 4.1.6)
|
62
|
+
activesupport (= 4.1.6)
|
63
|
+
rake (>= 0.8.7)
|
64
|
+
thor (>= 0.18.1, < 2.0)
|
65
|
+
rake (10.3.2)
|
66
|
+
sprockets (2.12.2)
|
67
|
+
hike (~> 1.2)
|
68
|
+
multi_json (~> 1.0)
|
69
|
+
rack (~> 1.0)
|
70
|
+
tilt (~> 1.1, != 1.3.0)
|
71
|
+
sprockets-rails (2.1.4)
|
72
|
+
actionpack (>= 3.0)
|
73
|
+
activesupport (>= 3.0)
|
74
|
+
sprockets (~> 2.8)
|
75
|
+
sqlite3 (1.3.9)
|
76
|
+
thor (0.19.1)
|
77
|
+
thread_safe (0.3.4)
|
78
|
+
tilt (1.4.1)
|
79
|
+
tzinfo (1.2.2)
|
80
|
+
thread_safe (~> 0.1)
|
81
|
+
|
82
|
+
PLATFORMS
|
83
|
+
ruby
|
84
|
+
|
85
|
+
DEPENDENCIES
|
86
|
+
action_access!
|
87
|
+
sqlite3
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2014 Matías A. Gagliano
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,283 @@
|
|
1
|
+
# Action Access
|
2
|
+
|
3
|
+
Action Access is an access control system for Ruby on Rails. It provides a
|
4
|
+
modular and easy way to secure applications and handle permissions.
|
5
|
+
|
6
|
+
It works at controller level focusing on what **actions** are accessible for
|
7
|
+
the current user instead of handling models and their attributes.
|
8
|
+
|
9
|
+
It also provides utilities for thorough control and some useful view helpers.
|
10
|
+
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Add `action_access` to the app's Gemfile, run the `bundle` command and restart
|
15
|
+
any running server.
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
# Gemfile
|
19
|
+
gem 'action_access'
|
20
|
+
```
|
21
|
+
|
22
|
+
|
23
|
+
## Basic configuration
|
24
|
+
|
25
|
+
The most important setting is the way to get the **clearance level** (role,
|
26
|
+
user group, etc.), other than that it works out of the box.
|
27
|
+
|
28
|
+
Action Access doesn't require users or authentication at all to function so
|
29
|
+
you can get creative with the way you set and identify clearance levels.
|
30
|
+
|
31
|
+
It only needs a `current_clearance_level` method that returns the proper
|
32
|
+
clearance level for the current request. It can be a string or symbol and
|
33
|
+
it doesn't matter if it's singular or plural, it'll be singularized.
|
34
|
+
|
35
|
+
* With `current_user`:
|
36
|
+
|
37
|
+
The default `current_clearance_level` method tests if it can get
|
38
|
+
`current_user.clearance_level` and defaults to `:guest` if not.
|
39
|
+
|
40
|
+
So, if you already have a `current_user` method you just need to add a
|
41
|
+
`clearance_level` method to the user. With a role based authorization you
|
42
|
+
may add the following to your `User` model:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
class User < ActiveRecord::Base
|
46
|
+
belongs_to :role
|
47
|
+
|
48
|
+
def clearance_level
|
49
|
+
role.name
|
50
|
+
end
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
* No `current_user`:
|
55
|
+
|
56
|
+
If there's no `current_user` you need to override `current_clearance_level`
|
57
|
+
with whatever logic that applies to your application.
|
58
|
+
|
59
|
+
Continuing with the role based example, you might do something like this:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
class ApplicationController < ActionController::Base
|
63
|
+
def current_clearance_level
|
64
|
+
session[:role] || :guest
|
65
|
+
end
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
|
70
|
+
## Setting permissions
|
71
|
+
|
72
|
+
Permissions are set through authorization statements using the **let** class
|
73
|
+
method available in every controller. The first parameter is the clearance
|
74
|
+
level (plural or singular) and the second is the action or list of actions.
|
75
|
+
|
76
|
+
As a simple example, to allow administrators (and only administrators in this
|
77
|
+
case) to delete articles you'd add the following to `ArticlesController`:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
class ArticlesController < ApplicationController
|
81
|
+
let :admins, :destroy
|
82
|
+
|
83
|
+
def destroy
|
84
|
+
# ...
|
85
|
+
end
|
86
|
+
|
87
|
+
# ...
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
This will automatically **lock** the controller and only allow administrators
|
92
|
+
accessing the destroy action. **Every other request** pointing to the controller
|
93
|
+
**will be rejected** and redirected with an alert.
|
94
|
+
|
95
|
+
### Real-life example:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
class ArticlesController < ApplicationController
|
99
|
+
let :admins, :all
|
100
|
+
let :editors, [:index, :show, :edit, :update]
|
101
|
+
let :all, [:index, :show]
|
102
|
+
|
103
|
+
def index
|
104
|
+
# ...
|
105
|
+
end
|
106
|
+
|
107
|
+
# ...
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
These statements lock the controller and set the following:
|
112
|
+
* _Administrators_ (admins) are authorized to access any action.
|
113
|
+
* _Editors_ can list, view and edit articles.
|
114
|
+
* _Anyone else_ can **only** list and view articles.
|
115
|
+
|
116
|
+
This case uses the special keyword `:all`, it means everyone if passed as the
|
117
|
+
first argument or every action if passed as the second one.
|
118
|
+
|
119
|
+
Again, any unauthorized request will be rejected and redirected with an alert.
|
120
|
+
|
121
|
+
### Note about clearance levels
|
122
|
+
|
123
|
+
Notice that in the previous examples we didn't need to define clearance levels
|
124
|
+
or roles anywhere else in the application. With the authorization statement you
|
125
|
+
both **define** them and **set their permissions**. The only requirement is
|
126
|
+
that the clearance levels from the authorizations match the one returned by
|
127
|
+
`current_clearance_level`.
|
128
|
+
|
129
|
+
|
130
|
+
## Advanced configuration
|
131
|
+
|
132
|
+
### Locked by default
|
133
|
+
|
134
|
+
The `lock_access` class method forces controllers to be locked even if no
|
135
|
+
permissions are defined, in such case every request will be redirected.
|
136
|
+
|
137
|
+
This allows to ensure that an entire application or scope (e.g. `Admin`) is
|
138
|
+
**locked by default**. Simply call `lock_access` inside `ApplicationController`
|
139
|
+
or from a scope's base controller.
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
class ApplicationController < ActionController::Base
|
143
|
+
lock_access
|
144
|
+
|
145
|
+
# ...
|
146
|
+
end
|
147
|
+
```
|
148
|
+
|
149
|
+
To **unlock** a single controller (to make it "public") add `let :all, :all`,
|
150
|
+
this will allow anyone to access any action in the controller.
|
151
|
+
|
152
|
+
### Redirection path
|
153
|
+
|
154
|
+
By default any unauthorized (or not explicitly authorized) access will be
|
155
|
+
redirected to the **root path**.
|
156
|
+
|
157
|
+
You can set or choose a different path by overriding the somewhat long but
|
158
|
+
very clear `unauthorized_access_redirection_path` method.
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
class ApplicationController < ActionController::Base
|
162
|
+
|
163
|
+
def unathorized_access_redirection_path
|
164
|
+
case current_user.clearance_level.to_sym
|
165
|
+
when :admin then admin_root_path
|
166
|
+
when :user then user_root_path
|
167
|
+
else root_path
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# ...
|
172
|
+
end
|
173
|
+
```
|
174
|
+
|
175
|
+
### Alert message
|
176
|
+
|
177
|
+
Redirections have a default alert message of "Not authorized.". To customize it
|
178
|
+
or use translations set `action_access.redirection_message` in your locales.
|
179
|
+
|
180
|
+
```yml
|
181
|
+
# config/locales/en.yml
|
182
|
+
en:
|
183
|
+
action_access:
|
184
|
+
redirection_message: "You are not allowed to do this!"
|
185
|
+
```
|
186
|
+
|
187
|
+
|
188
|
+
## Utilities
|
189
|
+
|
190
|
+
### Fine Grained Access Control
|
191
|
+
|
192
|
+
If further control is required, possibly because access depends on request
|
193
|
+
parameters or some result from the database, you can use the `not_authorized!`
|
194
|
+
method inside actions to reject the request and issue a redirection. It
|
195
|
+
optionally takes a redirection path and a custom alert message.
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
class ProfilesController < ApplicationController
|
199
|
+
let :user, [:edit, :update]
|
200
|
+
|
201
|
+
def update
|
202
|
+
unless params[:id] == current_user.profile_id
|
203
|
+
not_authorized! path: profile_path, message: "That's not your profile!"
|
204
|
+
end
|
205
|
+
|
206
|
+
# ...
|
207
|
+
end
|
208
|
+
|
209
|
+
# ...
|
210
|
+
end
|
211
|
+
```
|
212
|
+
|
213
|
+
There are better ways to handle this particular case but it serves to outline
|
214
|
+
the use of `not_authorized!` inside actions.
|
215
|
+
|
216
|
+
### Model additions
|
217
|
+
|
218
|
+
Action Access is bundled with some model utilities too. By calling
|
219
|
+
`add_access_utilities` in any model it will extend it with a `can?` instance
|
220
|
+
method that checks if the entity (commonly a user) is authorized to perform a
|
221
|
+
given action on a resource.
|
222
|
+
|
223
|
+
`can?` takes two arguments, the action and the resource, and a namespace option
|
224
|
+
if needed. The resource can be a string, symbol, controller class or model
|
225
|
+
instance. Action Access will do the possible to get the right controller out
|
226
|
+
of the resource and the namespace (optional). In the end it returns a boolean.
|
227
|
+
|
228
|
+
**Some examples:**
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
@user.can? :edit, :articles, namespace: :admin
|
232
|
+
@user.can? :edit, @admin_article # Admin::Article instance
|
233
|
+
@user.can? :edit, Admin::ArticlesController
|
234
|
+
# True if the user's clearance level allows her to access 'admin/articles#edit'.
|
235
|
+
```
|
236
|
+
|
237
|
+
`can?` depends on a `clearance_level` method in the model so don't forget it.
|
238
|
+
Continuing with the `User` model from before:
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
class User < ActiveRecord::Base
|
242
|
+
add_access_utilities
|
243
|
+
|
244
|
+
belongs_to :role
|
245
|
+
|
246
|
+
def clearance_level
|
247
|
+
role.name
|
248
|
+
end
|
249
|
+
end
|
250
|
+
```
|
251
|
+
|
252
|
+
```erb
|
253
|
+
<% if current_user.can? :edit, :articles %>
|
254
|
+
<%= link_to 'Edit article', edit_article_path(@article) %>
|
255
|
+
<% end %>
|
256
|
+
```
|
257
|
+
|
258
|
+
### The keeper
|
259
|
+
|
260
|
+
The **keeper** is the core of Action Access, it's the one that registers
|
261
|
+
permissions and who decides if a clearance level grants access or not.
|
262
|
+
|
263
|
+
It's available as `keeper` within controllers and views and as
|
264
|
+
`ActionAccess::Keeper.instance` anywhere else. You can use it check permissions
|
265
|
+
with the `lets?` method that takes the clearance level as the first argument,
|
266
|
+
the rest are the same as `can?`.
|
267
|
+
|
268
|
+
```ruby
|
269
|
+
# Filter a list of users to only those allowed to edit articles.
|
270
|
+
@users.select { |user| keeper.lets? user.role.name, :edit, :articles }
|
271
|
+
```
|
272
|
+
|
273
|
+
|
274
|
+
## License
|
275
|
+
|
276
|
+
Action Access is released under the [MIT License](http://opensource.org/licenses/MIT).
|
277
|
+
|
278
|
+
Copyright (c) 2014 Matías A. Gagliano.
|
279
|
+
|
280
|
+
|
281
|
+
## Contributing
|
282
|
+
|
283
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|