without framework example delete custom c# odata asp.net-web-api-odata

c# - example - odata web api without entity framework



API web OData Security por entidad (7)

Fondo:
Tengo un modelo OData muy grande que actualmente usa los servicios de datos WCF (OData) para exponerlo. Sin embargo, Microsoft ha declarado que WCF Data Services está dead y que Web API OData es la forma en que funcionará.

Así que estoy investigando formas de hacer funcionar Web API OData, así como los servicios de datos WCF.

Configuración del problema:
Algunas partes del modelo no necesitan ser aseguradas, pero algunas sí. Por ejemplo, la lista Clientes necesita seguridad para restringir quién puede leerla, pero tengo otras listas, como la lista de Productos, que cualquiera puede ver.

La entidad Clientes tiene muchas asociaciones que pueden alcanzarla. Si cuenta más de 2 asociaciones de nivel, existen cientos de formas en las que se puede llegar a los clientes (a través de asociaciones). Por ejemplo, Prodcuts.First().Orders.First().Customer . Dado que los clientes son el núcleo de mi sistema, puede comenzar con la mayoría de cualquier entidad y eventualmente asociar su camino a la lista de Clientes.

WCF Data Services tiene una manera de poner seguridad en una entidad específica a través de un método como este:

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

Cuando miro Web API OData, no veo nada como esto. Además, estoy muy preocupado porque los controladores que estoy creando no parecen ser llamados cuando se sigue una asociación. (Lo que significa que no puedo poner seguridad en CustomersController ).

Estoy preocupado de que tendré que tratar de enumerar de alguna manera todas las formas en que las asociaciones pueden llegar de alguna manera a los clientes y ponerles seguridad a cada uno.

Pregunta:
¿Hay alguna manera de poner seguridad en una entidad específica en Web API OData? (¿Sin tener que enumerar todas las asociaciones que de alguna manera podrían expandirse a esa entidad?)


ACTUALIZACIÓN : En este momento, le recomendaría que siga la solución publicada por vaccano, que se basa en los comentarios del equipo de OData.

Lo que debe hacer es crear un nuevo Atributo heredando de EnableQueryAttribute para OData 4 (o QuerableAttribute dependiendo de la versión de Web API / OData con la que esté hablando) y anule ValidateQuery (es el mismo método que cuando hereda de QuerableAttribute) a compruebe la existencia de un atributo SelectExpand adecuado.

Para configurar un nuevo proyecto nuevo para probar esto, haga lo siguiente:

  1. Crear un nuevo proyecto ASP.Net con Web API 2
  2. Crea el contexto de datos de tu entidad marco.
  3. Agregue un nuevo controlador "Web API 2 OData Controller ...".
  4. En el método WebApiConfigRegister (...) agregue lo siguiente:

Código:

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());

En lo anterior, Customer, Order y OrderDetail son entidades de mi entidad marco. El config.AddODataQueryFilter (nuevo SecureAccessAttribute ()) registra mi SecureAccessAttribute para su uso.

  1. SecureAccessAttribute se implementa de la siguiente manera:

Código:

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

Tenga en cuenta que autorizo ​​el acceso al controlador de Clientes, pero limito el acceso a Pedidos. El único controlador que he implementado es el siguiente:

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. Aplica el atributo en TODAS las acciones que quieras proteger. Funciona exactamente como EnableQueryAttribute. Se puede encontrar una muestra completa (incluidos los paquetes de Nuget, que hacen que sea una descarga de 50Mb) aquí: http://1drv.ms/1zRmmVj

Solo quiero comentar un poco sobre algunas otras soluciones:

  1. La solución de Leyenda no funciona simplemente porque es al revés, ¡sino que estuvo muy cerca! ¡La verdad es que el constructor buscará en el marco de entidades para expandir las propiedades y no golpeará al controlador de Clientes en absoluto! Ni siquiera tengo uno, y si elimina el atributo de seguridad, aún recuperará los pedidos muy bien si agrega el comando de expansión a su consulta.
  2. Configurar el generador de modelos prohibirá el acceso a las entidades que eliminó globalmente y de todos, por lo que no es una buena solución.
  3. La solución de Feng Zhao podría funcionar, pero tendría que eliminar manualmente los elementos que quería asegurar en cada consulta, en cualquier lugar, lo que no es una buena solución.

¿Sería factible mover esto a su base de datos? Suponiendo que está utilizando el servidor SQL, configure usuarios que coincidan con los perfiles que necesita para cada perfil de cliente. Manteniéndolo simple, una cuenta con acceso de cliente y otra sin él.

