Confused about authorization

Confused about authorization

Old forum URL: forums.lhotka.net/forums/t/4600.aspx


Cine posted on Wednesday, April 02, 2008

I've been trying to get my head around the authorization classes in CSLA (3.5), and I am having some problems.

Let me illustrate with a simple example: I have a class called User and some use cases:

Use case 1: The administrator can create a new User object.
Use case 2: The administrator can delete any User objects.
Use case 3: The administrator can change the password of any User object.
Use case 4: The user authenticated by the User object may edit the password of her own User object.
Use case 5: The user authenticated by the User object may delete her own User object.
Use case 6: The exception to uc5 are users in the role "simpleuser", who may not delete themselves.

The authentication we handle with the IIdentity and IPrincipal classes, so no need to worry about that part in my simple design.

UC1: In the GUI with the user list (presuming we have one such) I can use CanCreateObject(typeof(User)) to check if I can show the create button. I can set the rules for this in User.AddObjectAuthorizationRules by using AuthorizationRules.AllowCreate(typeof(User), "administrator"). Also when calling Dataportal.Create this will be double checked. Right so far?

UC2: Add AuthorizationRules.AllowDelete(typeof(User), "administrator") inside User.AddObjectAuthorizationRules. Similar to UC1

UC3: Add AuthorizationRules.AllowWrite(PasswordProperty, "administrator") inside the User.AddAuthorizationRules. In the GUI I can call userobj.CanWriteProperty("Password") to check if I can update the password, and should I forget this then whenever I access GetProperty(PasswordProperty) I will have my access double checked.

UC4: It seems the idea was to put this inside the User.AddInstanceAuthorizationRules, however it is called _before_ I put any values in my object and thus I cannot tell if the ID of the object is the same as the ID of the current IPrincipal. I could add a custom function MyAddInstanceAuthorizationRules which I could call after I initialize all my values, which could then add if (Csla.ApplicationContext.User.Identity.Name == this.Name) AuthorizationRules.InstanceAllowWrite(PasswordProperty, "everybody"). However I fear that if I should ever want to cache my User objects, this will fail miserably. Any hints?

UC5: No clue how to work around this, any hints?

UC6: The solution seems to be simple enough: Add AuthorizationRules.DenyDelete(typeof(User), "simpleuser") inside User.AddObjectAuthorizationRules. However this doesn't work since the check to see if I am allowed will only check the AllowDeleteRules due to UC2. Any hints?

I tried searching the achieves, but I couldn't find anything really. If I missed something, please let me know.

ajj3085 replied on Wednesday, April 02, 2008

UC1 - 3 should use one set of classes, maybe in a .Admin namespace.

UC4 - 6 should use an independent set of classes.  Users can ONLY get thier own user object (User.GetCurrent() perhaps).

Cine replied on Wednesday, April 02, 2008

I found a simple solution for UC4:

      public override bool CanWriteProperty(string propertyName)
      {
         IIdentity identity = ApplicationContext.User.Identity;
         if (propertyName == PasswordProperty.Name &&
             identity.IsAuthenticated &&
             identity.Name == ReadProperty(NameProperty))
            return true;
         return base.CanWriteProperty(propertyName);
      }

However, I still have no idea how I could possible achieve UC5 or 6.
UC5 is possible if I want to make a special case of it in the GUI and fetch the object for myself exclusively, however if I want to just "catch" this special case whenever I happen to have "myself", I don't see a way.
Anyone tried to implement this?

Cine replied on Thursday, April 03, 2008

I can hack the csla code a bit to get what I desire.
By implementing an interface (ICanDecideEditAndDeleteAuthorization) I can now have my object override the decision made deep within Security.AuthorizationRules.

