Безопасность OData веб-API для каждой сущности

Справочная информация:
У меня очень большая модель OData, которая в настоящее время использует службы данных WCF (OData) для ее раскрытия. Однако Microsoft заявила, что WCF Data Services - это dead, и этот веб-API OData - это то, как они будут работать.

Поэтому я изучаю способы заставить работать OData веб-API, а также службы данных WCF.

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

У организации «Клиенты» есть множество ассоциаций, с которыми она может связаться. Если вы посчитаете ассоциации уровня 2+, то сможете найти множество сотен способов связаться с клиентами (через ассоциации). Например Prodcuts.First().Orders.First().Customer. Поскольку клиенты являются ядром моей системы, вы можете начать с любого объекта и в конечном итоге связать свой путь со списком клиентов.

В WCF Data Services есть способ установить безопасность для определенного объекта с помощью такого метода:

[QueryInterceptor("Customers")]
public Expression<Func<Customer, bool>> CheckCustomerAccess()
{
     return DoesCurrentUserHaveAccessToCustomers();
}

Когда я смотрю на OData веб-API, я не вижу ничего подобного. Кроме того, я очень обеспокоен, потому что контроллеры, которые я делаю, не вызывают, когда отслеживается ассоциация. (Это означает, что я не могу поставить безопасность в CustomersController.)

Меня беспокоит, что мне придется попытаться как-то перечислить все способы, которыми ассоциации могут каким-то образом добраться до клиентов и обеспечить безопасность каждого из них.

Вопрос:
Есть ли способ установить безопасность для определенного объекта в OData веб-API? (Без необходимости перечислять все ассоциации, которые могут каким-либо образом расшириться до этого организация?)


person Vaccano    schedule 27.07.2014    source источник
comment
Из того, что я прочитал, QueryInterceptor должен по-прежнему работать со службами OData. Если вы установите точку останова, попадет ли ваш код в QueryInterceptor?   -  person Rots    schedule 30.07.2014
comment
@Rots - вы видели это для веб-API OData? Если да, опубликуйте это. Я мог видеть только примеры для версии OData WCF Data Services.   -  person Vaccano    schedule 31.07.2014
comment
Найдите QueryInterceptor на этой странице: msdn.microsoft.com/en-us/data /gg192996.aspx. Возможно, не для OData веб-API?   -  person Rots    schedule 31.07.2014
comment
@Vaccano Нормально ли решение OData 2 для веб-API ASP.Net?   -  person Savvas Kleanthous    schedule 31.07.2014
comment
@SKleanthous - Совершенно верно!   -  person Vaccano    schedule 01.08.2014
comment
@Rots - К сожалению, эта ссылка предназначена для служб данных WCF, а не для OData веб-API. Спасибо, что разместили это.   -  person Vaccano    schedule 01.08.2014
comment
Вы используете веб-API OData v4 или v3?   -  person Marcel N.    schedule 03.08.2014
comment
@Vaccano Я добавил ответ, который решает вашу проблему. Прокомментируйте, пожалуйста, мой ответ, если у вас возникнут проблемы.   -  person Savvas Kleanthous    schedule 03.08.2014
comment
@Vaccano любое решение для ASP.NET Core?   -  person tchelidze    schedule 26.09.2018
comment
@tchelidze Понятия не имею. Извините.   -  person Vaccano    schedule 26.09.2018


Ответы (7)


ОБНОВЛЕНИЕ: на данный момент я бы порекомендовал вам следовать решению, опубликованному Vacano, которое основано на данных, полученных от команды OData.

Что вам нужно сделать, так это создать новый атрибут, наследующий от EnableQueryAttribute для OData 4 (или QuerableAttribute в зависимости от того, с какой версией веб-API \ OData вы говорите) и переопределить ValidateQuery (это тот же метод, что и при наследовании от QuerableAttribute) на проверьте наличие подходящего атрибута SelectExpand.

