Вопросы о пользовательской настройке безопасности для MVC3 с использованием переопределенного атрибута AuthorizeAttribute, потокобезопасности, дочерних действиях и кэшировании

#asp.net-mvc #asp.net-mvc-3 #caching #thread-safety #security

#asp.net-mvc #asp.net-mvc-3 #кэширование #потокобезопасность #Безопасность

Вопрос:

Итак, после поиска надежного решения безопасности для моего приложения MVC3 я наткнулся на это сообщение в блоге Рика Андерсона. В нем подробно описывается подход с использованием белого списка, при котором пользовательская реализация AuthorizeAttribute применяется в качестве глобального фильтра, и вы оформляете действия / контроллеры, к которым хотите разрешить анонимный доступ, с использованием фиктивного атрибута AllowAnonymousAttribute (я говорю фиктивный, потому что внутри AllowAnonymousAttribute нет логики, это просто пустой класс атрибутов)

 bool allowAnnonymous = filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true)
|| filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true);
if (allowAnnonymous) return;
  

Это (наряду с другими рекомендациями по безопасности, упомянутыми в его блоге, такими как HTTPS) дает мне безопасную модель по умолчанию, в соответствии с которой мне не нужно применять проверку безопасности к каждому отдельному действию, и не забудьте также добавить ее к будущим дополнениям функций.

Первая часть вопроса

Теперь я не использую свойства Users / Roles в атрибуте AuthorizeAttribute, мне нужно извлечь этот материал из базы данных. Для меня это то, что было бы в AuthorizeCore, поскольку единственная ответственность заключается в возврате значения true false, есть ли у пользователя доступ. Однако у меня проблема, AuthorizeCore должен быть потокобезопасным на основе моего чтения исходного кода для класса AuthorizeAttribute, и я не уверен, что лучший способ получить доступ к моей базе данных, чтобы определить разрешения пользователя и придерживаться этого. Мое приложение использует IoC и в настоящее время позволяет моему контейнеру IoC внедрять мой репозиторий, обрабатывающий все это, в конструктор атрибута AuthorizeAttribute, но, делая это, а затем получая к нему доступ в AuthorizeCore, не создаю ли я проблем с безопасностью потоков? Или реализация IoC и MVC3 DependencyResolver, которые я использую для предоставления параметра моему пользовательскому конструктору AuthorizeAttribute, адекватно справятся с безопасностью потоков? Обратите внимание, что в моих репозиториях используется шаблон UnitOfWork, который включает мой NHibernate SessionFactory в качестве конструктора репозитория, а класс Unit of Work предоставляется из моего контейнера IoC, реализованного StructureMap с использованием строки ниже, прав ли я, полагая, что используемая здесь область будет обрабатывать проблемы с потоками?

 For<IUnitOfWork>().HybridHttpOrThreadLocalScoped().Use<UnitOfWork>();
  

Вторая часть вопроса

Моя модель данных (и, следовательно, модель безопасности) настроена так, что все мои основные бизнес-объекты определены таким образом, что это одна большая иерархическая модель, и когда я проверяю разрешения, я смотрю в этой иерархической модели, где была определена учетная запись пользователя, и предоставляю доступ ко всему, что находится под ней по умолчанию. Вторичная проверка разрешений — это та, которая использует разрешения бизнес-логики, определенные администратором, например, могут ли пользователи в роли X получить доступ к функциям удаления виджета. Для этого я использую данные маршрута и извлекаю имена контроллера и действий и использую их в сочетании с деталями из основных данных текущих пользователей, чтобы получить доступ к моей базе данных для разрешения разрешений для этого запроса. Однако эта логика повторяется и для каждого дочернего действия, используемого на странице, но поскольку я использую имена контроллера и действий из данных маршрута, я фактически не получаю информацию о дочернем действии. Оно остается именем родительского действия, а не дочернего действия, поскольку дочернее действие не выполняется через URL-запрос. Это приводит к избыточным проверкам безопасности в моей базе данных на предмет деталей родительского действия и ненужных обращений к ресурсам. При исследовании этого я решил просто обойти проверку безопасности для дочерних действий и положиться на родительское действие для этого.

 bool bypassChildAction = filterContext.ActionDescriptor.IsDefined(typeof (ChildActionOnlyAttribute), true) || filterContext.IsChildAction;
if (bypassChildAction) return;
  

