Solving bizarre authorization requirements

Henning Koch, makandra GmbH

henning.koch@makandra.de
@triskweline

Authentication ⇔ Login/Logout
Authorization ⇔ Permissions
I will talk but authorization today

Scar tissue

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

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 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

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
  1. Scopes of records the current user may see
    Lists of attributes the current user may assign
  2. Crazy authorization requirements go here.
    That repository should not be the user. Make it a "Power" or "Ability" object.
  3. Don't care how the set came to be.
  4. Stuff should work on the console.

Tools

consul

Scope-based authorization solution for Ruby on Rails.
github.com/makandra/consul

assignable_values

Enums for ActiveRecord attributes and associations.
github.com/makandra/assignable_values

... but you can do it in plain Ruby, cancan, etc.

The building blocks of authorization requirements

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
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
  • There is no mention of "roles" anywhere in the controller.
  • Authorization rules are completely hidden away in the Power repository.

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.
  • attr_accessible sucks because it pretends every controller needs the same kind of protection
  • attr_accessible sucks because it is not expressive enough to cover crazy authorization requirements
  • attr_accessible sucks because it changes the behavior of our models outside a controller context
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.
  • attr_accessible sucks because it pretends every controller needs the same kind of protection
  • attr_accessible sucks because it is not expressive enough to cover crazy authorization requirements
  • attr_accessible sucks because it changes the behavior of our models outside a controller context

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
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
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
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

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

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

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
  1. Scopes of records the current user may see
    Lists of attributes the current user may assign
  2. Crazy authorization requirements go here.
    That repository should not be the user. Make it a "Power" or "Ability" object.
  3. Don't care how the set came to be.
  4. Stuff should work on the console.

end

Get to know makandra:
http://www.makandra.de
http://makandra.com

Talk to me afterwards or send me a message:
henning.koch@makandra.de
@triskweline

Replay this talk:
makandra.com/talks

Solving bizarre auth… | Authentication ⇔ Log… | Scar tissue | Managers may edit bo… | Managers may edit bo… | Managers may edit bo… | Managers may edit bo… | Managers may edit bo… | Managers may edit bo… | Fighting back | Authorization anti-p… | Authorization anti-p… | Four rules to stay s… | Tools | The building blocks … | Restricting access t… | A user may only see … | A user may only see … | A user may only see … | A user may only see … | A user may only see … | A user may only see … | A user may only see … | One resource, differ… | A user may edit her … | A user may edit her … | A user may edit her … | Roles | What posts a user ma… | What posts a user ma… | Sensitive attributes | Authors may edit a p… | Authors may edit a p… | Authors may edit a p… | Restricting attribut… | Authors may change t… | Authors may change t… | Authors may change t… | Authors may change t… | Authors may change t… | Authors may change t… | Authors may change t… | Authors may change t… | Restricting what may… | Users with locked ac… | Authorized values th… | Authorized values th… | A user may change th… | The building blocks … | Four rules to stay s… | end | Get to know makandra…

Solving bizarre authorization requirements

Henning Koch, makandra GmbH

henning.koch@makandra.de
@triskweline

Start presentation