CanReadProperty not working with custom AuthorizationRule

CanReadProperty not working with custom AuthorizationRule

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


marthac posted on Monday, August 30, 2010

I have the visibility of my StackPanel dependant on the results of CanRead using a PropertyStatus like so:

                   <StackPanel Orientation="Horizontal" Margin="5,5,5,5"
                                Visibility="{Binding ElementName=SSNPropertyStatus, Path=CanRead, Converter={StaticResource VisibilityConverter}}">
                        <TextBlock Width="100" Foreground="Blue" Text="Birth Date Status: " TextAlignment="Right" />
                        <TextBox x:Name="SSNStatusTextBox" Width="150" Text="{Binding SsnStatusMoniker, Mode=TwoWay}" />
                        <csla:PropertyStatus x:Name="SSNPropertyStatus" Property="{Binding SsnStatusMoniker}"/>
                    </StackPanel>

This works if I explicitely add a check for the Property in my CanReadProperty function in my class:

        public override bool CanReadProperty(IPropertyInfo property)
        {   // handle per instance field security
            System.Diagnostics.Debug.WriteLine("CanReadProperty: " + property.Name);
            if (property.Name == SsnStatusMonikerProperty.Name)
                return false;

            return BusinessRules.HasPermission(Csla.Rules.AuthorizationActions.ReadProperty, property);
//            return base.CanReadProperty(property);
        }

But if I comment out the if statement above I'm expecting it to use the custom AuthorizationRule (used to do per instance auth checking). But it never executes the rule.
How do I get it to execute the custom AuthorizationRule??

It is defined as follows:

    public class UsiHiddenField : AuthorizationRule
    {
        private List<string> _roles;

        /// <summary>
        /// Creates an instance of the rule.
        /// </summary>
        /// <param name="roles">List of allowed roles.</param>
        public UsiHiddenField(IUsiPropertyInfo property, List<String> roles)
            : base(Csla.Rules.AuthorizationActions.ReadProperty, property)
        {
            System.Diagnostics.Debug.WriteLine("UsiHiddenField ctor" + property.DatabaseName);
            _roles = roles;
        }

        /// <summary>
        /// Rule implementation.
        /// </summary>
        /// <param name="context">Rule context.</param>
        protected override void Execute(AuthorizationContext context)
        {
            System.Diagnostics.Debug.WriteLine("UsiHiddenField.Execute()");
            if (_roles.Count > 0)
            {
                foreach (var item in _roles)
                {
                    if (Csla.ApplicationContext.User.IsInRole(item))
                    {
                        context.HasPermission = true;
                        break;
                    }
                }
            }
            else
            {
                context.HasPermission = true;
            }
            if (context.HasPermission == true)
            {
                System.Diagnostics.Debug.WriteLine("UsiHiddenField: has permission to read, checked roles - now if tgt not null check for hidden field");
                if (context.Target != null)
                {   // do instance auth check here - how???
                    bool hidden = false;
                    // invoke method in derived class to check for hidden fields
                    BindingFlags flags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;
                    System.Reflection.MethodInfo method = context.TargetType.GetMethod("UsiCheckHiddenField", flags, null, new Type[] { typeof(IMemberInfo) }, null);
                    System.Diagnostics.Debug.WriteLine("UsiCheckHiddenField()");
                    if (method != null)
                        hidden = (bool)method.Invoke(null, new object[] { Element });
                    context.HasPermission = !hidden;
                }
            }
        }
    }

The method referenced in the custom AuthorizationRule above is defined in my base class implementation as follows:

        protected static bool UsiCheckHiddenField(IMemberInfo property)
        {
            bool hidden = false;
            IUsiPropertyInfo usiProperty = property as IUsiPropertyInfo;
            String msg = String.Format("UsiCheckHiddenField(), client type: {0}, client phase: {1}, property: {2} ",
                _clientType, _clientPhase, property.Name);
            System.Diagnostics.Debug.WriteLine(msg);
            Int32 reqDefConfigId = UsiReqDefConfiguration.LoadReqDefConfigId(_clientType, _clientPhase);
            if (reqDefConfigId != -1)
            {   // if there exists a req_def_config - load any required fields for this table
//                IUsiPropertyInfo usiProperty = PrimaryProperty as IUsiPropertyInfo;
                if (UsiReqDefConfiguration.IsHiddenField(reqDefConfigId, TableName, usiProperty.DatabaseName) == true)
                {
                    msg = String.Format("IsHiddenField(), client type: {0}, client phase: {1}, property: {2} ",
                        _clientType, _clientPhase, property.Name);
                    System.Diagnostics.Debug.WriteLine(msg);
                    hidden = true;
                }
            }

            return hidden;
        }

