Saltar al contenido

Roles en ASP.NET Core e Identity

Compartir en:

Hoy hablaremos sobre cómo agregar roles de forma predeterminada en una aplicación, cómo asignar los roles a los usuarios y mucho más.

Los roles no se crean de forma predeterminada, es necesario crear la funcionalidad para poder usarlos. Esta es bastante genérica y una vez desarrollada podrás utilizarla en todos tus programas.

La gestión de roles en general no es una virtud de identity, explicaremos a continuación como crearlos y añadirlos a la base de datos para poder usarlos en nuestras aplicaciones.

Vamos a partir de la premisa que nuestra aplicación necesita 5 roles:

  1. ModoDios
  2. Admin
  3. Creador
  4. Lector
  5. Invitado

Esto lo podéis gestionar como mejor os convenga, pero lo más fácil es usar una clase enumerada para los roles «base» de la app. Lo primero que vamos a hacer para facilitar la gestión, es crear una clase enumerada con nuestros Roles.

public enum Roles
    {
        ModoDios,
        Admin,
        Creador,
        Lector,
        Invitado
    }

Ni Asp.NET Core ni Identity tienen por defecto roles o datos predefinidos, esto iría contra la filosofía de navaja suiza de Microsoft, por tanto, inicializar los datos es nuestro trabajo. Para grabarlos si no existen, debemos crear una función de sembrado los datos al inicial la app (si estos no existen) lo haremos en una clase aparte que llamaremos Seed. Esta tendrá 3 funciones (2 privadas y una pública), una para llamar desde el inicio, y 2 de sembrado (una para roles y otra para los usuarios por defecto):

public static class Seed
{
    public static void SeedDB(UserManager<ApplicationUser> userManager, 
        RoleManager<IdentityRole> roleManager){
        //Creamos los roles predetrminados
        SeedRoles(userManager, roleManager);
        //Creamos el usuario Super Administrador
        SeedGodAdmin (userManager, roleManager);
    }
    private static Dictionary<Roles, IdentityResult> SeedRoles(UserManager<ApplicationUser> userManager,
        RoleManager<IdentityRole> roleManager){
        //Este es el diccionario de salidas por si quereis debuguear el resultado
        Dictionary<Roles, IdentityResult> RoleResult = new Dictionary<Roles, IdentityResult>();
        if (!roleManager.RoleExistsAsync(Roles.ModoDios.ToString()).Result){
            IdentityResult roleResult = roleManager.CreateAsync(
                        new IdentityRole(Roles.ModoDios.ToString())
                ).Result;
            RoleResult.Add(Roles.ModoDios, roleResult);
        }
        if (!roleManager.RoleExistsAsync(Roles.Admin.ToString()).Result){
            IdentityResult roleResult = roleManager.CreateAsync(
                    new IdentityRole(Roles.Admin.ToString())
            ).Result;
            RoleResult.Add(Roles.Admin, roleResult);
        }
        if (!roleManager.RoleExistsAsync(Roles.Creador.ToString()).Result){
            IdentityResult roleResult = roleManager.CreateAsync(
                    new IdentityRole(Roles.Creador.ToString())
            ).Result;
            RoleResult.Add(Roles.Creador, roleResult);
        }
        if (!roleManager.RoleExistsAsync(Roles.Lector.ToString()).Result){
            IdentityResult roleResult = roleManager.CreateAsync(
                    new IdentityRole(Roles.Lector.ToString())
            ).Result;
            RoleResult.Add(Roles.Lector, roleResult);
        }
        if (!roleManager.RoleExistsAsync(Roles.Invitado.ToString()).Result){
            IdentityResult roleResult = roleManager.CreateAsync(
                    new IdentityRole(Roles.Invitado.ToString())
            ).Result;
            RoleResult.Add(Roles.Invitado, roleResult);
        }
        return RoleResult;
    }
    private static void SeedGodAdmin(UserManager<ApplicationUser> userManager, 
        RoleManager<IdentityRole> roleManager){
        //Si el usuario no existe
        if (userManager.FindByEmailAsync("superadmin@localhost").Result == null){
            var adminUserInitial = new ApplicationUser{
                UserName = "superadmin@localhost",
                Email = "superadmin@localhost",
                FirstName = "Super Admin",
                LastName = "Super Admin",
                EmailConfirmed = true
            };
            //Lo creamos (ni que decir tiene que el passwword debe cambiarse al inicial la APP)
            IdentityResult result= userManager.CreateAsync(
                    adminUserInitial, "$PasswordSegura$_20").Result;
            if (result.Succeeded){
                //Y le asignamos todos los roles
                userManager.AddToRoleAsync(
                    adminUserInitial, Roles.ModoDios.ToString()).Wait();
                userManager.AddToRoleAsync(
                    adminUserInitial, Roles.Admin.ToString()).Wait();
                userManager.AddToRoleAsync(
                    adminUserInitial, Roles.Creador.ToString()).Wait();
                userManager.AddToRoleAsync(
                    adminUserInitial, Roles.Lector.ToString()).Wait();
                userManager.AddToRoleAsync(
                    adminUserInitial, Roles.Invitado.ToString()).Wait();
            }
        }
    }
}

