Creación de una aplicación web de ASP.NET Core con datos de usuario protegidos por autorización

Por Rick Anderson y Joe Audette

En este tutorial se muestra cómo crear una aplicación web de ASP.NET Core con datos de usuario protegidos por autorización. Muestra una lista de contactos creados por los usuarios autenticados (registrados). La aplicación admite tres grupos de seguridad:

  • Los usuarios registrados pueden ver todos los datos aprobados y pueden editar o eliminar sus propios datos.
  • Los administradores pueden aprobar o rechazar los datos de contacto. Solo los contactos marcados como Aprobados son visibles para los usuarios.
  • Los administradores pueden aprobar, rechazar y editar o eliminar los datos.

Note

Las imágenes de este artículo no coinciden exactamente con las plantillas más recientes.

En la imagen siguiente, el usuario rick@contoso.com ha iniciado sesión en la aplicación web. Este usuario solo puede ver los contactos aprobados junto con los vínculos Editar/eliminar/crear nuevos para los contactos. En esta vista, solo el último registro (creado por este usuario) muestra los vínculos Editar y Eliminar . Otros usuarios no ven el último registro hasta que un responsable o administrador apruebe el registro.

Captura de pantalla que muestra el usuario

En la siguiente imagen, el manager@contoso.com usuario ha iniciado sesión y tiene acceso a las características de administración:

Captura de pantalla que muestra que el usuario hamanager@contoso.com iniciado sesión en la aplicación web con visibilidad de las características de administración.

Un administrador puede seleccionar un contacto para ver detalles sobre el usuario, como se muestra en la siguiente imagen:

Captura de pantalla que muestra la vista de administrador de un contacto en la aplicación web.

Las opciones Aprobar y Rechazar solo se muestran para administradores y administradores.

En la imagen siguiente, el admin@contoso.com usuario ha iniciado sesión y tiene acceso a las características de administración:

Captura de pantalla que muestra que el usuario haadmin@contoso.com iniciado sesión en la aplicación web con visibilidad de las características de administración.

Un administrador tiene todos los privilegios. Pueden leer, editar o eliminar cualquier contacto y cambiar el estado de los contactos.

La aplicación se creó mediante scaffolding del modelo siguiente Contact:

public class Contact
{
    public int ContactId { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }
}

El ejemplo contiene los siguientes controladores de autorización:

  • ContactIsOwnerAuthorizationHandler: garantiza que un usuario solo pueda editar sus datos.
  • ContactManagerAuthorizationHandler: permite a los administradores aprobar o rechazar contactos.
  • ContactAdministratorsAuthorizationHandler: permite a los administradores aprobar o rechazar contactos y editar o eliminar contactos.

Prerequisites

Este tutorial está avanzado. Debería estar familiarizado con lo siguiente:

La aplicación inicial y final

Descargar la aplicación completada. Pruebe la aplicación completada para familiarizarse con sus características de seguridad.

Tip

Puede usar el comando git sparse-checkout para descargar solo la subcarpeta de ejemplo.

Por ejemplo:

git clone --depth 1 --filter=blob:none https://github.com/dotnet/AspNetCore.Docs.git --sparse
cd AspNetCore.Docs
git sparse-checkout init --cone
git sparse-checkout set aspnetcore/security/authorization/secure-data/samples

La aplicación de inicio

Download la aplicación starter.

Ejecute la aplicación, pulse el vínculo ContactManager y compruebe que puede crear, editar y eliminar un contacto. Para crear la aplicación de inicio, consulte Creación de la aplicación de inicio.

Protección de datos de usuario

En las secciones siguientes se muestran todos los pasos principales para crear la aplicación de datos de usuario segura. Es posible que le resulte útil hacer referencia al proyecto completado.

Vinculación de los datos de contacto al usuario

Use el identificador de usuario de ASP.NET Identity para asegurarse de que los usuarios pueden editar sus datos, pero no otros datos de usuarios. Agregue los OwnerID campos y ContactStatus al Contact modelo:

public class Contact
{
    public int ContactId { get; set; }

    // user ID from AspNetUser table.
    public string? OwnerID { get; set; }

    public string? Name { get; set; }
    public string? Address { get; set; }
    public string? City { get; set; }
    public string? State { get; set; }
    public string? Zip { get; set; }
    [DataType(DataType.EmailAddress)]
    public string? Email { get; set; }

    public ContactStatus Status { get; set; }
}

public enum ContactStatus
{
    Submitted,
    Approved,
    Rejected
}

OwnerID es el identificador del usuario de la tabla AspNetUser de la base de datos de Identity. El campo Status determina si los usuarios generales pueden ver un contacto.

Cree una nueva migración y actualice la base de datos:

dotnet ef migrations add userID_Status
dotnet ef database update

Agregar servicios de rol a Identity

