背景:
現在、WCF Data Services(OData)を使用して公開している非常に大きなODataモデルがあります。ただし、Microsoftは、WCF Data Servicesは dead であり、Web API ODataが今後の道であると述べています。
そのため、Web API ODataをWCF Data Servicesと同様に機能させる方法を研究しています。
問題セットアップ:
モデルの一部を保護する必要はありませんが、一部は保護する必要があります。たとえば、顧客リストには、誰がそれを読むことができるかを制限するためのセキュリティが必要ですが、私は製品リストなど、誰でも閲覧できる他のリストを持っています。
Customersエンティティには、到達可能な多くの関連付けがあります。 2+レベルの関連付けを数えると、(関連付けを介して)顧客に到達できる何百もの方法があります。たとえば、Prodcuts.First().Orders.First().Customer
。顧客は私のシステムの中核なので、ほとんどすべてのエンティティから始めて、最終的に顧客リストに自分の方法を関連付けることができます。
WCF Data Servicesには、次のような方法で特定のエンティティにセキュリティを設定する方法があります。
[QueryInterceptor("Customers")]
public Expression<Func<Customer, bool>> CheckCustomerAccess()
{
return DoesCurrentUserHaveAccessToCustomers();
}
Web API ODataを見ると、このようなものは見当たりません。加えて、私が作っているコントローラーは、関連付けが行われたときに呼び出されないように見えるため、非常に心配しています。 (つまり、CustomersController
にセキュリティを置くことはできません。)
私は、アソシエーションが顧客に到達し、それぞれにセキュリティを設定する方法を何らかの方法で列挙しようとする必要があることを心配しています。
質問:
Web API ODataの特定のエンティティにセキュリティを設定する方法はありますか?そのエンティティまで展開しますか?)
[〜#〜] update [〜#〜]:この時点で、ベースになっているvaccanoが投稿したソリューションに従うことをお勧めします。 ODataチームからの入力。
OData 4のEnableQueryAttribute(またはWeb API\ODataのバージョンに応じてQuerableAttribute)を継承する新しい属性を作成し、ValidateQuery(QuerableAttributeから継承する場合と同じメソッド)をオーバーライドする必要があります適切なSelectExpand属性の存在を確認してください。
これをテストするために新しい新しいプロジェクトをセットアップするには、次の手順を実行します。
コード:
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(new SecureAccessAttribute())は、使用する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));
}
}
他のソリューションについても少しコメントしたいだけです。
Web 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モデルを作成します。サンプル。
@SKleanthousが提供するソリューションは非常に優れていると思います。ただし、より良い方法。大部分のケースでは問題にならないいくつかの問題がありますが、偶然に任せたくないほど十分な問題だと感じています。
TL; DR:特定のエンティティから保護したいが、より具体的には、タイプ誤検知なし
ODataQueryOptionsクラスからすべての型(技術的にはIEdmTypes)を取得するための拡張メソッドを次に示します。
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;
}
}
すばらしいです。1行のコードですべての展開されたプロパティのリストを取得できます。それはいいね!属性で使用してみましょう:
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」ではなく「Collection(MyLib.MyType)」と呼ばれるため、少し異なります。コレクションであるかどうかはあまり関係ないので、内部要素のタイプを取得します。
私はこれをしばらくの間実動コードで使用しており、大成功を収めています。このソリューションで同じ量が見つかることを願っています。
プログラムで特定のプロパティをEDMから削除できます。
var employees = modelBuilder.EntitySet<Employee>("Employees");
employees.EntityType.Ignore(emp => emp.Salary);
from http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/odata-security-guidance
ValidateQueryオーバーライドは、ユーザーが明示的にナビゲート可能なプロパティを明示的に展開または選択したときを検出するのに役立ちますが、ユーザーがワイルドカードを使用するときは役に立ちません。例えば、 /Customers?$expand=*
。代わりに、特定のユーザーのモデルを変更することをお勧めします。これは、EnableQueryAttributeのGetModelオーバーライドを使用して実行できます。
たとえば、最初に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のさまざまなバージョンをキャッシュして、各呼び出しのパフォーマンスを向上させることを検討してください。