While I think that the solution provided by @SKleanthous is very good. However, we can do better. It has some issues which aren't going to be an issue in a majority of cases, I feel like it they were sufficient enough of a problem that I didn't want to leave it to chance.
- The logic checks the RawExpand property, which can have a lot of stuff in it based on nested $selects and $expands. This means that the only reasonable way you can grab information out is with Contains(), which is flawed.
- Being forced into using Contains causes other matching problems, say you $select a property that contains that restricted property as a substring, Ex: Orders and 'OrdersTitle' or 'TotalOrders'
- Nothing is gaurenteeing that a property named Orders is of an "OrderType" that you are trying to restrict. Navigation property names are not set in stone, and could get changed without the magic string being changed in this attribute. Potential maintenance nightmare.
TL;DR: We want to protect ourselves from specific Entities, but more specifically, their types without false positives.
Here's an extension method to grab all the types (technically IEdmTypes) out of a ODataQueryOptions class:
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;
}
}
Great, we can get a list of all expanded properties in a single line of code! That's pretty cool! Let's use it in an attribute:
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);
}
}
From what I can tell, the only navigation properties are EdmEntityType (Single Property) and EdmCollectionType (Collection Property). Getting the type name of the collection is a little different just because it will call it a "Collection(MyLib.MyType)" instead of just a "MyLib.MyType". We don't really care if it's a collection or not, so we get the Type of the Inner Elements.
I've been using this in production code for a while now with great success. Hopefully you will find an equal amount with this solution.
QueryInterceptor
should still work with OData services. If you put a breakpoint in, does your code hit the QueryInterceptor? – RoyalroyalistQueryInterceptor
on this page: msdn.microsoft.com/en-us/data/gg192996.aspx. Perhaps not for Web API OData? – Royalroyalist