Чтобы настроить новый новый проект для проверки, сделайте следующее:

  1. Создайте новый проект ASP.Net с веб-API 2
  2. Создайте контекст данных своей структуры сущности.
  3. Добавьте новый контроллер «Web API 2 OData Controller ...».
  4. В методе WebApiConfigRegister (...) добавьте следующее:

Код:

ODataConventionModelBuilder builder = new ODataConventionModelBuilder();

builder.EntitySet<Customer>("Customers");
builder.EntitySet<Order>("Orders");
builder.EntitySet<OrderDetail>("OrderDetails");

config.Routes.MapODataServiceRoute("odata", "odata", builder.GetEdmModel());

//config.AddODataQueryFilter();
config.AddODataQueryFilter(new SecureAccessAttribute());

В приведенном выше примере Customer, Order и OrderDetail являются объектами моей структуры сущностей. Config.AddODataQueryFilter (новый SecureAccessAttribute ()) регистрирует мой SecureAccessAttribute для использования.

  1. SecureAccessAttribute реализован следующим образом:

Код:

public class SecureAccessAttribute : EnableQueryAttribute
{
    public override void ValidateQuery(HttpRequestMessage request, ODataQueryOptions queryOptions)
    {
        if(queryOptions.SelectExpand != null
            && queryOptions.SelectExpand.RawExpand != null
            && queryOptions.SelectExpand.RawExpand.Contains("Orders"))
        {
            //Check here if user is allowed to view orders.
            throw new InvalidOperationException();
        }

        base.ValidateQuery(request, queryOptions);
    }
}

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

public class CustomersController : ODataController
{
    private Entities db = new Entities();

    [SecureAccess(MaxExpansionDepth=2)]
    public IQueryable<Customer> GetCustomers()
    {
        return db.Customers;
    }

    // GET: odata/Customers(5)
    [EnableQuery]
    public SingleResult<Customer> GetCustomer([FromODataUri] int key)
    {
        return SingleResult.Create(db.Customers.Where(customer => customer.Id == key));
    }
}
  1. Примените атрибут ко ВСЕМ действиям, которые вы хотите защитить. Он работает точно так же, как EnableQueryAttribute. Полный образец (включая пакеты Nuget и все, что делает его загрузку 50 МБ) можно найти здесь: http://1drv.ms/1zRmmVj

Я просто хочу немного прокомментировать некоторые другие решения:

  1. Решение Leyenda не работает просто потому, что это наоборот, но в остальном было очень близко! Правда заключается в том, что конструктор будет искать в структуре сущностей для расширения свойств и вообще не попадет в контроллер Customers! У меня его даже нет, и если вы удалите атрибут безопасности, он все равно будет получать заказы, если вы добавите команду расширения в свой запрос.
  2. Установка построителя моделей запретит доступ к объектам, которые вы удалили глобально и от всех, поэтому это не лучшее решение.
  3. Решение Фэн Чжао может работать, но вам придется вручную удалять элементы, которые вы хотите защитить, в каждом запросе и везде, что не является хорошим решением.
