<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array"/>
  <modified type="array">
    <modified>
      <diff>@@ -1,3 +1,5 @@
+* New option :join_by for has_permission_on to allow AND'ing of statements in one has_permission_on block
+
 * Allow using_access_control to be called directly on ActiveRecord::Base, globally enabling model security
 
 * New operator:  intersects_with, comparing two Enumerables in if_attribute</diff>
      <filename>CHANGELOG</filename>
    </modified>
    <modified>
      <diff>@@ -156,7 +156,7 @@ module Authorization
         begin
           options[:skip_attribute_test] or
             rule.attributes.empty? or
-            rule.attributes.any? do |attr|
+            rule.attributes.send(rule.join_operator == :and ? :all? : :any?) do |attr|
               begin
                 attr.validate?( attr_validator )
               rescue NilAttributeValueError =&gt; e
@@ -203,8 +203,15 @@ module Authorization
       user, roles, privileges = user_roles_privleges_from_options(privilege, options)
       attr_validator = AttributeValidator.new(self, user, nil, options[:context])
       matching_auth_rules(roles, privileges, options[:context]).collect do |rule|
-        obligation = rule.attributes.collect {|attr| attr.obligation(attr_validator) }
-        obligation.empty? ? [{}] : obligation
+        obligations = rule.attributes.collect {|attr| attr.obligation(attr_validator) }
+        if rule.join_operator == :and and !obligations.empty?
+          merged_obligation = obligations.first
+          obligations[1..-1].each do |obligation|
+            merged_obligation = merged_obligation.deep_merge(obligation)
+          end
+          obligations = [merged_obligation]
+        end
+        obligations.empty? ? [{}] : obligations
       end.flatten
     end
     
@@ -322,12 +329,13 @@ module Authorization
   end
   
   class AuthorizationRule
-    attr_reader :attributes, :contexts, :role, :privileges
+    attr_reader :attributes, :contexts, :role, :privileges, :join_operator
     
-    def initialize (role, privileges = [], contexts = nil)
+    def initialize (role, privileges = [], contexts = nil, join_operator = :or)
       @role = role
       @privileges = Set.new(privileges)
       @contexts = Set.new((contexts &amp;&amp; !contexts.is_a?(Array) ? [contexts] : contexts))
+      @join_operator = join_operator
       @attributes = []
     end
     </diff>
      <filename>lib/declarative_authorization/authorization.rb</filename>
    </modified>
    <modified>
      <diff>@@ -210,23 +210,27 @@ module Authorization
       # The block form allows to describe restrictions on the permissions
       # using if_attribute.  Multiple has_permission_on statements are
       # OR'ed when evaluating the permissions.  Also, multiple if_attribute
-      # statements in one block are OR'ed.  To AND conditions, place them
-      # in one if_attribute statement.
+      # statements in one block are OR'ed if no :+join_by+ option is given
+      # (see below).  To AND conditions, either set :+join_by+ to :and or place
+      # them in one if_attribute statement.
       # 
       # Available options
       # [:+to+]
       #   A symbol or an array of symbols representing the privileges that
       #   should be granted in this statement.
+      # [:+join_by+]
+      #   Join operator to logically connect the constraint statements inside
+      #   of the has_permission_on block.  May be :+and+ or :+or+.  Defaults to :+or+.
       #
       def has_permission_on (context, options = {}, &amp;block)
         raise DSLError, &quot;has_permission_on only allowed in role blocks&quot; if @current_role.nil?
-        options = {:to =&gt; []}.merge(options)
+        options = {:to =&gt; [], :join_by =&gt; :or}.merge(options)
         
         privs = options[:to] 
         privs = [privs] unless privs.is_a?(Array)
         raise DSLError, &quot;has_permission_on either needs a block or :to option&quot; if !block_given? and privs.empty?
         
-        rule = AuthorizationRule.new(@current_role, privs, context)
+        rule = AuthorizationRule.new(@current_role, privs, context, options[:join_by])
         @auth_rules &lt;&lt; rule
         if block_given?
           @current_rule = rule