My code for UC5 and 6 then becomes these two functions in my User object:
      bool ICanDecideEditAndDeleteAuthorization.IsDeleteAllowed(bool rolesSaysAllowed)
      {
         IIdentity identity = ApplicationContext.User.Identity;
         if (!identity.IsAuthenticated)
            return rolesSaysAllowed;
         bool ispartofsimleuser = FunctionToDetermineIfUserIsIndeedThis();
         if (ispartofsimleuser)
            return false;
         bool ownedbyuser = identity.Name == ReadProperty(NameProperty);
         if (ownedbyuser)
            return true;
         return rolesSaysAllowed;
      }
      bool ICanDecideEditAndDeleteAuthorization.IsEditAllowed(bool rolesSaysAllowed)
      {
         IIdentity identity = ApplicationContext.User.Identity;
         if (!identity.IsAuthenticated)
            return rolesSaysAllowed;
         bool ownedbyuser = identity.Name == ReadProperty(NameProperty);
         if (ownedbyuser)
            return true;
         return rolesSaysAllowed;
      }


I don't know enough about bindable lists (yet) to figure out how to make it change when I change my selection though.

Property changes on: .
___________________________________________________________________
Name: svn:ignore
   + *.suo

Property changes on: Csla
___________________________________________________________________
Name: svn:ignore
   + bin
obj

Index: Csla/Csla.csproj
===================================================================
--- Csla/Csla.csproj    (revision 2235)
+++ Csla/Csla.csproj    (working copy)
@@ -120,6 +120,7 @@
     <Compile Include="FilteredBindingList.cs">
       <SubType>Code</SubType>
     </Compile>
+    <Compile Include="ICanDecideEditAndDeleteAuthorization.cs" />
     <Compile Include="Linq\CslaQueryProvider.cs" />
     <Compile Include="Linq\IIndex.cs" />
     <Compile Include="Linq\IIndexSearchable.cs" />
Index: Csla/BusinessListBase.cs
===================================================================
--- Csla/BusinessListBase.cs    (revision 2235)
+++ Csla/BusinessListBase.cs    (working copy)
@@ -126,7 +126,7 @@
     {
       get
       {
-        bool auth = Csla.Security.AuthorizationRules.CanEditObject(this.GetType());
+        bool auth = Csla.Security.AuthorizationRules.CanEditObject(this.GetType(), this as ICanDecideEditAndDeleteAuthorization);
         return (IsDirty && IsValid && auth);
       }
     }
Index: Csla/ICanDecideEditAndDeleteAuthorization.cs
===================================================================
--- Csla/ICanDecideEditAndDeleteAuthorization.cs    (revision 0)
+++ Csla/ICanDecideEditAndDeleteAuthorization.cs    (revision 0)
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Csla
+{
+   public interface ICanDecideEditAndDeleteAuthorization
+   {
+      bool IsDeleteAllowed(bool rolesSaysAllowed);
+      bool IsEditAllowed(bool rolesSaysAllowed);
+   }
+}
Index: Csla/BusinessListBase.cs
===================================================================
--- Csla/BusinessListBase.cs    (revision 2235)
+++ Csla/BusinessListBase.cs    (working copy)
@@ -126,7 +126,7 @@
     {
       get
       {
-        bool auth = Csla.Security.AuthorizationRules.CanEditObject(this.GetType());
+        bool auth = Csla.Security.AuthorizationRules.CanEditObject(this.GetType(), this as ICanDecideEditAndDeleteAuthorization);
         return (IsDirty && IsValid && auth);
       }
     }
Index: Csla/Core/BusinessBase.cs
===================================================================
--- Csla/Core/BusinessBase.cs    (revision 2235)
+++ Csla/Core/BusinessBase.cs    (working copy)
@@ -319,11 +319,11 @@
       {
         bool auth;
         if (IsDeleted)
-          auth = Csla.Security.AuthorizationRules.CanDeleteObject(this.GetType());
+          auth = Csla.Security.AuthorizationRules.CanDeleteObject(this.GetType(), this as ICanDecideEditAndDeleteAuthorization);
         else if (IsNew)
           auth = Csla.Security.AuthorizationRules.CanCreateObject(this.GetType());
         else
-          auth = Csla.Security.AuthorizationRules.CanEditObject(this.GetType());
+          auth = Csla.Security.AuthorizationRules.CanEditObject(this.GetType(), this as ICanDecideEditAndDeleteAuthorization);
         return (IsDirty && IsValid && auth);
       }
     }
