Workaround for Lightswitch HTML Client and OData not firing property IsReadOnly method

On Paul van Bladel’s excellent blog, Lightswitch for the Enterprise, there was a recent post about protecting entity properties which may be only updated by server side processing.

This post describes a common scenario: you want your server-side code to make changes to a property that normal users cannot themselves modify. As a concrete example, suppose you have an Orders table and a LastUpdatedByUser field in that table. You don’t want your users modifying LastUpdatedByUser directly – you only want the server itself to set that field’s value.

So you go to the Properties dialog for your Lightswitch application, and on the Access Control tab you add a permission. Let’s call it ManageAuditData.

Then in your LastUpdatedByUser_IsReadOnly method, you put this code:

result = !Application.User.HasPermission(Permissions.ManageAuditData);

And in your Orders_Updating method, you have this code:

using (Application.User.AddPermission(Permissions.ManageAuditData)
  entity.LastUpdatedByUser = Application.User.Name;

At first blush, you may think this works fine. If you’re developing a silverlight client with Lightswitch, you might not even notice that this code doesn’t actually protect the LastUpdatedByUser property from being directly edited by the user. The user can’t edit the field from the silverlight client itself. And the field can’t be edited from the server code, either, unless you grant the ManageAuditData permission as we have done above.

However, the OData endpoint leaves the property completely unprotected.

And if you create an HTML client for your app, you’ll notice that it, likewise, allows the user to edit the supposedly protected field. The IsReadOnly code is never even triggered.

I’ve logged this bug in this Microsoft Connect entry. Hopefully it will be addressed in an upcoming VS 2012 update.

In the meantime, I need to find a satisfactory solution.

Paul demonstrates one workaround – add code at the beginning of the entity’s Updating and Inserting methods to check for changes to your protected field and throw an exception if there are any.

However, what if you’ve got many entities with many ReadOnly properties? It would be nice to be able to use the IsReadOnly methods as they were designed… but have them actually work, regardless of whether the client is silverlight, html, or direct OData calls. And ideally, we’d like to do it without having to add extra code in every Updating and Inserting method for every entity in the project.

So, here’s my initial attempt at a workaround. The SaveChanges_Executing is called before any insert, update, or delete of any entity in the data workspace. (Take a look at the chart in this article from Code Magazine for more information on the Lightswitch save pipeline)

In the SaveChanges_Executing, I search through all the pending changes, and if any changed properties have IsReadOnly evaluating to true, I throw an exception.

By keeping this workaround code only in this one method, and keeping the relevant business logic in the IsReadOnly methods, whenever the Microsoft Lightswitch team introduces a fix for this bug, the only change needed in your code should be to remove the workaround code from SaveChanges_Executing.

private bool IsPropertyChanged(IEntityProperty p)
{
    return p.Entity.Details.EntityState == EntityState.Added && p.Value == null ?
            false
        : p is IEntityStorageProperty ?
            (p as IEntityStorageProperty).IsChanged
        : p is IEntityReferenceProperty ?
            (p as IEntityReferenceProperty).IsChanged
        : false;
            
}

partial void SaveChanges_Executing()
{
    EntityChangeSet changes = Details.GetChanges();
    var addedOrModified = changes.ModifiedEntities.Union(changes.AddedEntities);


    var changedReadOnlyProperties = addedOrModified
        .SelectMany(entity => entity.Details.Properties.All()
            .Where(property => IsPropertyChanged(property) && property.IsReadOnly)).ToList();

    if (changedReadOnlyProperties.Any())
    {
        throw new ValidationException(
            string.Format("The following properties are read-only:\n{0}",
                string.Join("\n", changedReadOnlyProperties.Select(
                    x => x.Entity.Details.DisplayName + ":" + x.DisplayName))));
    }
            
}

Update 3 May 2013:

Steve Lasker, program manager on the Visual Studio Lightswitch team, replied to the Microsoft Connect entry with a proposed workaround, and explains the following:

Unfortunately, there’s a bit of a disconnect that’s happened with how this property was intended to be used. IsReadOnly was a client scenario, specifically to help set UI Controls to a readonly state, and not intended as a full security access feature. It was not meant to enforce access on the server, but we completely understand that’s how it could be interpreted.
When the entity is sent to the server, we do not fire all the events for all the properties of each entity submitted to the server upon deserialization.
Even setting IsReadOnly will not cause an access exception to be thrown.

So the official word is that IsReadOnly works as designed. It’s a bit of an odd duck. It’s meant to help set UI Controls to a readonly state. But it also enforces that state depending on the tier that initiates the action.

It appears in both the “Desktop Client” and “Server” perspectives in the Entity Designer. Its description reads, “Returns whether the property is read-only. Runs on the tier where the property’s read-only value is checked.” This sounds awfully similar to the description for the Validate method, “Called when the property is validated. Runs on the tier where the property is validated.” The Validate method is applied regardless of tier, but IsReadOnly is applied only when initiated by the Desktop Client or the Server. One can easily see the potential confusion.

The Validate method, by the way, is less than ideal for this sort of validation – try using it alongside permission elevation scenario I’ve outlined here and it will fail to see the elevated permission.

For now, I’ll be sticking to the SaveChanges_Executing workaround I’ve outlined – it seems the cleanest approach.