Fin ora abbiamo visto come creare ed autenticare un utente, oltre che a recuperare la password e a confermare il proprio indirizzo e-mail. Passiamo adesso alla gestione dei ruoli su Identy Framework, visto che per l’applicazione è necessario distinguere i vari livelli di accesso.

Un piccolo passo indietro

Prima di continuare facciamo un ritocco al front-end sistemando l’area di login in alto a destra. Per farlo apriamo il file Index.razor ed eliminiamo il markup aggiunto nel precedente articolo, riportando il file al suo stato iniziale. Andiamo poi a creare un componente LoginDisplay.razor nella cartella Shared del progetto Client:

<AuthorizeView>
    <Authorized>
        <a href="#">Ciao, @context.User.Identity.Name!</a>
        <a href="account/logout" class="nav-link btn btn-link">Log out</a>
    </Authorized>
    <NotAuthorized>
        <a href="account/register">Register</a>
        <a href="account/login">Log in</a>
    </NotAuthorized>
</AuthorizeView>

Adesso modifichiamo il file MainLayout.razor per aggiungere il nostro nuovo componente:

@inherits LayoutComponentBase
<div class="sidebar">
    <NavMenu />
</div>
<div class="main">
    <div class="top-row px-4 auth">
        <LoginDisplay/>
    </div>

    <div class="content px-4">
        @Body
    </div>
</div>

In questo modo l’interfaccia risulta sicuramente più ordinata:

Lagin Area

Gestione Ruoli

Creiamo adesso una cartella chiamata Role nella cartella Pages del progetto Client, nella quale aggiungiamo un file razor chiamato Index.razor che si occuperà di visualizzare, aggiungere e modificare i ruoli che useremo nell’applicazione.. Ecco il suo contenuto:

@page "/role/index"
@layout MainLayout
@inject HttpClient Http
@using CompTrain.Shared.Models.Role;

<h2>Manage Roles</h2>
<div class="row">

    <div class="col-4">
        @if (roleResponses?.Count > 0)
        {
            <RadzenListBox @bind-Value="@value" Data="@roleResponses" Style="width:100%;min-height:200px;" TextProperty="Name" ValueProperty="Id" Change="@(args => Selected(args))" />
        }
        else
        {
            <p class="card-text"><small class="text-muted">empty</small></p>
        }
        <RadzenButton ButtonType="Radzen.ButtonType.Button" Click="@((args) => AddRole(args))" Text="New role" Icon="https" ButtonStyle="Radzen.ButtonStyle.Warning" class="btn-block btn-sm mr-2"></RadzenButton>
    </div>

    @if (model != null)
    {
        <div class="col-4">
            <EditForm Model="model" OnValidSubmit="SaveRole">
                <RadzenCard>
                    <label>Name</label>
                    <RadzenTextBox @bind-Value="model.Name"></RadzenTextBox>
                    <hr />
                    <DataAnnotationsValidator />
                    <ValidationSummary />
                    <Alert Title="Attenzione" ErrorList="editRoleResponse?.Errors" />
                    <RadzenButton ButtonType="Radzen.ButtonType.Submit" Text="@buttonTitle" Icon="save" ButtonStyle="Radzen.ButtonStyle.Warning" class="btn-block mr-2"></RadzenButton>
                </RadzenCard>
            </EditForm>
        </div>
    }
</div>
@code {
    EditRoleRequest model;
    EditRoleResponse editRoleResponse;
    List<RoleModel> roleResponses = null;
    string value;
    string buttonTitle = "Save";

    protected async override void OnInitialized()
    {
        roleResponses = await Http.GetJsonAsync<List<RoleModel>>("api/roles");
        Console.WriteLine(roleResponses.Count);
        StateHasChanged();
    }

    void Selected(object value)
    {
        buttonTitle = "Save";
        model = new EditRoleRequest {
            Id = value.ToString(),
            Name = roleResponses.First(x=> x.Id.Equals(value.ToString())).Name
        };
        StateHasChanged();
    }

    void AddRole(MouseEventArgs args)
    {
        buttonTitle = "Add";
        model = new EditRoleRequest();
    }

    public async Task SaveRole()
    {
        if (String.IsNullOrEmpty(model.Id))
        {
            editRoleResponse = await Http.PostJsonAsync<EditRoleResponse>("api/roles", model);
        } else
        {
            editRoleResponse = await Http.PutJsonAsync<EditRoleResponse>("api/roles", model);
        }

        if (editRoleResponse.IsSuccess)
        {
            if (String.IsNullOrEmpty(model.Id))
            {
                roleResponses.Add(new RoleModel { Id = editRoleResponse.Id, Name = model.Name });
            } else
            {
                roleResponses.First(x => x.Id.Equals(model.Id)).Name = model.Name;
            }
            model = null;
        }
    }
}