Index: Csla/Csla.csproj
===================================================================
--- Csla/Csla.csproj    (revision 2235)
+++ Csla/Csla.csproj    (working copy)
@@ -120,6 +120,7 @@
     <Compile Include="FilteredBindingList.cs">
       <SubType>Code</SubType>
     </Compile>
+    <Compile Include="ICanDecideEditAndDeleteAuthorization.cs" />
     <Compile Include="Linq\CslaQueryProvider.cs" />
     <Compile Include="Linq\IIndex.cs" />
     <Compile Include="Linq\IIndexSearchable.cs" />
Index: Csla/DataPortal/Client/DataPortal.cs
===================================================================
--- Csla/DataPortal/Client/DataPortal.cs    (revision 2235)
+++ Csla/DataPortal/Client/DataPortal.cs    (working copy)
@@ -349,7 +349,7 @@
         {
           methodName = "DataPortal_Execute";
           operation = DataPortalOperations.Execute;
-          if (!Csla.Security.AuthorizationRules.CanEditObject(objectType))
+          if (!Csla.Security.AuthorizationRules.CanEditObject(objectType, obj as ICanDecideEditAndDeleteAuthorization))
             throw new System.Security.SecurityException(string.Format(Resources.UserNotAuthorizedException,
               "execute",
               objectType.Name));
@@ -360,7 +360,7 @@
           if (tmp.IsDeleted)
           {
             methodName = "DataPortal_DeleteSelf";
-            if (!Csla.Security.AuthorizationRules.CanDeleteObject(objectType))
+            if (!Csla.Security.AuthorizationRules.CanDeleteObject(objectType, obj as ICanDecideEditAndDeleteAuthorization))
               throw new System.Security.SecurityException(string.Format(Resources.UserNotAuthorizedException,
                 "delete",
                 objectType.Name));
@@ -377,7 +377,7 @@
             else
             {
               methodName = "DataPortal_Update";
-              if (!Csla.Security.AuthorizationRules.CanEditObject(objectType))
+              if (!Csla.Security.AuthorizationRules.CanEditObject(objectType, obj as ICanDecideEditAndDeleteAuthorization))
                 throw new System.Security.SecurityException(string.Format(Resources.UserNotAuthorizedException,
                   "save",
                   objectType.Name));
@@ -386,7 +386,7 @@
         else
         {
           methodName = "DataPortal_Update";
-          if (!Csla.Security.AuthorizationRules.CanEditObject(objectType))
+          if (!Csla.Security.AuthorizationRules.CanEditObject(objectType, obj as ICanDecideEditAndDeleteAuthorization))
             throw new System.Security.SecurityException(string.Format(Resources.UserNotAuthorizedException,
               "save",
               objectType.Name));
Index: Csla/ICanDecideEditAndDeleteAuthorization.cs
===================================================================
--- Csla/ICanDecideEditAndDeleteAuthorization.cs    (revision 0)
+++ Csla/ICanDecideEditAndDeleteAuthorization.cs    (revision 0)
@@ -0,0 +1,8 @@
+namespace Csla
+{
+   public interface ICanDecideEditAndDeleteAuthorization
+   {
+      bool IsDeleteAllowed(bool rolesSaysAllowed);
+      bool IsEditAllowed(bool rolesSaysAllowed);
+   }
+}
Index: Csla/Security/AuthorizationRules.cs
===================================================================
--- Csla/Security/AuthorizationRules.cs    (revision 2235)
+++ Csla/Security/AuthorizationRules.cs    (working copy)
@@ -816,13 +816,25 @@
       return result;
     }
 
-    /// <summary>
+     /// <summary>
+     /// Gets a value indicating whether the current user
+     /// is allowed to edit (save) an instance of the business
+     /// object.
+     /// </summary>
+     /// <param name="objectType">Type of business object.</param>
+     public static bool CanEditObject(Type objectType)
+     {
+        return CanEditObject(objectType, null);
+     }
+
+     /// <summary>
     /// Gets a value indicating whether the current user
     /// is allowed to edit (save) an instance of the business
     /// object.
     /// </summary>
     /// <param name="objectType">Type of business object.</param>
-    public static bool CanEditObject(Type objectType)
+    /// <param name="obj">Allows the object to override the global authorization decision.</param>
+    public static bool CanEditObject(Type objectType, ICanDecideEditAndDeleteAuthorization obj)
     {
       bool result = true;
       var principal = ApplicationContext.User;
@@ -841,16 +853,30 @@
             result = false;
         }
       }
+      if (obj != null)
+         result = obj.IsEditAllowed(result);
       return result;
     }
 
-    /// <summary>
+     /// <summary>
+     /// Gets a value indicating whether the current user
+     /// is allowed to delete an instance of the business
+     /// object.
+     /// </summary>
+     /// <param name="objectType">Type of business object.</param>
+     public static bool CanDeleteObject(Type objectType)
+     {
+        return CanDeleteObject(objectType, null);
+     }
+
+     /// <summary>
     /// Gets a value indicating whether the current user
     /// is allowed to delete an instance of the business
     /// object.
     /// </summary>
     /// <param name="objectType">Type of business object.</param>
-    public static bool CanDeleteObject(Type objectType)
+    /// <param name="obj">Allows the object to override the global authorization decision.</param>
+    public static bool CanDeleteObject(Type objectType, ICanDecideEditAndDeleteAuthorization obj)
     {
       bool result = true;
       var principal = ApplicationContext.User;
@@ -869,6 +895,8 @@
             result = false;
         }
       }
