Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

OData Delta Patch Security

I have a working PATCH for my user class with Delta in Web API 2. By using the .patch method I can easily detect only the changes that were sent over and then update accordingly, rather than have to receive the entire user!

The problem is there are several fields that I want to protect so they are never updated.

I saw one example on SO but it didn't leverage Delta rather seemed to be slightly more dated and practically wrote all of the patch code by hand. Is there not a way to easily tell OData's patch to skip over properties you designate (maybe I need to override patch and tell it to avoid some properties)?

How would I even begin to go about doing this (or what should I search for / research to get started)? Do action filters / validation have a role here? Do I look into model binding? Is it overriding patch?

Thanks!

like image 751
NullHypothesis Avatar asked Jan 31 '15 06:01

NullHypothesis


2 Answers

Depending on what you want to do if someone tries to update protected fields you can either:

  1. Update only fields that can be modified. For this you can construct new Delta with only these fields like this:

    Delta<User> filteredDelta = new Delta<User>();
    if (originalDelta.GetChangedPropertyNames().Contains("FirstName"))
    {
        filteredDelta.TrySetPropertyValue("FirstName", originalDelta.GetEntity().FirstName);
    }
    if (originalDelta.GetChangedPropertyNames().Contains("LastName"))
    {
        filteredDelta.TrySetPropertyValue("LastName", originalDelta.GetEntity().LastName);
    }    
    filteredDelta.Patch(selectedUser);
    
  2. Fail the PATCH request (I would say this is preferred and least surprising way to deal with such requests). This would be even simpler:

    if (originalDelta.GetChangedPropertyNames().Contains("ModifiedDate"))
    {
        return InternalServerError(new ArgumentException("Attribue is read-only", "ModifiedDate"));
    }
    
like image 132
Vladas Cibulskis Avatar answered Oct 22 '22 17:10

Vladas Cibulskis


There's a couple of possibilities, depending on you use case...

  1. You want to exclude the changes if they are supplied
  2. You want to throw an error if non-editable fields are updated.

Start with an attribute to mark appropriate properties

/// <summary>
/// Marks a property as non-editable.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class NonEditableAttribute : Attribute
{
}

Then we can write some extensions against Delta to take advantage of this

public static class PatchExtensions
{
    /// <summary>
    /// Get the properties of a type that are non-editable.
    /// </summary>
    /// <param name="type"></param>
    /// <returns></returns>
    public static IList<string> NonEditableProperties(this Type type)
    {
        return type.GetProperties().Where(x => Attribute.IsDefined(x, typeof(NonEditableAttribute))).Select(prop => prop.Name).ToList();
    }

    /// <summary>
    /// Get this list of non-editable changes in a <see cref="Delta{T}"/>.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="delta"></param>
    /// <returns></returns>
    public static IList<string> NonEditableChanges<T>(this Delta<T> delta)
        where T : class
    {
        var nec = new List<string>();
        var excluded = typeof(T).NonEditableProperties();

        nec.AddRange(delta.GetChangedPropertyNames().Where(x => excluded.Contains(x)));

        return nec;
    }

    /// <summary>
    /// Exclude changes from a <see cref="Delta{T}"/> based on a list of property names
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="delta"></param>
    /// <param name="excluded"></param>
    /// <returns></returns>
    public static Delta<T> Exclude<T>(this Delta<T> delta, IList<string> excluded)
        where T : class
    {
        var changed = new Delta<T>();

        foreach (var prop in delta.GetChangedPropertyNames().Where(x => !excluded.Contains(x)))
        {
            object value;
            if (delta.TryGetPropertyValue(prop, out value))
            {
                changed.TrySetPropertyValue(prop, value);
            }
        }

        return changed;
    }

    /// <summary>
    /// Exclude changes from a <see cref="Delta{T}"/> where the properties are marked with <see cref="NonEditableAttribute"/>
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="delta"></param>
    /// <returns></returns>
    public static Delta<T> ExcludeNonEditable<T>(this Delta<T> delta)
        where T : class
    {
        var excluded = typeof(T).NonEditableProperties();
        return delta.Exclude(excluded);
    }
}

And a domain class

public class Customer 
{
    public int Id { get; set; }

    public string Name { get; set; }

    [NonEditable]
    public string SecurityId { get; set; }
}

Finally your controller can then take advantage of this in the Patch method

public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Customer> delta)
{
    var patch = delta.ExcludeNonEditable();

    // TODO: Your patching action here
}

or

public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Customer> delta)
{
    var nonEditable = delta.NonEditableChanges();
    if (nonEditable.Count > 0)
    {
        throw new HttpException(409, "Cannot update as non-editable fields included");
    }

    // TODO: Your patching action here
}
like image 4
Paul Hatcher Avatar answered Oct 22 '22 16:10

Paul Hatcher