Безопасность MVC3/4 и привязка модели для динамического объекта

Мне действительно нужен второй взгляд на это, поэтому я надеюсь, что некоторые из вас могут дать мне некоторые отзывы, я думаю, что я слишком долго смотрел на это.

Я пытаюсь настроить веб-сайт с помощью ASP.NET MVC3, и на этом сайте мне нужна гибкость создания динамических объектов. Но я имею в виду, что в моей базе данных есть ряд таблиц, настроенных для хранения информации о структуре и данных, содержащихся в этих динамических объектах. Я работаю с уже существующей базой данных, поэтому я ограничен (до определенной степени) тем, что я могу изменить. Когда я запрашиваю в базе данных динамический объект (не динамический объект .NET 4.0), я передаю свой идентификатор, и в ответ я получаю простой объект, возможно, с несколькими свойствами, которые предназначены только для внутреннего использования, и свойство, которое представляет собой коллекцию, содержащую все свойства моего динамического объекта. Итак, если бы мой динамический объект был для человека с именем, местом рождения и полом, в моей коллекции было бы три объекта, по одному для каждого свойства. Это позволяет администратору сайта добавлять новые поля во время выполнения, и веб-сайт будет автоматически отображать их, разрешать обновление и т. д. Теперь у меня есть привязка модели, работающая в настоящее время как для отображения, так и для обратной передачи для этой структуры данных. Для каждого объекта в коллекции я визуализировать две части данных: уникальный идентификатор свойства (которое в настоящее время является скрытым полем, а идентификатор — это идентификатор) и значение свойства. Моя проблема заключается в аспекте безопасности.

Если бы я имел дело со строго типизированными объектами, я мог бы создать собственные ViewModels и покончить с этим, или добавить атрибуты Bind() к сигнатуре действия, но поскольку свойства этих объектов являются гибкими коллекциями, я не уверен, как к этому подойти. Безопасность на уровне действия достаточно проста, я могу создать собственный атрибут Authorize и запросить разрешения у базы данных, но мне нужно иметь возможность ограничивать поведение коллекций для отображения и приема информации на основе разрешений пользователя. Например, если я добавлю свойство номера социального страхования к объекту человека, я не хочу, чтобы оно отображалось на экране для определенных людей. Но поскольку свойство может измениться во время выполнения, то же самое могут сделать и разрешения.

Вот где я нахожусь настолько далеко, насколько мои мысли идут ...

Поэтому мне нужен способ определить, какие объекты в наборе свойств могут отображаться на экране или привязываться к ним при отправке обратно, в зависимости от разрешений пользователя. Для отображения объекта я не думаю, что у меня есть большой выбор, кроме как каким-то образом включить разрешения в объект ViewModel и запросить эти разрешения в DisplayTemplate, предназначенном для типа объекта, который используется в коллекции свойств. Или я мог бы написать какой-то пользовательский ModelBinder, поскольку он используется для вызовов Html.Display() и Html.Editor() и изучить фильтрацию списка внутри ModelBinder.

У меня похожая проблема с обратными передачами. Когда он отправляется обратно, у меня есть коллекция данных, возвращаемых только с Guid и значением. Но мне нужно убедиться, что пользователь не внедрил свои собственные поля в форму, и мне также нужно убедиться, что для свойств, которые передаются обратно в действие, у пользователя есть соответствующие разрешения. В идеале я хотел бы интегрировать эту проверку в привязку модели и повторно использовать некоторую информацию, заполненную из метаданных, если я могу, например, чтобы она просто игнорировала переданные данные, которые пользователь не имеет права изменять, или в противном случае убедитесь, что у пользователя есть доступ ко всем атрибутам, которые он пытается установить, в проверке IsValid, выполняемой в начале действия, обрабатывающего обратную передачу.

Затем есть динамическое построение метаданных для использования при вызове Html.Display() и Html.Editor() для каждого свойства на основе информации в базе данных, поскольку у меня нет физических свойств, это класс, который я могу украсить Аннотации данных.

Проблема в том, что я не знаком с внутренностями MVC, когда дело доходит до переопределения стандартных реализаций таких вещей, как ModelBinders, ModelMetaDataProviders или ModelValidationProviders.

Можете ли вы дать несколько предложений о том, как лучше всего достичь того, что я описываю, или, если вы знаете другие статьи, посвященные этому примеру, я бы очень хотел их увидеть, мне не повезло с Google. по этому конкретному вопросу до сих пор.

EDIT: см. мой ответ ниже для получения полной информации о том, что я сделал

РЕДАКТИРОВАТЬ: у меня работает поставщик метаданных. Просто нужно было реализовать свой собственный класс и наследовать от 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, который наиболее точно соответствует имени столбца, из которого извлекаются данные. (Мой шаблон редактора помогает в этом, динамически выбирая, в каком столбце находятся данные, как определено в структуре данных, которая определяет эти данные)

Html.Editor(Model.PseudoObjectStructure.PseudoObjectControl.DataType)

С ней немного странно работать, но, как я уже сказал, это уже существующая структура данных.