Si luego asigna el usuario que realiza una solicitud de datos a uno de estos perfiles y modifica su cadena de conexión para incluir las credenciales relacionadas. Entonces, si hacen una solicitud a una entidad a la que no tienen permiso, recibirán una excepción.

En primer lugar, lo siento si esto es un malentendido del problema. Aunque lo estoy sugiriendo, puedo ver una cantidad de riesgos inmediatos, como el control de acceso a datos adicionales y el mantenimiento dentro de su base de datos.

Además, me pregunto si se puede hacer algo dentro de la plantilla T4 que genera su modelo de entidad. Donde se define la asociación, podría ser posible inyectar algún control de permisos allí. Una vez más, esto pondría el control en una capa diferente. Lo estoy exponiendo por si alguien que conoce a T4 mejor que yo puede ver la manera de hacer que esto funcione.


Aunque creo que la solución provista por @Skleanthous es muy buena. Sin embargo, podemos hacerlo mejor . Tiene algunos problemas que no van a ser un problema en la mayoría de los casos. Siento que son un problema suficiente como para no dejarlo al azar.

  1. La lógica comprueba la propiedad RawExpand, que puede contener muchas cosas en función de selecciones $ anidadas y $ expands. Esto significa que la única forma razonable de obtener información es con Contains (), que tiene fallas.
  2. Si se lo fuerza a usar Contains, ocasiona otros problemas coincidentes, por ejemplo, $ seleccione una propiedad que contenga esa propiedad restringida como una subcadena, por ejemplo: Orders y '' OrdersTitle '' o '' TotalOrders ''.
  3. No hay nada que garantice que una propiedad llamada Pedidos sea de un "Tipo de orden" que está intentando restringir. Los nombres de propiedades de navegación no están escritos en piedra, y podrían cambiarse sin que se cambie la cadena mágica en este atributo. Posible pesadilla de mantenimiento.

TL; DR : Queremos protegernos de Entidades específicas, pero más específicamente, sus tipos sin falsos positivos.

Aquí hay un método de extensión para tomar todos los tipos (técnicamente IEdmTypes) de una clase 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; } }

¡Genial, podemos obtener una lista de todas las propiedades expandidas en una sola línea de código! ¡Eso es muy bonito! Vamos a usarlo en un atributo:

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

Por lo que puedo decir, las únicas propiedades de navegación son EdmEntityType (Single Property) y EdmCollectionType (Collection Property). Obtener el nombre de tipo de la colección es un poco diferente solo porque lo llamará "Colección (MyLib.MyType)" en lugar de solo "MyLib.MyType". Realmente no nos importa si se trata de una colección o no, así que obtenemos el tipo de los elementos internos.

He estado usando esto en el código de producción desde hace un tiempo con gran éxito. Con suerte, encontrará una cantidad igual con esta solución.


La anulación de ValidateQuery ayudará a detectar cuándo un usuario expande o selecciona explícitamente una propiedad navegable, sin embargo, no lo ayudará cuando un usuario use un comodín. Por ejemplo, /Customers?$expand=* . En cambio, lo que probablemente desee hacer es cambiar el modelo para ciertos usuarios. Esto se puede hacer utilizando la anulación GetModel de EnableQueryAttribute.

Por ejemplo, primero crea un método para generar tu modelo 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(); }

... luego, en una clase que hereda de EnableQueryAttribute, anule 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); } }

Tenga en cuenta que esto creará muchos de los mismos modelos en llamadas múltiples. Considere el almacenamiento en caché de varias versiones de su IEdmModel para aumentar el rendimiento de cada llamada.



Puede poner su propio atributo Queryable en Customers.Get () o el método que se use para acceder a la entidad Clientes (ya sea directamente o a través de una propiedad de navegación). En la implementación de su atributo, puede anular el método ValidateQuery para verificar los derechos de acceso, como este:

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

No sé por qué su controlador no está llamado a las propiedades de navegación. Debería ser...


Recibí esta respuesta cuando le pregunté al equipo de Web API OData. Parece muy similar a la respuesta que acepté, pero usa un IAuthorizationFilter.

En interés de la integridad, pensé que lo publicaría aquí:

Para el conjunto de entidades o la propiedad de navegación que aparece en la ruta, podríamos definir un manejador de mensajes o un filtro de autorización, y en esa verificación el conjunto de entidades objetivo solicitado por el usuario. Por ejemplo, algunos fragmentos de código:

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());

Para $ expandir la autorización en la opción de consulta, una muestra.

O cree por usuario o por modelo de edm grupal. Una muestra.