person Savvas Kleanthous    schedule 03.08.2014
comment
Вот и все. Следует отметить, что EnableQueryAttribute доступен, начиная с OData 4 для веб-API, поэтому в типичном решении для веб-API необходимо Install-Package Microsoft.AspNet.Odata. - person Marcel N.; 03.08.2014
comment
@MarcelN. Я обновил свой ответ, чтобы отразить это. Спасибо за внимание и комментарии. - person Savvas Kleanthous; 05.08.2014
comment
Большое спасибо. Это требует больше голосов, так как это правильное и полное решение. - person Marcel N.; 05.08.2014
comment
Когда я попробовал это, добавление SecureAccessAttribute в конфигурацию привело к сбою всех вызовов расширения с ошибкой. Запрос, указанный в URI, недействителен. Не удалось найти свойство с именем «MyExpanedPropertyHere» в типе «System.Web.Http.OData.Query.Expressions.SelectAllAndExpand_1OfMyTypeThatIWasQueryingHere». Даже если я закомментирую запрос проверки, он все равно не работает. Как только я удалю config.AddODataQueryFilter(new SecureAccessAttribute()), расширение работает нормально. - person Vaccano; 05.08.2014
comment
@Vaccano Укажите версию OData, с которой вы работаете, а также версию веб-API. Также, когда с вашей стороны больше не будет кода, немного сложно помочь вам отладить это. Следуйте моим инструкциям, чтобы создать простое решение и использовать его для сравнения с тем, что у вас есть. То, что я упомянул, определенно работает. Кроме того, для вашего удобства я опубликую свое полное решение через некоторое время. - person Savvas Kleanthous; 05.08.2014
comment
Я думаю, вы правы. Я думаю, что у меня проблемы с пересечением версий nuget. Постараюсь сделать новый проект. В любом случае спасибо за решение. Это явно лучший вариант. Я приму это сейчас, чтобы не забыть, пока не истечет время. - person Vaccano; 05.08.2014
comment
Я добавил ответ, который получил от команды OData Web API. Если у вас есть мин, посмотрите, кажется ли он таким же хорошим, как то, что вы разместили. - person Vaccano; 07.08.2014
comment
Другой метод был бы аналогичен этим строкам, но вместо этого переопределил бы метод EnableQueryAttribute.GetModel(...), чтобы при необходимости возвращать модель, игнорируя свойство навигации Orders. - person jt000; 30.12.2016
comment
@SKleanthous какое-нибудь решение для ASP.NET Core? - person tchelidze; 26.09.2018

Я получил этот ответ, когда спросил команду OData Web API. Он кажется очень похожим на ответ, который я принял, но он использует IAuthorizationFilter.

Для полноты картины я решил разместить это здесь:


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

public class CustomAuthorizationFilter : IAuthorizationFilter
{
    public bool AllowMultiple { get { return false; } }

    public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(
        HttpActionContext actionContext,
        CancellationToken cancellationToken,
        Func<Task<HttpResponseMessage>> continuation)
    {
        // check the auth
        var request = actionContext.Request;
        var odataPath = request.ODataProperties().Path;
        if (odataPath != null && odataPath.NavigationSource != null &&
            odataPath.NavigationSource.Name == "Products")
        {
            // only allow admin access
            IEnumerable<string> users;
            request.Headers.TryGetValues("user", out users);
            if (users == null || users.FirstOrDefault() != "admin")
            {
                throw new HttpResponseException(HttpStatusCode.Unauthorized);
            }
        }

        return continuation();
    }
}

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new CustomAuthorizationFilter());

Для авторизации $ expand в параметре запроса образец.

Или создайте модель edm для каждого пользователя или группы. Образец.

person Vaccano    schedule 07.08.2014
comment
Ваккано, похоже, это хороший способ сделать это. Я продолжу исследование и обновлю свой ответ, включив в него пример, если это действительно так. К сожалению, это произойдет примерно через неделю из-за праздников :) - person Savvas Kleanthous; 08.08.2014
comment
@SKleanthous У меня очень похожая проблема, и я нашел как ваши ответы, так и ответы Vaccano интересными. Была ли у вас когда-нибудь возможность протестировать решение Vaccano, и если да, то порекомендуете ли вы это или то, что предложили? - person Jonathan Quinth; 05.09.2017
comment
@JonathanQuinth Прошу прощения за то, что не обновил свой ответ (мне действительно нужно это сделать, так как он все еще набирает просмотры). Сегодня я бы порекомендовал вам следовать этому решению, если вам не нужно защищать доступ к ресурсам в зависимости от того, что вы расширяете (в этом случае я бы использовал свое предложение с лучшим исключением). - person Savvas Kleanthous; 05.09.2017
comment
@Vaccano любое решение для ASP.NET Core? - person tchelidze; 26.09.2018
comment
@tchelidze Понятия не имею. Извините. - person Vaccano; 26.09.2018

Хотя я считаю, что решение, предоставленное @SKleanthous, очень хорошее. Однако мы можем добиться большего. У него есть некоторые проблемы, которые не будут проблемой в большинстве случаев, я чувствую, что их было достаточно, чтобы я не хотел оставлять это на волю случая.

  1. Логика проверяет свойство RawExpand, которое может содержать много данных на основе вложенных $ select и $ expands. Это означает, что единственный разумный способ получить информацию - использовать Contains (), который ошибочен.
  2. Принудительное использование Contains вызывает другие проблемы сопоставления, скажем, вы выбираете свойство, которое содержит это ограниченное свойство в качестве подстроки, например: Orders и 'OrdersTitle' или ' TotalOrders '
  3. Ничто не гарантирует, что свойство с именем Orders имеет «OrderType», который вы пытаетесь ограничить. Имена свойств навигации не высечены в камне и могут быть изменены без изменения волшебной строки в этом атрибуте. Возможный кошмар обслуживания.

TL; DR: мы хотим защитить себя от определенных сущностей, а точнее от их типов, без ложных срабатываний.

Вот метод расширения для получения всех типов (технически IEdmTypes) из класса ODataQueryOptions:

public static class ODataQueryOptionsExtensions
{
    public static List<IEdmType> GetAllExpandedEdmTypes(this ODataQueryOptions self)
    {
        //Define a recursive function here.
        //I chose to do it this way as I didn't want a utility method for this functionality. Break it out at your discretion.
        Action<SelectExpandClause, List<IEdmType>> fillTypesRecursive = null;
        fillTypesRecursive = (selectExpandClause, typeList) =>
        {
            //No clause? Skip.
            if (selectExpandClause == null)
            {
                return;
            }

            foreach (var selectedItem in selectExpandClause.SelectedItems)
            {
                //We're only looking for the expanded navigation items, as we are restricting authorization based on the entity as a whole, not it's parts. 
                var expandItem = (selectedItem as ExpandedNavigationSelectItem);
                if (expandItem != null)
                {
                    //https://msdn.microsoft.com/en-us/library/microsoft.data.odata.query.semanticast.expandednavigationselectitem.pathtonavigationproperty(v=vs.113).aspx
                    //The documentation states: "Gets the Path for this expand level. This path includes zero or more type segments followed by exactly one Navigation Property."
                    //Assuming the documentation is correct, we can assume there will always be one NavigationPropertySegment at the end that we can use. 
                    typeList.Add(expandItem.PathToNavigationProperty.OfType<NavigationPropertySegment>().Last().EdmType);

                    //Fill child expansions. If it's null, it will be skipped.
                    fillTypesRecursive(expandItem.SelectAndExpand, typeList);
                }
            }
        };

        //Fill a list and send it out.
        List<IEdmType> types = new List<IEdmType>();
        fillTypesRecursive(self.SelectExpand?.SelectExpandClause, types);
        return types;
    }
}

Отлично, мы можем получить список всех расширенных свойств в одной строке кода! Это круто! Используем его в атрибуте:

public class SecureEnableQueryAttribute : EnableQueryAttribute
{
    public List<Type> RestrictedTypes => new List<Type>() { typeof(MyLib.Entities.Order) }; 

    public override void ValidateQuery(HttpRequestMessage request, ODataQueryOptions queryOptions)
    {
        List<IEdmType> expandedTypes = queryOptions.GetAllExpandedEdmTypes();

        List<string> expandedTypeNames = new List<string>();
        //For single navigation properties
        expandedTypeNames.AddRange(expandedTypes.OfType<EdmEntityType>().Select(entityType => entityType.FullTypeName()));
        //For collection navigation properties
        expandedTypeNames.AddRange(expandedTypes.OfType<EdmCollectionType>().Select(collectionType => collectionType.ElementType.Definition.FullTypeName())); 

        //Simply a blanket "If it exists" statement. Feel free to be as granular as you like with how you restrict the types. 
        bool restrictedTypeExists =  RestrictedTypes.Select(rt => rt.FullName).Any(rtName => expandedTypeNames.Contains(rtName));

        if (restrictedTypeExists)
        {
            throw new InvalidOperationException();
        }

        base.ValidateQuery(request, queryOptions);
    }

}