Habilite la aplicación para que use los servicios de Role agregando el método AddRoles:

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(
    options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

Requerir usuarios autenticados

Establezca la política de autorización de respaldo para que requiera que los usuarios se autentiquen.

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(
    options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddRazorPages();

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

El código anterior establece la directiva de autorización alternativa. La directiva de autorización de reserva requiere que todos los usuarios estén autenticados, salvo las Razor páginas, los controladores o los métodos de acción que tengan un atributo de autorización. Por ejemplo, Razor Pages, controladores o métodos de acción con [AllowAnonymous] o [Authorize(PolicyName="MyPolicy")] usan el atributo de autorización aplicado en lugar de la directiva de autorización de reserva.

El RequireAuthenticatedUser método agrega la DenyAnonymousAuthorizationRequirement clase a la instancia actual, que exige que el usuario actual se autentique.

La directiva de autorización de reserva se aplica a todas las solicitudes que no especifican explícitamente una directiva de autorización. En el caso de las solicitudes atendidas por el enrutamiento de puntos de conexión, la directiva se aplica a cualquier punto de conexión que no especifique un atributo de autorización. Para las solicitudes atendidas por otro middleware después del middleware de autorización, como archivos estáticos, la directiva se aplica a todas las solicitudes.

Establecer la directiva de autorización predeterminada para exigir que los usuarios estén autenticados protege las Razor Pages y los controladores recién agregados. Tener la autorización necesaria de forma predeterminada es más seguro que confiar en los nuevos controladores y Razor Pages para incluir el atributo [Authorize].

La AuthorizationOptions clase también contiene la AuthorizationOptions.DefaultPolicy propiedad . DefaultPolicy es la directiva que se usa con el atributo [Authorize] cuando no se especifica ninguna directiva. [Authorize] no contiene una directiva con nombre, a diferencia de [Authorize(PolicyName="MyPolicy")].

Para obtener más información sobre las directivas, vea Autorización basada en directivas en ASP.NET Core.

Como enfoque alternativo, los controladores de MVC y Razor Pages pueden agregar un filtro de autorización para requerir que todos los usuarios se autentiquen:

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using ContactManager.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(
    options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddRazorPages();

builder.Services.AddControllers(config =>
{
    var policy = new AuthorizationPolicyBuilder()
                     .RequireAuthenticatedUser()
                     .Build();
    config.Filters.Add(new AuthorizeFilter(policy));
});

var app = builder.Build();

El código anterior utiliza un filtro de autorización. La configuración de la política de respaldo utiliza el enrutamiento de puntos de conexión. Establecer la política de respaldo es la manera preferida de asegurar que todos los usuarios se autentiquen.

Agregue el atributo AllowAnonymous a las Index páginas y Privacy para que los usuarios anónimos puedan obtener información sobre el sitio antes de registrarse:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace ContactManager.Pages;

[AllowAnonymous]
public class IndexModel : PageModel
{
    private readonly ILogger<IndexModel> _logger;

    public IndexModel(ILogger<IndexModel> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {

    }
}

Configuración de la cuenta de prueba

La clase SeedData crea dos cuentas: administrador y responsable. Use la Herramienta Administrador de secretos para establecer una contraseña para estas cuentas. Establezca la contraseña del directorio del proyecto (el directorio que contiene el archivo Program.cs ):

dotnet user-secrets set SeedUserPW <PW>

Si se especifica una contraseña débil, se produce una excepción cuando se llama al SeedData.Initialize método .

Actualice la aplicación para usar la contraseña de prueba:

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(
    options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddRazorPages();

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

// Authorization handlers.
builder.Services.AddScoped<IAuthorizationHandler,
                      ContactIsOwnerAuthorizationHandler>();

builder.Services.AddSingleton<IAuthorizationHandler,
                      ContactAdministratorsAuthorizationHandler>();

builder.Services.AddSingleton<IAuthorizationHandler,
                      ContactManagerAuthorizationHandler>();

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;
    var context = services.GetRequiredService<ApplicationDbContext>();
    context.Database.Migrate();
    // requires using Microsoft.Extensions.Configuration;
    // Set password with the Secret Manager tool.
    // dotnet user-secrets set SeedUserPW <pw>

    var testUserPw = builder.Configuration.GetValue<string>("SeedUserPW");

   await SeedData.Initialize(services, testUserPw);
}

Crear las cuentas de prueba y actualizar los contactos

Cree las cuentas de prueba mediante la actualización del Initialize método en la SeedData clase :

public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
{
    using (var context = new ApplicationDbContext(
        serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
    {
        // For sample purposes seed both with the same password.
        // Password is set with the following:
        // dotnet user-secrets set SeedUserPW <pw>
        // The admin user can do anything

        var adminID = await EnsureUser(serviceProvider, testUserPw, "admin@contoso.com");
        await EnsureRole(serviceProvider, adminID, Constants.ContactAdministratorsRole);

        // allowed user can create and edit contacts that they create
        var managerID = await EnsureUser(serviceProvider, testUserPw, "manager@contoso.com");
        await EnsureRole(serviceProvider, managerID, Constants.ContactManagersRole);

        SeedDB(context, adminID);
    }
}

private static async Task<string> EnsureUser(IServiceProvider serviceProvider,
                                            string testUserPw, string UserName)
{
    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    var user = await userManager.FindByNameAsync(UserName);
    if (user == null)
    {
        user = new IdentityUser
        {
            UserName = UserName,
            EmailConfirmed = true
        };
        await userManager.CreateAsync(user, testUserPw);
    }

    if (user == null)
    {
        throw new Exception("The password is probably not strong enough!");
    }

    return user.Id;
}

private static async Task<IdentityResult> EnsureRole(IServiceProvider serviceProvider,
                                                              string uid, string role)
{
    var roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>();

    if (roleManager == null)
    {
        throw new Exception("roleManager null");
    }

    IdentityResult IR;
    if (!await roleManager.RoleExistsAsync(role))
    {
        IR = await roleManager.CreateAsync(new IdentityRole(role));
    }

    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    //if (userManager == null)
    //{
    //    throw new Exception("userManager is null");
    //}

    var user = await userManager.FindByIdAsync(uid);

    if (user == null)
    {
        throw new Exception("The testUserPw password was probably not strong enough!");
    }

    IR = await userManager.AddToRoleAsync(user, role);

    return IR;
}

Agregue el identificador de usuario de administrador y el ContactStatus campo a los contactos. Marque uno de los contactos como Enviado y otro como Rechazado. Agregue el identificador de usuario y el estado a todos los contactos. Solo se muestra un contacto:

public static void SeedDB(ApplicationDbContext context, string adminID)
{
    if (context.Contact.Any())
    {
        return;   // DB has been seeded
    }

    context.Contact.AddRange(
        new Contact
        {
            Name = "Debra Garcia",
            Address = "1234 Main St",
            City = "Redmond",
            State = "WA",
            Zip = "10999",
            Email = "debra@example.com",
            Status = ContactStatus.Approved,
            OwnerID = adminID
        },

Creación de controladores de autorización para propietario, gerente y administrador

Cree una clase ContactIsOwnerAuthorizationHandler en la carpeta Autorización. ContactIsOwnerAuthorizationHandler Comprueba que el usuario que actúa en un recurso posee el recurso.

using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;
using System.Threading.Tasks;

namespace ContactManager.Authorization
{
    public class ContactIsOwnerAuthorizationHandler
                : AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        UserManager<IdentityUser> _userManager;

        public ContactIsOwnerAuthorizationHandler(UserManager<IdentityUser> 
            userManager)
        {
            _userManager = userManager;
        }

        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.CompletedTask;
            }

            // If not asking for CRUD permission, return.

            if (requirement.Name != Constants.CreateOperationName &&
                requirement.Name != Constants.ReadOperationName   &&
                requirement.Name != Constants.UpdateOperationName &&
                requirement.Name != Constants.DeleteOperationName )
            {
                return Task.CompletedTask;
            }

            if (resource.OwnerID == _userManager.GetUserId(context.User))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

El ContactIsOwnerAuthorizationHandler llama al método context.Succeed si el usuario autenticado actual es el propietario del contacto.

El controlador de autorización generalmente:

  • Llama al context.Succeed método cuando se cumplen los requisitos.
  • Cuando no se cumplen los requisitos, devuelve Task.CompletedTask. Cuando Task.CompletedTask se devuelve sin una llamada previa a context.Succeed o context.Fail, el resultado no es un éxito ni un fallo. En su lugar, permite que se ejecuten otros controladores de autorización.

Si necesita indicar explícitamente un fallo, llame al método context.Fail.

La aplicación permite a los propietarios de contactos editar, eliminar o crear sus propios datos. ContactIsOwnerAuthorizationHandler no es necesario comprobar la operación pasada en el parámetro de requisito.

Creación de un controlador de autorización de administrador

Cree una clase ContactManagerAuthorizationHandler en la carpeta Autorización. El ContactManagerAuthorizationHandler Comprueba que el usuario que actúa en el recurso es un administrador. Solo los administradores pueden aprobar o rechazar los cambios de contenido (nuevos o modificados).

using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;

namespace ContactManager.Authorization
{
    public class ContactManagerAuthorizationHandler :
        AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.CompletedTask;
            }

            // If not asking for approval/reject, return.
            if (requirement.Name != Constants.ApproveOperationName &&
                requirement.Name != Constants.RejectOperationName)
            {
                return Task.CompletedTask;
            }

            // Managers can approve or reject.
            if (context.User.IsInRole(Constants.ContactManagersRole))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

Creación de un controlador de autorización de administrador

Cree una clase ContactAdministratorsAuthorizationHandler en la carpeta Autorización. El ContactAdministratorsAuthorizationHandler Comprueba que el usuario que actúa en el recurso es un administrador. El administrador puede realizar todas las operaciones.

using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
    public class ContactAdministratorsAuthorizationHandler
                    : AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task HandleRequirementAsync(
                                              AuthorizationHandlerContext context,
                                    OperationAuthorizationRequirement requirement, 
                                     Contact resource)
        {
            if (context.User == null)
            {
                return Task.CompletedTask;
            }

            // Administrators can do anything.
            if (context.User.IsInRole(Constants.ContactAdministratorsRole))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

Registrar los controladores de autorización

Los servicios que utilizan Entity Framework Core deben registrarse para la inyección de dependencias con el método AddScoped. El ContactIsOwnerAuthorizationHandler usa ASP.NET Core Identity, que se basa en Entity Framework Core. Registre los controladores con la colección de servicios para que estén disponibles para el ContactsController mediante la inserción de dependencias. Agregue el código siguiente al final de ConfigureServices:

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(
    options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddRazorPages();

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

// Authorization handlers.
builder.Services.AddScoped<IAuthorizationHandler,
                      ContactIsOwnerAuthorizationHandler>();

builder.Services.AddSingleton<IAuthorizationHandler,
                      ContactAdministratorsAuthorizationHandler>();

builder.Services.AddSingleton<IAuthorizationHandler,
                      ContactManagerAuthorizationHandler>();

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;
    var context = services.GetRequiredService<ApplicationDbContext>();
    context.Database.Migrate();
    // requires using Microsoft.Extensions.Configuration;
    // Set password with the Secret Manager tool.
    // dotnet user-secrets set SeedUserPW <pw>

    var testUserPw = builder.Configuration.GetValue<string>("SeedUserPW");

   await SeedData.Initialize(services, testUserPw);
}

ContactAdministratorsAuthorizationHandler y ContactManagerAuthorizationHandler se agregan como singletons. Son singletons porque no usan Entity Framework y toda la información necesaria está en el Context parámetro del HandleRequirementAsync método .

Compatibilidad con la autorización

En esta sección, actualiza las Razor Páginas y agrega una clase de requisitos operativos.

Revisar los requisitos de la clase de operaciones de contacto

Revise la clase ContactOperations. Esta clase contiene los requisitos que admite la aplicación:

using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
    public static class ContactOperations
    {
        public static OperationAuthorizationRequirement Create =   
          new OperationAuthorizationRequirement {Name=Constants.CreateOperationName};
        public static OperationAuthorizationRequirement Read = 
          new OperationAuthorizationRequirement {Name=Constants.ReadOperationName};  
        public static OperationAuthorizationRequirement Update = 
          new OperationAuthorizationRequirement {Name=Constants.UpdateOperationName}; 
        public static OperationAuthorizationRequirement Delete = 
          new OperationAuthorizationRequirement {Name=Constants.DeleteOperationName};
        public static OperationAuthorizationRequirement Approve = 
          new OperationAuthorizationRequirement {Name=Constants.ApproveOperationName};
        public static OperationAuthorizationRequirement Reject = 
          new OperationAuthorizationRequirement {Name=Constants.RejectOperationName};
    }

    public class Constants
    {
        public static readonly string CreateOperationName = "Create";
        public static readonly string ReadOperationName = "Read";
        public static readonly string UpdateOperationName = "Update";
        public static readonly string DeleteOperationName = "Delete";
        public static readonly string ApproveOperationName = "Approve";
        public static readonly string RejectOperationName = "Reject";

        public static readonly string ContactAdministratorsRole = 
                                                              "ContactAdministrators";
        public static readonly string ContactManagersRole = "ContactManagers";
    }
}

Crear una clase base para Pages de Contactos

Cree una clase base que contenga los servicios utilizados en las páginas de contactos Razor Pages. La clase base coloca el código de inicialización en una ubicación:

using ContactManager.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace ContactManager.Pages.Contacts
{
    public class DI_BasePageModel : PageModel
    {
        protected ApplicationDbContext Context { get; }
        protected IAuthorizationService AuthorizationService { get; }
        protected UserManager<IdentityUser> UserManager { get; }

        public DI_BasePageModel(
            ApplicationDbContext context,
            IAuthorizationService authorizationService,
            UserManager<IdentityUser> userManager) : base()
        {
            Context = context;
            UserManager = userManager;
            AuthorizationService = authorizationService;
        } 
    }
}

El código anterior:

  • Agrega el servicio IAuthorizationService para acceder a los controladores de autorización.
  • Agrega el servicio IdentityUserManager.
  • Añada ApplicationDbContext.

Actualización de CreateModel

Actualice el modelo de creación de páginas:

  • Defina el constructor para usar la DI_BasePageModel clase base.
  • Configure el OnPostAsync método para:
    • Agregue el identificador de usuario al modelo Contact.
    • Llame al controlador de autorización para comprobar que el usuario tiene permiso para crear contactos.
using ContactManager.Authorization;
using ContactManager.Data;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;

namespace ContactManager.Pages.Contacts
{
    public class CreateModel : DI_BasePageModel
    {
        public CreateModel(
            ApplicationDbContext context,
            IAuthorizationService authorizationService,
            UserManager<IdentityUser> userManager)
            : base(context, authorizationService, userManager)
        {
        }

        public IActionResult OnGet()
        {
            return Page();
        }

        [BindProperty]
        public Contact Contact { get; set; }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            Contact.OwnerID = UserManager.GetUserId(User);

            var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                        User, Contact,
                                                        ContactOperations.Create);
            if (!isAuthorized.Succeeded)
            {
                return Forbid();
            }

            Context.Contact.Add(Contact);
            await Context.SaveChangesAsync();

            return RedirectToPage("./Index");
        }
    }
}

Actualización de IndexModel

Actualice el OnGetAsync método para que solo los contactos aprobados se muestren a los usuarios registrados estándar:

public class IndexModel : DI_BasePageModel
{
    public IndexModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public IList<Contact> Contact { get; set; }

    public async Task OnGetAsync()
    {
        var contacts = from c in Context.Contact
                       select c;

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        // Only approved contacts are shown UNLESS you're authorized to see them
        // or you are the owner.
        if (!isAuthorized)
        {
            contacts = contacts.Where(c => c.Status == ContactStatus.Approved
                                        || c.OwnerID == currentUserId);
        }

        Contact = await contacts.ToListAsync();
    }
}

Actualización de EditModel

Agregue un controlador de autorización para comprobar que el usuario posee el contacto. Dado que se está validando la autorización de recursos, el [Authorize] atributo no es suficiente. La aplicación no tiene acceso al recurso cuando los atributos son evaluados. La autorización basada en recursos debe ser imperativa. Las comprobaciones se deben realizar después de que la aplicación tenga acceso al recurso, ya sea cargandola en el modelo de página o cargandola dentro del propio controlador. Con frecuencia, accedes al recurso pasando la clave de recurso.

public class EditModel : DI_BasePageModel
{
    public EditModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact? contact = await Context.Contact.FirstOrDefaultAsync(
                                                         m => m.ContactId == id);
        if (contact == null)
        {
            return NotFound();
        }

        Contact = contact;

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                  User, Contact,
                                                  ContactOperations.Update);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        // Fetch Contact from DB to get OwnerID.
        var contact = await Context
            .Contact.AsNoTracking()
            .FirstOrDefaultAsync(m => m.ContactId == id);

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

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, contact,
                                                 ContactOperations.Update);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        Contact.OwnerID = contact.OwnerID;

        Context.Attach(Contact).State = EntityState.Modified;

        if (Contact.Status == ContactStatus.Approved)
        {
            // If the contact is updated after approval, 
            // and the user cannot approve,
            // set the status back to submitted so the update can be
            // checked and approved.
            var canApprove = await AuthorizationService.AuthorizeAsync(User,
                                    Contact,
                                    ContactOperations.Approve);

            if (!canApprove.Succeeded)
            {
                Contact.Status = ContactStatus.Submitted;
            }
        }

        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

Actualización de DeleteModel

Actualice el modelo de página de eliminación para usar el controlador de autorización y compruebe que el usuario tiene permiso de eliminación en el contacto.

public class DeleteModel : DI_BasePageModel
{
    public DeleteModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact? _contact = await Context.Contact.FirstOrDefaultAsync(
                                             m => m.ContactId == id);

        if (_contact == null)
        {
            return NotFound();
        }
        Contact = _contact;

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, Contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        var contact = await Context
            .Contact.AsNoTracking()
            .FirstOrDefaultAsync(m => m.ContactId == id);

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

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        Context.Contact.Remove(contact);
        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

Inyecta el servicio de autorización en las vistas

Actualmente, la interfaz de usuario muestra los vínculos de edición y eliminación de los contactos que el usuario no puede modificar.

Inserte el servicio de autorización en el archivo Pages/_ViewImports.cshtml para que esté disponible para todas las vistas:

@using Microsoft.AspNetCore.Identity
@using ContactManager
@using ContactManager.Data
@namespace ContactManager.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using ContactManager.Authorization;
@using Microsoft.AspNetCore.Authorization
@using ContactManager.Models
@inject IAuthorizationService AuthorizationService

El marcado anterior agrega varias sentencias using.

Actualice los vínculos Editar y Eliminar en el archivo Pages/Contacts/Index.cshtml para que solo se representen para los usuarios con los permisos adecuados:

@page
@model ContactManager.Pages.Contacts.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Address)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].City)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].State)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Zip)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Email)
            </th>
             <th>
                @Html.DisplayNameFor(model => model.Contact[0].Status)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.Contact) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Address)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.City)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.State)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Zip)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Email)
            </td>
                           <td>
                    @Html.DisplayFor(modelItem => item.Status)
                </td>
                <td>
                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Update)).Succeeded)
                    {
                        <a asp-page="./Edit" asp-route-id="@item.ContactId">Edit</a>
                        <text> | </text>
                    }

                    <a asp-page="./Details" asp-route-id="@item.ContactId">Details</a>

                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Delete)).Succeeded)
                    {
                        <text> | </text>
                        <a asp-page="./Delete" asp-route-id="@item.ContactId">Delete</a>
                    }
                </td>
            </tr>
        }
    </tbody>