La pagina utilizza tre modelli che sono stati creati nel progetto Shared condiviso tra Client e Server. Partiamo da RoleModel:

public class RoleModel
{
    [Required]
    public string Id { get; set; }

    [Required]
    [StringLength(16, ErrorMessage = "{0} length must be between {2} and {1}.", MinimumLength = 3)]
    public string Name { get; set; }
}

Questa classe ha solo due proprietà, che ci consentono di mappare le corrispondenti proprietà del modello IdentityRole che viene usato da ASP.NET Identity. Gli alti due modelli sono EditRoleRequest e EditRoleResponse, che utilizzeremo per le richieste di creazione e modifica provenienti dal front-end e gestite dal back-end.

public class EditRoleRequest
{
    public string Id { get; set; }

    [Required]
    [StringLength(16, ErrorMessage = "{0} length must be between {2} and {1}.", MinimumLength = 3)]
    public string Name { get; set; }
}

public class EditRoleResponse
{
    public string Id { get; set; }
    public bool IsSuccess { get; set; }
    public IEnumerable<string> Errors { get; set; }
}

Aggiungiamo anche il modello per l’aggiunta/rimozione dell’utente in una role, che chiamiamo AddRemoveRoleRequest.cs. Questo è il suo contenuto:

public class AddRemoveRoleRequest
{
    public string RoleId { get; set; }
    public string UserId { get; set; } 
    public bool Add { get; set; }
}

Infine il modello RoleUserResponse.cs che usiamo per restituire l’esito dell’aggiunta o della rimozione di un utente in una role:

public class RoleUserResponse
{
    public string Id { get; set; }
    public string Name { get; set; }
    public bool OnRule { get; set; }
}

Il vero punto cardine però è il controller che ho chiamato RolesController.cs e che contiene tutti i metodi richiamati da Blazor per gestire le roles. Il costruttore, tramite Dependency Injection, ci fornisce la solita interfaccia di log e il RoleManager necessario a gestire i ruoli:

private readonly ILogger _logger; private readonly RoleManager _roleManager;

public RolesController(ILogger<RolesController> logger, RoleManager<IdentityRole> roleManager)
{
    _logger = logger;
    _roleManager = roleManager;
}

Il metodo che ci restituisce tutte le roles presenti è il seguente:

[HttpGet]
public IActionResult Get()
{
    try
    {
        List<RoleModel> roleResponses = _roleManager.Roles.Select(x => new RoleModel()
        {
            Id = x.Id,
            Name = x.Name
        }).ToList();

        return Ok(roleResponses);
    } catch (Exception ex)
    {
        _logger.LogError($"Get error: {ex.Message}");
        return BadRequest();
    }
}

L’aggiunta di un ruolo avviene tramite questo metodo:

[HttpPost]
public async Task<IActionResult> Post([FromBody]EditRoleRequest request)
{
    EditRoleResponse response = new EditRoleResponse();
    try
    {
        IdentityRole identityRole = new IdentityRole
        {
            Name = request.Name
        };

        IdentityResult result = await _roleManager.CreateAsync(identityRole);

        if (result.Succeeded)
        {
            identityRole = await _roleManager.FindByNameAsync(identityRole.Name);
            response.Id = identityRole.Id;
            response.IsSuccess = true;
        } else
        {
            response.Errors = result.Errors.Select(x => x.Description);
        }

        return Ok(response);
    } catch(Exception ex)
    {
        _logger.LogError($"Post error: {ex.Message} - Name: {request.Name}");
        return BadRequest();
    }
}

La modifica di un ruolo invece è così implementata:

