ASP .NET Core 3.1 - Usando o Identity de cabo a rabo - XXV


Hoje vamos continuar a série de artigos mostrando como usar o ASP .NET Core Identiy na versão 3.1 da ASP .NET .Core e do EF Core.

Continuando a vigésima quarta parte do artigo veremos como gerenciar as claims dos usuários.

Gerenciando as claims dos usuários

Agora veremos como adicionar ou remover as claims para um dado usuário usando a API Identity.

Você pode pensar em claims como sendo declarações ou atributos que se faz sobre o usuário e que são representadas por um par nome/valor que podem ser usadas para tomar decisões de controle de acesso.

Por exemplo, queremos permitir que um usuário conectado possa editar os detalhes do funcionário, apenas se ele tiver uma claim definida como  "Editar funcionário" atribuída; assim, podemos usar claims para fazer verificações de autorização como esta. Como estamos usando declarações para fazer verificações de autorização, isso é chamado de autorização baseada em declarações ou claims-based authorization.

As claims são baseadas em políticas. Criamos uma política(policy) e incluímos uma ou mais claims nessa política. A politica é então usada junto com o parâmetro policy do atributo Authorize para implementar a autorização baseada em declarações. A sintaxe usada é :

 [Authorize(Policy = "DeleteRolePolicy")]
 public async Task<IActionResult> DeleteRole(string id)
 {
    //...
 }

Vamos a seguir implementar o gerenciamento das claims dos usuários para criar, editar e excluir uma role.

Quando implementamos a exibição dos usuários ao editar um usuário temos a view exibida a seguir:

Para gerenciar as claims do usuário, quando o botão - Gerencia Claims - for acionado vamos direcionar o usuário para a view ManageUserClaims.

Para isso vamos incluir o código abaixo na view EditUser.cshtml, no botão para gerenciar as roles:

 <div class="card-footer">
            <a asp-action="ManageUserClaims" asp-route-userId="@Model.Id"
               class="btn btn-primary">
                Gerenciar Claims
            </a>
 </div>

As claims ou declarações do usuário são armazenadas na tabela AspNeUserClaims que possui a seguinte estrutura :

Id UserId ClaimType ClaimValue
57frdt... c6784hyduij... Cria Role Cria Role

Assim, em nosso projeto vamos tratar as claims do usuário considerando as atribuições que ele vai ter para criar, editar e deletar uma role. Poderíamos trabalhar com qualquer outro tipo e qualquer quantidade de claims mas para deixar o processo mais simples vamos tratar com essas atribuições ou claims do usuário.

Assim vamos gerenciar essas claims do usuário exibindo suas atribuições como: Cria Role, Edita Role e Deleta Role.

Para tratar essas informações vamos criar uma classe estática chamada ClaimsStore de forma que podermos armazená-la em um arquivo de configuração, banco de dados, etc.

Crie a classe ClaimsStore na pasta Models do projeto com o código a seguir:

using System.Collections.Generic;
using System.Security.Claims;
namespace FuncionariosWeb.Models
{
    public static class ClaimsStore
    {
        public static List<Claim> AllClaims = new List<Claim>()
        {
            new Claim("Cria Role", "Cria Role"),
           new Claim("Edita Role","Edita Role"),
           new Claim("Deleta Role","Deleta Role")
         };
    }
}

Nesta classe estamos definindo o ClaimType e o ClaimValue. 

Como existe um relacionamento entre o usuário e a claim vamos criar a classe UserClaim na pasta ViewModels e definir o tipo da claim, e, se ela esta selecionada ou não:

public class UserClaim
{
        public string ClaimType { get; set; }
        public bool IsSelected { get; set; }
}

O próximo passo será criar o método Action ManagaerUserClaims, mas antes precisamos criar a view Model UserClaimsViewModel que vamos usar para exibir as informações das claims do usuário.

Na pasta ViewModels crie a classe UserClaimsViewModel com o código abaixo:

using System.Collections.Generic;
namespace FuncionariosWeb.ViewModels
{
    public class UserClaimsViewModel
    {
        public UserClaimsViewModel()
        {
            Claims = new List<UserClaim>();
        }
        public string UserId { get; set; }
        public List<UserClaim> Claims { get; set; }
    }
}

Nesta classe temos a propriedade UserId e a propriedade Claims que é do tipo List<UserClaim> e representa uma lista de claims atribuida ao usuário que estamos inicializando no construtor da classe. Esta classe vai transferir dados entre o controlador e a view e vice-versa.