La llamada a este método, en mi opinión debe realizarse en el fichero Program.cs, por manía más que otra cosa, no tengo una teoría sobre esto, pero se puede llamar en otros muchas partes del programa, yo lo haré de esta manera, en la clase Proram.cs:

public static void Main(string[] args){
    var server = CreateHostBuilder(args).Build();
    using (var scope = server.Services.CreateScope()){
        var serviceProvider = scope.ServiceProvider;
        try{
            var userManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
            var roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
            //Llamamos a nuestro método estático
            Seed.SeedDB(userManager, roleManager);
        }
        catch (Exception ex){
            Debug.WriteLine(ex.Message);
        }
    }
    server.Run();
}

Y vemos como se insertan nuestros roles y nuestro usuario admin de manera óptima:

En esta última tabla vemos a nuestro viejo amigo Jon de nuestro anterior post. Si consultamos la tabla de relaciones de Roles-Usuario también tenemos las conexiones entre estos:

¡Ya tenemos el usuario administrador en MODO DIOS! ¡Probémoslo!

Muy bien, el administrador principal está creado, este será el encargado de poner en marcha nuestra aplicación y asignar roles a los demás usuarios. De manera predeterminada, debemos asignar uno de los roles a los usuarios que se registren en nuestra app, en mi caso como invitados, será una manera de asignarles una “Zona de aterrizaje” en nuestra app.

Esta parte es bastante sencilla, simplemente tenemos que ir a la página de registro, y al crear el usuario usar el UserManager de Identity para asignarle el rol de invitado, vamos a ello. Abrimos el fichero Register.cshtml de Identity y nos situamos en el método asíncrono  OnPostAsync. Añadimos la línea siguiente después de validar al usuario creado:

await _userManager.AddToRoleAsync(user, Roles.Invitado.ToString());

Quedando el método de la siguiente forma (es el método base de identity con la línea anterior):

public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
    if (ModelState.IsValid){
        var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email };
        var result = await _userManager.CreateAsync(user, Input.Password);
        if (result.Succeeded){
            _logger.LogInformation("User created a new account with password.");
            //Aquí añadimoe el ROL por defecto de invitado
            await _userManager.AddToRoleAsync(user, Roles.Invitado.ToString());
            var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
            code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
            var callbackUrl = Url.Page(
                "/Account/ConfirmEmail",
                pageHandler: null,
                values: new { area = "Identity", userId = user.Id, code = code, returnUrl = returnUrl },
                protocol: Request.Scheme);
            await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
            if (_userManager.Options.SignIn.RequireConfirmedAccount)
            {
                return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
            }
            else
            {
                await _signInManager.SignInAsync(user, isPersistent: false);
                return LocalRedirect(returnUrl);
            }
        }
        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }
    // If we got this far, something failed, redisplay form
    return Page();
}

Ya tenemos los roles creados, el rol por defecto, y el administrador global, pero esto de nada sirve si no podemos hacer una gestión eficiente de estos.