</table>

Warning

Ocultar los enlaces a los usuarios que no tienen permiso para cambiar los datos no protege la aplicación. Ocultar vínculos hace que la aplicación sea más fácil de usar mostrando solo vínculos válidos. Los usuarios pueden hackear las direcciones URL generadas para invocar operaciones de edición y eliminación en los datos que no poseen. El Razor Page o controlador debe realizar comprobaciones de acceso para proteger los datos.

Detalles de actualización

Actualice la vista de detalles para que los administradores puedan aprobar o rechazar contactos:

        @*Preceding markup omitted for brevity.*@
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Contact.Email)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Contact.Email)
        </dd>
    <dt>
            @Html.DisplayNameFor(model => model.Contact.Status)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Contact.Status)
        </dd>
    </dl>
</div>

@if (Model.Contact.Status != ContactStatus.Approved)
{
    @if ((await AuthorizationService.AuthorizeAsync(
     User, Model.Contact, ContactOperations.Approve)).Succeeded)
    {
        <form style="display:inline;" method="post">
            <input type="hidden" name="id" value="@Model.Contact.ContactId" />
            <input type="hidden" name="status" value="@ContactStatus.Approved" />
            <button type="submit" class="btn btn-xs btn-success">Approve</button>
        </form>
    }
}

@if (Model.Contact.Status != ContactStatus.Rejected)
{
    @if ((await AuthorizationService.AuthorizeAsync(
     User, Model.Contact, ContactOperations.Reject)).Succeeded)
    {
        <form style="display:inline;" method="post">
            <input type="hidden" name="id" value="@Model.Contact.ContactId" />
            <input type="hidden" name="status" value="@ContactStatus.Rejected" />
            <button type="submit" class="btn btn-xs btn-danger">Reject</button>
        </form>
    }
}