Agora vamos criar o método Action ManageUserClaims no controlador AdministrationController.

Primeiro vamos definir o método HttpGet:

        [HttpGet]
        public async Task<IActionResult> ManageUserClaims(string userId)
        {
            var user = await userManager.FindByIdAsync(userId);
            if (user == null)
            {
                ViewBag.ErrorMessage = $"Usuário com  Id = {userId} não foi encontrado";
                return View("NotFound");
            }
            // o método GetClaimsAsync obtém todas as claims atuais do usuário
            var existingUserClaims = await userManager.GetClaimsAsync(user);
            var model = new UserClaimsViewModel
            {
                UserId = userId
            };
            // Percorre cada claim existente
            foreach (Claim claim in ClaimsStore.AllClaims)
            {
                UserClaim userClaim = new UserClaim
                {
                    ClaimType = claim.Type
                };
                // Se o usuário tem uma claim define a propriedade IsSelected 
                // como true para marcar o checkbox
                if (existingUserClaims.Any(c => c.Type == claim.Type))
                {
                    userClaim.IsSelected = true;
                }
                model.Claims.Add(userClaim);
            }
            return View(model);
        }

Neste código localizamos o usuário pelo id e obtemos as claims para o usuário selecionado exibindo-a na view.

Agora temos o código do método HttpPost para o mesmo método Action:

        [HttpPost]
        public async Task<IActionResult> ManageUserClaims(UserClaimsViewModel model)
        {
            var user = await userManager.FindByIdAsync(model.UserId);
            if (user == null)
            {
                ViewBag.ErrorMessage = $"Usuário com  Id = {model.UserId} não foi encontrado";
                return View("NotFound");
            }
            // Obtém todas as claims existentes e as remove
            var claims = await userManager.GetClaimsAsync(user);
            var result = await userManager.RemoveClaimsAsync(user, claims);
            if (!result.Succeeded)
            {
                ModelState.AddModelError("", "Não foi possível remover as claims do usuário");
                return View(model);
            }
            // Adiciona todas as claims que foram selecionadas
            result = await userManager.AddClaimsAsync(user, model.Claims
                                      .Where(c => c.IsSelected)
                                      .Select(c => new Claim(c.ClaimType, c.ClaimType)));
            if (!result.Succeeded)
            {
                ModelState.AddModelError("", "Não foi possível adicionar as claims selecionadas");
                return View(model);
            }
            return RedirectToAction("EditUser", new { Id = model.UserId });
        }

No código acima estamos recebendo o código do usuário e as claims que foram marcadas ou desmarcadas indicando se serão incluídas ou removidas das claims deste usuário.

A seguir adicionamos as claims selecionadas ao usuário e retornamos para a view EditUser.

Agora vamos criar a view ManageUserClaims conforme código a seguir:

@model UserClaimsViewModel
<form method="post">
    <div class="card">
        <div class="card-header">
            <h2>Gerenciar Claims do Usuário</h2>
        </div>
        <div class="card-body">
            @for (int i = 0; i < Model.Claims.Count; i++)
            {
                <div class="form-check m-1">
                    <input type="hidden" asp-for="@Model.Claims[i].ClaimType" />
                    <input asp-for="@Model.Claims[i].IsSelected" class="form-check-input" />
                    <label class="form-check-label" asp-for="@Model.Claims[i].IsSelected">
                        @Model.Claims[i].ClaimType
                    </label>
                </div>
            }
            <div asp-validation-summary="All" class="text-danger"></div>
        </div>
        <div class="card-footer">
            <input type="submit" value="Atualizar" class="btn btn-primary"
                   style="width:auto" />
            <a asp-action="EditUser" asp-route-id="@Model.UserId"
               class="btn btn-primary" style="width:auto">Cancelar</a>
        </div>
    </div>
</form>

Executando o projeto e testando a funcionalidade implementada obtemos o seguinte resultado:

No próximo artigo vamos fazer algumas considerações sobre a autorização baseada em roles e a autorização baseada em claims.

Pegue o projeto aqui:  IdentityTotal_GerenciaClaims.zip (sem as referências)

"Não se turbe o vosso coração; credes em Deus, crede também em mim.
Na casa de meu Pai há muitas moradas; se não fosse assim, eu vo-lo teria dito. Vou preparar-vos lugar."

João 14:1,2

Referências:


José Carlos Macoratti