+      if (obj != null)
+         result = obj.IsDeleteAllowed(result);
       return result;
     }
 
Index: Csla/Wpf/CslaDataProviderCommandManager.cs
===================================================================
--- Csla/Wpf/CslaDataProviderCommandManager.cs    (revision 2235)
+++ Csla/Wpf/CslaDataProviderCommandManager.cs    (working copy)
@@ -106,7 +106,7 @@
         if (list != null)
         {
           result = list.AllowNew;
-          if (result && !Csla.Security.AuthorizationRules.CanEditObject(ctl.Provider.Data.GetType()))
+          if (result && !Csla.Security.AuthorizationRules.CanEditObject(list.GetType(), list as ICanDecideEditAndDeleteAuthorization))
             result = false;
         }
       }
@@ -130,7 +130,7 @@
         if (list != null)
         {
           result = list.AllowRemove;
-          if (result && !Csla.Security.AuthorizationRules.CanEditObject(ctl.Provider.Data.GetType()))
+          if (result && !Csla.Security.AuthorizationRules.CanEditObject(list.GetType(), list as ICanDecideEditAndDeleteAuthorization))
             result = false;
         }
       }

Cine replied on Thursday, April 03, 2008

Unittests for above:

using System;
using System.Security.Principal;
using Csla.Security;

using NUnit.Framework;
using TestClass = NUnit.Framework.TestFixtureAttribute;
using TestInitialize = NUnit.Framework.SetUpAttribute;
using TestCleanup = NUnit.Framework.TearDownAttribute;
using TestMethod = NUnit.Framework.TestAttribute;

namespace Csla.Test.Authorization
{
   [TestClass]
   public class ICanDecideEditAndDeleteAuthorizationTest
   {
      private const string MYUSERNAME = "me";
      [Serializable]
      public class MyUser : BusinessBase<MyUser>, ICanDecideEditAndDeleteAuthorization
      {
         private MyUser()
         {
            /* Use factory method */
         }

         private static readonly PropertyInfo<string> NameProperty = RegisterProperty(typeof(MyUser), new PropertyInfo<string>("Name"));
         public string Name
         {
            get { return GetProperty<string>(NameProperty, NoAccessBehavior.ThrowException); }
            set { SetProperty<string>(NameProperty, value, NoAccessBehavior.ThrowException); }
         }
         private static readonly PropertyInfo<string> PasswordProperty = RegisterProperty(typeof(MyUser), new PropertyInfo<string>("Password"));
         public string Password
         {
            get { return GetProperty<string>(PasswordProperty, NoAccessBehavior.ThrowException); }
            set { SetProperty<string>(PasswordProperty, value, NoAccessBehavior.ThrowException); }
         }

         protected override void AddAuthorizationRules()
         {
            AuthorizationRules.AllowWrite(PasswordProperty, "administrator");
         }
         static public void AddObjectAuthorizationRules()
         {
            AuthorizationRules.AllowCreate(typeof(MyUser), "administrator");
            AuthorizationRules.AllowDelete(typeof(MyUser), "administrator");
         }