Имеет ли смысл это делать, и если да / нет, то почему? На мой взгляд, если действие оформлено атрибутом ChildActionOnlyAttribute, оно в любом случае недоступно публично через URL. И если это выполняется как дочернее действие, но не является исключительно дочерним действием, я могу обойти проверку безопасности только для этого выполнения, поскольку родительское действие будет обрабатывать разрешения. Возникала ли у вас когда-нибудь ситуация, когда вам нужно было бы ограничить доступ к дочернему действию? Зная, что дочерние действия обычно представляют собой очень маленькие частичные представления, я не ожидаю, что это проблема, но я также видел ссылку на строку в реализации OnAuthorization по умолчанию, в которой излагаются некоторые опасения по поводу кэширования. Кто-нибудь знает, влияет ли это на предлагаемое мной решение?

Общие проблемы:

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

Любые мнения или помощь по этим аспектам были бы высоко оценены!

Комментарии:

1. Это отличные вопросы. Спасибо, что задали их; Меня тоже интересуют ответы на вопрос ChildActionOnly. Я надеюсь, что кто-нибудь сможет нам помочь. Моя логика авторизации довольно «тяжеловесная», и я съеживаюсь, когда вижу, что она запускается 5 раз подряд из-за 1 HttpGet / Post / Delete, а затем 4 последующих дочерних действий.

Ответ №1:

Heya Yarx — Часть 1 — кэширование всех разрешений для пользователя при входе в систему. Тогда многопоточный доступ не является проблемой, поскольку ваше AuthorizeCore просто получает роли из кэша, которые в то время могут считаться доступными только для чтения.

Часть 2: Снова переходим к пункту 1 выше : ) — если ваши проверки безопасности настолько сложны, почему бы не загрузить все разрешения для пользователя при входе в систему и кэшировать их. После нажатия на ваши дочерние действия вы можете запросить разрешения и в это время проверить кеш на их наличие.

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

Я не на 100% следую вашему механизму авторизации на основе маршрута. Вы упомянули, что извлекаете информацию из маршрута — можете ли вы привести пример здесь?

Абсолютно имеет смысл защитить ваши дочерние действия. Что, если два представления вызывают Html.Действие — одно конкретно от имени администратора, а другое ошибочно скопировано и вставлено в другое представление? Ваши дочерние действия всегда должны быть защищены, не думайте, что с ними все в порядке, поскольку они вызываются только из другого представления.

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

Комментарии:

1. Ну, безопасность зависит не столько от самого маршрута, скажем так, сколько от значений маршрута. Я использую имя контроллера и название действия в качестве идентификаторов при моих проверках разрешений. Моя модель не высечена на камне, но прямо сейчас она выглядит примерно так: controllerName, ActionName, Role. Затем моя проблема с защитой дочерних действий сводится к тому, как мне получить доступ к имени контроллера и имени действия из метода AuthorizeCore атрибута авторизации, доступен только контекст HTTP, и у него есть доступ только к информации о маршруте из исходного запроса URL.

2. Насколько я понимаю, добавление внешней зависимости с отслеживанием состояния к методу, который должен быть потокобезопасным, является плохим дизайнерским решением? Должны ли когда-либо быть какие-либо запросы к базе данных в AuthorizeCore? Я мог бы сохранить разрешения в пользовательской реализации IPrincipal и сохранить их вместе с данными пользователя. Они могут быть заполнены из обработчика запроса авторизации приложений. Мне просто нужно убедиться, что я украсил свою папку content / scripts (и т.д.), Чтобы разрешить анонимный доступ, Иначе каждый ресурс, вероятно, приведет к попаданию в мой кеш для разрешений, которые ему не понадобятся.

3. Ваши действия по-прежнему будут обрабатываться в одном порядке, а не все сразу. Первое нажатие для «проверки» разрешений загрузит разрешения в некоторую коллекцию. Если только вы не используете здесь асинхронные контроллеры? Если вы можете загружать все разрешения пользователей при входе в систему и кэшировать их (я не уверен, насколько большой или масштабируемой должна быть эта система), вы бы сохраняли эти вызовы при каждом запросе. Стандартный метод, подобный тому, который вы описали, использует IPrincipal и перезагружается при каждом отдельном запросе либо из базы данных, либо из вашего кэша, все еще намного лучше, чем каждый раз обращаться к базе данных. Некоторые из них будут сериализованы для авторизации в формах

4. cookie — но мне не нравится этот подход, особенно с тех пор, как уязвимость POET показала, что они могут быть подделаны. Просто из любопытства — зачем внедрять пользовательскую схему, основанную на маршруте, в отличие от уровня действия / контроллера?

5. Поскольку, насколько я понимаю, проверки авторизации должны выполняться в AuthorizeCore (), и у меня не было доступа к AuthorizationContext оттуда. Есть ли лучший способ получить доступ к этой информации из AuthorizeCore () или я делаю это не в том месте? Я бы предпочел использовать эту информацию, если это возможно.