Para este cometido, crearemos la funcionalidad necesaria para actuar sobre los roles, crear nuevos y poder asignarlos a los usuarios de nuestra aplicación.

Lo haremos en 2 partes, primero con un RoleManger y después con un UserManager, ambos solo accesibles por los roles Admin y ModoDios.

Role manager

Al estar trabajando en MVC usaremos un controlador para trabajar con los roles y las vistas para interactuar con este. Crearemos primero el controlador llamado RoleManager, este tendrá su método index para ver la tabla con los roles, y otra acción para crear un rol nuevo (aparte de los de base de la app).

Creamos el controlador de roles. Nos quedaría algo así:

public class RoleManagerController : Controller
{
    private readonly RoleManager<IdentityRole> _roleManager;
    public RoleManagerController(RoleManager<IdentityRole> roleManager)
    {
        _roleManager = roleManager;
    }
    // GET: RoleManagerController
    public async Task<IActionResult> Index()
    {
        var rolesApp = await _roleManager.Roles.ToListAsync();
        return View(rolesApp);
    }
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> AddRol(string role)
    {
        if (role != null)
        {
            await _roleManager.CreateAsync(new IdentityRole(role.Trim()));
        }
        return RedirectToAction("Index");
    }
}

El método Index nos envía a una vista la lista de roles, y el método AddRol añade un nuevo rol en el sistema, ¿Simple verdad?. Vamos entonces con la vista.

Creamos la vista para Index:

@model List<Microsoft.AspNetCore.Identity.IdentityRole>
@{
    ViewData["Title"] = "Administrador de Roles!";
    Layout = "~/Views/Shared/_Layout.cshtml";
}
<h1>Administrador de Roles</h1>
<table class="table">
    <thead>
        <tr>
            <th>Id</th>
            <th>Rol</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var role in Model)
        {
            <tr>
                <td>@role.Id</td>
                <td>@role.Name</td>
            </tr>
        }
    </tbody>
</table>
<form method="post" asp-action="AddRol" asp-controller="RoleManager">
    <div class="input-group">
        <input name="role" class="form-control w-25">
        <span class="input-group-btn">
            <button class="btn btn-success">Crear un ROL</button>
        </span>
    </div>
</form>

La vista simplemente tiene una tabla con los roles, y un formulario para incluir uno nuevo.

Probamos a crear uno nuevo para testear nuestro AddRol del controlador, en este caso añadiremos el rol «Editor»:

¡Bien! Todo funciona, aunque no estaría muy bien dejar que cualquiera administre los roles ¿verdad?, bueno, de hecho sería una gran ca**da. Para solucionar esto añadimos un decorador en el controlador RoleManagerController:

[Authorize(Roles = "ModoDios,Admin")]
public class RoleManagerController : Controller
    {
    .
    .
    .

De esta manera solo los usuarios con el rol ModoDios o Admin podrán trabajar con el controlador. Si intentamos ahora acceder con la cuenta del bueno de Jon, este sería el resultado:

User Manager

Bien, es hora de poderle asignar a nuestro usuario creado el post anterior Jon Doe un rol para que pueda aprovechar las características de nuestro aplicativo. Generaremos para esto un controlador de roles capaz de trabajar con usuarios y roles.

Para trabajar de manera más sencilla con la vista usaremos el patrón ViewModel y juntaremos los datos del usurario (nombre y email por ejemplo) y los roles que tiene este asignados.

Creamos una nueva clase llamada UserViewModel:

public class UserViewModel
    {
        public string Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public IEnumerable<string> Roles { get; set; }
    }

Y un controlador que recupere los usuarios y sus roles:

[Authorize(Roles = "ModoDios,Admin")]
public class UserController : Controller
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly RoleManager<IdentityRole> _roleManager;
    public UserController(UserManager<ApplicationUser> userManager,
        RoleManager<IdentityRole> roleManager)
    {
        _roleManager = roleManager;
        _userManager = userManager;
    }
    public async Task<IActionResult> Index()
    {
        var users = await _userManager.Users.ToListAsync();
        var userViewModel = new List<UserViewModel>();
        foreach (ApplicationUser user in users)
        {
            var usVM = new UserViewModel();
            usVM.Id = user.Id;
            usVM.FirstName = user.FirstName;
            usVM.LastName = user.LastName;
            usVM.Email = user.Email;
            usVM.Roles = new List<string>(await _userManager.GetRolesAsync(user));
            userViewModel.Add(usVM);
        }
        return View(userViewModel);
    }
}

