web-dev-qa-db-fra.com

Sécurité OData de l'API Web par entité

Contexte:
J'ai un très grand modèle OData qui utilise actuellement WCF Data Services (OData) pour l'exposer. Cependant, Microsoft a déclaré que WCF Data Services est mort et que l'API Web OData est la voie à suivre.

Je recherche donc des moyens de faire fonctionner OData API Web ainsi que WCF Data Services.

Configuration du problème:
Certaines parties du modèle n'ont pas besoin d'être sécurisées, mais d'autres le sont. Par exemple, la liste des clients a besoin de sécurité pour restreindre qui peut la lire, mais j'ai d'autres listes, comme la liste des produits, que tout le monde peut voir.

L'entité Clients possède de nombreuses associations qui peuvent y accéder. Si vous comptez plus de 2 associations de niveau, les clients peuvent atteindre plusieurs centaines de façons (via des associations). Par exemple Prodcuts.First().Orders.First().Customer. Étant donné que les clients sont au cœur de mon système, vous pouvez commencer avec la plupart des entités et éventuellement associer votre chemin à la liste des clients.

WCF Data Services a un moyen pour moi de mettre la sécurité sur une entité spécifique via une méthode comme celle-ci:

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

En regardant Web API OData, je ne vois rien de tel. De plus, je suis très inquiet parce que les contrôleurs que je fais ne semblent pas appelés quand une association est suivie. (Cela signifie que je ne peux pas mettre de sécurité dans le CustomersController.)

Je crains de devoir essayer d'énumérer d'une manière ou d'une autre toutes les façons dont les associations peuvent accéder aux clients et sécuriser chacune d'elles.

Question:
Existe-t-il un moyen de sécuriser une entité spécifique dans OData API Web? (Sans avoir à énumérer toutes les associations qui pourraient en quelque sorte étendre à cette entité?)

46
Vaccano

[~ # ~] mise à jour [~ # ~] : À ce stade, je vous recommande de suivre la solution publiée par vaccano, qui est basée sur sur la contribution de l'équipe OData.

Ce que vous devez faire est de créer un nouvel attribut héritant de EnableQueryAttribute pour OData 4 (ou QuerableAttribute selon la version de Web API\OData avec laquelle vous parlez) et remplacez ValidateQuery (c'est la même méthode que lorsque vous héritez de QuerableAttribute) pour vérifier l'existence d'un attribut SelectExpand approprié.

Pour configurer un nouveau projet pour le tester, procédez comme suit:

  1. Créer un nouveau projet ASP.Net avec Web API 2
  2. Créez votre contexte de données de structure d'entité.
  3. Ajoutez un nouveau contrôleur "Web API 2 OData Controller ...".
  4. Dans la méthode WebApiConfigRegister (...), ajoutez ce qui suit:

Code:

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

Dans ce qui précède, Customer, Order et OrderDetail sont mes entités de structure d'entité. Le config.AddODataQueryFilter (nouveau SecureAccessAttribute ()) enregistre mon SecureAccessAttribute pour utilisation.

  1. SecureAccessAttribute est implémenté comme ci-dessous:

Code:

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

Veuillez noter que j'autorise l'accès au contrôleur des clients, mais je limite l'accès aux commandes. Le seul contrôleur que j'ai implémenté est celui ci-dessous:

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. Appliquez l'attribut dans TOUTES les actions que vous souhaitez sécuriser. Il fonctionne exactement comme EnableQueryAttribute. Un exemple complet (y compris les packages Nuget finit tout, ce qui en fait un téléchargement de 50 Mo) peut être trouvé ici: http://1drv.ms/1zRmmVj

Je voudrais également commenter un peu d'autres solutions:

  1. La solution de Leyenda ne fonctionne pas simplement parce que c'est l'inverse, mais sinon c'était super proche! La vérité est que le constructeur cherchera dans le cadre de l'entité pour développer les propriétés et ne frappera pas du tout le contrôleur des clients! Je n'en ai même pas, et si vous supprimez l'attribut de sécurité, il récupérera très bien les commandes si vous ajoutez la commande expand à votre requête.
  2. La définition du générateur de modèle interdira l'accès aux entités que vous avez supprimées globalement et de tout le monde, ce n'est donc pas une bonne solution.
  3. La solution de Feng Zhao pourrait fonctionner, mais vous devrez supprimer manuellement les éléments que vous souhaitez sécuriser dans chaque requête, partout, ce qui n'est pas une bonne solution.
44
SKleanthous

J'ai obtenu cette réponse lorsque j'ai demandé à l'équipe Web API OData. Cela semble très similaire à la réponse que j'ai acceptée, mais il utilise un IAuthorizationFilter.

Par souci d'exhaustivité, j'ai pensé le publier ici:


Pour l'ensemble d'entités ou la propriété de navigation apparaît dans le chemin, nous pourrions définir un gestionnaire de messages ou un filtre d'autorisation, et dans cette vérification l'ensemble d'entités cible demandé par l'utilisateur. Par exemple, un extrait de code:

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

Pour $ expand autorisation dans l'option de requête, un exemple.

Ou créez un modèle EDM par utilisateur ou par groupe. Un échantillon.

18
Vaccano

Bien que je pense que la solution fournie par @SKleanthous est très bonne. Cependant, nous pouvons faire mieux. Il y a des problèmes qui ne vont pas être un problème dans la majorité des cas, j'ai l'impression qu'ils étaient suffisamment un problème que je ne voulais pas laisser au hasard.

  1. La logique vérifie la propriété RawExpand, qui peut contenir beaucoup de choses en fonction des $ selects et $ expands imbriqués. Cela signifie que la seule façon raisonnable d'extraire des informations est d'utiliser Contains (), qui est défectueux.
  2. Être forcé d'utiliser Contains provoque d'autres problèmes de correspondance, disons que vous sélectionnez une propriété qui contient cette propriété restreinte comme sous-chaîne, Ex: Orders and ' OrdersTitle 'ou' TotalOrders '
  3. Rien ne garantit qu'une propriété nommée Orders est d'un "OrderType" que vous essayez de restreindre. Les noms des propriétés de navigation ne sont pas définis dans la pierre et pourraient être modifiés sans que la chaîne magique soit modifiée dans cet attribut. Cauchemar potentiel d'entretien.

TL; DR : Nous voulons nous protéger contre des entités spécifiques, mais plus spécifiquement , leurs types sans faux positifs.

Voici une méthode d'extension pour récupérer tous les types (techniquement IEdmTypes) d'une classe 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;
    }
}