Ответ №2:

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

 public class LogonAuthorize : AuthorizeAttribute
{
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        if (filterContext == null)
        {
            throw new ArgumentNullException("filterContext");
        }

        if (OutputCacheAttribute.IsChildActionCacheActive(filterContext))
        {
            // If a child action cache block is active, we need to fail immediately, even if authorization
            // would have succeeded. The reason is that there's no way to hook a callback to rerun
            // authorization before the fragment is served from the cache, so we can't guarantee that this
            // filter will be re-run on subsequent requests.
            throw new InvalidOperationException("AuthorizeAttribute cannot be used within a child action caching block."); //Text pulled from System.Web.Mvc.Resources
        }
        // Bypass authorization on any action decorated with AllowAnonymousAttribute, indicationg the page allows anonymous access and
        // does not restrict access anyone (Similar to a WhiteList security model).
        bool allowAnnonymous = filterContext.ActionDescriptor.IsDefined(typeof (AllowAnonymousAttribute), true) || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof (AllowAnonymousAttribute), true);
        if (allowAnnonymous) return;

        if (CustomAuthorizeCore(filterContext))
        {
            HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
            cachePolicy.SetProxyMaxAge(new TimeSpan(0));
            cachePolicy.AddValidationCallback(CacheValidateHandler, filterContext); //CacheValidateHandler doesn't have access to our AuthorizationContext, so we pass it in using the data object.
        }

        HandleUnauthorizedRequest(filterContext);
    }

    private void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
    {
        var filterContext = (AuthorizationContext)data;
        validationStatus = CustomOnCacheAuthorization(filterContext);
    }

    protected HttpValidationStatus CustomOnCacheAuthorization(AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext == null)
        {
            throw new ArgumentNullException("filterContext.HttpContext");
        }

        bool isAuthorized = CustomAuthorizeCore(filterContext);
        return (isAuthorized) ? HttpValidationStatus.Valid : HttpValidationStatus.IgnoreThisRequest;
    }

    protected bool CustomAuthorizeCore(AuthorizationContext filterContext)
    {
        HttpContextBase httpContext = filterContext.HttpContext;

        if (httpContext == null)
            throw new ArgumentNullException("filterContext.HttpContext");

        Trace.WriteLine("Current User: "   (httpContext.User.Identity.IsAuthenticated ? httpContext.User.Identity.Name : "Anonymous"));
        if (!httpContext.User.Identity.IsAuthenticated)
            return false;

        string objectId = (httpContext.Request.RequestContext.RouteData.Values["id"] ?? Guid.Empty).ToString();
        Trace.WriteLine("Hierarchy Permissions check for Object: "   objectId);

        string controllerName = httpContext.Request.RequestContext.RouteData.GetRequiredString("controller");
        string actionName = httpContext.Request.RequestContext.RouteData.GetRequiredString("action");
        Trace.WriteLine("Policy Permissions check for Controller: "   controllerName   ", and Action: "   actionName);
        //if(!CheckHierarchyPermissions  || (!CheckHierarchyPermissions amp;amp; !CheckBusinessLogicPermissions))
        //{
        //    //Check database permissions by getting DB reference from DependancyResolver
        //    DependencyResolver.Current.GetService(typeof (SecurityService)); //change this to an interface later
        //    return false;
        //}

        return true;
    }

    #region Old methods decorated with Obsolete() attributes to track down unintended uses
    [Obsolete("This overridden implementation of AuthorizeAttribute does not use the Users collection.", true)]
    public new string Users { get; set; }

    [Obsolete("This overridden implementation of AuthorizeAttribute does not use the Roles collection.", true)]
    public new string Roles { get; set; }

    [Obsolete("This overridden implementation of AuthorizeAttribute does not use the AuthorizeCore method.", true)]
    protected new bool AuthorizeCore(HttpContextBase httpContext)
    {
        return false;
    }

    [Obsolete("This overridden implementation of AuthorizeAttribute does not use the OnCacheAuthorization method.", true)]
    protected new virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
    {
        return HttpValidationStatus.Invalid;
    }
    #endregion
}
  

ОБНОВЛЕНИЕ: Просто краткое обновление по этому вопросу, я так и не нашел способ динамического создания имени роли, которую я проверял, из комбинации имени действия и имени контроллера, и все еще работаю в рамках ограничений способа выполнения запросов и кэширования и т.д. Однако шаблон подхода белого списка к авторизации, подробно описанный в блоге, на который я ссылался выше, включен в MVC4. MVC4 на данный момент только бета-версия, но я не ожидаю, что они удалят ее между этим моментом и окончательной версией.