[HttpPut]
public async Task<IActionResult> Put([FromBody]EditRoleRequest request)
{
    EditRoleResponse response = new EditRoleResponse();
    try
    {
        IdentityRole identityRole = await _roleManager.FindByIdAsync(request.Id);
        if (identityRole == null)
            throw new Exception("Role not found");

        identityRole.Name = request.Name;
        IdentityResult result = await _roleManager.UpdateAsync(identityRole);

        if (result.Succeeded)
        {
            response.Id = identityRole.Id;
            response.IsSuccess = true;
        }
        else
        {
            response.Errors = result.Errors.Select(x => x.Description);
        }

        return Ok(response);
    }
    catch (Exception ex)
    {
        _logger.LogError($"Post error: {ex.Message} - Name: {request.Name}");
        return BadRequest();
    }
}

Per prelevare gli utenti che sono presenti in un ruolo, ho implementato questo metodo:

[HttpGet("{id}", Name = "Get")]
public async Task<IActionResult> Get(string id)
{
    try
    {
        IdentityRole identityRole = await _roleManager.FindByIdAsync(id);
        if (identityRole == null)
            throw new Exception("Role not found");

        List<RoleUserResponse> roleUserResponses = new List<RoleUserResponse>();
        foreach (var user in _userManager.Users)
        {
            var roleUserResponse = new RoleUserResponse
            {
                Id = user.Id,
                Name = user.Name,
                OnRule = await _userManager.IsInRoleAsync(user, identityRole.Name)
            };
            roleUserResponses.Add(roleUserResponse);
        }

        return Ok(roleUserResponses);
    }
    catch (Exception ex)
    {
        _logger.LogError($"Get error: {ex.Message} - Id: {id}");
        return BadRequest();
    }
}

Infine, il metodo che permette di aggiungere o rimuovere un utente da un ruolo:

[HttpPost("[action]")]
public async Task<IActionResult> AddRemove([FromBody]AddRemoveRoleRequest addRemoveRoleRequest)
{
    try
    {
        IdentityRole role = await _roleManager.FindByIdAsync(addRemoveRoleRequest.RoleId);
        if (role == null)
            throw new Exception("Role not found");

        var user = await _userManager.FindByIdAsync(addRemoveRoleRequest.UserId);

        if (user == null)
            throw new Exception("User not found");

        if (addRemoveRoleRequest.Add)
        {
            await _userManager.AddToRoleAsync(user, role.Name);
        } else
        {
            await _userManager.RemoveFromRoleAsync(user, role.Name);
        }

        return Ok(true);
    }
    catch (Exception ex)
    {
        _logger.LogError($"Get error: {ex.Message} - UserId: {addRemoveRoleRequest.UserId} - RoleId: {addRemoveRoleRequest.RoleId}");
        return BadRequest();
    }
}

Il risultato finale è una pagina che ci consente di gestire al meglio le roles:

Gestione ruoli

E’ possibile vedere i ruoli presenti, aggiungerne di nuovi e soprattutto è possibile aggiungere utenti ad una role attraverso l’autocomplete che compare solo se ci sono utenti non inseriti già nella role selezionata:

Gestione utente a ruolo

Dopo aver associato all’utente le roles di competenza, andremo a mostrare tale link (con relativa impostazione sulla pagina) solo all’utente con role Admin. Ecco la LoginDisplay.razor:

<AuthorizeView Roles="Admin">
    <a href="role/index" class="nav-link btn btn-link">Roles</a>
</AuthorizeView>
<AuthorizeView>
    <Authorized>
        <a href="#">Ciao, @context.User.Identity.Name!</a>
        <a href="account/logout" class="nav-link btn btn-link">Log out</a>
    </Authorized>
    <NotAuthorized>
        <a href="account/register">Register</a>
        <a href="account/login">Log in</a>
    </NotAuthorized>
</AuthorizeView>

Se si effettua la login con un utente che non ha la role Admin il link non viene visualizzato. Andiamo anche a rendere più sicura la nostra applicazione aggiungendo gli opportuni attributi sia alla pagina Index.razor che al controller. Iniziamo dalla pagina:

@page "/role/index"
@attribute [Authorize(Roles = "Admin")]

Modifichiamo anche il controller:

[Route("api/[controller]")]
[ApiController]
[Authorize(Roles ="Admin")]
public class RolesController : ControllerBase
{
    ...
}

E’ fondamentale aggiungere in Startup.cs, subito dopo app.UseRouting(), queste 2 direttive:

app.UseAuthentication();
app.UseAuthorization();

Finito! Da questo momento abbiamo vincolato l’accesso alle pagine alla gestione dei ruoli.