El controlador se encarga de crear una lista de UserViewModel y devolverlos a una vista que crearemos.

Para esto, botón derecho en el Index del controlador de usuarios y pulsamos en “Agregar vista…”. Creamos una lista vacía tal que así:

@using IdentityPruebas.Models
@model List<IdentityPruebas.Models.UserViewModel>
@{
    ViewData["Title"] = "Lista de Usuarios";
    Layout = "~/Views/Shared/_Layout.cshtml";
}
<table class="table">
    <thead>
        <tr>
            <th>Nombre</th>
            <th>Apellido</th>
            <th>Email</th>
            <th>Roles</th>
            <th>Edición</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var user in Model)
        {
        <tr>
            <td>@user.FirstName</td>
            <td>@user.LastName</td>
            <td>@user.Email</td>
            <td class="text-success">@string.Join(" - ", user.Roles.ToList())</td>
            <td>
                <a class="btn btn-primary" 
                asp-controller="User" 
                asp-action="Edit" 
                asp-route-userId="@user.Id">Editar Roles</a>
            </td>
        </tr>
        }
    </tbody>
</table>

Esta tabla simple nos permite ver los usuarios y sus roles, he añadido un botón para acceder a la edición de roles, por tanto, debemos crear la acción Edit en nuestro controlador.

Empezaremos con la infraestructura, el ViewModel para gestionar la edición de roles. En la vista solo necesitamos una lista de los roles de este, además añadimos un campo para tener un CheckBox para el Rol, creamos, por tanto, una nueva clase llamada UserRolesViewModel.cs :

public class UserRolesViewModel
{
    public bool IsSelected { get; set; }
    public string RoleId { get; set; }
    public string RoleName { get; set; }        
}

Hay que crear dos métodos para la edición en el controlador UserController, un GET para la vista de edición y un POST para las modificaciones. Comencemos creando el primero:

public async Task<IActionResult> Edit(string userId)
{
    ViewBag.userId = userId;
    var user = await _userManager.FindByIdAsync(userId);
    if (user == null){
        //No se ha encontrado el usuario
        return View("NotFound");
    }
    var model = new List<UserRolesViewModel>();
    foreach (var rol_ in _roleManager.Roles)
    {
        var userRolesViewModel = new UserRolesViewModel {
            RoleId = rol_.Id,
            RoleName = rol_.Name
        };
        //IsInRole es muy util para saber si un usuario pertenece o no a un ROL
        if (await _userManager.IsInRoleAsync(user, rol_.Name)){
            userRolesViewModel.IsSelected = true;
        }
        else { 
            userRolesViewModel.IsSelected = false;
        }
        model.Add(userRolesViewModel);
    }
    ViewBag.UserName = user.Email;
    return View(model);
}

Usamos nuestra clase viewModel y una lista de los roles y comprobamos que estos están o no en cada usuario, marcamos el campo booleano con el resultado. Finalmente, pasamos el modelo a la vista que crearemos a continuación:

@model List<UserRolesViewModel>
@{
    ViewData["Title"] = "Permisos de usuario";
    Layout = "~/Views/Shared/_Layout.cshtml";
}
<form method="post">
    <h4>Editar Roles --> @ViewBag.UserName</h4>
    <div>
    @for (int i = 0; i < Model.Count; i++)
    {
    <div class="form-check m-1">
        <input asp-for="@Model[i].IsSelected" class="form-check-input" />
        <label class="form-check-label" asp-for="@Model[i].IsSelected">
            @Model[i].RoleName
        </label>
        <input type="hidden" asp-for="@Model[i].RoleId" />
        <input type="hidden" asp-for="@Model[i].RoleName" />
    </div>
    }
    <div asp-validation-summary="All" class="text-danger"></div>
    </div>
    <input type="submit" value="Actualizar" class="btn btn-primary"
            style="width:auto"/>
    <a asp-action="EditUser" asp-route-id="@ViewBag.UserName"
        class="btn btn-primary" style="width:auto">Cancelar</a>
