Authentication ⇔ Login/Logout
Authorization ⇔ Permissions
Scar tissue
- makandra is a MVP factory
- We start a new Rails project every 2 months
- We've seen many authorization requirements in the wild
- When it comes to authorization rules there is no limit to the human imagination
Managers may edit bookings
Managers may edit bookings
... of their subordinates
Managers may edit bookings
... of their subordinates
... if the booking isn't older than two days
Managers may edit bookings
... of their subordinates
... if the booking isn't older than two days
... but only on workdays
Managers may edit bookings
... of their subordinates
... if the booking isn't older than two days
... but only on workdays
... and when the manager has a confirmed contract
Managers may edit bookings
... of their subordinates
... if the booking isn't older than two days
... but only on workdays
... and when the manager has a confirmed contract
... or when they're acting as a proxy for a sick manager colleague
Fighting back
- "We need Turing-complete authorization descriptions"
(Zed Shaw)
- We need to quarantine the crazy
- We need a solution that works with large amounts of data
Authorization anti-patterns (1)
Authorization in the model
class Post < ActiveRecord::Base
belongs_to :author
validate :author_is_admin
def author_is_admin
unless author.role == 'admin'
errors.add(:author_id, 'must be an admin')
end
end
end
- Authorization logic is spread through many models.
- You can no longer save Post models outside a web form. This will give you pain in tests, on the console, in background jobs, etc.
- Skip authorization when not in a controller context.
Authorization anti-patterns (2)
Authorization everywhere
class UsersController < ApplicationController
def update
user = User.find(params[:id])
unless user.supervisor == current_user
raise "Trying to edit unauthorized user!"
end
if params[:user][:role] == 'admin'
raise "Trying to set unauthorized value!"
end
user.update_attributes(params[:user])
end
end
- Authorization logic is spread throughout the application
- Crazy to test
- Changing authorization rules means reviewing all code and tests
Four rules to stay sane
1 Reduce requirements to sets of accessible things
2 Contain sets of accessible things in a central repository
3 Authorize against sets of accessible things from the repository
4 Skip authorization when not in a controller context
The building blocks of authorization requirements
- Restricting access to a resource
- One resource, different restrictions
- Roles
- Sensitive attributes
- Restricting attribute values
- Restricting what may be associated
- Authorized values that change with time
- Authorized values that depend on other fields
Restricting access to a resource
A user may only see her own posts
A user may only see her own posts
1 Reduce requirements to sets of accessible things
Post.where(:author_id => user.id)
A user may only see her own posts
2 Contain sets of accessible things in a central repository
class Power
include Consul::Power
def initialize(user)
@user = user
end
power :posts do
Post.where(:author_id => user.id)
end
end
Note how we are not storing permissions with the user.
A user may only see her own posts
2 Contain sets of accessible things in a central repository
Power.current = Power.new(user)
Power.current.posts # => #<ActiveRecord::Relation>
Power.current.posts? # => true or false
Power.current.post?(post) # => true or false
A user may only see her own posts
3 Authorize against sets of accessible things from the repository
class PostsController < ApplicationController
def show
@object = end_of_association_chain.find(params[:id])
end
def index
@collection = end_of_association_chain.all
end
def end_of_association_chain
Power.current.posts
end
end
A user may only see her own posts
3 Authorize against sets of accessible things from the repository
class PostsController < ApplicationController
resource_controller
def end_of_association_chain
Power.current.posts
end
end
A user may only see her own posts
3 Authorize against sets of accessible things from the repository
class PostsController < ApplicationController
resource_controller
power :posts, :as => :end_of_association_chain
end
One resource, different restrictions
A user may edit her posts if they aren't published yet
A user may edit her posts if they aren't published yet
1 Reduce requirements to sets of accessible things
class Power
# ...
power :posts do
Post.where(:author_id => user.id)
end
power :updatable_posts do
posts.where(:published => false)
end
end
A user may edit her posts if they aren't published yet
3 Authorize against sets of accessible things from the repository
class PostsController < ApplicationController
resource_controller
def end_of_association_chain
if action_name == 'edit' || action_name == 'update'
Power.current.updatable_posts
else
Power.current.posts
end
end
end
A user may edit her posts if they aren't published yet
3 Authorize against sets of accessible things from the repository
class PostsController < ApplicationController
resource_controller
power :crud => :posts, :as => :end_of_association_chain
end
Roles
What posts a user may see depends on her role
What posts a user may see depends on her role
1 Reduce requirements to sets of accessible things
class Power
def initialize(user)
@user = user
end
power :posts do
case role
when :admin then Post
when :author then Post.where(:author_id => user.id)
when :guest then Post.where(:open => true)
end
end
private
def role
@user.role.to_sym
end
end
What posts a user may see depends on her role
3 Authorize against sets of accessible things from the repository
class PostsController < ApplicationController
resource_controller
power :crud => :posts, :as => :end_of_association_chain
end
-
Note that the controller did not change
-
This is because the only thing the controller knows about
authorization is that there are sets of accessible things
Sensitive attributes
Authors may edit a post, but only admins may change its state
Authors may edit a post, but only admins may change its state
1 Reduce requirements to sets of accessible things
class Power
# ...
power :assignable_post_fields do
case role
when :admin then %w[subject body state]
when :author then %w[subject body]
end
end
end
Authors may edit a post, but only admins may change its state
3 Authorize against sets of accessible things from the repository
class PostsController < ApplicationController
# ...
def update
@object = end_of_association_chain.find(params[:id])
@object.update_attributes!(object_params)
redirect_to @object
end
def object_params
params[:post].slice(*Power.current.assignable_post_fields)
end
end
Note that we're not using Rails' mass assignment protection.
Restricting attribute values
Authors may change the state of a post,
but only admins may set it to 'published'
Authors may change the state of a post,
but only admins may set it to 'published'
1 Reduce requirements to sets of accessible things
class Post < ActiveRecord::Base
validates_inclusion_of :state,↲
:in => %w[draft delivered published]
end
- Unquarantained authorization logic in the model
- The list of assignable values cannot change depending on circumstances, e.g. a different user role.
Authors may change the state of a post,
but only admins may set it to 'published'
1 Reduce requirements to sets of accessible things
class Post < ActiveRecord::Base
assignable_values_for :state do
%w[draft delivered published]
end
end
- Better in that the list of assignable values can now be dynamic
- Now let's move it into the
Power
repository
Authors may change the state of a post,
but only admins may set it to 'published'
2 Contain sets of accessible things in a central repository
class Post < ActiveRecord::Base
assignable_values_for :state, ↲
:through => lambda { Power.current }
end
Authors may change the state of a post,
but only admins may set it to 'published'
2 Contain sets of accessible things in a central repository
class Post < ActiveRecord::Base
authorize_values_for :state
end
Authors may change the state of a post,
but only admins may set it to 'published'
2 Contain sets of accessible things in a central repository
class Power
# ...
power :assignable_post_states do
case role
when :admin then %w[draft delivered published]
when :author then %w[draft delivered]
end
end
end
Authors may change the state of a post,
but only admins may set it to 'published'
3 Authorize against sets of accessible things from the repository
The authorization rule manifests as a validation in the Post
model:
post = Post.new(:state => 'published')
Power.current = Power.new(admin_user)
post.assignable_states # => ['draft', 'delivered', 'published']
post.valid? # => true
Power.current = Power.new(author_user)
post.assignable_states # => ['draft', 'delivered']
post.valid? # => false
Authors may change the state of a post,
but only admins may set it to 'published'
4 Skip authorization when not in a controller context
post = Post.new(:state => 'published')
Power.current = Power.new(author_user)
post.assignable_states # => ['draft', 'delivered']
post.valid? # => false
Power.current = nil
post.valid? # => true
Authors may change the state of a post,
but only admins may set it to 'published'
3 Authorize against sets of accessible things from the repository
class PostsController < ApplicationController
resource_controller
power :crud => :posts
end
Again the controller didn't need to change.
Restricting what may be associated
Users with locked accounts may not be assigned as post authors
Users with locked accounts may not be assigned as post authors
- Essentially the same as restricting attribute values, except you're dealing with foreign keys and scopes instead of values and arrays.
- assignable_values gives you the same syntax for both cases.
class Post < ActiveRecord::Base
belongs_to :author
authorize_values_for :author
end
class Power
# ...
power :assignable_post_authors do
User.where(:locked => false)
end
end
Authorized values that change with time
What may be valid today may not be valid tomorrow
What happens to a user's posts when I lock her account?
class Power
# ...
power :assignable_post_authors do
User.where(:locked => false)
end
end
- All her posts become invalid!
- This happens all the time in even moderately complex scenarios
- We cannot have invalid records in the database
- Solution: Validate only when a value changes
Authorized values that depend on other fields
A user may change the state of all posts,
but only publish posts by their subordinates.
A user may change the state of all posts,
but only publish posts by their subordinates.
class Post < ActiveRecord::Base
authorize_values_for :state
end
class Power
# ...
power :assignable_post_states do |post|
if user.supervisor_of?(post.author)
%w[draft pending published]
else
%w[draft pending]
end
end
end
end