@@ -295,7 +299,8 @@ module Authorization
       #   end
       # 
       # Multiple attributes in one :if_attribute statement are AND'ed.
-      # Multiple if_attribute statements are OR'ed.  Thus, the following would
+      # Multiple if_attribute statements are OR'ed if the join operator for the
+      # has_permission_on block isn't explicitly set.  Thus, the following would
       # require the current user either to be of the same branch AND the employee
       # to be &quot;changeable_by_coworker&quot;.  OR the current user has to be the
       # employee in question.</diff>
      <filename>lib/declarative_authorization/reader.rb</filename>
    </modified>
    <modified>
      <diff>@@ -82,6 +82,42 @@ class AuthorizationTest &lt; Test::Unit::TestCase
       engine.obligations(:test, :context =&gt; :permissions, 
           :user =&gt; MockUser.new(:test_role, :attr =&gt; 1))
   end
+
+  def test_obligations_with_anded_conditions
+    reader = Authorization::Reader::DSLReader.new
+    reader.parse %{
+      authorization do
+        role :test_role do
+          has_permission_on :permissions, :to =&gt; :test, :join_by =&gt; :and do
+            if_attribute :attr =&gt; is { user.attr }
+            if_attribute :attr_2 =&gt; is { user.attr_2 }
+          end
+        end
+      end
+    }
+    engine = Authorization::Engine.new(reader)
+    assert_equal [{:attr =&gt; [:is, 1], :attr_2 =&gt; [:is, 2]}],
+      engine.obligations(:test, :context =&gt; :permissions,
+          :user =&gt; MockUser.new(:test_role, :attr =&gt; 1, :attr_2 =&gt; 2))
+  end
+
+  def test_obligations_with_deep_anded_conditions
+    reader = Authorization::Reader::DSLReader.new
+    reader.parse %{
+      authorization do
+        role :test_role do
+          has_permission_on :permissions, :to =&gt; :test, :join_by =&gt; :and do
+            if_attribute :attr =&gt; { :deeper_attr =&gt; is { user.deeper_attr }}
+            if_attribute :attr =&gt; { :deeper_attr_2 =&gt; is { user.deeper_attr_2 }}
+          end
+        end
+      end
+    }
+    engine = Authorization::Engine.new(reader)
+    assert_equal [{:attr =&gt; { :deeper_attr =&gt; [:is, 1], :deeper_attr_2 =&gt; [:is, 2] } }],
+      engine.obligations(:test, :context =&gt; :permissions,
+          :user =&gt; MockUser.new(:test_role, :deeper_attr =&gt; 1, :deeper_attr_2 =&gt; 2))
+  end
   
   def test_obligations_with_conditions_and_empty
     reader = Authorization::Reader::DSLReader.new
@@ -629,6 +665,58 @@ class AuthorizationTest &lt; Test::Unit::TestCase
               :user =&gt; MockUser.new(:test_role),
               :object =&gt; perm_data_attr_2)
   end
