MVC3/4 Security & Model Binding для динамического объекта
Мне действительно нужен второй взгляд на это, так что я надеюсь, что некоторые из вас могут дать мне некоторую обратную связь, я думаю, что слишком долго на это смотрю.
Я пытаюсь настроить веб-сайт с помощью ASP.NET MVC3, и на этом сайте мне нужна гибкость создания динамических объектов. Но я имею в виду, что в моей базе данных есть ряд таблиц, настроенных для хранения информации о структуре и данных, содержащихся в этих динамических объектах. Я работаю с уже существующей базой данных, поэтому я ограничен (в определенной степени) тем, что я могу изменить. Когда я запрашиваю в базе данных динамический объект (не динамический объект.NET 4.0), я передаю свой идентификатор, и я получаю простой объект с, возможно, несколькими свойствами, которые предназначены только для внутреннего использования, и свойство, которое представляет собой коллекцию, содержащую все свойства для моего динамического объекта. Поэтому, если бы мой динамический объект был для человека с именем, DoB и полом, в моей коллекции было бы три объекта, по одному для каждого свойства. Это позволяет администратору сайта добавлять новые поля во время выполнения, и веб-сайт автоматически отображает их, разрешать обновление и т. Д. Теперь у меня есть привязка модели, работающая в настоящее время как для отображения, так и для обратной передачи для этой структуры данных, для каждого объекта в коллекции. отобразить две части данных, уникальный идентификатор свойства (которое в настоящее время является скрытым полем, а идентификатор - Guid) и значение свойства. Моя проблема - это аспект безопасности.
Если бы я имел дело со строго типизированными объектами, я мог бы создать собственные ViewModels и покончить с этим, или добавить атрибуты Bind() к сигнатуре действия, но поскольку свойства этих объектов являются гибкими коллекциями, я не уверен, как к этому подойти. Безопасность на уровне действий достаточно проста, я могу создать собственный атрибут Authorize и запросить у базы данных разрешения, но мне нужно иметь возможность ограничивать поведение коллекций для отображения и приема информации на основе пользовательских разрешений. Например, если бы я добавил свойство "Номер социального страхования" к объекту "человек", я бы не хотел, чтобы оно отображалось на экране для определенных людей. Но поскольку свойство - это то, что может изменяться во время выполнения, то же самое можно сказать и о разрешениях.
Вот где я нахожусь настолько далеко, насколько мои мысли идут...
Поэтому мне нужен способ определить, какие объекты в наборе свойств можно отобразить на экране или привязать к обратной записи, в зависимости от прав пользователя. Для отображения объекта я не думаю, что у меня есть большой выбор, кроме как каким-то образом включить разрешения в объект ViewModel и запросить эти разрешения в DisplayTemplate, предназначенном для типа объекта, который используется в коллекции свойств. Или я мог бы написать какой-то пользовательский ModelBinder, так как он используется для вызовов Html.Display() и Html.Editor(), и посмотреть на фильтрацию списка внутри ModelBinder.
У меня есть похожая проблема для постбэков, хотя. Когда он отправляется обратно, у меня есть набор данных, которые передаются обратно только с Guid и значением. Но мне нужно убедиться, что пользователь не внедрил свои собственные поля в форму, и мне также нужно убедиться, что для свойств, которые передаются обратно в действие, у пользователя есть соответствующие разрешения. В идеале я хотел бы интегрировать эту проверку в привязку модели и повторно использовать некоторую информацию, заполненную из метаданных, если можно, например, так, чтобы он просто игнорировал данные, передаваемые в том случае, если у пользователя нет прав на изменение, или в противном случае убедитесь, что у пользователя есть доступ ко всем атрибутам, которые он пытается установить в проверке IsValid, выполняемой в начале действия, обрабатывающего обратную передачу.
Затем есть динамическое построение MetaData для использования в вызове Html.Display() и Html.Editor() для каждого свойства, основанного на информации в базе данных, так как у меня нет физических свойств, это класс, который я могу декорировать Аннотации данных.
Проблема в том, что я не знаком с внутренностями MVC, когда дело доходит до переопределения реализаций по умолчанию таких вещей, как ModelBinder, ModelMetaDataProviders или ModelValidationProviders.
Можете ли вы дать несколько советов о том, как лучше всего достичь того, что я описываю, или если вам известны другие статьи, посвященные этому примеру, я бы очень хотел их увидеть, мне не очень повезло с Google на эту конкретную тему до сих пор.
РЕДАКТИРОВАТЬ: см. Мой ответ ниже для получения полной информации о том, что я сделал
РЕДАКТИРОВАТЬ: у меня работает поставщик метаданных. Просто нужно было реализовать мой собственный класс и наследовать от ModelMetadataProvider.
public override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName)
{
ModelMetadata metadata;
if (containerType == typeof(PseudoObjectAttributeViewModel))
{
switch (propertyName)
{
case "StringValue":
metadata = new ModelMetadata(this, typeof(PseudoObjectAttribute), modelAccessor, typeof(string), propertyName);
break;
case "DateValue":
metadata = new ModelMetadata(this, typeof(PseudoObjectAttribute), modelAccessor, typeof(DateTime?), propertyName);
break;
case "DoubleValue":
metadata = new ModelMetadata(this, typeof(PseudoObjectAttribute), modelAccessor, typeof(double?), propertyName);
break;
case "LongValue":
metadata = new ModelMetadata(this, typeof(PseudoObjectAttribute), modelAccessor, typeof(long?), propertyName);
break;
case "BooleanValue":
metadata = new ModelMetadata(this, typeof(PseudoObjectAttribute), modelAccessor, typeof(bool?), propertyName);
break;
case "GuidValue":
metadata = new ModelMetadata(this, typeof(PseudoObjectAttribute), modelAccessor, typeof(Guid?), propertyName);
break;
default:
return defaultMetadataProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName);
break;
}
DataAnnotationsModelMetadata daMetadata = (DataAnnotationsModelMetadata)metadata;
System.Reflection.FieldInfo container = modelAccessor.Target.GetType().GetField("vdi");
AddSupplimentalMetadata(daMetadata, (PseudoObjectAttributeViewModel)((System.Web.Mvc.ViewDataInfo)container.GetValue(modelAccessor.Target)).Container);
}
else
metadata = defaultMetadataProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName);
return metadata;
}
Первая часть довольно понятна, она начинается с заполнения метаданных с помощью GetMetadataForType() путем передачи типа.NET, который наиболее точно соответствует имени столбца, из которого извлекаются данные. (Мой редактор teplate помогает в этом, динамически выбирая, в каком столбце находятся данные, как определено в структуре данных, которая определяет эти данные)
Html.Editor(Model.PseudoObjectStructure.PseudoObjectControl.DataType)
С этим немного странно работать, но, как я уже сказал, это уже существующая структура данных.
После оператора switch это стало странным. Как я понимаю, в MVC2 GetMetadataForProperty()
метод больше не принимает саму модель в качестве параметра и находит свойство с помощью propertyName
вместо этого он передается в выражении типа Func<object>
это указывает на свойство, для которого MVC хочет метаданные. Это представляло проблему, потому что мне нужно, чтобы корневая Модель использовала другое свойство для определения деталей структуры. Здесь я нашел другое решение, которое говорит, что вы можете использовать отражение, чтобы получить модель, но для этого требуется отражение. Не то, на что я надеялся, но это работает. После того, как у меня есть Модель, я передаю Метаданные и Модель в метод, созданный под названием AddSupplimentalMetadata()
и я установлю остальные свойства на DataAnnotationsModelMetadata
объект, который MVC использует оттуда.
Теперь мне просто нужно найти способ динамически выбирать отображение или не отображать определенные свойства в зависимости от прав пользователя. Я думаю, что мне, возможно, придется отфильтровать список свойств перед передачей модели в представление, используя LINQ или что-то в этом роде. Мне не нравится идея поместить бизнес-логику в шаблон Display/EditorTemplate. Для сохранения изменений мне все еще нужно взглянуть на систему проверки и посмотреть, смогу ли я подключиться к ней для проверки того, для каких свойств пользователь пытается передать информацию.
2 ответа
Публикация ответа, который нужно принять, так как я сам нашел решение, в котором нуждаюсь, хотя у меня осталась одна часть, я опубликую его как отдельный вопрос, если мне понадобится дополнительная помощь, а не оставлю этот вопрос без ответа. Смотрите мой оригинальный пост для деталей.
Понижающие голоса послужили отличным напоминанием, чтобы вернуться к этому, так что вот вы:-)
Итак, вот где я закончил свою реализацию. Надеюсь, это поможет некоторым из вас в ваших собственных ситуациях. Я должен сообщить, что я даю на это печать " Работы на моей машине", и вам следует проверить ее самостоятельно, чтобы убедиться, что она соответствует вашим потребностям. Определенные решения были приняты для соответствия существующим данным / практикам. Если у вас возникнут какие-либо проблемы с этим кодом, которые вам удастся решить, не стесняйтесь внести свой вклад в этот пост, чтобы другие могли получить пользу. Я попытаюсь сократить код для краткости, и потому что некоторые из особенностей принадлежат моему работодателю. Это суть этого, поскольку это относится к MVC.
Примечание: в настоящее время я использую MVC4, но это также может работать и для MVC3. Виртуальные модификаторы для nHibernate
POCO которые
public class PseudoObject
{
// Other properties and such...
public virtual IList<PseudoObjectAttribute> Attributes { get; set; }
// Other methods, etc...
}
public class PseudoObjectAttribute
{
// Other properties and such...
public virtual string Value { get; set; }
//This holds all of the info I need for determine metadata & validation
public virtual PseudoObjectStructure Structure { get; set; }
// Other methods, etc...
}
public class PseudoObjectStructure
{
public virtual bool IsRequired { get; set; }
public virtual string RegularExpression { get; set; }
public virtual string RegularExpressionErrorMessage { get; set; }
}
MetadataProvider
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
//We only care about providing custom model metadata to PseudoObjectAttribute objects
if ((containerType != typeof(PseudoObjectAttribute) && modelType != typeof(PseudoObjectAttribute)) || modelAccessor == null)
return base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
ModelMetadata metadata = null;
PseudoObjectAttribute attributeViewModel = null;
System.Reflection.FieldInfo container = null;
//The contents of this if statement allows me to get the PseudoObjectAttribute instance I need to work with.
//This happens when we want metadata for the PseudoObjectAttribute type as a whole, not a specific attribute
if (modelType == typeof(PseudoObjectAttribute) && containerType == null)
{
//
if (modelAccessor.Target is ViewDataDictionary)
attributeViewModel = (PseudoObjectAttribute)((ViewDataDictionary)modelAccessor.Target).Model;
else
{
container = modelAccessor.Target.GetType().GetField("item");
if (container != null)
{
attributeViewModel = (PseudoObjectAttribute)container.GetValue(modelAccessor.Target);
}
container = modelAccessor.Target.GetType().GetField("model");
if (container != null)
attributeViewModel = (PseudoObjectAttribute)container.GetValue(modelAccessor.Target);
}
}
else if(!string.IsNullOrEmpty(propertyName))
{
if (modelAccessor.Method.Name.Contains("FromStringExpression"))
{
//This happens when we want metadata for a specific property on the PseudoObjectAttribute
container = modelAccessor.Target.GetType().GetField("vdi");
attributeViewModel = (PseudoObjectAttribute)((System.Web.Mvc.ViewDataInfo)container.GetValue(modelAccessor.Target)).Container;
}
//GetPropertyValueAccessor is used when you bind the posted back form
else if (modelAccessor.Method.Name.Contains("FromLambdaExpression") || modelAccessor.Method.Name.Contains("GetPropertyValueAccessor"))
{
//Accessed property via lambda
container = modelAccessor.Target.GetType().GetField("container");
var accessor = container.GetValue(modelAccessor.Target);
//Sometimes the property is access straight from the parent object for display purposes in the view ex. someRegistration["ProductId"].GuidValue
//In these situations the access is the Registration object and is not something we can use to derive the attribute.
if (accessor is PseudoObjectAttribute)
attributeViewModel = (PseudoObjectAttribute)accessor;
}
}
// At this point I have an instance of the actual PseudoObjectAttribute object I'm trying to derive Metadata for and can build my Metadata easily using it.
// I'm using typeof (String) as a starting point to build my custom metadata from but it could be any value type if you wanted to befit from the defaults
metadata = new ModelMetadata(this, typeof (PseudoObjectAttribute), modelAccessor, typeof (String), propertyName);
// Be sure to store any of the information you've obtained here that is needed to derive validation rules in the AdditionalValues
metadata.AdditionalValues.Add("Structure", attributeViewModel.Structure);
//TODO: Populate the rest of the Metadata here....
return metadata;
}
ValidatorProvider
public class PseudoObjectAttributeValidatorProvider : ModelValidatorProvider
{
public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context)
{
if (metadata.ContainerType == typeof(PseudoObjectAttribute) && metadata.PropertyName == "StringValue")
{
PseudoObjectStructure structure = null;
try
{
if (metadata.AdditionalValues.Any())
structure = (PseudoObjectStructure)metadata.AdditionalValues["Structure"];
}
catch (KeyNotFoundException) { }
if (structure != null)
{
if (structure.IsRequired)
yield return new RequiredAttributeAdapter(metadata, context, new RequiredAttribute());
if (structure.RegularExpression != null)
yield return new RegularExpressionAttributeAdapter(metadata, context, new RegularExpressionAttribute(structure.RegularExpression) { ErrorMessage = structure.RegularExpressionErrorMessage });
}
}
else
yield break;
}
}
Я думаю, что это все. если я что-то пропустил, дайте мне знать.
Сначала я бы порекомендовал вам использовать словарь в качестве viewModel. Это позволяет вам добавить любое свойство (имя / значение), которое вам нравится.
Во-вторых, чтобы выполнить требования безопасности, я бы а) аутентифицировал (Forms/Windows) пользователей и б) создал некоторую функциональность, которая запрашивает базу данных, чтобы увидеть, как выглядит объект, который пользователю разрешено передавать / редактировать / просматривать. Результатом запроса может быть просто массив строк, содержащий разрешенные имена полей - это ВАШИ МЕТАДАННЫЕ. С помощью этих данных вы можете легко удалить несанкционированные значения. ModelBinder - это место для этого.
В-третьих, для проверки вы можете расширить METADATA, например, заменив массив строк списком Touple(Of string, bool), где вы храните логическое значение, указывающее, является ли значение обязательным пользовательским вводом. Вы по-прежнему можете положиться на ASP.NET MVC по умолчанию, реализовав MetaDataProvider. Это может быть для начала: http://buildstarted.com/2010/09/14/creating-your-own-modelmetadataprovider-to-handle-custom-attributes/
Наконец, DisplayTemplates и EditorTemplates позволят легко управлять динамическим пользовательским интерфейсом. Создайте шаблоны для общих типов данных и шаблон для словаря. Последний на итерирует свой KeyValuePairs записывает метку и вызывает шаблон конкретных типов данных. Здесь метаданные снова могут быть расширены и предоставлены ASP.NET MVC с использованием MetaDataProvider.
--Daniel