Насколько я могу судить, единственными навигационными свойствами являются EdmEntityType (отдельное свойство) и EdmCollectionType (свойство коллекции). Получение имени типа коллекции немного отличается только потому, что она будет называть ее «Коллекция (MyLib.MyType)», а не просто «MyLib.MyType». Нам все равно, коллекция это или нет, поэтому мы получаем Тип внутренних элементов.

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

person Zachary Dow    schedule 09.11.2015

Вы можете программно удалить определенные свойства из EDM:

var employees = modelBuilder.EntitySet<Employee>("Employees");
employees.EntityType.Ignore(emp => emp.Salary);

из http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/odata-security-guidance

person phish_bulb    schedule 01.08.2014
comment
Увы, просто удалить его - это не та безопасность, которую я ищу. Мне нужно, чтобы у некоторых был доступ, а у других - нет. - person Vaccano; 02.08.2014

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

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

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

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

person kidshaw    schedule 02.08.2014

Переопределение ValidateQuery поможет определить, когда пользователь явно раскрывает или выбирает свойство с возможностью навигации, но не поможет, если пользователь использует подстановочный знак. Например, /Customers?$expand=*. Вместо этого вы, вероятно, захотите изменить модель для определенных пользователей. Это можно сделать с помощью переопределения GetModel EnableQueryAttribute.

Например, сначала создайте метод для создания модели OData.

public IEdmModel GetModel(bool includeCustomerOrders)
{
    ODataConventionModelBuilder builder = new ODataConventionModelBuilder();

    var customerType = builder.EntitySet<Customer>("Customers").EntityType;
    if (!includeCustomerOrders)
    {
        customerType.Ignore(c => c.Orders);
    }
    builder.EntitySet<Order>("Orders");
    builder.EntitySet<OrderDetail>("OrderDetails");

    return build.GetModel();
}

... затем в классе, который наследуется от EnableQueryAttribute, переопределите GetModel:

public class SecureAccessAttribute : EnableQueryAttribute
{
    public override IEdmModel GetModel(Type elementClrType, HttpRequestMessage request, HttpActionDescriptor actionDescriptor)
    {
        bool includeOrders = /* Check if user can access orders */;
        return GetModel(includeOrders);
    }
}

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

person jt000    schedule 29.12.2016

Вы можете поместить свой собственный атрибут Queryable в Customers.Get () или любой другой метод, используемый для доступа к сущности Customers (напрямую или через свойство навигации). В реализации вашего атрибута вы можете переопределить метод ValidateQuery для проверки прав доступа, например:

public class MyQueryableAttribute : QueryableAttribute
{
    public override void ValidateQuery(HttpRequestMessage request, 
    ODataQueryOptions queryOptions)
    {
        if (!DoesCurrentUserHaveAccessToCustomers)
        {
            throw new ODataException("User cannot access Customer data");
        }

        base.ValidateQuery(request, queryOptions);
    }
}

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

person Leyenda    schedule 02.08.2014
comment
Должен ли нормально называться контроллер? Если OP использует EF, возможно, серверная часть WebAPI просто использует свойства навигации EF для получения отношений. - person Marcel N.; 03.08.2014
comment
Это не сработает, поскольку построитель модели OData все еще может создавать данные, используя связи, предоставленные из контекста сущности. На самом деле, если вы прочитаете мой ответ, вы заметите, что у меня есть только контроллер Customers, но я все еще полностью могу расширять заказы (например). - person Savvas Kleanthous; 05.08.2014
comment
@Leyenda: этот пост обсуждается на сайте meta: http://meta.stackoverflow.com/questions/267772/answers-which-are-wrong. - person Patrick Hofman; 05.08.2014