Génial, nous pouvons obtenir une liste de toutes les propriétés développées dans une seule ligne de code! C'est plutôt cool! Utilisons-le dans un attribut:

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

}

D'après ce que je peux dire, les seules propriétés de navigation sont EdmEntityType (Single Property) et EdmCollectionType (Propriété de collection). Obtenir le nom de type de la collection est un peu différent simplement parce qu'il l'appellera "Collection (MyLib.MyType)" au lieu de simplement "MyLib.MyType". Peu importe que ce soit une collection ou non, nous obtenons donc le type des éléments internes.

Je l'utilise depuis longtemps dans le code de production avec beaucoup de succès. J'espère que vous trouverez un montant égal avec cette solution.

4
Zachary Dow

Vous pouvez supprimer certaines propriétés de l'EDM par programme:

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

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

1
phish_bulb

Serait-il possible de déplacer cela vers votre base de données? En supposant que vous utilisez SQL Server, configurez des utilisateurs qui correspondent aux profils dont vous avez besoin pour chaque profil client. Pour rester simple, un compte avec accès client et un sans.

Si vous mappez ensuite l'utilisateur effectuant une demande de données à l'un de ces profils et modifiez votre chaîne de connexion pour inclure les informations d'identification associées. Ensuite, s'ils font une demande à une entité à laquelle ils ne sont pas autorisés, ils obtiendront une exception.

Tout d'abord, désolé s'il s'agit d'une mauvaise compréhension du problème. Même si je le suggère, je peux voir un certain nombre d'écueils les plus immédiats étant le contrôle d'accès aux données supplémentaire et la maintenance au sein de votre base de données.

De plus, je me demande si quelque chose peut être fait dans le modèle T4 qui génère votre modèle d'entité. Lorsque l'association est définie, il peut être possible d'y injecter un certain contrôle d'autorisation. Encore une fois, cela placerait le contrôle dans une couche différente - je le mets juste au cas où quelqu'un qui connaît mieux les T4 que moi pourrait voir un moyen de faire fonctionner cela.

0
kidshaw

La substitution ValidateQuery aidera à détecter quand un utilisateur étend ou sélectionne explicitement une propriété navigable, mais cela ne vous aidera pas lorsqu'un utilisateur utilise un caractère générique. Par exemple, /Customers?$expand=*. Au lieu de cela, vous souhaiterez probablement modifier le modèle pour certains utilisateurs. Cela peut être fait à l'aide de la substitution GetModel de EnableQueryAttribute.

Par exemple, créez d'abord une méthode pour générer votre modèle 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();
}

... puis dans une classe qui hérite de EnableQueryAttribute, remplacez 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);
    }
}

Notez que cela créera un tas des mêmes modèles sur plusieurs appels. Pensez à mettre en cache différentes versions de votre IEdmModel pour augmenter les performances de chaque appel.

0
jt000