Class: Rend::Acl

Inherits:
Object
  • Object
show all
Includes:
Core::Helpers::Php
Defined in:
lib/rend/acl.rb,
lib/rend/acl/role.rb,
lib/rend/acl/version.rb,
lib/rend/acl/resource.rb,
lib/rend/acl/assertion.rb,
lib/rend/acl/exception.rb,
lib/rend/acl/role/registry.rb,
lib/rend/acl/role/registry/exception.rb

Defined Under Namespace

Modules: Version Classes: Assertion, Exception, Resource, Role

Constant Summary collapse

TYPE_ALLOW =
:TYPE_ALLOW
TYPE_DENY =
:TYPE_DENY
OP_ADD =
:OP_ADD
OP_REMOVE =
:OP_REMOVE

Instance Method Summary collapse

Constructor Details

#initializeAcl

Returns a new instance of Acl.



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/rend/acl.rb', line 18

def initialize
  # @var Rend::Acl::Role::Registry
  @_role_registry = nil

  # @var Hash
  @_resources = {}

  # @var Rend::Acl::Role
  @_is_allowed_role     = nil

  # @var Rend::Acl::Resource
  @_is_allowed_resource = nil

  # @var String
  @_is_allowed_privilege = nil

  # ACL rules whitelist (deny everything to all) by default
  # @var Hash
  @_rules = {
    :all_resources => {
      :all_roles => {
        :all_privileges => {
          :type       => TYPE_DENY,
          :assertion  => nil
        },
        :by_privilege_id => {}
      },
      :by_role_id => {}
    },
    :by_resource_id => {}
  }
end

Instance Method Details

#add!(*args) ⇒ Object

Adds Roles & Resources in various ways.

  • Roles

    • Arguments .add! Rend::Acl::Role.new(“editor”) # Single Role .add! Rend::Acl::Role.new(“editor”), ‘guest’ # Single Role w/ Single Inheritance .add! Rend::Acl::Role.new(“editor”), [‘guest’, ‘contributor’] # Single Role w/ Multiple Inheritance

    • Hash .add! :role => ‘editor’ # Single Role .add! :role => => ‘guest’ # Single Role w/ Single Inheritance .add! :role => => [‘guest’, ‘contributor’] # Single Role w/ Multiple Inheritance .add! :role => [‘guest’, ‘editor’] # Multiple Roles .add! :role => [‘guest’, ‘contributor’, => ‘guest’] # Multiple Roles w/ Single Inheritance .add! :role => [‘guest’, ‘contributor’, => [‘guest’, ‘contributor’]] # Multiple Roles w/ Multiple Inheritance

  • Resources

    • Arguments .add! Rend::Acl::Resource.new(“city”) # Single Resource .add! Rend::Acl::Resource.new(“building”), ‘city’ # Single Resource w/ Inheritance

    • Hash .add! :resource => ‘city’ # Single Resource .add! :resource => => ‘city’ # Single Resource w/ Inheritance .add! :resource => [‘city’, ‘building’] # Multiple Resources .add! :resource => [‘city’, ‘building’, => ‘city’] # Multiple Resources w/ Inheritance

  • Combined Roles & Resources

    .add! :role => ['guest', {'editor' => 'guest'}], :resource => ['city', {'building' => 'city'}]
    

Raises:

  • (ArgumentError)


77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/rend/acl.rb', line 77

def add!(*args)
  raise ArgumentError, "wrong number of arguments(0 for 1..2)" if args.empty?
  method_args = {:role => [], :resource => []}
  case args[0]
  when Rend::Acl::Role      then method_args[:role]     << args
  when Rend::Acl::Resource  then method_args[:resource] << args
  when Hash
    args[0].each do |key, value|
      if [:role, :resource].include?(key.to_sym)
        case value
        when String then method_args[key] << value
        when Hash   then method_args[key] << value.flatten
        when Array  then value.each {|x| method_args[key] << (x.is_a?(Hash) ? x.flatten : x) }
        else
          raise Rend::Acl::Exception, "Invalid value (#{value.inspect}) for key (#{key.to_s}) in options hash."
        end
      else
        raise Rend::Acl::Exception, "Invalid key (#{key.to_s}) in options hash."
      end
    end
  else
    raise Rend::Acl::Exception, "First argument is not an instance of Rend::Acl::Role, Rend::Acl::Resource, or Hash."
  end
  method_args.each do |type, arguments|
    method = "add_#{type.to_s}!".to_sym
    arguments.each {|args| send(method, *args)}
  end
  self