После оператора switch это стало странным. Насколько я понимаю, в MVC2 метод GetMetadataForProperty() больше не принимает саму Модель в качестве параметра и не находит свойство с помощью propertyName, вместо этого он передает выражение типа Func<object>, которое указывает на свойство, для которого MVC хочет метаданные. Это представляло проблему, потому что мне нужно было, чтобы корневая модель использовала другое свойство для определения деталей структуры. Я нашел здесь другое решение, в котором говорилось, что вы можете использовать отражение для получения модели, но для этого требуется отражение. Не то, на что я надеялся, но это работает. После того, как у меня есть модель, я передаю метаданные и модель созданному методу с именем AddSupplimentalMetadata(), и я установлю остальные свойства объекта DataAnnotationsModelMetadata, который MVC использует оттуда.

Теперь мне просто нужно найти способ динамически выбирать, отображать или не отображать определенные свойства в зависимости от разрешений пользователя. Я думаю, что мне, возможно, придется отфильтровать список свойств перед передачей модели в представление, используя LINQ или что-то в этом роде. Мне не нравится идея помещать бизнес-логику в Display/EditorTemplate. Для сохранения изменений мне все еще нужно взглянуть на систему проверки и посмотреть, смогу ли я подключиться к ней для проверки того, для каких свойств пользователь пытается передать информацию.


person Nick Albrecht    schedule 09.03.2011    source источник


Ответы (2)


Сначала я бы порекомендовал вам использовать словарь в качестве модели представления. Это позволяет вам добавить любое свойство (имя/значение), которое вам нравится.

Во-вторых, для выполнения требований безопасности я бы а) аутентифицировал (Forms/Windows) пользователей и б) создал некоторую функциональность, которая запрашивает базу данных, чтобы увидеть, как выглядит объект, который пользователю разрешено отправлять/редактировать/просматривать. Результатом запроса может быть просто массив строк, содержащих разрешенные имена полей — это ВАШИ МЕТАДАННЫЕ. С помощью этих данных вы можете легко удалить неавторизованные значения. ModelBinder — это то место, где это можно сделать.

В-третьих, для проверки вы можете расширить МЕТАДАННЫЕ, например, заменив массив строк списком Touple(Of string, bool), где вы храните логическое значение, указывающее, является ли значение обязательным пользовательским вводом. Вы по-прежнему можете полагаться на ASP.NET MVC по умолчанию, реализовав A MetaDataProvider. Это может быть началом: http://buildstarted.com/2010/09/14/creating-your-own-modelmetadataprovider-to-handle-custom-attributes/

Наконец, DisplayTemplates и EditorTemplates облегчают управление динамическим пользовательским интерфейсом. Создайте шаблоны для общих типов данных и шаблон для Словаря. Последний просто повторяет свой KeyValuePairs, записывает метку и вызывает шаблон конкретных типов данных. Опять же, здесь METADATA можно расширить и предоставить ASP.NET MVC с помощью MetaDataProvider.

--Даниэль

person Daniel Fisher lennybacon    schedule 08.04.2011

Публикация ответа для принятия, поскольку я сам придумал решение, которое мне нужно, хотя у меня все еще осталась одна часть, я опубликую его как отдельный вопрос, если мне понадобится дополнительная помощь, а не оставлю этот вопрос без ответа. Подробности смотрите в моем исходном посте.

Голоса против послужили отличным напоминанием вернуться к этому, так что вот :-)

Итак, вот к чему я пришел со своей реализацией. Надеюсь, это поможет некоторым из вас в ваших собственных ситуациях. Я должен сообщить, что даю этому Работает на моем компьютере, и вам следует протестировать его самостоятельно, чтобы убедиться, что он соответствует вашим потребностям. Определенные решения были приняты в соответствии с существующими данными/практикой. Если у вас возникнут какие-либо проблемы с этим кодом, которые вам удастся решить, не стесняйтесь вносить свой вклад в этот пост, чтобы другие могли извлечь пользу. Я постараюсь сократить код для краткости, а также потому, что некоторые особенности принадлежат моему работодателю. В этом суть, поскольку она относится к 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; }
}

Провайдер метаданных

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;
}

Провайдер проверки

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;        
   }
}

Я Думаю, что это все. если я что-то пропустил, дайте мне знать.

person Nick Albrecht    schedule 21.04.2011
comment
Было бы полезно, если бы вы отредактировали этот ответ с подробностями решения. - person Jon B; 21.04.2011
comment
Пожалуйста, добавьте детали вашего ответа, это справедливо и по отношению к другим, которые приложили усилия, чтобы ответить на ваш вопрос. - person Redeemed1; 07.03.2012
comment
Я добавил часть своего ответа к первоначальному вопросу, но я хотел вернуться к этому и опубликовать более подробный ответ о том, как я решаю это требование. Как только у меня будет время, чтобы собрать все это вместе, я опубликую обновленный ответ. - person Nick Albrecht; 07.03.2012
comment
Обновленные детали теперь включены! - person Nick Albrecht; 12.02.2013