<div>
    @if ((await AuthorizationService.AuthorizeAsync(
         User, Model.Contact,
         ContactOperations.Update)).Succeeded)
    {
        <a asp-page="./Edit" asp-route-id="@Model.Contact.ContactId">Edit</a>
        <text> | </text>
    }
    <a asp-page="./Index">Back to List</a>
</div>

Actualización del modelo de página de detalles

public class DetailsModel : DI_BasePageModel
{
    public DetailsModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact? _contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);

        if (_contact == null)
        {
            return NotFound();
        }
        Contact = _contact;

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        if (!isAuthorized
            && currentUserId != Contact.OwnerID
            && Contact.Status != ContactStatus.Approved)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id, ContactStatus status)
    {
        var contact = await Context.Contact.FirstOrDefaultAsync(
                                                  m => m.ContactId == id);

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

        var contactOperation = (status == ContactStatus.Approved)
                                                   ? ContactOperations.Approve
                                                   : ContactOperations.Reject;

        var isAuthorized = await AuthorizationService.AuthorizeAsync(User, contact,
                                    contactOperation);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }
        contact.Status = status;
        Context.Contact.Update(contact);
        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

Agregar o quitar roles de usuario

La actualización de las asignaciones de roles de un usuario permite controlar los privilegios disponibles para el usuario. Quite un usuario de un rol y reduzca su capacidad de cambiar los datos, como editar o eliminar un contacto. Agregue un usuario a un rol y aumente sus privilegios para realizar cambios globales. También puede usar asignaciones de roles para limitar la participación de los usuarios, como silenciar a un usuario en una conversación de chat.

Para obtener más información, vea GitHub problema dotnet/aspnetcore #8502 - Mute o quitar privilegios de un usuario. Cambios de administrador.

Diferencias entre Desafío y Prohibición

Esta aplicación establece la directiva predeterminada para requerir usuarios autenticados. El siguiente código permite a usuarios anónimos. Los usuarios anónimos pueden mostrar las diferencias entre Desafío y Prohibición.

[AllowAnonymous]
public class Details2Model : DI_BasePageModel
{
    public Details2Model(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact? _contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);

        if (_contact == null)
        {
            return NotFound();
        }
        Contact = _contact;

        if (!User.Identity!.IsAuthenticated)
        {
            return Challenge();
        }

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        if (!isAuthorized
            && currentUserId != Contact.OwnerID
            && Contact.Status != ContactStatus.Approved)
        {
            return Forbid();
        }

        return Page();
    }
}

En el código anterior:

  • Cuando el usuario no está autenticado, se devuelve ChallengeResult. Cuando se devuelve un ChallengeResult, se redirige al usuario a la página de inicio de sesión.
  • Cuando el usuario se autentica, pero no está autorizado, se devuelve ForbidResult. Cuando se devuelve un ForbidResult, se redirige al usuario a la página de acceso denegado.

Prueba de la aplicación completada

Warning

En este artículo se utiliza la herramienta Secret Manager para almacenar la contraseña de las cuentas de usuario preconfiguradas. La herramienta Secret Manager se utiliza para almacenar información confidencial durante el desarrollo local. Para obtener información sobre los procedimientos de autenticación que se pueden usar cuando se implementa una aplicación en un entorno de prueba o producción, consulta Flujos de autenticación seguros.

Si aún no ha establecido una contraseña para las cuentas de usuario de inicialización, use la Herramienta de Administrador de secretos para establecer una contraseña:

  • Elige una contraseña segura:

    • Al menos 12 caracteres de longitud, pero 14 o más es mejor.
    • Combinación de letras mayúsculas, minúsculas, números y símbolos.
    • No es una palabra que se pueda encontrar en un diccionario o en el nombre de una persona, un carácter, un producto o una organización.
    • Presenta diferencias notables con respecto a contraseñas anteriores.
    • Fácil de recordar para ti, pero difícil de adivinar para los demás. Considere la posibilidad de usar una frase memorable como 6MonkeysRLooking^.
  • Ejecute el siguiente comando desde la carpeta del project, donde <PW> es la contraseña:

    dotnet user-secrets set SeedUserPW <PW>
    

Si la aplicación tiene contactos:

  • Elimine todos los registros de la tabla Contact.
  • Reinicie la aplicación para inicializar la base de datos.

Una manera sencilla de probar la aplicación completada es iniciar tres exploradores diferentes (o sesiones de incógnito/InPrivate). En un explorador, registre un nuevo usuario (por ejemplo, test@contoso.com). Inicie sesión en cada explorador con un usuario diferente. Compruebe las siguientes operaciones:

  • Los usuarios registrados pueden ver todos los datos de contacto aprobados .
  • Los usuarios registrados pueden editar o eliminar sus propios datos.
  • Los administradores pueden aprobar o rechazar los datos de contacto. La vista Details muestra los botones Aprobar y Rechazar.
  • Los administradores pueden aprobar, rechazar y editar o eliminar todos los datos.
User Aprobar o rechazar contactos Options
test@contoso.com No Edite y elimine sus datos.
manager@contoso.com Yes Edite y elimine sus datos.
admin@contoso.com Yes Edite y elimine todos los datos.

Cree un contacto en el explorador del administrador. Copie la dirección URL para eliminar y editar desde el contacto del administrador. Pegue estos vínculos en el explorador del usuario de prueba y compruebe que el usuario de prueba no puede realizar estas operaciones.

Crear la aplicación de inicio

  • Cree una aplicación de Razor Pages:

    • Cree la aplicación con cuentas individuales.
    • Asigne el nombre ContactManager a la aplicación, por lo que el espacio de nombres coincide con el espacio de nombres usado en el ejemplo.
    • Use la -uld marca para especificar LocalDB en lugar de SQLite.
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • Agregue el archivo Models/Contact.cs :

    using System.ComponentModel.DataAnnotations;
    
    namespace ContactManager.Models
    {
        public class Contact
        {
            public int ContactId { get; set; }
            public string? Name { get; set; }
            public string? Address { get; set; }
            public string? City { get; set; }
            public string? State { get; set; }
            public string? Zip { get; set; }
            [DataType(DataType.EmailAddress)]
            public string? Email { get; set; }
        }
    }
    
  • Generar el esqueleto del modelo Contact.

  • Creación de una migración inicial y actualización de la base de datos:

    dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
    dotnet tool install -g dotnet-aspnet-codegenerator
    dotnet-aspnet-codegenerator razorpage -m Contact -udl -dc ApplicationDbContext -outDir Pages\Contacts --referenceScriptLibraries
    dotnet ef database drop -f
    dotnet ef migrations add initial
    dotnet ef database update
    

    Note

    De forma predeterminada, la arquitectura de los archivos binarios de .NET que se van a instalar representa la arquitectura del sistema operativo que se está ejecutando actualmente. Para especificar una arquitectura diferente, revise cómo usar el comando con la dotnet tool installopción "--arch". Para obtener más información, vea GitHub dotnet/aspnetcore.docs issue #29262 - Add "-a arm64" en Apple Silicon.

  • Actualice el ancla ContactManager en el archivo Pages/Shared/_Layout.cshtml:

    <a class="nav-link text-dark" asp-area="" asp-page="/Contacts/Index">Contact Manager</a>
    
  • Probar la aplicación mediante la creación, edición y eliminación de un contacto