end

#add_resource!(resource, parent = nil) ⇒ Object

Adds a Resource having an identifier unique to the ACL

The parent parameter may be a reference to, or the string identifier for, the existing Resource from which the newly added Resource will inherit.

Parameters:

  • Rend::Acl::Resource|string

    resource

  • Rend::Acl::Resource|string

    parent

Returns:

  • Rend::Acl Provides a fluent interface

Raises:



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/rend/acl.rb', line 230

def add_resource!(resource, parent = nil)
  resource = Rend::Acl::Resource.new(resource) if resource.is_a?(String)
  type_hint! Rend::Acl::Resource, resource, :is_required => true

  resource_id = resource.id

  raise Rend::Acl::Exception, "Resource id 'resource_id' already exists in the ACL" if resource?(resource_id)

  resource_parent = nil

  if parent
    begin
      resource_parent_id  = (parent.class <= Rend::Acl::Resource) ? parent.id : parent
      resource_parent     = resource!(resource_parent_id)
    rescue Rend::Acl::Exception
      raise Rend::Acl::Exception, "Parent Resource id 'resource_parent_id' does not exist"
    end
    @_resources[resource_parent_id][:children][resource_id] = resource
  end

  @_resources[resource_id] = { :instance => resource, :parent => resource_parent, :children => {} }
  self
end

#add_role!(role, parents = nil) ⇒ Object

Adds a Role having an identifier unique to the registry

The parents parameter may be a reference to, or the string identifier for, a Role existing in the registry, or parents may be passed as an array of these - mixing string identifiers and objects is ok - to indicate the Roles from which the newly added Role will directly inherit.

In order to resolve potential ambiguities with conflicting rules inherited from different parents, the most recently added parent takes precedence over parents that were previously added. In other words, the first parent added will have the least priority, and the last parent added will have the highest priority.

Parameters:

  • Rend::Acl::Role|string

    role

  • Rend::Acl::Role|string|array

    parents

Returns:

  • Rend::Acl Provides a fluent interface



124
125
126
127
128
129
# File 'lib/rend/acl.rb', line 124

def add_role!(role, parents = nil)
  role = Rend::Acl::Role.new(role) if role.is_a?(String)
  type_hint! Rend::Acl::Role, role, :is_required => true
  role_registry.add!(role, parents)
  self
end

#allow!(roles = nil, resources = nil, privileges = nil, assertion = nil) ⇒ Object

Adds an “allow” rule to the ACL

Parameters:

  • Rend::Acl::Role|string|array

    roles

  • Rend::Acl::Resource|string|array

    resources

  • string|array

    privileges

  • Rend::Acl::Assertion

    assertion

Returns:

  • Rend::Acl Provides a fluent interface



367
368
369
370
371
372
373
374
375
376
377
378
379
# File 'lib/rend/acl.rb', line 367

def allow!(roles = nil, resources = nil, privileges = nil, assertion = nil)
  if roles.is_a?(Hash)
    options     = roles
    roles       = options.fetch(:role,      nil)
    resources   = options.fetch(:resource,  nil)
    privileges  = options.fetch(:privilege, nil)
    assertion   = options.fetch(:assertion, nil)
  end

  type_hint! Rend::Acl::Assertion, assertion

  set_rule!(OP_ADD, TYPE_ALLOW, roles, resources, privileges, assertion)
end

#allowed?(role = nil, resource = nil, privilege = nil) ⇒ Boolean

Returns true if and only if the Role has access to the Resource

The role and resource parameters may be references to, or the string identifiers for, an existing Resource and Role combination.

If either role or resource is nil, then the query applies to all Roles or all Resources, respectively. Both may be nil to query whether the ACL has a “blacklist” rule (allow everything to all). By default, Rend::Acl creates a “whitelist” rule (deny everything to all), and this method would return false unless this default has been overridden (i.e., by executing acl->allow()).

