Adding a permissions DSL to Active Record

Robert Thau

Smartleaf

The tease...

class Order < ActiveRecord::Base

  access_control_keys ['id', 'owner_id', 'paid']

  require_privilege :place,
    :for_action => :create, 
    :to_update_attribute => [:payment_authenticator, :paid]

  require_privilege :edit,      # LineItem also checks this for attr changes
    :to_associate_as  => ['LineItem#order'],
    :to_dissociate_as => ['LineItem#order'],
    :to_update_attribute => [ :shipping_address ]
  
  require_privilege :ship,       :to_update_attribute => :shipped

  ...

end

The tease...

<% form_for resource, :if_not_permitted => :present_text do |f| %>
  <%= error_messages_for resource_name %>
  Store: <%= f.collection_select :store_id, 
	     Store.find( :all ), :id, :name %>
Shipped: <%= f.check_box :shipped, {}, true, false %>
Ship to: <%= f.text_field :shipping_address %>
Notes: <%= f.text_area :notes %>
... <% end %>

Talk outline:

So, why do this?

Users have access rights

Based on...

Users might be allowed to...

Supporting user "levels"

Stock Rails: Threats and defenses

Threat: faked IDs

Threat: forged inputs

What we'd like...

Users have permission to:

... model objects matching some pattern or test.

Permissioning model objects:

Permissions...

Permissions...

Permissions:

Permission examples

Declarations, examples

class Order < ActiveRecord::Base

  access_control_keys ['id', 'owner_id', 'paid']

  require_privilege :place,
    :for_action => :create, 
    :to_update_attribute => [:payment_authenticator, :paid]

  require_privilege :edit,      # LineItem also checks this for attr changes
    :to_associate_as  => ['LineItem#order'],
    :to_dissociate_as => ['LineItem#order'],
    :to_update_attribute => [ :shipping_address ]
  
  require_privilege :ship,       :to_update_attribute => :shipped

  ...

end

Declarations, the language

require_privilege :privilege, 
  :on_associated => attr,
  :to_invoke               => [:method, :method, ...],
  :to_initialize_attribute => [:attr, :attr, ...],
  :to_update_attribute     => [:attr, :attr, ...],
  :to_set_attribute        => [:attr, :attr, ...],
  :to_access_attribute     => [:attr, :attr, ...],
  :to_associate_as         => ["Class#assoc_name", ...],
  :to_dissociate_as        => ["Class#assoc_name", ...],
  :at_callback             => {:after_find, :before_create, ... },
  :for_action              => {:create, :find, :update, :destroy}

Implementation

Permission checks: in core

Permission checks: in core --- the guts

    return false if perm.target_owned_by_self && 
       obj.owner_id != user.id

    obj.class.access_control_keys.each do |obj_attr|
      target_attr = 'target_' + obj_attr
      target_attr_val = perm[ target_attr ]
      if !target_attr_val.nil? &&
           target_attr_val != obj[ obj_attr ]
        return false
      end
    end

    return true

Enumerating objects

Enumerating objects --- the guts:

  clauses = ["(p.target_owned_by_self = 0 or #{table}.owner_id = :user)"]

  self.access_control_keys.each do |attr|
    clauses <<
      "(p.target_#{attr} is null or " + 
          "#{table}.#{attr} = p.target_#{attr})"
  end

  return <<-END_SQL
    exists
      (select 'x' from permissions p
       where exists (select 'x' from role_assignments
                     where user_id = :user
                       and role_assignments.role_id = p.role_id)
         and (p.privilege  = :privilege or p.privilege = 'any')
         and (p.class_name = :class_name)
         and #{sanitize_sql( [clauses.join(' and '), 
                              { :user => ... }] )
       )
  END_SQL

Declarations: what do they do?

Privilege checks: on events

Privilege checks: on attribute read/write

Privilege checks: on associations

Reflection

It's just Ruby! Class variables and class methods:

Limitations of current code

  • Doesn't work with STI (where_permits gets... tricky)
  • Association priv checks don't work with polymorphic associations
  • User object bundled with application-specific functionality (e.g. single-sign-on support) which may not be of general use.

Questions?

  • [slide title]

    • [point one]
    • [point two]
    • [point three]
    • [point four]
    • [point five]
    [any material that should appear in print but not on the slide]