Inicializar la base de datos

Agregue la clase SeedData a la carpeta Data:

using ContactManager.Models;
using Microsoft.EntityFrameworkCore;

// dotnet aspnet-codegenerator razorpage -m Contact -dc ApplicationDbContext -udl -outDir Pages\Contacts --referenceScriptLibraries

namespace ContactManager.Data
{
    public static class SeedData
    {
        public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw="")
        {
            using (var context = new ApplicationDbContext(
                serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
            {
                SeedDB(context, testUserPw);
            }
        }

        public static void SeedDB(ApplicationDbContext context, string adminID)
        {
            if (context.Contact.Any())
            {
                return;   // DB has been seeded
            }

            context.Contact.AddRange(
                new Contact
                {
                    Name = "Debra Garcia",
                    Address = "1234 Main St",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "debra@example.com"
                },
                new Contact
                {
                    Name = "Thorsten Weinrich",
                    Address = "5678 1st Ave W",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "thorsten@example.com"
                },
                new Contact
                {
                    Name = "Yuhong Li",
                    Address = "9012 State st",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "yuhong@example.com"
                },
                new Contact
                {
                    Name = "Jon Orton",
                    Address = "3456 Maple St",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "jon@example.com"
                },
                new Contact
                {
                    Name = "Diliana Alexieva-Bosseva",
                    Address = "7890 2nd Ave E",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "diliana@example.com"
                }
             );
            context.SaveChanges();
        }

    }
}

Llame al método SeedData.Initialize en el archivo Program.cs:

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using ContactManager.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    await SeedData.Initialize(services);
}

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

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

app.MapRazorPages();

app.Run();

Pruebe la aplicación y confirme que la base de datos está inicializada. Si hay filas en la base de datos de contactos, el método de inicialización no se ejecuta.

En este tutorial se muestra cómo crear una aplicación web de ASP.NET Core con datos de usuario protegidos por autorización. Muestra una lista de contactos que han creado los usuarios autenticados (registrados). Hay tres grupos de seguridad:

  • Los usuarios registrados pueden ver todos los datos aprobados y pueden editar o eliminar sus propios datos.
  • Los administradores pueden aprobar o rechazar los datos de contacto. Solo los contactos aprobados son visibles para los usuarios.
  • Los administradores pueden aprobar, rechazar y editar o eliminar los datos.

Las imágenes de este documento no coinciden exactamente con las plantillas más recientes.

En la imagen siguiente, el usuario Rick (rick@example.com) ha iniciado sesión. Rick solo puede ver los contactos aprobados y Editar/Eliminar/Crear nuevos vínculos para sus contactos. Solo el último registro, creado por Rick, muestra los vínculos Editar y Eliminar. Otros usuarios no verán el último registro hasta que un administrador o responsable cambie el estado a "Aprobado".

Captura de pantalla de Rick con la sesión iniciada

En la imagen siguiente, manager@contoso.com ha iniciado sesión y se encuentra en el rol del administrador:

Captura de pantalla que muestra a manager@contoso.com con la sesión iniciada

En la imagen siguiente se muestra la vista de detalles de los administradores de un contacto:

Vista del administrador de un contacto

Los botones Aprobar y Rechazar solo se muestran para administradores y responsables.

En la imagen siguiente, admin@contoso.com ha iniciado sesión y se encuentra en el rol del administrador:

Captura de pantalla que muestra a admin@contoso.com con la sesión iniciada

El administrador tiene todos los privilegios. Puede leer, editar o eliminar cualquier contacto y cambiar el estado de los contactos.

La aplicación se creó mediante scaffolding del modelo siguiente Contact:

public class Contact
{
    public int ContactId { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }
}

El ejemplo contiene los siguientes controladores de autorización:

  • ContactIsOwnerAuthorizationHandler: garantiza que un usuario solo pueda editar sus datos.
  • ContactManagerAuthorizationHandler: permite a los administradores aprobar o rechazar contactos.
  • ContactAdministratorsAuthorizationHandler: permite a los administradores:
    • Aprobar o rechazar contactos
    • Editar y eliminar contactos

Prerequisites

Este tutorial está avanzado. Debería estar familiarizado con lo siguiente:

La aplicación inicial y final

Descargar la aplicación completada. Pruebe la aplicación completada para familiarizarse con sus características de seguridad.

La aplicación de inicio

Download la aplicación starter.

Ejecute la aplicación, pulse el vínculo ContactManager y compruebe que puede crear, editar y eliminar un contacto. Para crear la aplicación de inicio, consulte Creación de la aplicación de inicio.

Protección de datos de usuario

Las secciones siguientes tienen todos los pasos principales para crear la aplicación de datos de usuario segura. Es posible que le resulte útil hacer referencia al proyecto completado.

Vinculación de los datos de contacto al usuario

Use el identificador de usuario de ASP.NET Identity para asegurarse de que los usuarios pueden editar sus datos, pero no otros datos de usuarios. Agregue OwnerID y ContactStatus al modelo Contact:

public class Contact
{
    public int ContactId { get; set; }

    // user ID from AspNetUser table.
    public string OwnerID { get; set; }

    public string Name { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }

    public ContactStatus Status { get; set; }
}

public enum ContactStatus
{
    Submitted,
    Approved,
    Rejected
}

OwnerID es el identificador del usuario de la tabla AspNetUser de la base de datos de Identity. El campo Status determina si los usuarios generales pueden ver un contacto.

Cree una nueva migración y actualice la base de datos:

dotnet ef migrations add userID_Status
dotnet ef database update

Agregar servicios de rol a Identity

Anexe AddRoles para agregar servicios de rol:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
        options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

Requerir usuarios autenticados

Establecer la directiva de autenticación suplente para requerir que los usuarios se autentiquen:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
        options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddRazorPages();

    services.AddAuthorization(options =>
    {
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
    });

El código resaltado anterior establece la directiva de autenticación alternativa. La directiva de autenticación de reserva requiere que todos los usuarios se autentiquen, excepto para Razor Pages, controladores o métodos de acción con un atributo de autenticación. Por ejemplo, las Razor páginas, los controladores o los métodos de acción con [AllowAnonymous] o [Authorize(PolicyName="MyPolicy")] usan el atributo de autenticación aplicado en lugar de la directiva de autenticación alternativa.

RequireAuthenticatedUser agrega DenyAnonymousAuthorizationRequirement a la instancia actual, lo que exige que el usuario actual se autentique.

La directiva de autenticación de respaldo:

  • Se aplica a todas las solicitudes que no especifican explícitamente una directiva de autenticación. En el caso de las solicitudes atendidas por el enrutamiento de puntos de conexión, esto incluiría cualquier punto de conexión que no especifique un atributo de autorización. Para las solicitudes atendidas por otro middleware después del middleware de autorización, como los archivos estáticos, esto aplicaría la directiva a todas las solicitudes.

Establecer la directiva de autenticación predeterminada para exigir que los usuarios estén autenticados protege las páginas Razor y los controladores recién agregados. Tener la autenticación necesaria de forma predeterminada es más seguro que confiar en los nuevos controladores y Razor Pages para incluir el atributo [Authorize].

La clase AuthorizationOptions también contiene AuthorizationOptions.DefaultPolicy. DefaultPolicy es la directiva que se usa con el atributo [Authorize] cuando no se especifica ninguna directiva. [Authorize] no contiene una directiva con nombre, a diferencia de [Authorize(PolicyName="MyPolicy")].

Para obtener más información sobre las directivas, vea Autorización basada en directivas en ASP.NET Core.

Una manera alternativa de que los controladores MVC y Razor Pages requieran que todos los usuarios se autentiquen es agregar un filtro de autorización:

public void ConfigureServices(IServiceCollection services)
{

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
        options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddRazorPages();

    services.AddControllers(config =>
    {
        // using Microsoft.AspNetCore.Mvc.Authorization;
        // using Microsoft.AspNetCore.Authorization;
        var policy = new AuthorizationPolicyBuilder()
                         .RequireAuthenticatedUser()
                         .Build();
        config.Filters.Add(new AuthorizeFilter(policy));
    });

El código anterior utiliza un filtro de autorización. La configuración de la política de respaldo utiliza el enrutamiento de puntos de conexión. Establecer la política de respaldo es la manera preferida de asegurar que todos los usuarios se autentiquen.

Agregue AllowAnonymous a las páginas Index y Privacy para que los usuarios anónimos puedan obtener información sobre el sitio antes de registrarse:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace ContactManager.Pages
{
    [AllowAnonymous]
    public class IndexModel : PageModel
    {
        private readonly ILogger<IndexModel> _logger;

        public IndexModel(ILogger<IndexModel> logger)
        {
            _logger = logger;
        }

        public void OnGet()
        {

        }
    }
}

Configuración de la cuenta de prueba

La clase SeedData crea dos cuentas: administrador y responsable. Use la Herramienta Administrador de secretos para establecer una contraseña para estas cuentas. Establezca la contraseña del directorio project (el directorio que contiene Program.cs):

dotnet user-secrets set SeedUserPW <PW>

Si no se especifica una contraseña segura, se produce una excepción cuando se llama a SeedData.Initialize.

Actualice Main para usar la contraseña de prueba:

public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();

        using (var scope = host.Services.CreateScope())
        {
            var services = scope.ServiceProvider;

            try
            {
                var context = services.GetRequiredService<ApplicationDbContext>();
                context.Database.Migrate();

                // requires using Microsoft.Extensions.Configuration;
                var config = host.Services.GetRequiredService<IConfiguration>();
                // Set password with the Secret Manager tool.
                // dotnet user-secrets set SeedUserPW <pw>

                var testUserPw = config["SeedUserPW"];

                SeedData.Initialize(services, testUserPw).Wait();
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred seeding the DB.");
            }
        }

        host.Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

Crear las cuentas de prueba y actualizar los contactos

Actualice el método Initialize de la clase SeedData para crear las cuentas de prueba:

public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
{
    using (var context = new ApplicationDbContext(
        serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
    {
        // For sample purposes seed both with the same password.
        // Password is set with the following:
        // dotnet user-secrets set SeedUserPW <pw>
        // The admin user can do anything

        var adminID = await EnsureUser(serviceProvider, testUserPw, "admin@contoso.com");
        await EnsureRole(serviceProvider, adminID, Constants.ContactAdministratorsRole);

        // allowed user can create and edit contacts that they create
        var managerID = await EnsureUser(serviceProvider, testUserPw, "manager@contoso.com");
        await EnsureRole(serviceProvider, managerID, Constants.ContactManagersRole);

        SeedDB(context, adminID);
    }
}

private static async Task<string> EnsureUser(IServiceProvider serviceProvider,
                                            string testUserPw, string UserName)
{
    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    var user = await userManager.FindByNameAsync(UserName);
    if (user == null)
    {
        user = new IdentityUser
        {
            UserName = UserName,
            EmailConfirmed = true
        };
        await userManager.CreateAsync(user, testUserPw);
    }

    if (user == null)
    {
        throw new Exception("The password is probably not strong enough!");
    }

    return user.Id;
}

private static async Task<IdentityResult> EnsureRole(IServiceProvider serviceProvider,
                                                              string uid, string role)
{
    var roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>();

    if (roleManager == null)
    {
        throw new Exception("roleManager null");
    }

    IdentityResult IR;
    if (!await roleManager.RoleExistsAsync(role))
    {
        IR = await roleManager.CreateAsync(new IdentityRole(role));
    }

    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    //if (userManager == null)
    //{
    //    throw new Exception("userManager is null");
    //}

    var user = await userManager.FindByIdAsync(uid);

    if (user == null)
    {
        throw new Exception("The testUserPw password was probably not strong enough!");
    }

    IR = await userManager.AddToRoleAsync(user, role);

    return IR;
}

Agregue el identificador de usuario de administrador y ContactStatus a los contactos. Haga que uno de los contactos sea "Enviados" y el otro "Rechazado". Agregue el identificador de usuario y el estado a todos los contactos. Solo se muestra un contacto:

public static void SeedDB(ApplicationDbContext context, string adminID)
{
    if (context.Contact.Any())
    {
        return;   // DB has been seeded
    }

    context.Contact.AddRange(
        new Contact
        {
            Name = "Debra Garcia",
            Address = "1234 Main St",
            City = "Redmond",
            State = "WA",
            Zip = "10999",
            Email = "debra@example.com",
            Status = ContactStatus.Approved,
            OwnerID = adminID
        },

Creación de controladores de autorización para propietario, gerente y administrador

Cree una clase ContactIsOwnerAuthorizationHandler en la carpeta Autorización. ContactIsOwnerAuthorizationHandler Comprueba que el usuario que actúa en un recurso posee el recurso.

using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;
using System.Threading.Tasks;

namespace ContactManager.Authorization
{
    public class ContactIsOwnerAuthorizationHandler
                : AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        UserManager<IdentityUser> _userManager;

        public ContactIsOwnerAuthorizationHandler(UserManager<IdentityUser> 
            userManager)
        {
            _userManager = userManager;
        }

        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.CompletedTask;
            }

            // If not asking for CRUD permission, return.

            if (requirement.Name != Constants.CreateOperationName &&
                requirement.Name != Constants.ReadOperationName   &&
                requirement.Name != Constants.UpdateOperationName &&
                requirement.Name != Constants.DeleteOperationName )
            {
                return Task.CompletedTask;
            }

            if (resource.OwnerID == _userManager.GetUserId(context.User))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

El ContactIsOwnerAuthorizationHandler llama a context.Succeed si el usuario autenticado actual es el propietario del contacto. Los controladores de autorización generalmente:

  • Llame a context.Succeed cuando se cumplan los requisitos.
  • Devuelve Task.CompletedTask cuando no se cumplen los requisitos. Devolver Task.CompletedTask sin haber llamado antes a context.Success o context.Fail no es un éxito ni un error; permite que se ejecuten otros controladores de autorización.

Si necesita producir un error explícito, llame al context.Fail.

La aplicación permite a los propietarios de contactos editar, eliminar o crear sus propios datos. ContactIsOwnerAuthorizationHandler no es necesario comprobar la operación pasada en el parámetro de requisito.

Creación de un controlador de autorización de administrador

Cree una clase ContactManagerAuthorizationHandler en la carpeta Autorización. El ContactManagerAuthorizationHandler Comprueba que el usuario que actúa en el recurso es un administrador. Solo los administradores pueden aprobar o rechazar los cambios de contenido (nuevos o modificados).

using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;

namespace ContactManager.Authorization
{
    public class ContactManagerAuthorizationHandler :
        AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.CompletedTask;
            }

            // If not asking for approval/reject, return.
            if (requirement.Name != Constants.ApproveOperationName &&
                requirement.Name != Constants.RejectOperationName)
            {
                return Task.CompletedTask;
            }

            // Managers can approve or reject.
            if (context.User.IsInRole(Constants.ContactManagersRole))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

Creación de un controlador de autorización de administrador

Cree una clase ContactAdministratorsAuthorizationHandler en la carpeta Autorización. El ContactAdministratorsAuthorizationHandler Comprueba que el usuario que actúa en el recurso es un administrador. El administrador puede realizar todas las operaciones.

using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
    public class ContactAdministratorsAuthorizationHandler
                    : AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task HandleRequirementAsync(
                                              AuthorizationHandlerContext context,
                                    OperationAuthorizationRequirement requirement, 
                                     Contact resource)
        {
            if (context.User == null)
            {
                return Task.CompletedTask;
            }

            // Administrators can do anything.
            if (context.User.IsInRole(Constants.ContactAdministratorsRole))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

Registrar los controladores de autorización

Los servicios que usan Entity Framework Core deben registrarse para la inserción de dependencias mediante AddScoped. El ContactIsOwnerAuthorizationHandler usa ASP.NET Core Identity, que se basa en Entity Framework Core. Registre los controladores con la colección de servicios para que estén disponibles para el ContactsController mediante la inserción de dependencias. Agregue el código siguiente al final de ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
        options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddRazorPages();

    services.AddAuthorization(options =>
    {
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
    });

    // Authorization handlers.
    services.AddScoped<IAuthorizationHandler,
                          ContactIsOwnerAuthorizationHandler>();

    services.AddSingleton<IAuthorizationHandler,
                          ContactAdministratorsAuthorizationHandler>();

    services.AddSingleton<IAuthorizationHandler,
                          ContactManagerAuthorizationHandler>();
}

ContactAdministratorsAuthorizationHandler y ContactManagerAuthorizationHandler se agregan como singletons. Son singletons porque no usan EF y toda la información necesaria se encuentra en el parámetro Context del método HandleRequirementAsync.

Compatibilidad con la autorización

En esta sección, actualiza las Razor Páginas y agrega una clase de requisitos operativos.

Revisar los requisitos de la clase de operaciones de contacto

Revise la clase ContactOperations. Esta clase contiene los requisitos que admite la aplicación:

using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
    public static class ContactOperations
    {
        public static OperationAuthorizationRequirement Create =   
          new OperationAuthorizationRequirement {Name=Constants.CreateOperationName};
        public static OperationAuthorizationRequirement Read = 
          new OperationAuthorizationRequirement {Name=Constants.ReadOperationName};  
        public static OperationAuthorizationRequirement Update = 
          new OperationAuthorizationRequirement {Name=Constants.UpdateOperationName}; 
        public static OperationAuthorizationRequirement Delete = 
          new OperationAuthorizationRequirement {Name=Constants.DeleteOperationName};
        public static OperationAuthorizationRequirement Approve = 
          new OperationAuthorizationRequirement {Name=Constants.ApproveOperationName};
        public static OperationAuthorizationRequirement Reject = 
          new OperationAuthorizationRequirement {Name=Constants.RejectOperationName};
    }

    public class Constants
    {
        public static readonly string CreateOperationName = "Create";
        public static readonly string ReadOperationName = "Read";
        public static readonly string UpdateOperationName = "Update";
        public static readonly string DeleteOperationName = "Delete";
        public static readonly string ApproveOperationName = "Approve";
        public static readonly string RejectOperationName = "Reject";

        public static readonly string ContactAdministratorsRole = 
                                                              "ContactAdministrators";
        public static readonly string ContactManagersRole = "ContactManagers";
    }
}

Crear una clase base para Pages de Contactos

Cree una clase base que contenga los servicios utilizados en las páginas de contactos Razor Pages. La clase base coloca el código de inicialización en una ubicación:

using ContactManager.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace ContactManager.Pages.Contacts
{
    public class DI_BasePageModel : PageModel
    {
        protected ApplicationDbContext Context { get; }
        protected IAuthorizationService AuthorizationService { get; }
        protected UserManager<IdentityUser> UserManager { get; }

        public DI_BasePageModel(
            ApplicationDbContext context,
            IAuthorizationService authorizationService,
            UserManager<IdentityUser> userManager) : base()
        {
            Context = context;
            UserManager = userManager;
            AuthorizationService = authorizationService;
        } 
    }
}

El código anterior:

  • Agrega el servicio IAuthorizationService para acceder a los controladores de autorización.
  • Agrega el servicio IdentityUserManager.
  • Añada ApplicationDbContext.

Actualización de CreateModel

Actualice el constructor del modelo de página de creación para usar la clase base DI_BasePageModel:

public class CreateModel : DI_BasePageModel
{
    public CreateModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

Actualice el método CreateModel.OnPostAsync a:

  • Agregue el identificador de usuario al modelo Contact.
  • Llame al controlador de autorización para comprobar que el usuario tiene permiso para crear contactos.
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    Contact.OwnerID = UserManager.GetUserId(User);

    // requires using ContactManager.Authorization;
    var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                User, Contact,
                                                ContactOperations.Create);
    if (!isAuthorized.Succeeded)
    {
        return Forbid();
    }

    Context.Contact.Add(Contact);
    await Context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

Actualización de IndexModel

Actualice el método OnGetAsync para que solo los contactos aprobados se muestren a los usuarios generales:

public class IndexModel : DI_BasePageModel
{
    public IndexModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public IList<Contact> Contact { get; set; }

    public async Task OnGetAsync()
    {
        var contacts = from c in Context.Contact
                       select c;

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        // Only approved contacts are shown UNLESS you're authorized to see them
        // or you are the owner.
        if (!isAuthorized)
        {
            contacts = contacts.Where(c => c.Status == ContactStatus.Approved
                                        || c.OwnerID == currentUserId);
        }

        Contact = await contacts.ToListAsync();
    }
}

Actualización de EditModel

Agregue un controlador de autorización para comprobar que el usuario posee el contacto. Dado que se está validando la autorización de recursos, el atributo [Authorize] no es suficiente. La aplicación no tiene acceso al recurso cuando los atributos son evaluados. La autorización basada en recursos debe ser imperativa. Las comprobaciones se deben realizar una vez que la aplicación tiene acceso al recurso, ya sea cargándolo en el modelo de página o cargándolo dentro del propio manejador. Con frecuencia, accedes al recurso pasando la clave de recurso.

public class EditModel : DI_BasePageModel
{
    public EditModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(
                                             m => m.ContactId == id);

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

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                  User, Contact,
                                                  ContactOperations.Update);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        // Fetch Contact from DB to get OwnerID.
        var contact = await Context
            .Contact.AsNoTracking()
            .FirstOrDefaultAsync(m => m.ContactId == id);

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

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, contact,
                                                 ContactOperations.Update);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        Contact.OwnerID = contact.OwnerID;

        Context.Attach(Contact).State = EntityState.Modified;

        if (Contact.Status == ContactStatus.Approved)
        {
            // If the contact is updated after approval, 
            // and the user cannot approve,
            // set the status back to submitted so the update can be
            // checked and approved.
            var canApprove = await AuthorizationService.AuthorizeAsync(User,
                                    Contact,
                                    ContactOperations.Approve);

            if (!canApprove.Succeeded)
            {
                Contact.Status = ContactStatus.Submitted;
            }
        }

        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

Actualización de DeleteModel

Actualice el modelo de página de eliminación para usar el controlador de autorización para comprobar que el usuario tiene permiso para eliminar el contacto.

public class DeleteModel : DI_BasePageModel
{
    public DeleteModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(
                                             m => m.ContactId == id);

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

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, Contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        var contact = await Context
            .Contact.AsNoTracking()
            .FirstOrDefaultAsync(m => m.ContactId == id);

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

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        Context.Contact.Remove(contact);
        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

Inyecta el servicio de autorización en las vistas

Actualmente, la interfaz de usuario muestra los vínculos de edición y eliminación de los contactos que el usuario no puede modificar.

Inserte el servicio de autorización en el archivo Pages/_ViewImports.cshtml para que esté disponible para todas las vistas:

@using Microsoft.AspNetCore.Identity
@using ContactManager
@using ContactManager.Data
@namespace ContactManager.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using ContactManager.Authorization;
@using Microsoft.AspNetCore.Authorization
@using ContactManager.Models
@inject IAuthorizationService AuthorizationService

El marcado anterior agrega varias sentencias using.

Actualice los vínculos Editar y Eliminar en Pages/Contacts/Index.cshtml para que solo se representen para los usuarios con los permisos adecuados:

@page
@model ContactManager.Pages.Contacts.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Address)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].City)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].State)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Zip)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Email)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Status)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Contact)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Address)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.City)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.State)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Zip)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Email)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Status)
                </td>
                <td>
                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Update)).Succeeded)
                    {
                        <a asp-page="./Edit" asp-route-id="@item.ContactId">Edit</a>
                        <text> | </text>
                    }

                    <a asp-page="./Details" asp-route-id="@item.ContactId">Details</a>

                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Delete)).Succeeded)
                    {
                        <text> | </text>
                        <a asp-page="./Delete" asp-route-id="@item.ContactId">Delete</a>
                    }
                </td>
            </tr>
        }
    </tbody>