If a privilege is not provided, then this method returns false if and only if the Role is denied access to at least one privilege upon the Resource. In other words, this method returns true if and only if the Role is allowed all privileges on the Resource.

This method checks Role inheritance using a depth-first traversal of the Role registry. The highest priority parent (i.e., the parent most recently added) is checked first, and its respective parents are checked similarly before the lower-priority parents of the Role are checked.

Parameters:

  • Rend::Acl::Role|string

    role

  • Rend::Acl::Resource|string

    resource

  • string

    privilege

Returns:

  • (Boolean)

    boolean



540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
# File 'lib/rend/acl.rb', line 540

def allowed?(role = nil, resource = nil, privilege = nil)
  # reset role & resource to nil
  @_is_allowed_role       = nil
  @_is_allowed_resource   = nil
  @_is_allowed_privilege  = nil

  # Readability
  if role.is_a?(Hash)
    options   = role
    role      = options.fetch(:role,      nil)
    resource  = options.fetch(:resource,  nil)
    privilege = options.fetch(:privilege, nil)
  end

  if role
    # keep track of originally called role
    @_is_allowed_role = role
    role = role_registry.get!(role)
    @_is_allowed_role = role unless @_is_allowed_role.class <= Rend::Acl::Role
  end

  if resource
    # keep track of originally called resource
    @_is_allowed_resource = resource
    resource = resource!(resource)
    unless @_is_allowed_resource.class <= Rend::Acl::Resource
      @_is_allowed_resource = resource
    end
  end


  if privilege.nil?
    # query on all privileges
    loop do # loop terminates at :all_resources pseudo-parent
      # depth-first search on role if it is not :all_roles pseudo-parent
      if !role.nil? && !(result = _role_dfs_all_privileges(role, resource)).nil?
        return result
      end


      # look for rule on :all_roles psuedo-parent
      rules = _rules(resource, nil)
      if rules
        rules[:by_privilege_id].each do |priv, rule|
          rule_type_one_privilege = _rule_type(resource, nil, priv)
          return false if rule_type_one_privilege == TYPE_DENY
        end
        rule_type_one_privilege = _rule_type(resource, nil, nil)
        return rule_type_one_privilege == TYPE_ALLOW if rule_type_one_privilege
      end

      # try next Resource
      resource = @_resources[resource.id][:parent]
    end
  else
    @_is_allowed_privilege = privilege
    # query on one privilege
    loop do # loop terminates at :all_resources pseudo-parent
      # depth-first search on role if it is not :all_roles pseudo-parent
      if !role.nil? && !(result = _role_dfs_one_privilege(role, resource, privilege)).nil?
        return result
      end

      # look for rule on 'allRoles' pseudo-parent
      if nil != (rule_type = _rule_type(resource, nil, privilege))
        return TYPE_ALLOW == rule_type
      elsif nil != (rule_type_all_privileges = _rule_type(resource, nil, nil))
        return TYPE_ALLOW == rule_type_all_privileges
      end

      # try next Resource
      resource = @_resources[resource.id][:parent]
    end
  end
end

#denied?(*args) ⇒ Boolean

Inverse of allowed? method

Returns:

  • (Boolean)


617
618
619
# File 'lib/rend/acl.rb', line 617

def denied?(*args)
  !allowed?(*args)
end

#deny!(roles = nil, resources = nil, privileges = nil, assertion = nil) ⇒ Object

Adds a “deny” rule to the ACL

Parameters:

  • Rend::Acl::Role|string|array

    roles

  • Rend::Acl::Resource|string|array

    resources

  • string|array

    privileges

  • Rend::Acl::Assertion

    assertion

Returns:

  • Rend::Acl Provides a fluent interface



389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/rend/acl.rb', line 389

def deny!(roles = nil, resources = nil, privileges = nil, assertion = nil)
  if roles.is_a?(Hash)
    options     = roles
    roles       = options.fetch(:role,      nil)
    resources   = options.fetch(:resource,  nil)
    privileges  = options.fetch(:privilege, nil)
    assertion   = options.fetch(:assertion, nil)
  end

  type_hint! Rend::Acl::Assertion, assertion

  set_rule!(OP_ADD, TYPE_DENY, roles, resources, privileges, assertion)