         public static MyUser NewMyUser()
         {
            return DataPortal.Create<MyUser>();
         }
         [RunLocal]
         protected override void DataPortal_Create()
         {
            LoadProperty<string>(NameProperty, MYUSERNAME);
            LoadProperty<string>(PasswordProperty, "me");
            ValidationRules.CheckRules();
         }

         public override bool CanWriteProperty(string propertyName)
         {
            IIdentity identity = ApplicationContext.User.Identity;
            if (propertyName == PasswordProperty.Name &&
                identity.IsAuthenticated &&
                identity.Name == ReadProperty(NameProperty))
               return true;
            return base.CanWriteProperty(propertyName);
         }

         #region ICanDecideEditAndDeleteAuthorization Members

         bool ICanDecideEditAndDeleteAuthorization.IsDeleteAllowed(bool rolesSaysAllowed)
         {
            IIdentity identity = ApplicationContext.User.Identity;
            if (!identity.IsAuthenticated)
               return rolesSaysAllowed;
            bool ownedbyuser = identity.Name == ReadProperty(NameProperty);
            if (ownedbyuser)
               return true;
            return rolesSaysAllowed;
         }

         bool ICanDecideEditAndDeleteAuthorization.IsEditAllowed(bool rolesSaysAllowed)
         {
            IIdentity identity = ApplicationContext.User.Identity;
            if (!identity.IsAuthenticated)
               return rolesSaysAllowed;
            bool ownedbyuser = identity.Name == ReadProperty(NameProperty);
            if (ownedbyuser)
               return true;
            return rolesSaysAllowed;
         }

         #endregion
      }

      private MyUser u;
      private IPrincipal origprincipal;
      [TestInitialize]
      public void setup()
      {
         origprincipal = ApplicationContext.User;
         ApplicationContext.User = GetAdmin();
         u = MyUser.NewMyUser();
         ApplicationContext.User = origprincipal;
      }
      [TestCleanup]
      public void cleanup()
      {
         ApplicationContext.User = origprincipal;
      }

      private static GenericPrincipal GetAdmin()
      {
         return new GenericPrincipal(new GenericIdentity("administrator"), new[] { "administrator" });
      }
      private static GenericPrincipal GetRandom()
      {
         return new GenericPrincipal(new GenericIdentity("random"), new[] { "somegroup" });
      }
      private static GenericPrincipal GetMe()
      {
         return new GenericPrincipal(new GenericIdentity(MYUSERNAME), null);
      }

      [TestMethod]
      public void Delete_ShouldNotAllowRandomDeleteOfObjectByTypeOrByObject()
      {
         ApplicationContext.User = GetRandom();
         Assert.IsFalse(AuthorizationRules.CanDeleteObject(typeof(MyUser)));
         Assert.IsFalse(AuthorizationRules.CanDeleteObject(typeof(MyUser), u));
      }
      [TestMethod]
      public void Delete_ShouldAllowAdminDeleteOfObjectByTypeOrByObject()
      {
         ApplicationContext.User = GetAdmin();
         Assert.IsTrue(AuthorizationRules.CanDeleteObject(typeof(MyUser)));
         Assert.IsTrue(AuthorizationRules.CanDeleteObject(typeof(MyUser), u));
      }
      [TestMethod]
      public void Delete_ShouldNotAllowDeleteOfObjectByTypeButShouldAllowSelfViaObject()
      {
         ApplicationContext.User = GetMe();
         Assert.IsFalse(AuthorizationRules.CanDeleteObject(typeof(MyUser)));
         Assert.IsTrue(AuthorizationRules.CanDeleteObject(typeof(MyUser), u));
      }

     
     
      [TestMethod]
      public void Password_ShouldNotAllowRandomToWritePassword()
      {
         ApplicationContext.User = GetRandom();
         Assert.IsFalse(u.CanWriteProperty("Password"));
      }
      [TestMethod]
      public void Password_ShouldAllowAdminToWritePassword()
      {
         ApplicationContext.User = GetAdmin();
         Assert.IsTrue(u.CanWriteProperty("Password"));
      }
      [TestMethod]
      public void Password_ShouldAllowSelfToWritePassword()
      {
         ApplicationContext.User = GetMe();
         Assert.IsTrue(u.CanWriteProperty("Password"));
      }
   }
}