</table>

Warning

Ocultar los enlaces a los usuarios que no tienen permiso para cambiar los datos no protege la aplicación. Ocultar vínculos hace que la aplicación sea más fácil de usar mostrando solo vínculos válidos. Los usuarios pueden hackear las direcciones URL generadas para invocar operaciones de edición y eliminación en los datos que no poseen. El Razor Page o controlador debe realizar comprobaciones de acceso para proteger los datos.

Detalles de actualización

Actualice la vista de detalles para que los administradores puedan aprobar o rechazar contactos:

        @*Precedng markup omitted for brevity.*@
        <dt>
            @Html.DisplayNameFor(model => model.Contact.Email)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Contact.Email)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Contact.Status)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Contact.Status)
        </dd>
    </dl>
</div>

@if (Model.Contact.Status != ContactStatus.Approved)
{
    @if ((await AuthorizationService.AuthorizeAsync(
     User, Model.Contact, ContactOperations.Approve)).Succeeded)
    {
        <form style="display:inline;" method="post">
            <input type="hidden" name="id" value="@Model.Contact.ContactId" />
            <input type="hidden" name="status" value="@ContactStatus.Approved" />
            <button type="submit" class="btn btn-xs btn-success">Approve</button>
        </form>
    }
}

@if (Model.Contact.Status != ContactStatus.Rejected)
{
    @if ((await AuthorizationService.AuthorizeAsync(
     User, Model.Contact, ContactOperations.Reject)).Succeeded)
    {
        <form style="display:inline;" method="post">
            <input type="hidden" name="id" value="@Model.Contact.ContactId" />
            <input type="hidden" name="status" value="@ContactStatus.Rejected" />
            <button type="submit" class="btn btn-xs btn-danger">Reject</button>
        </form>
    }
}