end

#inherits_resource?(resource, inherit, only_parent = false) ⇒ Boolean

Returns true if and only if resource inherits from inherit

Both parameters may be either a Resource or a Resource identifier. If only_parent is true, then resource must inherit directly from inherit in order to return true. By default, this method looks through the entire inheritance tree to determine whether resource inherits from inherit through its ancestor Resources.

Parameters:

  • Rend::Acl::Resource|string

    resource

  • Rend::Acl::Resource|string

    inherit

  • boolean

    only_parent

Returns:

  • (Boolean)

    boolean



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/rend/acl.rb', line 292

def inherits_resource?(resource, inherit, only_parent = false)
  resource_id = resource!(resource).id
  inherit_id  = resource!(inherit).id

  if @_resources[resource_id][:parent]
    parent_id = @_resources[resource_id][:parent].id
    return true   if inherit_id == parent_id
    return false  if only_parent
  else
    return false
  end

  while @_resources[parent_id][:parent]
    parent_id = @_resources[parent_id][:parent].id
    return true if inherit_id == parent_id
  end
  false
end

#inherits_role?(role, inherit, only_parents = false) ⇒ Boolean

Returns true if and only if role inherits from inherit

Both parameters may be either a Role or a Role identifier. If only_parents is true, then role must inherit directly from inherit in order to return true. By default, this method looks through the entire inheritance DAG to determine whether role inherits from inherit through its ancestor Roles.

Parameters:

  • Rend::Acl::Role|string

    role

  • Rend::Acl::Role|string

    inherit

  • boolean

    only_parents

Returns:

  • (Boolean)

    boolean



166
167
168
# File 'lib/rend/acl.rb', line 166

def inherits_role?(role, inherit, only_parents = false)
  role_registry.inherits?(role, inherit, only_parents)
end

#remove_allow!(roles = nil, resources = nil, privileges = nil, assertion = nil) ⇒ Object

Removes “allow” permissions from the ACL

Parameters:

  • Rend::Acl::Role|string|array

    roles

  • Rend::Acl::Resource|string|array

    resources

  • string|array

    privileges

Returns:

  • Rend::Acl Provides a fluent interface



410
411
412
413
414
415
416
417
418
419
420
# File 'lib/rend/acl.rb', line 410

def remove_allow!(roles = nil, resources = nil, privileges = nil, assertion = nil)
  if roles.is_a?(Hash)
    options     = roles
    roles       = options.fetch(:role,      nil)
    resources   = options.fetch(:resource,  nil)
    privileges  = options.fetch(:privilege, nil)
    assertion   = options.fetch(:assertion, nil)
  end

  set_rule!(OP_REMOVE, TYPE_ALLOW, roles, resources, privileges, assertion)
end

#remove_deny!(roles = nil, resources = nil, privileges = nil, assertion = nil) ⇒ Object

Removes “deny” restrictions from the ACL

Parameters:

  • Rend::Acl::Role|string|array

    roles

  • Rend::Acl::Resource|string|array

    resources

  • string|array

    privileges

Returns:

  • Rend::Acl Provides a fluent interface



429
430
431
432
433
434
435
436
437
438
439
# File 'lib/rend/acl.rb', line 429

def remove_deny!(roles = nil, resources = nil, privileges = nil, assertion = nil)
  if roles.is_a?(Hash)
    options     = roles
    roles       = options.fetch(:role,      nil)
    resources   = options.fetch(:resource,  nil)
    privileges  = options.fetch(:privilege, nil)
    assertion   = options.fetch(:assertion, nil)
  end

  set_rule!(OP_REMOVE, TYPE_DENY, roles, resources, privileges, assertion)
end

#remove_resource!(resource) ⇒ Object

Removes a Resource and all of its children

The resource parameter can either be a Resource or a Resource identifier.

Parameters:

  • Rend::Acl::Resource|string

    resource

Returns:

  • Rend::Acl Provides a fluent interface



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/rend/acl.rb', line 318

def remove_resource!(resource)
  resource_id       = resource!(resource).id
  resources_removed = [resource_id]

  if resource_parent = @_resources[resource_id][:parent]
    @_resources[resource_parent.id][:children].delete(resource_id)
  end

  @_resources[resource_id][:children].each do |child_id, child|
    remove_resource!(child_id)
    resources_removed.push(child_id)
  end

  resources_removed.each do |resource_id_removed|
    @_rules[:by_resource_id].each do |resource_id_current, rules|
      if resource_id_removed == resource_id_current
        @_rules[:by_resource_id].delete(resource_id_current)
      end
    end
  end

  @_resources.delete(resource_id)

  self
end

#remove_resource_all!Object

Removes all Resources

Returns:

  • Rend::Acl Provides a fluent interface



347
348
349
350
351
352
353
354
355
356
357
# File 'lib/rend/acl.rb', line 347

def remove_resource_all!
  @_resources.each do |resource_id, resource|
    @_rules[:by_resource_id].each do |resource_id_current, rules|
      @_rules[:by_resource_id].delete(resource_id_current) if resource_id == resource_id_current
    end
  end

  @_resources = {}

  self
end

#remove_role!(role) ⇒ Object

Removes the Role from the registry

The role parameter can either be a Role or a Role identifier.

Parameters:

  • Rend::Acl::Role|string

    role

Returns:

  • Rend::Acl Provides a fluent interface



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/rend/acl.rb', line 177

def remove_role!(role)
  role_registry.remove!(role)

  role_id = (role.class <= Rend::Acl::Role) ? role.id : role

  @_rules[:all_resources][:by_role_id].each do |role_id_current, rules|
    if role_id == role_id_current
      @_rules[:all_resources][:by_role_id].delete(role_id_current)
    end
  end

  @_rules[:by_resource_id].each do |resource_id_current, visitor|
    if visitor.has_key?(:by_role_id)
      visitor[:by_role_id].each do |role_id_current, rules|
        if role_id == role_id_current
          @_rules[:by_resource_id][resource_id_current][:by_role_id].delete(role_id_current)
        end
      end
    end
  end

  self
end

#remove_role_all!Object

Removes all Roles from the registry

Returns:

  • Rend::Acl Provides a fluent interface



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/rend/acl.rb', line 205

def remove_role_all!
  role_registry.remove_all!

  @_rules[:all_resources][:by_role_id].each do |role_id_current, rules|
    @_rules[:all_resources][:by_role_id].delete(role_id_current)
  end

  @_rules[:by_resource_id].each do |resource_id_current, visitor|
    visitor[:by_role_id].each do |role_id_current, rules|
      @_rules[:by_resource_id][resource_id_current][:by_role_id].delete(role_id_current)
    end
  end

  self
end

#resource!(resource) ⇒ Object

Returns the identified Resource

The resource parameter can either be a Resource or a Resource identifier.

Parameters:

  • Rend::Acl::Resource|string

    resource

Returns:

  • Rend::Acl::Resource

Raises:



262
263
264
265
266
# File 'lib/rend/acl.rb', line 262

def resource!(resource)
  resource_id = (resource.class <= Rend::Acl::Resource) ? resource.id : resource.to_s
  raise Rend::Acl::Exception, "Resource 'resource_id' not found" unless resource?(resource)
  @_resources[resource_id][:instance]
end

#resource?(resource) ⇒ Boolean

Returns true if and only if the Resource exists in the ACL

The resource parameter can either be a Resource or a Resource identifier.

Parameters:

  • Rend::Acl::Resource|string

    resource

Returns:

  • (Boolean)

    boolean



274
275
276
277
# File 'lib/rend/acl.rb', line 274

def resource?(resource)
  resource_id = (resource.class <= Rend::Acl::Resource) ? resource.id : resource.to_s
  @_resources.keys.include?(resource_id)
end

#resourcesObject

Returns array of registered resources.

Returns:

  • array of registered resources



642
643
644
# File 'lib/rend/acl.rb', line 642

def resources
  @_resources.keys
end

#role!(role) ⇒ Object

Returns the identified Role