</form>

Y comprobamos que todo funciona antes de pasar al método POST (pulsando en «Editar Roles» en /Users):

El pobre Jon Doe no tiene ningún permiso aun, y la función de actualizar estos aún no funciona, por tanto, vamos a implementarla en el UserController:

[HttpPost]
public async Task<IActionResult> Edit(List<UserRolesViewModel> viewModel, string userId)
{
    //Obtenemos el usuario
    var user = await _userManager.FindByIdAsync(userId);
    if (user == null)
    {
        return RedirectToAction("Index");
    }
    //Obtenemos los roles del usuario en cuestión
    var roles = await _userManager.GetRolesAsync(user);
    //Vamos a deshacernos primero de todos los roles para trabajar comodamente
    var result = await _userManager.RemoveFromRolesAsync(user, roles);
    //Si no se han podido borrar los roles, retornamos la vista
    if (!result.Succeeded)
    {
        return View(viewModel);
    }
    //Si los roles ya no estan, podemos asignar los nuevos valores obtenidos de la vista y el UserViewModel
    var rolesSelected = viewModel.Where(x => x.IsSelected).Select(y => y.RoleName);
    result = await _userManager.AddToRolesAsync(user, rolesSelected);
    //Si no se han podido agregar los roles, retornamos la vista
    if (!result.Succeeded)
    {
        return View(viewModel);
    }
    // Si todo ha ido bien, regresamos a la tabla de usuarios
    return RedirectToAction("Index");
}

Probamos a asignar un par de permisos:

¡Y vemos que este ya los tiene disponibles!

Conclusión

Ya tenemos una gestión aceptable de los roles y los usuarios, esto proporciona a nuestras apps empresariales las funcionalidades para discriminar entre los usuarios y sus roles.

En el siguiente post veremos algún que otra implementación más para dejar el sistema completo y listo para usar de plantilla en nuestras aplicaciones corporativas.


Juan Ibero

Inmerso en la Evolución Tecnológica. Ingeniero Informático especializado en la gestión segura de entornos TI e industriales, con un profundo énfasis en seguridad, arquitectura y programación. Siempre aprendiendo, siempre explorando.

Compartir en:

4 comentarios en «Roles en ASP.NET Core e Identity»

    1. Buenas Mario!
      Disculpa la tardanza, que se me había colado tu mensaje entre el SPAM.

      Efectivamente, antes de hacer el seed de la base de datos, es necesario que la estructura de tablas de Identity este creada. Por tanto, como bien apuntas, lo primero que hay que hacer al crear la aplicación y configurar Identity, es hacer un update-database en el contexto en el que estés trabajando.

      Una vez este creada la estructura de tablas, puedes utilizar el seed para crear los usuarios o los roles.

      Espero haberte contestado. Cualquier duda me consultas.

  1. Quiero expresar mi gratitud por encontrar un blog tan útil y informativo para la implementación de funcionalidades en mi proyecto. Este blog fue realmente una bendición, ya que me permitió ahorrar tiempo y esfuerzo en la investigación.

    Antes de encontrar este blog, estaba lidiando con dificultades y estaba buscando información dispersa en diferentes fuentes. Sin embargo, gracias a esta publicación, pude obtener la orientación necesaria de manera clara y concisa. Me proporcionó los conocimientos y las pautas necesarias para crear las funcionalidades requeridas en mi proyecto.

    En general, estoy muy satisfecho con el blog y el impacto positivo que tuvo en mi proyecto. Me permitió avanzar más rápidamente y obtener resultados concretos. Recomiendo este blog a otros usuarios que estén buscando orientación en temas similares, ya que sé que les será de gran ayuda.

    ¡Agradezco al autor del blog por compartir su conocimiento y ayudar a la comunidad de desarrolladores!

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *