WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

ASP.NET Identity のロール管理 (CORE)

by WebSurfer 2020年6月1日 10:07

ASP.NET Core Identity のユーザー認証を実装した ASP.NET Core 3.1 MVC アプリに、管理者がロールの表示・追加・変更・削除およびユーザーへのロールのアサイン・解除を行う機能を実装してみます。

ロール情報の表示

先の記事「ASP.NET Identity のロール管理 (MVC)」で .NET Framework 版を書きましたが、この記事は Core 3.1 版です。

ベースとしているアプリは、先の記事「ASP.NET Identity のユーザー管理 (CORE)」で、管理者用のユーザー管理ページを実装したものです。

(1) ロールサービスの追加

ロールを使うにはサービスの追加が必要になります。以下のように、既存のコードに .AddRoles<IdentityRole>() を一行追加するだけで OK です。.NET Framework 版のアプリとは違って Migration 操作は不要です。

ロールサービスの追加

この記事のベースとしているアプリは「認証なし」で作成したあとスキャフォールディングで ASP.NET Core Identity を実装していますので、既存のコードは Areas/Identity フォルダの IdentityHostingStartup.cs ファイルにあります。上の画像がそれです。

認証を「個別のユーザーアカウント」としてプロジェクトを作成すると、サービス関係のコードは上の画像とは異なり、アプリケーションルート直下の Startup.cs フォルダに配置されます。

これだけでロールは使えるようになりますが、ロールの作成・変更・削除や、登録済みユーザーへのロールのアサイン・解除を行う機能は自力でコード書いて実装しなければなりません。その基本的な方法を以下に書きます。

(2) Model の作成

プロジェクトのルート直下の Models フォルダに RoleModel.cs というクラスファイルを追加し、その中に RoleModel クラスを定義して使います。(ファイル名、モデル名は任意です)

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace MySQLIdentity.Models
{
    // ロールの表示、追加、変更、削除用の Model
    // Index, Details, Create, Edit に使用
    public class RoleModel
    {
        public string Id { get; set; }

        [Required(ErrorMessage = "{0}は必須です。")]
        [StringLength(
          100,
          ErrorMessage = "{0} は {2} 文字以上",
          MinimumLength = 3)]
        [Display(Name = "ロール名")]
        public string Name { get; set; }
    }

    // 各ユーザーへのロールのアサイン状況一覧表示と
    // ロールのアサイン・解除用の Model
    // UserWithRoles, EditRoleAssignment に使用
    public class UserWithRoleInfo
    {
        public UserWithRoleInfo()
        {
            UserRoles = new List<RoleInfo>();
        }

        public string UserId { set; get; }

        [Display(Name = "ユーザー名")]
        public string UserName { set; get; }
        public string UserEmail { set; get; }

        public IList<RoleInfo> UserRoles { set; get; }
    }

    public class RoleInfo
    {
        public string RoleName { set; get; }
        public bool IsInThisRole { set; get; }
    }
}

RoleModel クラスはアクションメソッド Index, Details, Create, Edit でロールの表示、追加、変更、削除を行うために用いられる Model です。

フレームワークのライブラリに定義済みの IdentityRole クラスを利用することも可能です。

しかし、IdentityRole クラスを使うと (a) スキャフォールディング機能を使って View を生成できない(デザイナの「Razor ビューの追加」のダイアログで[モデルクラス(M):]の一覧に IdentityRole が現れない)、(b) 表示名を自由に付けられない・・・という理由で RoleModel クラスを定義して使うことにしました。

RoleModel クラスを使うと、Controller で Linq To Entities を使って取得した IdentityRole オブジェクトを RoleModel クラスに詰め替えなければならないという手間が増えますがやむを得ません。

UserWithRoleInfo, RoleInfo クラスは、アクションメソッド UserWithRoles, EditRoleAssignment で、ユーザーへのロールのアサイン状況の表示(この記事の一番上の画像参照)およびユーザーへのロールのアサイン・解除を行うために用いられる Model です。

(3) Controller の作成

.NET Framework 版の ASP.NET MVC5 アプリと違って、Core 版には組み込みの DI 機能があります。今回のケースではコントローラーを初期化する際に UserManager<MySQLIdentityUser> と RoleManager<IdentityRole> オブジェクトが注入されるよう実装します。

ロールの表示・追加・変更・削除を行うためコントローラーのコードは以下の通りです(この記事では Details アクションメソッドは実装していませんが、別の記事にロールに属するユーザー一覧を表示する機能を Details として実装してみましたので、興味があればそちらも見てください)。細かい注意点はコメントで入れましたのでそれを見てください。

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
// 追加
using Microsoft.AspNetCore.Identity;
using MySQLIdentity.Areas.Identity.Data;
using Microsoft.EntityFrameworkCore;
using MySQLIdentity.Models;