<div>
    @if ((await AuthorizationService.AuthorizeAsync(
         User, Model.Contact,
         ContactOperations.Update)).Succeeded)
    {
        <a asp-page="./Edit" asp-route-id="@Model.Contact.ContactId">Edit</a>
        <text> | </text>
    }
    <a asp-page="./Index">Back to List</a>
</div>

Actualice el modelo de página de detalles:

public class DetailsModel : DI_BasePageModel
{
    public DetailsModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);

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

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        if (!isAuthorized
            && currentUserId != Contact.OwnerID
            && Contact.Status != ContactStatus.Approved)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id, ContactStatus status)
    {
        var contact = await Context.Contact.FirstOrDefaultAsync(
                                                  m => m.ContactId == id);

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

        var contactOperation = (status == ContactStatus.Approved)
                                                   ? ContactOperations.Approve
                                                   : ContactOperations.Reject;

        var isAuthorized = await AuthorizationService.AuthorizeAsync(User, contact,
                                    contactOperation);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }
        contact.Status = status;
        Context.Contact.Update(contact);
        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

Agregar o quitar un usuario a un rol

Consulte this issue para obtener información sobre:

  • Quitar privilegios de un usuario. Por ejemplo, silenciar a un usuario en una aplicación de chat.
  • Agregar privilegios a un usuario.

Diferencias entre Desafío y Prohibición

Esta aplicación establece la directiva predeterminada para requerir usuarios autenticados. El siguiente código permite a usuarios anónimos. Los usuarios anónimos pueden mostrar las diferencias entre Desafío y Prohibido.

[AllowAnonymous]
public class Details2Model : DI_BasePageModel
{
    public Details2Model(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);

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

        if (!User.Identity.IsAuthenticated)
        {
            return Challenge();
        }

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        if (!isAuthorized
            && currentUserId != Contact.OwnerID
            && Contact.Status != ContactStatus.Approved)
        {
            return Forbid();
        }

        return Page();
    }
}

En el código anterior:

  • Cuando el usuario no está autenticado, se devuelve ChallengeResult. Cuando se devuelve un ChallengeResult, se redirige al usuario a la página de inicio de sesión.
  • Cuando el usuario se autentica, pero no está autorizado, se devuelve ForbidResult. Cuando se devuelve un ForbidResult, se redirige al usuario a la página de acceso denegado.

Prueba de la aplicación completada

Si aún no ha establecido una contraseña para las cuentas de usuario de inicialización, use la Herramienta de Administrador de secretos para establecer una contraseña:

  • Elija una contraseña segura: use ocho o más caracteres y al menos un carácter de mayúscula, un número y un símbolo. Por ejemplo, Passw0rd! cumple los requisitos de contraseña segura.

  • Ejecute el siguiente comando desde la carpeta del project, donde <PW> es la contraseña:

    dotnet user-secrets set SeedUserPW <PW>
    

Si la aplicación tiene contactos:

  • Elimine todos los registros de la tabla Contact.
  • Reinicie la aplicación para inicializar la base de datos.

Una manera sencilla de probar la aplicación completada es iniciar tres exploradores diferentes (o sesiones de incógnito/InPrivate). En un explorador, registre un nuevo usuario (por ejemplo, test@example.com). Inicie sesión en cada explorador con un usuario diferente. Compruebe las siguientes operaciones:

  • Los usuarios registrados pueden ver todos los datos de contacto aprobados.
  • Los usuarios registrados pueden editar o eliminar sus propios datos.
  • Los administradores pueden aprobar o rechazar los datos de contacto. La vista Details muestra los botones Aprobar y Rechazar.
  • Los administradores pueden aprobar, rechazar y editar o eliminar todos los datos.
User Provisto por la aplicación Options
test@example.com No Editar o eliminar los propios datos.
manager@contoso.com Yes Apruebe, rechace y edite o elimine datos propios.
admin@contoso.com Yes Apruebe, rechace y edite o elimine todos los datos.

Cree un contacto en el explorador del administrador. Copie la dirección URL para eliminar y editar desde el contacto del administrador. Pegue estos vínculos en el explorador del usuario de prueba para comprobar que el usuario de prueba no puede realizar estas operaciones.

Crear la aplicación de inicio

  • Creación de una aplicación de Razor Pages denominada "ContactManager"

    • Cree la aplicación con cuentas individuales.
    • Asígnele el nombre "ContactManager" para que el espacio de nombres coincida con el espacio de nombres usado en el ejemplo.
    • -uld especifica LocalDB en lugar de SQLite
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • Agregue Models/Contact.cs:

    public class Contact
    {
        public int ContactId { get; set; }
        public string Name { get; set; }
        public string Address { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string Zip { get; set; }
        [DataType(DataType.EmailAddress)]
        public string Email { get; set; }
    }
    
  • Generar el esqueleto del modelo Contact.

  • Creación de una migración inicial y actualización de la base de datos:

dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet tool install -g dotnet-aspnet-codegenerator
dotnet aspnet-codegenerator razorpage -m Contact -udl -dc ApplicationDbContext -outDir Pages\Contacts --referenceScriptLibraries
dotnet ef database drop -f
dotnet ef migrations add initial
dotnet ef database update

Note

De forma predeterminada, la arquitectura de los archivos binarios de .NET que se van a instalar representa la arquitectura del sistema operativo que se está ejecutando actualmente. Para especificar una arquitectura diferente, revise cómo usar el comando con la dotnet tool installopción "--arch". Para obtener más información, vea GitHub dotnet/aspnetcore.docs issue #29262 - Add "-a arm64" en Apple Silicon.

Si experimenta un error con el comando dotnet aspnet-codegenerator razorpage, consulte esta incidencia de GitHub.

  • Actualice el ancla ContactManager en el archivo Pages/Shared/_Layout.cshtml:
<a class="navbar-brand" asp-area="" asp-page="/Contacts/Index">ContactManager</a>
  • Probar la aplicación mediante la creación, edición y eliminación de un contacto

Inicializar la base de datos

Agregue la clase SeedData a la carpeta Data:

using ContactManager.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Threading.Tasks;

// dotnet aspnet-codegenerator razorpage -m Contact -dc ApplicationDbContext -udl -outDir Pages\Contacts --referenceScriptLibraries

namespace ContactManager.Data
{
    public static class SeedData
    {
        public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
        {
            using (var context = new ApplicationDbContext(
                serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
            {              
                SeedDB(context, "0");
            }
        }        

        public static void SeedDB(ApplicationDbContext context, string adminID)
        {
            if (context.Contact.Any())
            {
                return;   // DB has been seeded
            }

            context.Contact.AddRange(
                new Contact
                {
                    Name = "Debra Garcia",
                    Address = "1234 Main St",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "debra@example.com"
                },
                new Contact
                {
                    Name = "Thorsten Weinrich",
                    Address = "5678 1st Ave W",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "thorsten@example.com"
                },
                new Contact
                {
                    Name = "Yuhong Li",
                    Address = "9012 State st",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "yuhong@example.com"
                },
                new Contact
                {
                    Name = "Jon Orton",
                    Address = "3456 Maple St",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "jon@example.com"
                },
                new Contact
                {
                    Name = "Diliana Alexieva-Bosseva",
                    Address = "7890 2nd Ave E",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "diliana@example.com"
                }
             );
            context.SaveChanges();
        }

    }
}

Llame a SeedData.Initialize desde Main:

using ContactManager.Data;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;

namespace ContactManager
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;

                try
                {
                    var context = services.GetRequiredService<ApplicationDbContext>();
                    context.Database.Migrate();
                    SeedData.Initialize(services, "not used");
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred seeding the DB.");
                }
            }

            host.Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

Pruebe que la aplicación ha inicializado la base de datos. Si hay filas en la base de datos de contacto, el método seed no se ejecuta.