In my base class implementaton it is used as follows:

        protected override void AddBusinessRules()
        {
            base.AddBusinessRules();
           
            // db field rules (length, required, etc)
            BusinessRules.AddRule(new Csla.Rules.CommonRules.Required(OidProperty));
            BusinessRules.AddRule(new Csla.Rules.CommonRules.MaxLength(OidProperty, 32));

            // now load field security settings from db for each property
            List<String> readRoles = null;
            List<String> writeRoles = null;
            SqlCommand sqlCommand;
            String  sqlQuery,
                    user;
            Int32   canRead,
                    canWrite;

            using (SqlConnection connection = new SqlConnection(SqlHelper.ConnectionString))
            {
                connection.Open();
                sqlQuery = String.Format("SELECT DISTINCT USER_OR_GROUP, CAN_READ_DATA, CAN_WRITE_DATA FROM SECURITY_REPORT_PERMISSIONS WHERE SEC_TABLE = '{0}' AND SEC_COLUMN = @p_SEC_COLUMN", TableName);
                sqlCommand = new SqlCommand(sqlQuery, connection);
                foreach (var field in UsiProperties)
                {
                    readRoles = new List<String>();
                    writeRoles = new List<String>();
                    sqlCommand.Parameters.AddWithValue("@p_SEC_COLUMN", field.DatabaseName);
                    System.Diagnostics.Debug.WriteLine(sqlQuery + " where @p_SEC_COLUMN = " + field.DatabaseName);
                    using (var reader = new SafeDataReader(sqlCommand.ExecuteReader()))
                    {
                        if (reader.Read())
                        {
                            do
                            {
                                user = reader.GetString("USER_OR_GROUP");
                                canRead = reader.GetInt32("CAN_READ_DATA");
                                canWrite = reader.GetInt32("CAN_WRITE_DATA");
                                if (canRead == 1)
                                    readRoles.Add(user);
                                if (canWrite == 1)
                                    writeRoles.Add(user);
                            } while (reader.Read());
                        }
                    }
                    // if no one has access - store "NOBODY" as the role - by default everyone has access when no roles are defined
                    if (readRoles.Count == 0)
                    {
                        System.Diagnostics.Debug.WriteLine("No one can read field");
                        readRoles.Add("NOBODY");
                    }
                    if (writeRoles.Count == 0)
                    {
                        System.Diagnostics.Debug.WriteLine("No one can write field");
                        writeRoles.Add("NOBODY");
                    }
                    // store roles for this property
//                    if (InClientTree() == true)
                    //{   // if in client tree - store roles for lookup in custom Authorization rule class
                    BusinessRules.AddRule(new UsiHiddenField(field, readRoles));
                    //}
                    //else
                    //{   // use class level auth - all instances of this class use IsInRole auth
                        //BusinessRules.AddRule(new Csla.Rules.CommonRules.IsInRole(Csla.Rules.AuthorizationActions.ReadProperty, field, readRoles));
                    //}
                    BusinessRules.AddRule(new Csla.Rules.CommonRules.IsInRole(Csla.Rules.AuthorizationActions.WriteProperty, field, writeRoles));
                    sqlCommand.Parameters.Clear();
                }
            }
            if (InClientTree() == true)
            {   // if in client tree - implement custom rule classes that work differently based on provider type, client type & client phase
                // here we deal with required/desired fields - hidden fields need to be dealt with in the CanReadProperty function
                //  - cannot have duplicate rules for AuthorizationActions.ReadProperty action
                foreach (var field in UsiProperties)
                {
                    BusinessRules.AddRule(new UsiRequiredField(field, ClientTypeProperty, ClientPhaseProperty, TableName));
                    BusinessRules.AddRule(new UsiDesiredField(field, ClientTypeProperty, ClientPhaseProperty, TableName));
                }
            }

            AddUsiBusinessRules();
        }

 

marthac replied on Monday, August 30, 2010

Actually if I take the custom auth rule out of the equation and just use the regular old CSLA auth rule

i.e.
BusinessRules.AddRule(new Csla.Rules.CommonRules.IsInRole(Csla.Rules.AuthorizationActions.ReadProperty, field, readRoles));

It is always returning true from CanReadProperty(), even though the roles don't match. BTW it works fine on the .NET side, only having a problem in Silverlight.

Could it be something I'm doing wrong in the XAML?

Here is the XAML:

                   <StackPanel Orientation="Horizontal" Margin="5,5,5,5"
                                Visibility="{Binding ElementName=SSNPropertyStatus, Path=CanRead, Converter={StaticResource VisibilityConverter}}">
                        <TextBlock Width="100" Foreground="Blue" Text="Birth Date Status: " TextAlignment="Right" />
                        <TextBox x:Name="SSNStatusTextBox" Width="150" Text="{Binding SsnStatusMoniker, Mode=TwoWay}" />
                        <csla:PropertyStatus x:Name="SSNPropertyStatus" Property="{Binding SsnStatusMoniker}"/>
                    </StackPanel>

RockfordLhotka replied on Monday, August 30, 2010

Are you running into an issue with authz results caching?

BusinessBase caches authz results on a per-property basis, and only flushes the cache if the current principal object is changed (to a new object instance).