namespace MySQLIdentity.Controllers
{
    public class RoleController : Controller
    {
        // コントローラーを呼び出すと DI により自動的にコンストラクタに
        // UserManager<MySQLIdentityUser> と RoleManager<IdentityRole>
        // オブジェクトへの参照が渡される
        private readonly UserManager<MySQLIdentityUser> _userManager;
        private readonly RoleManager<IdentityRole> _roleManager;

        public RoleController(UserManager<MySQLIdentityUser> userManager,
                              RoleManager<IdentityRole> roleManager)
        {
            _userManager = userManager;
            _roleManager = roleManager;
        }

        // GET: Role/Index
        // Model は RoleModel
        public async Task<IActionResult> Index()
        {
            var roles = from r in _roleManager.Roles
                        orderby r.Name
                        select new RoleModel
                        {
                            Id = r.Id,
                            Name = r.Name
                        };

            return View(await roles.ToListAsync());
        }


        // GET: Role/Details/5・・・省略
        // ロール名以外の情報は含まれないので不要


        // GET: Role/Create
        // Model は RoleModel クラス
        public IActionResult Create()
        {
            return View();
        }

        // POST: Role/Create
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create(
                            [Bind("Id,Name")] RoleModel model)
        {
            if (ModelState.IsValid)
            {
                // ユーザーが入力したロール名を model.Name から
                // 取得し IdentityRole オブジェクトを生成
                var role = new IdentityRole { Name = model.Name };

                // 上の IdentityRole から新規ロールを作成・登録
                var result = await _roleManager.CreateAsync(role);

                if (result.Succeeded)
                {
                    // 登録に成功したら Role/Index にリダイレクト
                    return RedirectToAction("Index", "Role");
                }

                // result.Succeeded が false の場合 ModelSate にエ
                // ラー情報を追加しないとエラーメッセージが出ない。
                // Register.cshtml.cs のものをコピー
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, 
                                             error.Description);
                }
            }

            // ロールの登録に失敗した場合、登録画面を再描画
            return View(model);
        }

        // GET: Role/Edit/5
        // Edit でできるのはロール名の変更のみ
        // Model は RoleModel クラス
        public async Task<IActionResult> Edit(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var target = await _roleManager.FindByIdAsync(id);

            if (target == null)
            {
                return NotFound();
            }

            RoleModel model = new RoleModel
            { 
                Name = target.Name 
            };

            return View(model);
        }

        // POST: Role/Edit/5
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(string id, 
                        [Bind("Id,Name")] RoleModel model)
        {
            if (id == null)
            {
                return NotFound();
            }

            if (ModelState.IsValid)
            {
                var target = await _roleManager.FindByIdAsync(id);
                if (target == null)
                {
                    return NotFound();
                }

                // ユーザーが入力したロール名を model.Name から取得し
                // て IdentityRole の Name を書き換え
                target.Name = model.Name;

                // Name を書き換えた IdentityRole で更新をかける
                var result = await _roleManager.UpdateAsync(target);

                if (result.Succeeded)
                {
                    // 更新に成功したら Role/Index にリダイレクト
                    return RedirectToAction("Index", "Role");
                }

                // result.Succeeded が false の場合 ModelSate にエ
                // ラー情報を追加しないとエラーメッセージが出ない。
                // Register.cshtml.cs のものをコピー
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, 
                                             error.Description);
                }
            }

            // 更新に失敗した場合、編集画面を再描画
            return View(model);
        }

        // GET: Role/Delete/5
        // 階層更新が行われているようで、ユーザーがアサインされて
        // いるロールも削除可能。
        // Model は RoleModel
        public async Task<IActionResult> Delete(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var role = await _roleManager.FindByIdAsync(id);

            if (role == null)
            {
                return NotFound();
            }

            var model = new RoleModel
            {
                Id = role.Id,
                Name = role.Name
            };

            return View(model);
        }

        // POST: Role/Delete/5
        // 上の Delete(string id) と同シグネチャのメソッド
        // は定義できないので、メソッド名を変えて、下のよう
        // に ActionName("Delete") を設定する
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var role = await _roleManager.FindByIdAsync(id);

            if (role == null)
            {
                return NotFound();
            }

            // ユーザーがアサインされているロールも以下の一行で
            // 削除可能。内部で階層更新が行われているらしい。
            var result = await _roleManager.DeleteAsync(role);

            if (result.Succeeded)
            {
                // 削除に成功したら Role/Index にリダイレクト
                return RedirectToAction("Index", "Role");
            }

            // result.Succeeded が false の場合 ModelSate にエ
            // ラー情報を追加しないとエラーメッセージが出ない。
            // Register.cshtml.cs のものをコピー
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError(string.Empty, 
                                         error.Description);
            }
            // 削除に失敗した場合、削除画面を再描画
            var model = new RoleModel
            {
                Id = role.Id,
                Name = role.Name
            };

            return View(model);
        }
    }
}

登録済みユーザーへのロールのアサイン状況の表示、個々のユーザーへのロールの追加・削除を行うためのアクションメソッドは以下の通りです。実際は、上のコードの RoleController 内に実装しています(分けたのは単にその方が読みやすいと思ったからです)。

