前回の記事Breeze.js + Entity Framework + Angular.js =ブラウザーから直接データベースエンティティを操作する便利な作業では、ブラウザーのjavascriptから直接選択してデータベースにデータを保存する簡単なアプリケーションを作成することを検討しました。 もちろん、読者が最初にセキュリティの質問をしました。 したがって、今日はアクセス制御を整理する方法を検討します。 これを行うために、前の記事からアプリケーションをわずかに変更し、属性の助けを借りて、特定のユーザーまたはロールにデータを追加、削除、変更、表示するための特定のアクセス権を配布できるようにします。
残念ながら、ライブラリにはこのための組み込みツールは提供されていません。 そして、ここで、開発者は2つの方法を提供します。
最初の方法
アプリケーションのすべての変更を完全に保存するには、唯一のDbControllerコントローラーのSaveChangesメソッドを使用しました。 そして、柔軟なアクセス制御を必要とせず、誰かにデータを保存または拒否することを許可する場合、最も簡単な方法はSaveChangesのAuthorizeAttribute属性をハングさせることです。そして、WebApiはアクセスを許可/拒否しますデータ変更。 このオプションは非常に単純で、絶対に柔軟性がなく、すべてまたは何もありません。通常、実際のプロジェクトではこれは常に十分ではありません。
ウェイ2
SaveChangesメソッドは1つのJObjectパラメーターを受け取り、1つのパッケージに保存する必要があるすべてのデータを含みます。 次に、SaveChangesメソッドでEFContextProviderに渡します。既にデータオブジェクトを解析し、変更をデータベースに保存しています。 彼には、エンティティを保存する前に毎回呼び出されるBeforeSaveEntityという仮想メソッドがあり、それを使用します。
セキュリティに関する記事なので、念のために、記事のコードは、セキュリティメカニズムを実装し、プロジェクトの複雑化を避けるためのいくつかの方法を示す目的にのみ役立つことを言及する必要があると考えています。実際のプロジェクトのこのコードは完全に受け入れられません。
おそらく、私たちはすぐに練習に行き、途中で、何をどのように分析するのでしょう。 最後の記事で停止した場所から始めましょう。 このリンクからプロジェクトをダウンロードできます。 ダウンロード後、プロジェクトをビルドして、NuGetがプロジェクトから削除したすべてのパッケージを復元してサイズを縮小するようにする必要があります。続行できます。
まず、認証を実装する必要があります。 最も単純なCookieベースの認証を行います。これには、NuGetパッケージMicrosoft ASP.NET Identity Owin 、 Microsoft ASP.NET Web API 2.2 OWIN 、およびMicrosoft.Owin.Host.SystemWebをインストールします。アプリケーションでOWINを最後に使用しなかったため、作成します。 OWINスタートアップクラスStartup.cs、およびその中にWebApiコントローラーの標準ルートを登録し、認証タイプをDefaultAuthenticationTypes.ApplicationCookieに設定し、CamelCasePropertyNamesContractResolverを使用してWebApiからデータをcamelCaseに送信します。
using System; using System.Threading.Tasks; using Microsoft.Owin; using Owin; using System.Web.Http; using Newtonsoft.Json.Serialization; using Microsoft.Owin.Security.Cookies; using Microsoft.AspNet.Identity; [assembly: OwinStartup(typeof(BreezeJsDemo.App_Start.Startup))] namespace BreezeJsDemo.App_Start { public class Startup { public void Configuration(IAppBuilder app) { HttpConfiguration config = new HttpConfiguration(); config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie }); config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); app.UseWebApi(config); } } }
次に、コントローラーLoginController.csを作成します
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Http; using Microsoft.Owin; using Microsoft.Owin.Security; using System.Security.Claims; using Microsoft.AspNet.Identity; namespace BreezeJsDemo.Controllers { public class LoginController : ApiController { public class LoginViewModel { public string user { get; set; } public string role { get; set; } } public IHttpActionResult Post(LoginViewModel login) { var authenticationManager = HttpContext.Current.GetOwinContext().Authentication; if (authenticationManager.User.Identity.IsAuthenticated) { authenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie); } var claims = new Claim[] { new Claim( ClaimTypes.Name, login.user), new Claim( ClaimTypes.Role, login.role) }; var identity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie); authenticationManager.SignIn(identity); return Ok(); } } }
ここでは、Postメソッドを使用してユーザー名とロール名を受け入れ、それらに基づいてClaimsIdentityを作成し、ログインします。 この例を複雑にしないために、いわばチェック、パスワード、またはユーザーベースを行いません。
-ここには紳士がいます。みんながお互いの言葉を信じています。
次に、適切なフィールドをインターフェースに追加します。 まず、/ app / shoppingList / shoppingList.controller.jsをわずかに変更する必要があります。 $ httpサービスが必要なので、それに応じて追加します
... ShoppingListController.$inject = ['$scope', '$http', 'breeze']; function ShoppingListController($scope, $http, breeze) { ...
そして、ログイン()関数
... vm.login = login; ... function login() { $http.post('api/login', { user: vm.user, role: vm.role }); } ...
ユーザー名と役割の入力フィールドを/app/shoppingList/shoppingList.htmlマークアップに追加します(例:navbar)
<nav class="navbar navbar-default"> <ul class="navbar-nav nav"> <li ng-if="vm.hasChanges()"><a ng-click="vm.saveChanges()"><span class="glyphicon glyphicon-thumbs-up"></span> </a></li> <li ng-if="vm.hasChanges()"><a ng-click="vm.rejectChanges()"><span class="glyphicon glyphicon-thumbs-down"></span> </a></li> <li><a ng-click="vm.refreshData()"><span class="glyphicon glyphicon-refresh"></span> </a></li> </ul> <form class="navbar-form navbar-right"> <div class="form-group"> <input ng-model="vm.user" class="form-control" placeholder=" " /> <input ng-model="vm.role" class="form-control" placeholder="" /> </div> <button ng-click="vm.login()" class="btn btn-link"><span class="glyphicon glyphicon-user"></span> </button> </form> </nav>
必要に応じてアプリケーションを紹介できるようになったので、次に属性にアクセスします。 CanAddAttribute(データベースに新しいレコードを追加する権利を与える)、CanDeleteAttribute(削除する権利)、CanEditAttribute(変更する権利)を使用してアクセス権を分配すると仮定します。クラスのプロパティでCanEditAttributeをハングさせると、ユーザーはそれらの値を変更します。クラスに値を掛けると、ユーザーは例外なくすべてのプロパティを変更できます。 もちろん、実際のプロジェクトでは、このようなスキームは非常に不便で実行不可能になりますが、このセットのアイデアを説明するには十分です。
public class HasRightsAttribute: Attribute { public String User { get; set; } public String Role { get; set; } } [AttributeUsage(AttributeTargets.Class, AllowMultiple=true)] public class CanAddAttribute: HasRightsAttribute { } [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public class CanDeleteAttribute : HasRightsAttribute { } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = true)] public class CanEditAttribute: HasRightsAttribute { }
これらの属性をモデルに配布します。例えば
[CanEdit(User = "User")] [CanAdd(Role = "Role")] [CanDelete(Role = "Role2")] public class ListItem { public int Id { get; set; } public String Name { get; set; } [CanEdit(User = "User2", Role = "Role2")] public Boolean IsBought { get; set; } public int CategoryId { get; set; } public Category Category { get; set; } }
そして、これはそれが意味するものです:
- Roleロールを持つユーザーには、ListItemオブジェクトをデータベースに追加する権利があります。
- Role2ユーザーはそれらを削除できます
- 「User」という名前のユーザーは、そのフィールドの値を変更できます
- Role2ロールを持つユーザーは、IsBoughtフィールドの値を変更できます
- 「User2」という名前のユーザーは、IsBoughtフィールドの値を変更できます
そして、Categoryクラスに似たものを追加します。
[CanEdit( User = "User")] [CanAdd( Role = "Role")] public class Category { public int Id { get; set; } public String Name { get; set; } public List<ListItem> ListItems { get; set; } }
メインの面倒を見てみましょう。SecureEFContextProviderクラスを作成します-EFContextProviderの後継で、アクセス制御を実行します
using Breeze.ContextProvider; using Breeze.ContextProvider.EF6; using BreezeJsDemo.Classes.Attributes; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Reflection; using System.Web.Http; using System.Net; using System.Security.Claims; using System.Data.Entity.Infrastructure; using System.Data.Entity.Core.Objects; using BreezeJsDemo.Model; namespace BreezeJsDemo.Classes { public class SecureEFContextProvider<T> : EFContextProvider<T> where T : class, new() { protected override bool BeforeSaveEntity(EntityInfo entityInfo) { var user = HttpContext.Current.GetOwinContext().Authentication.User; if (user.Identity.IsAuthenticated) { var userName = user.FindFirst(ClaimTypes.Name).Value; var role = user.FindFirst(ClaimTypes.Role).Value; var entityType = entityInfo.Entity.GetType(); switch (entityInfo.EntityState) { case EntityState.Added: // CanAddAttribute if (entityType.GetCustomAttributes<CanAddAttribute>().Any(x => x.Role == role || x.User == userName)) { return true; } break; case EntityState.Deleted: // CanDeleteAttribute if (entityType.GetCustomAttributes<CanDeleteAttribute>().Any(x => x.Role == role || x.User == userName)) { return true; } break; case EntityState.Modified: // CanEditAttribute if (entityType.GetCustomAttributes<CanEditAttribute>().Any(x => x.Role == role || x.User == userName)) { return true; } // if (entityInfo.OriginalValuesMap.All(x => entityType .GetProperty(x.Key) // CanEditAttribute .GetCustomAttributes<CanEditAttribute>().Any(y => y.Role == role || y.User == userName))) { return true; } break; } } // throw new HttpResponseException(HttpStatusCode.Forbidden); } } }
このクラスでは、BeforeSaveEntityメソッドをオーバーロードし、EFContextProviderは各エンティティを保存する前にそれを呼び出します。 この場所は、特にオブジェクトの最後の変更の日付など、保存する前に変更内容を正確に確認したり、変更を検証したり、一部のデータを変更したりするために、開発者によって提供されます。 メソッドがfalseを返す場合、エンティティは保存されません;メソッドで例外がスローされる場合、変更パッケージ全体がキャンセルされ、例外がクライアントに返されます。 メソッドの基本バージョンは常にtrueを返すため、呼び出す代わりにreturn trueを記述できます。
保護されたディクショナリ<Type、List> BeforeSaveEntities(Dictionary <Type、List> saveMap)メソッドをオーバーロードすることもできます。保存パッケージ全体が一度にそれに入ります。また、Dictionary <Type、List>を返す必要があります。これは保存する新しいパッケージになります。
これらのメソッドは、EntityInfo型のオブジェクトを入力として受け取ります。このオブジェクトには、保存するエンティティと実行する操作のタイプに関するデータが含まれます。そのプロパティの一部を検討してください
- ContextProvider ContextProvider-ContextProviderへのリンク
- オブジェクトエンティティ-.NETオブジェクトの形式で直接保存されたエンティティで、パッケージ内のクライアントからのプロパティ値
- EntityState EntityState-オブジェクトのステータス(追加、変更、削除)
- 辞書<String、Object> OriginalValuesMap-保存する前の元のプロパティ値。 そよ風は、レコードの残りの部分に触れることなく、名前がこの辞書のキーであるフィールドのみの値をデータベース内で変更します。 そして、これは変更パッケージのクライアントから送られたデータです。つまり、このデータを信頼することはできません 。 そよ風自体は、そこに格納されているプロパティの値に注意を払っていません(同時実行性チェックに使用されるフィールドを除く)。重要なのは、ディクショナリで変更されるフィールドの名前を持つキーがあることだけです。 つまり、たとえば、クライアント側で変更されていないサーバー側のEditDateプロパティを変更する場合、最初にEntityで値を変更し、OriginalValuesMapで「EditDate」キーを追加します(nullなど)。 。 逆に、クライアントが変更したいフィールドの値を変更したくない場合、OriginalValuesMapから対応するキーを削除する必要があります。
- bool ForceUpdate-trueに設定されている場合、OriginalValuesMapのコンテンツに関係なく、breezeはエンティティのすべてのフィールドの値を更新します。デフォルトはfalseです。
- 辞書<String、Object> UnmappedValuesMap-jsonの保存パッケージに付属しているがモデルに収まらないエンティティの他のプロパティの値。
まず、メソッドでは、ユーザーが認証されているか(user.Identity.IsAuthenticated)を確認し、そうでない場合は保存を禁止します。 次に、エンティティのステータスを確認し、ユーザー名またはロールに対応する属性を探します(存在する場合は保存を許可します)。 ステータスがEntityState.Modifiedであり、ユーザーがオブジェクト全体を変更する権限を持っていない場合、OriginalValuesMapで変更されたプロパティを調べ、プロパティの目的の属性を探します。ない場合は、保存を禁止します。
各エンティティが保存前にこのメソッドに分類されることを考慮すると、フィールドレベルではなく、個々のレコードのレベルで差別化を実装することもできます。 たとえば、ユーザーが識別子1のエントリを削除することを禁止するには、たとえば属性の代わりに、すべてのアクセス権をデータベースのどこかに保存して、実行時に変更できるようにすることもできます。
次に、DbControllerで、EFContextProviderをSecureEFContextProviderに置き換える必要があります
... private SecureEFContextProvider<ShoppingListDbContext> _contextProvider = new SecureEFContextProvider<ShoppingListDbContext>(); ...
ここで、すべてのエンティティを管理します。 ただし、ここで変更を行おうとすると、保存時にエラーを処理しなかったため、ユーザーはすべてがうまくいったと思うでしょう。 saveChangesメソッドに小さな変更を加えます。
function saveChanges() { manager.saveChanges().then(null, function (error) { if (error.status === 403) { manager.rejectChanges(); alert(" "); } }); }
サーバーが403を返した場合、変更はロールバックされ、失敗し、ユーザーにメッセージが表示されます。
保存が整理されています。 次に、特定のデータへの読み取りアクセスについて説明します。 もちろん、最も論理的で信頼性の高い方法は、操作するエンティティごとに独自のDTOを作成し、それらの間を接続し、それらのための特別なDbContextを作成することです。これにより、EFContextProviderを使用してクライアントメタデータを簡単に生成できます。 DbControllerで、各DTOに適切なメソッドを作成します。 一般に、すべてはアプリケーションとまったく同じですが、BeforeSaveEntityメソッドでは-DTOを受け入れ、実際のコンテキストと実際のエンティティを操作し、DTOが保存を試みないようにメソッドからfalseを返します。 この場合、BeforeSaveEntitiesメソッドの方が優れています。保存パッケージ全体が一度に取得され、したがって、一度にすべての変更を一度に保存できるため、DTOコンテキストが実行されないように、メソッドから空のディクショナリ<Type、List>を返す必要があります何でも保存し始めました。
このアプローチでは、クライアントは余分なデータを取得しません。また、データベースのスキームを開示しません。 しかし、当然、DTOの作成とその保存の追加処理に関する作業量は増加しています。 もちろん、これで誰かを怖がらせるのは難しいですが、なぜ少し想像力を持たないのですか...
まず、エンティティとプロパティを読み取る権利を配布する属性を作成します
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = true)] public class CanReadAttribute: HasRightsAttribute { }
属性をクラスに分配しましょう
[CanEdit(User = "User")] [CanAdd(Role = "Role")] [CanDelete(Role = "Role2")] [CanRead(User = "User", Role = "Role")] public class ListItem { [CanRead(Role = "Role2", User = "User2")] public int Id { get; set; } public String Name { get; set; } [CanEdit(User = "User2", Role = "Role2")] [CanRead(Role="Role2", User="User2")] public Boolean IsBought { get; set; } [CanRead(Role = "Role2", User = "User2")] public int CategoryId { get; set; } public Category Category { get; set; } }
[CanEdit(User = "User")] [CanAdd(Role = "Role")] [CanRead(User="User", Role="Role")] [CanRead(Role = "Role2", User = "User2")] public class Category { public int Id { get; set; } public String Name { get; set; } public List<ListItem> ListItems { get; set; } }
クラスに属性がある場合、原則は同じです。ユーザーはすべてのプロパティを表示する権利があり、そうでない場合は属性を持つプロパティのみを表示できます。 ObjectContext ObjectMaterializedイベントのデータを非表示にします。このため、SecureEFContextProviderクラスを少し追加します
public class SecureEFContextProvider<T> : EFContextProvider<T> where T : class, new() { public SecureEFContextProvider() { ObjectContext.ObjectMaterialized += ObjectContext_ObjectMaterialized; } private void ObjectContext_ObjectMaterialized(object sender, ObjectMaterializedEventArgs e) { var user = HttpContext.Current.GetOwinContext().Authentication.User; String userName = null; String role = null; if (user.Identity.IsAuthenticated) { userName = user.FindFirst(ClaimTypes.Name).Value; role = user.FindFirst(ClaimTypes.Role).Value; } var entityType = e.Entity.GetType(); // CanReadAttribute - if (entityType.GetCustomAttributes<CanReadAttribute>().Any(x => x.Role == role || x.User == userName)) { return; } // , var _forbiddenProperties = e.Entity.GetType().GetProperties() .Where(x => !x.GetCustomAttributes<CanReadAttribute>() .Any(y => y.Role == role || y.User == userName)); foreach (var property in _forbiddenProperties) { // property.SetValue(e.Entity, null); } }
ここでは、ユーザーがアクセスできないすべてのプロパティにnullを設定します。 これで、プロジェクトを開始すると、権限のないユーザーにはオブジェクトのキーを読み取る権限すらありません。ユーザーユーザーまたはロールの役割にはすべてのプロパティを表示する権限がありますが、ユーザー2とロール2はリストアイテムの名前を表示できません。 これが私たちが求めていたものです。 しかし、ユーザーがオブジェクトの一部のプロパティのデータに直接アクセスできないという事実にもかかわらず、クライアントではデータモデルの完全なスキームが完全にわかっています。
それが、おそらく、今日お話ししたかったことのすべてでした。 ユーザーは、どのフィールドを編集でき、どのフィールドを編集できないのかわからず、非常にがっかりすることに気付くでしょう。また、User2は、リストアイテムの名前が空であることにショックを受けます。 したがって、次回は、メタデータを簡単に操作することを検討し、ユーザーがアプリケーションでどのような権利を持っているかを視覚的に示します。 完成したプロジェクトはここからダウンロードできます 。
PS記事を書いている間、/ fluent interface属性を使用してそのようなアクセス制御を実装するライブラリを書くことにしました。 機能や実装に関するアイデア、ヒント、提案がある場合は、コメントを歓迎します。 多かれ少なかれまともな何かの準備ができている方法-私はそれをGitHub、NuGetで公開し、ここでチュートリアルを書きます。