Подтвердить что ты не робот

Безопасность веб-API OData для каждого объекта

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

Итак, я изучаю способы использования OData Web API, а также служб данных WCF.

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

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

У служб данных WCF есть возможность установить защиту для определенного объекта с помощью такого метода:

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

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

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

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

4b9b3361

Ответ 1

UPDATE. На данный момент я бы рекомендовал вам следовать решению, опубликованному вакцино, которое основано на вводе данных OData команда.

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

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

  • Создайте новый проект ASP.Net с Web API 2
  • Создайте контекст данных структуры сущности.
  • Добавьте новый контроллер "Контроллер OData...".
  • В методе 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);
    }
}

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

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

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

  • Решение Leyenda не работает просто потому, что это наоборот, но в противном случае было супер близко! Истина заключается в том, что строитель будет искать в инфраструктуре сущности для расширения свойств и вообще не ударит по контроллеру клиентов! У меня его даже нет, и если вы удалите атрибут безопасности, он все равно будет получать заказы, если вы добавите команду expand в ваш запрос.
  • Настройка создателя модели запретит доступ к сущностям, удаленным глобально и от всех, поэтому это нехорошее решение.
  • Решение Feng Zhao может работать, но вам придется вручную удалять элементы, которые вы хотите защитить в каждом запросе, везде, что не является хорошим решением.

Ответ 2

Я получил этот ответ, когда спросил у команды API API OData. Это похоже на ответ, который я принял, но использует 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-модели. Образец.

Ответ 3

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

  • Логика проверяет свойство RawExpand, которое может содержать много вещей в зависимости от вложенных $selects и $expand. Это означает, что единственный разумный способ получить информацию - это Contains(), что является недостатком.
  • Включение в использование Содержит вызывает другие совпадающие проблемы, скажем, вы выбираете свойство, которое содержит это ограниченное свойство как подстроку, Ex: Orders и 'OrdersTitle' или 'TotalOrders'
  • Ничто не gaurenteeing, что свойство с именем 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 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 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 (Single Property) и EdmCollectionType (Свойство Collection). Получение типа имени коллекции немного отличается, потому что оно назовет его "Collection (MyLib.MyType)" вместо "MyLib.MyType". Нам все равно, если это коллекция или нет, поэтому мы получаем Тип Внутренних Элементов.

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

Ответ 5

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

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

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

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

Ответ 6

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

Например, сначала создайте метод для создания вашей модели OData​​p >

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 для повышения производительности каждого вызова.

Ответ 7

Вы можете поместить свой собственный атрибут Queryable в Client.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);
    }
}

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