// 以下は:
// (1) UserWithRoles で登録済みユーザーの一覧と各ユーザーへの
//     ロールのアサイン状況を表示し、
// (2) Edit ボタンクリックで EditRoleAssignment に遷移し、当該
//     ユーザーへのロールのアサインを編集して保存
// ・・・を行うアクションメソッド。

// GET: Role/UserWithRoles
// ユーザー一覧と各ユーザーにアサインされているロールを表示
// Model は UserWithRoleInfo クラス
public async Task<IActionResult> UserWithRoles()
{
    var model = new List<UserWithRoleInfo>();

    // ToListAsync() を付与してここで DB からデータを取得して
    // DataReader を閉じておかないと、下の IsInRole メソッド
    // でエラーになるので注意
    var users = await _userManager.Users.
                OrderBy(user => user.UserName).ToListAsync();
    var roles = await _roleManager.Roles.
                OrderBy(role => role.Name).ToListAsync();

    foreach (MySQLIdentityUser user in users)
    {
        var info = new UserWithRoleInfo
        {
            UserId = user.Id,
            UserName = user.UserName,
            UserEmail = user.Email
        };

        foreach (IdentityRole role in roles)
        {
            RoleInfo roleInfo = new RoleInfo();
            roleInfo.RoleName = role.Name;
            roleInfo.IsInThisRole = await _userManager.
                                    IsInRoleAsync(user, role.Name);
            info.UserRoles.Add(roleInfo);
        }
        model.Add(info);
    }

    return View(model);
}

// GET: Role/EditRoleAssignment/Id
// 指定 Id のユーザーのロールへのアサインの編集
// Model は UserWithRoleInfo クラス
public async Task<IActionResult> EditRoleAssignment(string id)
{
    if (id == null)
    {
        return NotFound();
    }

    var user = await _userManager.FindByIdAsync(id);
    if (user == null)
    {
        return NotFound();
    }

    var model = new UserWithRoleInfo
    {
        UserId = user.Id,
        UserName = user.UserName,
        UserEmail = user.Email
    };

    // ToListAsync() を付与しておかないと下の IsInRole メソッド
    // で DataReader が閉じてないというエラーになる
    var roles = await _roleManager.Roles.
                  OrderBy(role => role.Name).ToListAsync();

    foreach (IdentityRole role in roles)
    {
        RoleInfo roleInfo = new RoleInfo();
        roleInfo.RoleName = role.Name;
        roleInfo.IsInThisRole = await _userManager.
                                IsInRoleAsync(user, role.Name);
        model.UserRoles.Add(roleInfo);
    }

    return View(model);
}

// POST: Role/EditRoleAssignment/Id
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditRoleAssignment(string id, 
  [Bind("UserId,UserName,UserEmail,UserRoles")] UserWithRoleInfo model)
{
    if (id == null)
    {
        return NotFound();
    }

    // IsInRoleAsync, AddToRoleAsync, RemoveFromRoleAsync メソッド
    // の引数が MVC5 とは異なり、id ではなく MySQLIdentityUser が
    // 必要なのでここで取得しておく
    var user = await _userManager.FindByIdAsync(id);
    if (user == null)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        IdentityResult result;

        foreach (RoleInfo roleInfo in model.UserRoles)
        {
            // id のユーザーが roleInfo.RoleName のロールに属して
            // いるか否か。以下でその情報が必要。
            bool isInRole = await _userManager.
                            IsInRoleAsync(user, roleInfo.RoleName);

            // roleInfo.IsInThisRole には編集画面でロールのチェッ
            // クボックスのチェック結果が格納されている
            if (roleInfo.IsInThisRole)
            {
                // チェックが入っていた場合

                // 既にロールにアサイン済みのユーザーを AddToRole
                // するとエラーになるので以下の判定が必要
                if (isInRole == false)
                {
                    result = await _userManager.AddToRoleAsync(user, 
                                                      roleInfo.RoleName);
                    if (!result.Succeeded)
                    {
                        // result.Succeeded が false の場合 ModelSate にエ
                        // ラー情報を追加しないとエラーメッセージが出ない。
                        // Register.cshtml.cs のものをコピー
                        foreach (var error in result.Errors)
                        {
                            ModelState.AddModelError(string.Empty, 
                                                     error.Description);
                        }
                        return View(model);
                    }
                }
            }
            else
            {
                // チェックが入っていなかった場合

                // ロールにアサインされてないユーザーを
                // RemoveFromRole するとエラーになるので以下の
                // 判定が必要
                if (isInRole == true)
                {
                    result = await _userManager.
                             RemoveFromRoleAsync(user, roleInfo.RoleName);
                    if (!result.Succeeded)
                    {
                        foreach (var error in result.Errors)
                        {
                            ModelState.AddModelError(string.Empty, 
                                                     error.Description);
                        }
                        return View(model);
                    }
                }
            }
        }

        // 編集に成功したら Role/UserWithRoles にリダイレクト
        return RedirectToAction("UserWithRoles", "Role");
    }

    // 編集に失敗した場合、編集画面を再描画
    return View(model);
}

(4) View の作成

View は Controller のアクションメソッドを右クリックして[ビューの追加(D)...]からスキャフォールディング機能を使って生成します。

Razor ビューの追加

アクションメソッド EditRoleAssignment に対応する View は自動生成されるコードだけでは不十分でいろいろ追加・修正が必要です。以下にその完成版のコードを載せておきます。

@model MySQLIdentity.Models.UserWithRoleInfo

@{
    ViewData["Title"] = "EditRoleAssignment";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>EditRoleAssignment</h1>

<h4>UserWithRoleInfo</h4>
<hr />
<form asp-action="EditRoleAssignment">
    <table class="table">
        <thead>
            <tr>
                <th>
                    @Html.DisplayNameFor(model => model.UserName)
                </th>
                @foreach (var role in Model.UserRoles)
                {
                    <th>
                        @Html.DisplayFor(modelItem => role.RoleName)
                    </th>
                }
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>
                    @Html.DisplayFor(model => model.UserName)
                    @Html.HiddenFor(model => model.UserName)
                    @Html.HiddenFor(model => model.UserEmail)
                    @Html.HiddenFor(model => model.UserId)
                </td>
                @for (int i = 0; i < Model.UserRoles.Count; i++)
                {
                    <td>
                        @Html.EditorFor(model => model.UserRoles[i].IsInThisRole)
                        @Html.HiddenFor(model => model.UserRoles[i].RoleName)
                    </td>
                }

            </tr>
        </tbody>
    </table>
    <div class="form-group">
        <input type="submit" value="Save" class="btn btn-primary" />
    </div>
</form>

<div>
    <a asp-action="UserWithRoles">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

上の View のコードで注意すべき点は、コレクション(上の例では Model.UserRoles で取得できる RoleInfo クラスのコレクション) についてはレンダリングされる html 要素の name 属性が連番のインデックスを含むようにすることです。具体的には、name="prefix[index].Property" というパターンにします。

そうしないとモデルバインディングがうまくいきません。 その理由など詳しくは先の記事「コレクションのデータアノテーション検証」を見てください。

Tags: , , ,

CORE

ASP.NET Identity のロール管理 (MVC)

by WebSurfer 2017年11月2日 16:40

ASP.NET Identity ベースのフォーム認証を実装した ASP.NET MVC5 アプリケーションで、管理者がロール管理を行う機能を実装する例を書きます。先の記事「ASP.NET Identity のユーザー管理 (MVC)」の続きです。

ロール管理画面

CodeZine の記事「ASP.NET Identityでユーザーに役割(ロール)を持たせる 」に、ASP.NET Web Forms アプリ用のユーザー管理画面を作る手順が載っていますが、その MVC 版です。

ベースとなるのは、先の記事「ASP.NET Identity のユーザー管理 (MVC)」と同様、Visual Studio 2015 Community で MVC のテンプレートを利用し、認証に「個別のユーザーアカウント」を指定して自動生成させたプロジェクトです。

それにロール情報の追加、変更、削除および登録済みユーザーのロールのアサインを行う機能を実装するには以下のようにします:

(1) コードの追加

ロール情報の追加、変更、削除を行うのに用いるクラスは、IdentityRole クラスRoleManager<TRole, TKey> クラスです。

テンプレートで自動生成されたコードにはそれらのコードは含まれていませんので追記する必要があります。

具体的には、Models\IdentityModels.cs, App_Start\IdentityConfig.cs, App_Start\Startup.Auth.cs の 3 つの既存のファイルに、CodeZine の記事「ASP.NET Identityでユーザーに役割(ロール)を持たせる」を参考にコードを追加します。(記事のコードをそのままコピペすれば OK)

何を追加するかを簡単に書きますと、IdentityModels.cs, IdentityConfig.cs, Startup.Auth.cs の順に、以下の通りです。

  1. IdentityRole クラスを継承した ApplicationRole クラスを IdentityModels.cs に追加。
  2. RoleManager<TRole, TKey> クラスを継承した ApplicationRoleManager クラスを IdentityConfig.cs に追加。定義には ApplicationRoleManager クラスのインスタンスを生成して OwinContext に登録するための Create メソッドを含める。Create メソッドにはロールストアに何も定義されてない場合 Administrator という名前のロールを作成するコードも含まれています。
  3. ApplicationRoleManager のインスタンスを生成して OwinContext に登録するコードを Startup.Auth.cs の ConfigureAuth メソッドに追加。これにより、Controller で HttpContext.GetOwinContext() メソッドにより OwinContext を取得し、それから Get<ApplicationRoleManager>() メソッドで ApplicationRolerManager オブジェクトを取得できるようになります。

(2) Migrations を実施

上記 (1) の作業後アプリを実行すると、先にユーザー情報を登録して EF Code First の機能で DB を生成済みの場合は以下のようなエラーが出ると思います。ちなみに、下の画像でエラーとなっている行は、上記 (1) - 2 で追加した Create メソッドでロールストアにあるロール情報を取得するものです。

エラー画面

その場合は Migrations 実施してから再度実行してください。

具体的には、Visual Studio のパッケージマネージャーコンソールで、Enable-Migrations コマンドを実行して Code First Migrations を有効にし、次に Update-Database –Verbose コマンドを実行して保留中の移行をデータベースに適用します。

(3) AspNetRoles テーブルの確認

上記 (2) でエラーなく無事実行できれば、以下の画像のようにロール情報のストアとなる AspNetRoles テーブルに Discriminator フィールドが追加され、「結果」ウィンドウに示すように Name が Administrator というレコードができているはずです。

AspNetRoles テーブル

(4) ロール管理用 Controller / View の実装

ロール情報の追加、変更、削除および登録済みユーザーのロールのアサインを行うための Controller / View を実装します。Controller の実装例を以下に示します。説明はコード内のコメントに書きましたので、それを見てください。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

// 上は自動生成されたもの。それに以下の名前空間を追加
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using System.Net;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Mcv5App2.Models;

namespace Mcv5App2.Controllers
{
  // 以下の RoleModel, UserWithRoleInfo, RoleInfo の 3 クラス
  // はここで利用する Model。Controller 内に定義したのは単に
  // 分けるのが面倒だから

  // アクションメソッド Create, Edit 用の Model
  public class RoleModel
  {
    [Required]
    [StringLength(
      100, 
      ErrorMessage = "{0} は {2} 文字以上",
      MinimumLength = 3)]
    [Display(Name = "Role")]
    public string Role { get; set; }
  }

  // アクションメソッド UserWithRoles, EditRoleAssignment
  //  (各ユーザーのロール一覧表示、ロールのアサイン・削除) 
  // 用の Model
  public class UserWithRoleInfo
  {
    public UserWithRoleInfo()
    {
      UserRoles = new List<RoleInfo>();
    }

    public string UserId { set; get; }
    public string UserName { set; get; }
    public string UserEmail { set; get; }

    public IList<RoleInfo> UserRoles { set; get; }
  }

  public class RoleInfo
  {
    public string RoleName { set; get; }
    public bool IsInThisRole { set; get; }
  }


  // ここから Controller のコード
  public class RolesController : Controller
  {
    private ApplicationUserManager _userManager;
    private ApplicationRoleManager _roleManager;

    public ApplicationUserManager UserManager
    {
      get
      {
        return _userManager ?? 
            HttpContext.GetOwinContext().
            GetUserManager<ApplicationUserManager>();
      }
      private set
      {
        _userManager = value;
      }
    }

    public ApplicationRoleManager RoleManager
    {
      get
      {
        return _roleManager ?? 
            HttpContext.GetOwinContext().
            Get<ApplicationRoleManager>();
      }
      private set
      {
        _roleManager = value;
      }
    }

    // GET: Roles(登録済みロールの一覧)
    // Model は ApplicationRole
    public ActionResult Index()
    {
      var roles = 
        RoleManager.Roles.OrderBy(role => role.Name);
      return View(roles);
    }

    // GET: Roles/Details/Id(指定 Id のロール詳細)
    // 上の一覧以上の情報は含まれないので不要かも・・・
    // Model は ApplicationRole
    public async Task<ActionResult> Details(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                        HttpStatusCode.BadRequest);
      }

      var target = await RoleManager.FindByIdAsync(id);
      if (target == null)
      {
        return HttpNotFound();
      }
      return View(target);
    }

    // GET: Roles/Create(新規ロール作成・登録)
    // Model は上に定義した RoleModel クラス
    public ActionResult Create()
    {
      return View();
    }

    // POST: Roles/Create(新規ロール作成・登録)
    // Model は上に定義した RoleModel クラス
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Create(RoleModel model)
    {
      if (ModelState.IsValid)
      {
        // ユーザーが入力したロール名を model.Role から取得し
        // て ApplicationRole を生成
        var role = new ApplicationRole { Name = model.Role };

        // 上の ApplicationRole から新規ロールを作成・登録
        var result = await RoleManager.CreateAsync(role);

        if (result.Succeeded)
        {
          // 登録に成功したら Roles/Index にリダイレクト
          return RedirectToAction("Index", "Roles");
        }

        // result.Succeeded が false の場合 ModelSate にエ
        // ラー情報を追加しないとエラーメッセージが出ない。
        // AccountController と同様に AddErrors メソッドを
        // 定義して利用(一番下に定義あり)
        AddErrors(result);
      }

      // ロールの登録に失敗した場合、登録画面を再描画
      return View(model);
    }

    // GET: Roles/Edit/Ed(指定 Id のロール情報の更新)
    // ここで更新できるのはロール名のみ
    // Model は上に定義した RoleModel クラス
    public async Task<ActionResult> Edit(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                            HttpStatusCode.BadRequest);
      }

      var target = await RoleManager.FindByIdAsync(id);

      if (target == null)
      {
        return HttpNotFound();
      }

      RoleModel model = new RoleModel() { Role = target.Name };

      return View(model);
    }

    // POST: Roles/Edit/Id(指定 Id のロール情報の更新)
    // ここで更新できるのはロール名のみ
    // Model は上に定義した RoleModel クラス
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> 
                        Edit(string id, RoleModel model)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                            HttpStatusCode.BadRequest);
      }

      if (ModelState.IsValid)
      {
        var target = await RoleManager.FindByIdAsync(id);

        // ユーザーが入力したロール名を model.Role から取得し
        // て ApplicationRole の Name を書き換え
        target.Name = model.Role;

        // Name を書き換えた ApplicationRole で更新をかける
        var result = await RoleManager.UpdateAsync(target);

        if (result.Succeeded)
        {
          // 更新に成功したら Roles/Index にリダイレクト
          return RedirectToAction("Index", "Roles");
        }

        // result.Succeeded が false の場合 ModelSate にエ
        // ラー情報を追加しないとエラーメッセージが出ない。
        AddErrors(result);
      }

      // 更新に失敗した場合、編集画面を再描画
      return View(model);
    }

    // GET: Roles/Delete/Id(指定 Id のロールを削除)
    // 階層更新が行われているようで、ユーザーがアサインされて
    // いるロールも削除可能。
    // Model は ApplicationRole
    public async Task<ActionResult> Delete(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                            HttpStatusCode.BadRequest);
      }

      var target = await RoleManager.FindByIdAsync(id);

      if (target == null)
      {
        return HttpNotFound();
      }

      return View(target);
    }

    // POST: Roles/Delete/Id(指定 Id のロールを削除)
    // Model は ApplicationRole
    // 上の Delete(string id) と同シグネチャのメソッド
    // は定義できないので、メソッド名を変えて、下のよう
    // に ActionName("Delete") を設定する
    [HttpPost, ActionName("Delete")]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> 
                        DeleteConfirmed(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                            HttpStatusCode.BadRequest);
      }

      var target = await RoleManager.FindByIdAsync(id);

      if (target == null)
      {
        return HttpNotFound();
      }

      // ユーザーがアサインされているロールも以下の一行で
      // 削除可能。内部で階層更新が行われているらしい。
      var result = await RoleManager.DeleteAsync(target);

      if (result.Succeeded)
      {
        // 削除に成功したら Roles/Index にリダイレクト
        return RedirectToAction("Index", "Roles");
      }

      // result.Succeeded が false の場合 ModelSate にエ
      // ラー情報を追加しないとエラーメッセージが出ない。
      AddErrors(result);

      // 削除に失敗した場合、削除画面を再描画
      return View(target);
    }


    // 以下は:
    // (1) UserWithRoles で、上の画像のように、登録済みユーザー
    //     の一覧と各ユーザーへのロールのアサイン状況を表示し、
    // (2) Edit ボタンクリックで EditRoleAssignment に遷移し、
    //     当該ユーザーへのロールのアサインを編集して保存する
    // ・・・ためのもの。

    // GET: Roles/UserWithRoles
    // ユーザー一覧と各ユーザーにアサインされているロールを表示
    // Model は上に定義した UserWithRoleInfo クラス
    public async Task<ActionResult> UserWithRoles()
    {
      var model = new List<UserWithRoleInfo>();

      // ToList() を付与してここで DB からデータを取得して
      // DataReader を閉じておかないと、下の IsInRole メソッド
      // でエラーになるので注意
      var users = UserManager.Users.
                  OrderBy(user => user.UserName).ToList();
      var roles = RoleManager.Roles.
                  OrderBy(role => role.Name).ToList();
            
      foreach (ApplicationUser user in users)
      {
        UserWithRoleInfo info = new UserWithRoleInfo();
        info.UserId = user.Id;
        info.UserName = user.UserName;
        info.UserEmail = user.Email;

        foreach (ApplicationRole role in roles)
        {
          RoleInfo roleInfo = new RoleInfo();
          roleInfo.RoleName = role.Name;
          roleInfo.IsInThisRole = await
              UserManager.IsInRoleAsync(user.Id, role.Name);
          info.UserRoles.Add(roleInfo);
        }
        model.Add(info);
      }

      return View(model);
    }

    // GET: Roles/EditRoleAssignment/Id
    // 指定 Id のユーザーのロールへのアサインの編集
    // Model は上に定義した UserWithRoleInfo クラス
    public async Task<ActionResult> 
                        EditRoleAssignment(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                            HttpStatusCode.BadRequest);
      }

      var user = await UserManager.FindByIdAsync(id);

      if (user == null)
      {
        return HttpNotFound();
      }

      UserWithRoleInfo model = new UserWithRoleInfo();
      model.UserId = user.Id;
      model.UserName = user.UserName;
      model.UserEmail = user.Email;

      // ToList() を付与しておかないと下の IsInRole メソッド
      // で DataReader が閉じてないというエラーになる
      var roles = RoleManager.Roles.
                  OrderBy(role => role.Name).ToList();

      foreach (ApplicationRole role in roles)
      {
        RoleInfo roleInfo = new RoleInfo();
        roleInfo.RoleName = role.Name;
        roleInfo.IsInThisRole = await
            UserManager.IsInRoleAsync(user.Id, role.Name);
        model.UserRoles.Add(roleInfo);
      }

      return View(model);
    }

    // GET: Roles/EditRoleAssignment/Id
    // 指定 Id のユーザーのロールへのアサインの編集
    // Model は上に定義した UserWithRoleInfo クラス
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> 
      EditRoleAssignment(string id, UserWithRoleInfo model)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                            HttpStatusCode.BadRequest);
      }

      if (ModelState.IsValid)
      {
        IdentityResult result;

        foreach (RoleInfo roleInfo in model.UserRoles)
        {
          // id のユーザーが roleInfo.RoleName のロールに属して
          // いるか否か。以下でその情報が必要。
          bool isInRole = await 
            UserManager.IsInRoleAsync(id, roleInfo.RoleName);

          // roleInfo.IsInThisRole には編集画面でロールのチェッ
          // クボックスのチェック結果が格納されている
          if (roleInfo.IsInThisRole)
          {
            // チェックが入っていた場合

            // 既にロールにアサイン済みのユーザーを AddToRole
            // するとエラーになるので以下の判定が必要
            if (isInRole == false)
            {
              result = await UserManager.
                       AddToRoleAsync(id, roleInfo.RoleName);
              if (!result.Succeeded)
              {
                AddErrors(result);
                return View(model);
              }
            }
          }
          else
          {
            // チェックが入っていなかった場合

            // ロールにアサインされてないユーザーを
            // RemoveFromRole するとエラーになるので以下の
            // 判定が必要
            if (isInRole == true)
            {
              result = await UserManager.
                       RemoveFromRoleAsync(id, roleInfo.RoleName);
              if (!result.Succeeded)
              {
                AddErrors(result);
                return View(model);
              }
            }
          }                    
        }

        // 編集に成功したら Roles/UserWithRoles にリダイレクト
        return RedirectToAction("UserWithRoles", "Roles");
      }

      // 編集に失敗した場合、編集画面を再描画
      return View(model);
    }

    protected override void Dispose(bool disposing)
    {
      if (disposing)
      {
        if (_userManager != null)
        {
          _userManager.Dispose();
          _userManager = null;
        }

        if (_roleManager != null)
        {
          _roleManager.Dispose();
          _roleManager = null;
        }
      }

      base.Dispose(disposing);
    }

    // ModelSate にエラー情報を追加するためのヘルパメソッド
    private void AddErrors(IdentityResult result)
    {
      foreach (var error in result.Errors)
      {
        ModelState.AddModelError("", error);
      }
    }
  }
}