+
+  def test_attribute_with_permissions_and_anded_rules
+    reader = Authorization::Reader::DSLReader.new
+    reader.parse %{
+      authorization do
+        role :test_role do
+          has_permission_on :permissions, :to =&gt; :test do
+            if_attribute :test_attr =&gt; 1
+          end
+          has_permission_on :permission_children, :to =&gt; :test, :join_by =&gt; :and do
+            if_permitted_to :test, :permission
+            if_attribute :test_attr =&gt; 1
+          end
+        end
+      end
+    }
+    engine = Authorization::Engine.new(reader)
+
+    perm_data_attr_1 = PermissionMock.new({:test_attr =&gt; 1})
+    perm_data_attr_2 = PermissionMock.new({:test_attr =&gt; 2})
+    assert engine.permit?(:test, :context =&gt; :permission_children,
+              :user =&gt; MockUser.new(:test_role),
+              :object =&gt; MockDataObject.new(:permission =&gt; perm_data_attr_1, :test_attr =&gt; 1))
+    assert !engine.permit?(:test, :context =&gt; :permission_children,
+              :user =&gt; MockUser.new(:test_role),
+              :object =&gt; MockDataObject.new(:permission =&gt; perm_data_attr_2, :test_attr =&gt; 1))
+    assert !engine.permit?(:test, :context =&gt; :permission_children,
+              :user =&gt; MockUser.new(:test_role),
+              :object =&gt; MockDataObject.new(:permission =&gt; perm_data_attr_1, :test_attr =&gt; 2))
+  end
+
+  def test_attribute_with_anded_rules
+    reader = Authorization::Reader::DSLReader.new
+    reader.parse %{
+      authorization do
+        role :test_role do
+          has_permission_on :permissions, :to =&gt; :test, :join_by =&gt; :and do
+            if_attribute :test_attr =&gt; 1
+            if_attribute :test_attr_2 =&gt; 2
+          end
+        end
+      end
+    }
+    engine = Authorization::Engine.new(reader)
+
+    assert engine.permit?(:test, :context =&gt; :permissions,
+              :user =&gt; MockUser.new(:test_role),
+              :object =&gt; MockDataObject.new(:test_attr =&gt; 1, :test_attr_2 =&gt; 2))
+    assert !engine.permit?(:test, :context =&gt; :permissions,
+              :user =&gt; MockUser.new(:test_role),
+              :object =&gt; MockDataObject.new(:test_attr =&gt; 1, :test_attr_2 =&gt; 3))
+  end
   
   def test_raise_on_if_attribute_hash_on_collection
     reader = Authorization::Reader::DSLReader.new</diff>
      <filename>test/authorization_test.rb</filename>
    </modified>
    <modified>
      <diff>@@ -407,6 +407,33 @@ class ModelTest &lt; Test::Unit::TestCase
     TestAttr.delete_all
   end
   
+  def test_named_scope_with_anded_rules
+    reader = Authorization::Reader::DSLReader.new
+    reader.parse %{
+      authorization do
+        role :test_role do
+          has_permission_on :test_attrs, :to =&gt; :read, :join_by =&gt; :and do
+            if_attribute :test_model =&gt; is { user.test_model }
+            if_attribute :attr =&gt; 1
+          end
+        end
+      end
+    }
+    Authorization::Engine.instance(reader)
+    
+    test_model_1 = TestModel.create!
+    test_model_1.test_attrs.create!(:attr =&gt; 1)
+    TestModel.create!.test_attrs.create!(:attr =&gt; 1)
+    TestModel.create!.test_attrs.create!
+    
+    user = MockUser.new(:test_role, :test_model =&gt; test_model_1)
+    assert_equal 1, TestAttr.with_permissions_to(:read, 
+      :context =&gt; :test_attrs, :user =&gt; user).length
+    
+    TestModel.delete_all
+    TestAttr.delete_all
+  end
+  
   def test_named_scope_with_contains
     reader = Authorization::Reader::DSLReader.new
     reader.parse %{</diff>
      <filename>test/model_test.rb</filename>
    </modified>
  </modified>
  <removed type="array"/>
  <parents type="array">
    <parent>
      <id>5af43ff1c238a5ef276c4ef54e3c86960b4cbffd</id>
    </parent>
  </parents>
  <author>
    <name>Steffen Bartsch</name>
    <email>sbartsch@tzi.org</email>
  </author>
  <url>http://github.com/stffn/declarative_authorization/commit/2162e8755ecdc2e5f0581d84847f68a5bc9d678b</url>
  <id>2162e8755ecdc2e5f0581d84847f68a5bc9d678b</id>
  <committed-date>2009-04-01T08:08:19-07:00</committed-date>
  <authored-date>2009-04-01T08:08:19-07:00</authored-date>
  <message>New join_by option for has_permission_on to allow statements inside to be logically AND'ed</message>
  <tree>1164e2f8398dfecea3149005901b82fbe8a7a741</tree>
  <committer>
    <name>Steffen Bartsch</name>
    <email>sbartsch@tzi.org</email>
  </committer>
</commit>