The role parameter can either be a Role or Role identifier.

Parameters:

  • Rend::Acl::Role|string

    role

Returns:

  • Rend::Acl::Role



138
139
140
# File 'lib/rend/acl.rb', line 138

def role!(role)
  role_registry.get!(role)
end

#role?(role) ⇒ Boolean

Returns true if and only if the Role exists in the registry

The role parameter can either be a Role or a Role identifier.

Parameters:

  • Rend::Acl::Role|string

    role

Returns:

  • (Boolean)

    boolean



149
150
151
# File 'lib/rend/acl.rb', line 149

def role?(role)
  role_registry.has?(role)
end

#role_registryObject

Returns the Role registry for this ACL

If no Role registry has been created yet, a new default Role registry is created and returned.

Returns:

  • Rend::Acl::Role::Registry



627
628
629
# File 'lib/rend/acl.rb', line 627

def role_registry
  @_role_registry ||= Rend::Acl::Role::Registry.new
end

#rolesObject

Returns an array of registered roles.

Note that this method does not return instances of registered roles, but only the role identifiers.

Returns:

  • array of registered roles



637
638
639
# File 'lib/rend/acl.rb', line 637

def roles
  role_registry.roles.keys
end

#set_rule!(operation, type, roles = nil, resources = nil, privileges = nil, assertion = nil) ⇒ Object

Performs operations on ACL rules

The operation parameter may be either OP_ADD or OP_REMOVE, depending on whether the user wants to add or remove a rule, respectively:

OP_ADD specifics:

A rule is added that would allow one or more Roles access to [certain privileges
upon] the specified Resource(s).

OP_REMOVE specifics:

The rule is removed only in the context of the given Roles, Resources, and privileges.
Existing rules to which the remove operation does not apply would remain in the
ACL.

The type parameter may be either TYPE_ALLOW or TYPE_DENY, depending on whether the rule is intended to allow or deny permission, respectively.

The roles and resources parameters may be references to, or the string identifiers for, existing Resources/Roles, or they may be passed as arrays of these - mixing string identifiers and objects is ok - to indicate the Resources and Roles to which the rule applies. If either roles or resources is nil, then the rule applies to all Roles or all Resources, respectively. Both may be nil in order to work with the default rule of the ACL.

The privileges parameter may be used to further specify that the rule applies only to certain privileges upon the Resource(s) in question. This may be specified to be a single privilege with a string, and multiple privileges may be specified as an array of strings.

Parameters:

  • string

    operation

  • string

    type

  • Rend::Acl::Role|string|array

    roles

  • Rend::Acl::Resource|string|array

    resources

  • string|array

    privileges

  • Rend::Acl::Assert::Interface

    assertion

Returns:

  • Rend::Acl Provides a fluent interface



481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
# File 'lib/rend/acl.rb', line 481

def set_rule!(operation, type, roles = nil, resources = nil, privileges = nil, assertion = nil)
  type_hint! Rend::Acl::Assertion, assertion

  # ensure that the rule type is valid normalize input to uppercase
  if type != TYPE_ALLOW && type != TYPE_DENY
    raise Rend::Acl::Exception, "Unsupported rule type must be either '#{TYPE_ALLOW}' or '#{TYPE_DENY}'"
  end

  # ensure that all specified Roles exist normalize input to array of Role objects or nil
  roles = Array(roles)
  roles << nil if roles.empty?
  roles = roles.reduce([]) {|seed, role| seed << (role ? role_registry.get!(role) : nil)}

  # ensure that all specified Resources exist normalize input to array of Resource objects or nil
  if resources
    resources = Array(resources)
    resources << nil if resources.empty?
    resources = resources.reduce([]) {|seed, resource| seed << (resource ? resource!(resource) : nil)}
  end

  # normalize privileges to array
  privileges = Array(privileges).compact

  case operation
  when OP_ADD     then _add_rule!(type, roles, resources, privileges, assertion)
  when OP_REMOVE  then _remove_rule!(type, roles, resources, privileges, assertion)
  else
    raise Rend::Acl::Exception, "Unsupported operation must be either '#{OP_ADD}' or '#{OP_REMOVE}'"
  end

  self
end