Class: Credentials::Rulebook

Inherits:
Object
  • Object
show all
Defined in:
lib/credentials/rulebook.rb

Overview

Represents a collection of rules for granting and denying permissions to instances of a class. This is the return type of a call to a class’s credentials method.

Constant Summary collapse

DEFAULT_OPTIONS =
{
  :default => :deny
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(klass) ⇒ Rulebook

Returns a new instance of Rulebook.



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/credentials/rulebook.rb', line 20

def initialize(klass)
  if klass.to_s =~ /^#<Class:#<([\w_]+(?:\:\:[\w_]+)*)/ # there must be a better way
    self.klass = superklass = $1.constantize
  else
    self.klass = klass
    superklass = klass.superclass
  end

  @rules = []
  if superklass == Object
    @superklass_rulebook = nil
    @options = {}
  else
    @superklass_rulebook = superklass.credentials
    @options = superklass_rulebook.options.dup
  end
end

Instance Attribute Details

#klassObject

Returns the value of attribute klass.



11
12
13
# File 'lib/credentials/rulebook.rb', line 11

def klass
  @klass
end

#optionsObject

Returns the value of attribute options.



12
13
14
# File 'lib/credentials/rulebook.rb', line 12

def options
  @options
end

#rulesObject

Returns the value of attribute rules.



13
14
15
# File 'lib/credentials/rulebook.rb', line 13

def rules
  @rules
end

#superklass_rulebookObject (readonly)

Returns the value of attribute superklass_rulebook.



14
15
16
# File 'lib/credentials/rulebook.rb', line 14

def superklass_rulebook
  @superklass_rulebook
end

Class Method Details

.for(klass) ⇒ Object

Creates a Rulebook for the given class. Should not be called directly: instead, use class.credentials (q.v.).



41
42
43
44
# File 'lib/credentials/rulebook.rb', line 41

def self.for(klass)
  @rulebooks ||= {}
  @rulebooks[klass] ||= new(klass)
end

Instance Method Details

#allow?(*args) ⇒ Boolean

Decides whether to allow the requested permission.

Match algorithm

  1. Set ALLOWED to true if permission is specifically allowed by any allow_rules; otherwise, false.

  2. Set DENIED to true if permission is specifically denied by any deny_rules; otherwise, false.

  3. The final result depends on the value of default:

    1. if :allow: ALLOWED OR !DENIED

    2. if :deny: ALLOWED AND !DENIED

Expressed as a table: <table> <thead><tr><th>Matching rules</th><th>Default allow</th><th>Default deny</th></tr></thead> <tbody> <tr><td>None of the allow or deny rules matched.</td><td>allow</td><td>deny</td></tr> <tr><td>Some of the allow rules matched, none of the deny rules matched.</td><td>allow</td><td>allow</td></tr> <tr><td>None of the allow rules matched, some of the deny rules matched.</td><td>deny</td><td>deny</td></tr> <tr><td>Some of the allow rules matched, some of the deny rules matched.</td><td>allow</td><td>deny</td></tr> </tbody> </table>

Returns:

  • (Boolean)


195
196
197
198
199
200
201
202
203
204
# File 'lib/credentials/rulebook.rb', line 195

def allow?(*args)
  allowed = allow_rules.inject(false) { |memo, rule| memo || rule.allow?(*args) }
  denied = deny_rules.inject(false) { |memo, rule| memo || rule.deny?(*args) }
  
  if default == :allow
    allowed or !denied
  else
    allowed and !denied
  end
end

#allow_rulesObject

Subset of rules that grant permission by exposing an allow? method.



207
208
209
210
# File 'lib/credentials/rulebook.rb', line 207

def allow_rules
  @allow_rules ||= (superklass_rulebook ? superklass_rulebook.allow_rules : []) + 
  rules.select { |rule| rule.respond_to? :allow? }
end

#can(*args) ⇒ Object

Declaratively specify a permission. This is usually done in the context of a credentials block (see Credentials::ObjectExtensions::ClassMethods#credentials). The examples below assume that context.

Simple (intransitive) permissions

class User
  credentials do |user|
    user.can :log_in
  end
end

Permission is expressed as a symbol; usually an intransitive verb. Permission can be tested with:

user.can? :log_in

or

user.can_log_in?

Resource (transitive) permissions

class User
  credentials do |user|
    user.can :edit, Post
  end
end

As above, but a resource type is specified. Permission can be tested with:

user.can? :edit, Post.first

or

user.can_edit? Post.first

if and unless

You can specify complex conditions with the if and unless options. These options can be either a symbol (which is assumed to be a method of the instance under test), or a proc, which is passed any non-symbol arguments from the can? method.

class User
  credentials do |user|
    user.can :create, Post, :if => :administrator?
    user.can :edit, Post, :if => lambda { |user, post| user == post.author }
  end
end

user.can? :create, Post  # checks user.administrator?
user.can? :edit, post    # checks user == post.author

Both if and unless options can be specified for the same rule:

class User
  credentials do |user|
    user.can :eat, "chunky bacon", :if => :hungry?, :unless => :vegetarian?
  end
end

So, only hungry users who are not vegetarian can eat chunky bacon.

You can also specify multiple options for if and unless. If there are multiple options for if, any one match will do:

class User
  credentials do |user|
    user.can :go_backstage, :if => [ :crew?, :good_looking? ]
  end
end

However, multiple options for unless must all match:

class User
  credentials(:default => :allow) do |user|
    user.cannot :record, Album, :unless => [ :talented?, :dedicated? ]
  end
end

You cannot record an album unless you are both talented and dedicated. Note that we have specified the default permission as allow in this example: otherwise, the rule would never match.

If your rules are any more complicated than that, you might want to consider using the lambda form of arguments to if and/or unless.

Reflexive permissions (:self)

The following two permissions are identical:

class User
  credentials do |user|
    user.can :edit, User, :if => lambda { |user, another| user == another }
    user.can :edit, :self
  end
end

Prepositions (:for, :on, etc)

You can do the following:

class User
  credentials do |user|
    user.can :delete, Comment, :on => :post
  end
end

user.can? :delete, post.comments.first, :on => post

…means that Credentials will check if:

  • post has a user_id method matching user.id

  • user has a post_id method matching post.id

  • user has a post method matching post

  • user has a posts method that returns an array including post

See Credentials::Prepositions for the list of available prepositions.



149
150
151
# File 'lib/credentials/rulebook.rb', line 149

def can(*args)
  self.rules << AllowRule.new(klass, *args)
end

#cannot(*args) ⇒ Object

Declaratively remove a permission. This is handy to explicitly remove a permission in a child class that you have granted in a parent class. It is also useful if your default is set to allow and you want to tighten up some permissions:

class User
  credentials(:default => :allow) do |user|
    user.cannot :delete, :self
  end
end

See Credentials::Rulebook#can for more on specifying permissions: just remember that everything is backwards!



165
166
167
# File 'lib/credentials/rulebook.rb', line 165

def cannot(*args)
  self.rules << DenyRule.new(klass, *args)
end

#defaultObject

Determines whether to :allow or :deny by default.



170
171
172
# File 'lib/credentials/rulebook.rb', line 170

def default
  options[:default] && options[:default].to_sym
end

#deny_rulesObject

Subset of rules that deny permission by exposing an deny? method.



213
214
215
216
# File 'lib/credentials/rulebook.rb', line 213

def deny_rules
  @deny_rules ||= (superklass_rulebook ? superklass_rulebook.deny_rules : []) + 
  rules.select { |rule| rule.respond_to? :deny? }
end

#empty?Boolean

Returns true if there are no rules defined in this Rulebook.

Returns:

  • (Boolean)


47
48
49
# File 'lib/credentials/rulebook.rb', line 47

def empty?
  rules.empty?
end