There's an item in the wish list to add the ability for an authz rule to indicate that its results can't be cached, but right now there's no way to avoid the caching without overriding CanReadProperty/CanWriteProperty.

marthac replied on Tuesday, August 31, 2010

I've read the post about the caching issue and I'm overriding CanReadProperty like so:

        public override bool CanReadProperty(IPropertyInfo property)
        {
            System.Diagnostics.Debug.WriteLine("CanReadProperty: " + property.Name);
//            if (property.Name == SsnStatusMonikerProperty.Name)
//                return false;

            return BusinessRules.HasPermission(Csla.Rules.AuthorizationActions.ReadProperty, property);
//            return base.CanReadProperty(property);
        }

Shouldn't that fix the caching issue? Or am I doing something wrong?

RockfordLhotka replied on Tuesday, August 31, 2010

Yes, that should defeat the caching. Are you saying that this doesn't cause the rule to be re-run each time?

marthac replied on Tuesday, August 31, 2010

It looks like the rule is re-run every time, I have a debug msg in the CanReadProperty/CanWriteProperty functions that is displaying multiple times.

I wonder if it has something to do with the fact that I've doing a db fetch in the AddBusinessRules()function to load info on user access to fields.

If I explicitly call AddRule with a specific field it works,

i.e.

BusinessRules.AddRule(

 

new Csla.Rules.CommonRules.IsInRole(Csla.Rules.AuthorizationActions.ReadProperty, SuffixMonikerProperty, "NOBODY"));

But if I let my base class load the info from the db to set this up it doesn't work.

marthac replied on Tuesday, August 31, 2010

OK so I moved the db fetch to it's own class that does a DataPortal_Fetch() hoping this would fix the problem. No luck.

I guess my question now is... Is it safe to assume that AddBusinessRules is always called on the server side, so it is safe to call the synchronous factory method to do the DataPortal_Fetch()?

If the answer is yes, then I'm doing that and it still doesn't work.

If the answer is no, then how do you wait for the async fetch to complete before you use the data loaded from the db to set up the rules?

RockfordLhotka replied on Tuesday, August 31, 2010

Your rules are loading their roles or whatever from the server as the rule is initialized? I wonder what performance ramifications that'll have?

But to your immediate issue, if this is a SL app the data portal is always async, so this approach really won't work. Or let me put it another way - it will work, but it won't be reliable unless you come up with a way to prevent anyone from actually using the business object instance until all the rules have initialized themselves - and there's no mechanism for such a thing built into CSLA.

AddBusinessRules is called once per AppDomain per type. So it is called on the server and on the client - they each load the rules into memory and keep them there for the life of that AppDomain. On the client this is until the user closes the app. On the server it is until the AppDomain is recycled (which could be seconds, minutes, hours, days or weeks - there are many factors at work there).

AddBusinessRules is called synchronously by CSLA. If you have a rule that invokes the async data portal to load itself, that async process will almost certainly be still running long after AddBusinessRules is complete and the application has started interacting with the business object.

I don't know of any realistic way to prevent that.

What I'd consider doing is having a cached ROB that can get all the roles for all your objects from the server in one shot. As your app starts up, fetch that ROB. Since at that level a fetch operation is predictable and visible to the UI (or at least a viewmodel) you can block user interaction with your app until the ROB fetch is done. Then have your roles load themselves from this ROB - from a pre-loaded in-memory cache.

marthac replied on Wednesday, September 01, 2010

Well I was trying to load the information on an as needed basis. We have tons of objects to load the field read/write info for. If the user only uses say 5 of these objects, I was hoping to load info for each object as it is used. But if the load must happen at the time of app initialization, then so be it.

It's funny that I can load the required fields on a per instance basis using my custom required field BusinessRule. But the Authorization rule does not work this way.

Thanks for your help. Smile

RockfordLhotka replied on Wednesday, September 01, 2010

I'm just trying to figure out the async issue.

You should be able to load the data for each rule object as long as you can guarantee the business object won't be used until everything is loaded.

Obviously any use of a property prior to the async operations completing would mean that the rule will run without its data.

That's true of the business rules too. If your custom business rule does an async data portal request to initialize itself, it obviously won't have its data instantly, and there's a race condition where the business object property could be used before that rule has the data necessary to do the right thing.

RockfordLhotka replied on Wednesday, September 01, 2010

In any case (looking at your original code) you must NOT access the database directly in AddBusinessRules. That code might be running on the client, and therefore wouldn't have access to the database.

If you are not building for Silverlight/WP7, you can use the synchronous data portal to get a ROB with the metadata. That's easy enough.

But you must only talk to the database in a DataPortal_XYZ method or factory object. Absolutely no other location should database code ever be found.

marthac replied on Wednesday, September 01, 2010

Yeah I did move th db access to a DataPortal_Fetch yesterday (see previous post).

And yes it works great outside Silverlight when I can do a synchronous fetch...

Copyright (c) Marimer LLC