MSDN ライブラリの UserManager<TUser> クラスRoleManager<TRole, TKey> クラスの説明には非同期版メソッドしか書いてないのでそれらを使っていますが、同期版も拡張メソッドとして定義されているようです。ケースバイケースでどちらが適切かを考えて使い分けた方が良いかもしれません。

View はコントローラをベースにスキャフォールディング機能を利用して自動生成できます。Visual Studio でアクションメソッドのコードを右クリックして[ビューを追加(D)...]を選ぶと、そのためのダイアログが表示されます。

アクションメソッド EditRoleAssignment に対応する View は自動生成されるコードだけでは不十分でいろいろ追加・修正が必要です。以下にその完成版のコードを載せておきます。

@model Mcv5App2.Controllers.UserWithRoleInfo

@{
  ViewBag.Title = "EditRoleAssignment";
  Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>EditRoleAssignment</h2>


@using (Html.BeginForm())
{
  @Html.AntiForgeryToken()
    
  <h4>UserWithRoleInfo</h4>
  <hr />
  @Html.ValidationSummary("",new { @class="text-danger" })
  <table class="table">
    <tr>
      <th>
        @Html.DisplayNameFor(model => model.UserName)
      </th>
    @foreach (var role in Model.UserRoles)
    {
      <th>
        @Html.DisplayFor(modelItem => role.RoleName)
      </th>
    }
    </tr>
            
    <tr>
      <td>
        @Html.DisplayFor(model => model.UserName)
        @Html.HiddenFor(model => model.UserName)
        @Html.HiddenFor(model => model.UserEmail)
        @Html.HiddenFor(model => model.UserId)
      </td>
    @for (int i = 0; i < Model.UserRoles.Count; i++)
    {
      <td>
        @Html.EditorFor(model =>
                     model.UserRoles[i].IsInThisRole)
        @Html.HiddenFor(model => 
                     model.UserRoles[i].RoleName)
      </td>
    }
                    
    </tr>
  </table>
  <div class="form-group">
    <div class="col-md-offset-2 col-md-10">
      <input type="submit" value="Save" 
             class="btn btn-default" />
    </div>
  </div>
}

<div>
  @Html.ActionLink("Back to List", "UserWithRoles")
</div>

@section Scripts {
  @Scripts.Render("~/bundles/jqueryval")
}

上の View のコードで注意すべき点は、コレクション(上の例では Model.UserRoles で取得できる RoleInfo クラスのコレクション) についてはレンダリングされる html 要素の name 属性が連番のインデックスを含むようにすることです。具体的には、name="prefix[index].Property" というパターンにします。

そうしないとモデルバインディングがうまくいきません。その理由など詳しくは先の記事「コレクションのデータアノテーション検証」を見てください。

Tags: ,

MVC

Windows 認証でのロール

by WebSurfer 2011年2月5日 16:48

ASP.NET の承認は、"ユーザー" に加えて "ロール" というレベルでの管理をサポートしています。ロール管理では、ユーザーをロールに割り当てることにより、ユーザーのグループを単位として扱うことができます。 詳しくは MSDN ライブラリの ASP.NET のロールによる承認の管理 が参考になると思います。

Forms 認証の場合は、サイト管理者がロールを定義し、ロール情報は SQL Server などのデータベースに格納します。

一方、Windows 認証の場合は Windows アカウントを利用しますので、ロールはそのユーザーが属するグループになります。従って、ロールを定義するのはサーバーの管理者で、ロール情報は Active Directory ドメインコントローラーに格納されるということになります。

当然、Windows 認証の場合もロールによるアクセス制限は可能です。例えば、あるフォルダにロールによるアクセス制限をしておくと、そのロールに属さないユーザーがフォルダ内のページを要求すると下の画像のようなダイアログが出てきます。

Windows 認証のダイアログ

ロールによるアクセス制限は、Forms 認証と同様に web.config で定義します。フォルダ直下の web.config に以下のような定義をしておくと、Domain Users グループに属するユーザー以外のアクセスは拒否されます。

<configuration>
  <system.web>
    <authorization>
      <allow roles="<ドメイン名>\Domain Users" />
      <deny users="*" />
    </authorization>
  </system.web>
</configuration>

なお、Windows 認証の場合、web.config でロールを有効にする(web.config で <roleManager enabled="true" /> とする)のは意味がなさそうです。というより、そういう設定はしてはいけないようです。

web.config でロールを有効にすると、Page.User は System.Security.Principal.IPrincipal ではなく、それから派生した System.Web.Security.RolePrincipal になります。そこで WindowsPrincipal オブジェクトを取得するため (WindowsPrincipal)User とすると、"型 'System.Web.Security.RolePrincipal' のオブジェクトを型 'System.Security.Principal.WindowsPrincipal' にキャストできません。" というエラーになりますので。

ここからは先は余談です。

SID の表示

Windows アカウントでは、ユーザーもグループもセキュリティ識別子 (SID) を認識に用いています。よく知られた SID は MSDN ライブラリの Windows サーバー オペレーティング システムの既知のセキュリティ識別子 に一覧がありますので見てください。

SID は S-1-1-0 のように人間にとっては意味不明の文字列ですが、これを Everyone のように ACL エディタで使われている名前に変換する方法を忘れないように書いておきます。

Everyone のような名前は NTAccount オブジェクトから取得できます。NTAccount オブジェクトは IdentityReference.Translate メソッドで取得できます。その例を以下のコードに示します。これを実行すると、上の画像のように表示されます。

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Security.Principal" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">
    
  protected void Page_Load(object sender, EventArgs e)
  {
    WindowsPrincipal principal = (WindowsPrincipal)User;

    // IsInRole("S-1-2-35-545") ではダメ。
    if (principal.IsInRole("BUILTIN\\Users"))
    {
      Label1.Text = "BUILTIN\\Users";
    }

    WindowsIdentity identity = 
      (WindowsIdentity)principal.Identity;
    SecurityIdentifier sid = identity.Owner;
    StringBuilder sb = new StringBuilder();
    NTAccount nt = (NTAccount)sid.Translate(typeof(NTAccount));
    sb.Append("ユーザーの SID: " + nt.Value + 
      " (" + sid.ToString() + ")<br /><br />");
    sb.Append("ユーザーが属するグループ<br />");
    IdentityReferenceCollection irc = identity.Groups;
    foreach (IdentityReference ir in irc)
    {
      NTAccount ntAccount = 
        (NTAccount)ir.Translate(typeof(NTAccount));
      sb.Append("&nbsp;&nbsp;" + ntAccount.Value + 
        " (" + ir.Value + ")<br />");
    }
        
    Label2.Text = sb.ToString();
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title></title>
</head>
<body>
  <form id="form1" runat="server">
  <div>
    ユーザ: <asp:LoginName ID="LoginName1" runat="server" />
    <br />
    ロール: <asp:Label ID="Label1" runat="server" />
    <hr />
    <h1>Windows Users のページ</h1>
    <asp:Label ID="Label2" runat="server" />
    <hr />
    <asp:HyperLink ID="HyperLink1" 
        runat="server" 
        NavigateUrl="~/Default.aspx">
        入り口に戻る
    </asp:HyperLink>
  </div>
  </form>
</body>
</html>

Tags: ,

Authentication

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar