Strategy to prevent users from updating readonly viewmodel properties by Role/Permission on the server
The platform is ASP.NET MVC 2.
We have a user story that states:
On the [view], don't allow a user to edit [property] unless the user is a [proper role]. They still must be able to view the [property].
So, I must show the field for these people, just prevent them from changing or updating the property value.
I know that I c开发者_如何学Pythonan place a read only control in the view using an attribute for the current user. That should give the client a visual cue that edits are not permitted. But a CSS style won't prevent someone from hacking their post to alter the property's value.
My question pertains to protecting the property on the server side. What methods can I employ to detect changes to my incoming view model in this situation -- where a user can't edit a certain property?
EDIT
I would need to stay away from binds and whitelists -- I appreciate the ideas! They caused me to realize that I omitted a key piece of information.
My product owner wishes to add properties willy-nilly and at their pleasure -- which I took to read: non-static solutions need not apply. Additionally, she wishes to apply other conditional logic to their application -- "if the state of a related property is 'X', then they can edit regardless of permission", etc. I can handle that part. I just need to know where to dynamically apply them.
I am thinking that this is a custom model binder solution.
BTW, we append this particular permission to the roles:
var hasPermission = User.IsInRole(permission);
You could use the Bind attribute that lets you specify properties that are to be included or excluded on binding. here is a good basic article for more information.
Exclude attributes using Bind attribute
I decided that the custom model binder was the way to go. I can already disable the HTML controls. But I need to selectively authorize them.
I know the example object is contrived -- surely, you wouldn't have users post an object with two uneditable properties -- but the point is that I don't want to allow the user to persist their value. I will NULL out any value and then not update any NULL values. By ignoring NULL values, I don't have to go to the data access to retrieve the current value for replacement of the offending update.
This code has me on my way (using MSPEC as the testing framework):
public class TestSplitDetailViewModel
{
public int Id { get; set; }
[CanEdit]
public string RestrictedProperty { get; set; }
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class CanEditAttribute : Attribute
{
}
public class CanEditAttributeBinder : DefaultModelBinder
{
private readonly ISecurityTasks _securityTask;
private readonly ISecurityContext _securityContext;
public CanEditAttributeBinder(ISecurityTasks securityTask, ISecurityContext securityContext)
{
this._securityTask = securityTask;
this._securityContext = securityContext;
}
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
{
var canEditAttribute = propertyDescriptor.Attributes
.OfType<CanEditAttribute>()
.FirstOrDefault();
if (canEditAttribute != null)
{
bool allowed = IsAllowed();
if (allowed)
{
propertyDescriptor.SetValue(bindingContext.Model, null);
}
else
{
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
else
{
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
private bool IsAllowed()
{
return !this._securityTask.DoesUserHaveOperation(this._securityContext.User.Username, UserOperations.ReclassAllowed);
}
}
public class TestModelSpec : Specification<CanEditAttributeBinder>
{
protected static HomeController controller;
private static MockRepository mocks;
protected static ISecurityTasks securityTasks;
private static ISecurityContext securityContext;
protected static ModelBindingContext bindingContext;
Establish context = () =>
{
ServiceLocatorHelper.AddUserServiceWithTestUserContext();
securityTasks = DependencyOf<ISecurityTasks>().AddToServiceLocator();
securityContext = DependencyOf<ISecurityContext>().AddToServiceLocator();
user = new User("CHUNKYBACON");
securityContext.User = user;
// When we restricted access on the client,
// Chunky submitted a FORM POST in which he HACKED a value
var formCollection = new NameValueCollection
{
{ "TestSplitDetailViewModel.Id", "2" },
{ "TestSplitDetailViewModel.RestrictedProperty", "12" } // Given this is a hacked value
};
var valueProvider = new NameValueCollectionValueProvider(formCollection, null);
var modelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(TestSplitDetailViewModel));
bindingContext = new ModelBindingContext
{
ModelName = "TestSplitDetailViewModel",
ValueProvider = valueProvider,
ModelMetadata = modelMetadata
};
controller = new HomeController(null, null, null, null, null);
mocks = new MockRepository();
MvcMockHelpers.SetFakeControllerContext(mocks, controller);
};
protected static User user;
protected static TestSplitDetailViewModel incomingModel;
}
public class when_a_restricted_user_changes_a_restricted_property : TestModelSpec
{
private Establish context = () => securityTasks.Stub(st =>
st.DoesUserHaveOperation(user.Username, UserOperations.ReclassAllowed)).Return(false);
Because of = () => incomingModel = (TestSplitDetailViewModel)subject.BindModel(controller.ControllerContext, bindingContext);
It should_null_that_value_out = () => incomingModel.RestrictedProperty.ShouldBeNull();
}
public class when_an_unrestricted_user_changes_a_restricted_property : TestModelSpec
{
private Establish context = () => securityTasks.Stub(st =>
st.DoesUserHaveOperation(user.Username, UserOperations.ReclassAllowed)).Return(true);
Because of = () => incomingModel = (TestSplitDetailViewModel)subject.BindModel(controller.ControllerContext, bindingContext);
It should_permit_the_change = () => incomingModel.RestrictedProperty.ShouldEqual("12");
}
EDIT
This is now my answer. I see where some might question my testing DefaultModelBinder.BindProperty. I am testing my custom override.
I would use either a whitelist or blacklist and invoke model binding explicitly on your model. e.g.
[HttpPost]
public ActionResult Edit(int id) {
var item = db.GetByID(id); // get from DB
var whitelist = [ "Name", "Title", "Category", etc ]; // setup the list of fields you want to update
UpdateModel(item, whitelist);
}
精彩评论