Cine replied on Friday, April 11, 2008

Is this possible in any other way than changing CSLA sourcecode?

sergeyb replied on Friday, April 11, 2008

Does every user have access to the list of all users or just his/her own object?

 

Sergey Barskiy

Senior Consultant

office: 678.405.0687 | mobile: 404.388.1899

cid:_2_0648EA840648E85C001BBCB886257279
Microsoft Worldwide Partner of the Year | Custom Development Solutions, Technical Innovation

 

From: Cine [mailto:cslanet@lhotka.net]
Sent: Friday, April 11, 2008 8:10 AM
To: Sergey Barskiy
Subject: Re: [CSLA .NET] Confused about authorization

 

Is this possible in any other way than changing CSLA sourcecode?


ajj3085 replied on Friday, April 11, 2008

Are you trying to use one object to satisfy all six use cases?  As I stated earlier, you're probably better off having a completly seperate class to handle the user creating or deleting their own profile.  It's a very different use case than your first three use cases, so you should have a seperate set of classes to handle it.

Cine replied on Sunday, April 13, 2008

My overall use-case is "display users in grid", for the purpose of simplification everyone has access to the grid and all users. I cannot figure out how to put all my behaviour in one place, when I have both a grid view, and I will eventually also have a details view.Perhaps my problem is I am still to data-centric. I am trying to figure out how to build my architecture when the behaviour has to be one place.

One thing I do definately want to avoid is a special check in the upper layers to work with "myself". Why would I want to make a different implementation for the administrative user editing a existing user, a new user or a user editing himself? For me it seems like the same GUI working on the same object, just different users doing so.

I could always make a special function on my CSLA object to determine if I can delete, and can change the password and then double check these in the setter and delete functions. However, I do not see how this is vastly different from changing CSLA's build-in features to do it for me, and thereby avoiding yet-another-authorization-layer.

Ajj3085, You are right about not having all the behaviour in one class though, I would choose to extract the delete and changepassword use-cases into their own classes,  ChangePasswordCommand and a similar DeleteMyUserCommand, and then from the MyUser object delegate the responsibility to those classes. However, that doesn't change the interface of the MyUser object. Correct me if I am wrong.

Torb replied on Thursday, June 05, 2008

hi
had the same problem with UC4.

one solution might be:

1. Set instance rules after fetch:

       private void PostAddInstanceAuthorizationRules()
        {
            if (ApplicationContext.User.Identity is ZIdentity)
                loggedInUser = ((ZIdentity)ApplicationContext.User.Identity).UserId;

            if (loggedInUser != 0 && loggedInUser == _userId)
            {
                //get the users first role
                string firstRole = string.Empty;
                ZIdentity zIdent = Csla.ApplicationContext.User.Identity as ZIdentity;

                if (zIdent != null && zIdent.Roles.Count > 0)
                {
                    firstRole = zIdent.Roles[0].Identifier;

                   
                    AuthorizationRules.InstanceAllowRead("Password", firstRole);

                    AuthorizationRules.InstanceAllowWrite("Alias", firstRole);
                    AuthorizationRules.InstanceAllowWrite("Password", firstRole);
...

2.
it seems like the GetReadProperty() doesnt work as expected.. (bug?) so, i did this:

        public override bool CanReadProperty(string propertyName)
        {
            //for property "password"
            bool canread = base.CanReadProperty(propertyName); //Is False
            bool canReadByAuthRules = AuthorizationRules.IsReadAllowed(propertyName); // is True!

            if(canReadByAuthRules)
                return canReadByAuthRules;

            //Old stuff...
            //if (propertyName.Equals("Password") &&
            //    (canread || (loggedInUser == _userId)))
            // return true;

            return base.CanReadProperty(propertyName);
        }



ajj3085 replied on Thursday, June 05, 2008

For that you may want to override CanWriteProperty / CanReadProperty.

Copyright (c) Marimer LLC