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 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".

Screenshot showing Rick signed in

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

Screenshot showing manager@contoso.com signed in

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

Manager's view of a contact

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:

Screenshot showing admin@contoso.com signed in

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 y editar o eliminar contactos.

Prerrequisitos

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

La aplicación de inicio y completada

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

La aplicación de inicio

Descargue la aplicación de inicio.

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:

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 directiva de autorización de reserva para requerir 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 resaltado anterior establece la directiva de autorización de reserva. La directiva de autorizació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 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.

RequireAuthenticatedUser agrega DenyAnonymousAuthorizationRequirement a la instancia actual, lo 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, esto incluye 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 aplica la directiva a todas las solicitudes.

Establecer la directiva de autorización de reserva para requerir que los usuarios se autentiquen protege 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 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:

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 usa un filtro de autorización, la configuración de la directiva de reserva usa el enrutamiento de puntos de conexión. Establecer la directiva de reserva es la manera preferida de requerir 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;

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 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 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

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 de propietario, administrador y responsable

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:

  • Llaman a context.Succeed cuando se cumplen los requisitos.
  • Devuelven Task.CompletedTask cuando no se cumplen los requisitos. Devolver Task.CompletedTask sin una llamada anterior a context.Success o context.Fail, no es correcto o incorrecto, y 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;
        }
    }
}

Registro de 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:

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 EF y toda la información necesaria se encuentra en el parámetro Context del método HandleRequirementAsync.

Autorización de soporte técnico

En esta sección, actualizará Razor Pages y agregará una clase de requisitos de operaciones.

Revisar la clase de requisitos 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 los contactos de Razor Pages

Cree una clase base que contenga los servicios usados en los contactos de 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.
  • Agregue la ApplicationDbContext.

Actualización de CreateModel

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

  • Constructor para usar la clase base DI_BasePageModel.
  • Método OnPostAsync 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 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 se evalúan los atributos. La autorización basada en recursos debe ser imperativa. Las comprobaciones se deben realizar una vez que la aplicación tenga acceso al recurso, ya sea cargándola en el modelo de página o cargándola dentro del propio controlador. Para acceder con frecuencia al recurso, pase 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 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? _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");
    }
}

Inserción del 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 instrucciones 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";
}

<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>

Advertencia

Oculte los vínculos de 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. Razor Page o el controlador deben aplicar comprobaciones de acceso para proteger los datos.

Detalles de la 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 un usuario a un rol

Consulte este problema 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 Prohibido

Esta aplicación establece la directiva predeterminada para requerir usuarios autenticados. El código siguiente permite a los 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? _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

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 proyecto, 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.
Usuario Aprobar o rechazar contactos Opciones
test@example.com No Edite y elimine sus datos.
manager@contoso.com Edite y elimine sus datos.
admin@contoso.com 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 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 de usuario 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
    
  • Agregar Models/Contact.cs: secure-data\samples\starter6\ContactManager\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; }
        }
    }
    
  • Aplicar scaffolding al 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

Nota:

De manera predeterminada, la arquitectura de los binarios .NET que se van a instalar representa la arquitectura del sistema operativo que se está ejecutando en ese momento. Para especificar una arquitectura de SO diferente, consulte dotnet tool install, --arch option. Para más información, consulte la incidencia de GitHub dotnet/AspNetCore.Docs #29262.

  • Actualice el delimitador 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 a SeedData.Initialize desde 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 que la aplicación ha inicializado la base de datos. Si hay filas en la base de datos de contacto, 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".

Screenshot showing Rick signed in

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

Screenshot showing manager@contoso.com signed in

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

Manager's view of a contact

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:

Screenshot showing admin@contoso.com signed in

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

Prerrequisitos

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

La aplicación de inicio y completada

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

La aplicación de inicio

Descargue la aplicación de inicio.

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 de reserva 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 de reserva. 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, Razor Pages, los controladores o 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 de reserva.

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

La directiva de autenticación de reserva:

  • 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 de reserva para requerir que los usuarios se autentiquen protege a Razor Pages 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 usa un filtro de autorización, la configuración de la directiva de reserva usa el enrutamiento de puntos de conexión. Establecer la directiva de reserva es la manera preferida de requerir 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 del proyecto (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 de propietario, administrador y responsable

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:

  • Llaman a context.Succeed cuando se cumplen los requisitos.
  • Devuelven Task.CompletedTask cuando no se cumplen los requisitos. Devolver Task.CompletedTask sin una llamada anterior a context.Success o context.Fail, no es correcto o incorrecto, y 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;
        }
    }
}

Registro de 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.

Autorización de soporte técnico

En esta sección, actualizará Razor Pages y agregará una clase de requisitos de operaciones.

Revisar la clase de requisitos 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 los contactos de Razor Pages

Cree una clase base que contenga los servicios usados en los contactos de 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.
  • Agregue la 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 se evalúan los atributos. La autorización basada en recursos debe ser imperativa. Las comprobaciones se deben realizar una vez que la aplicación tenga acceso al recurso, ya sea cargándola en el modelo de página o cargándola dentro del propio controlador. Para acceder con frecuencia al recurso, pase 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");
    }
}

Inserción del 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 instrucciones 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>

Advertencia

Oculte los vínculos de 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. Razor Page o el controlador deben aplicar comprobaciones de acceso para proteger los datos.

Detalles de la 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 este problema 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 Prohibido

Esta aplicación establece la directiva predeterminada para requerir usuarios autenticados. El código siguiente permite a los 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 proyecto, 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.
Usuario Inicialización por la aplicación Opciones
test@example.com No Editar o eliminar los propios datos.
manager@contoso.com Apruebe, rechace y edite o elimine datos propios.
admin@contoso.com 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 de usuario 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; }
    }
    
  • Aplicar scaffolding al 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

Nota:

De manera predeterminada, la arquitectura de los binarios .NET que se van a instalar representa la arquitectura del sistema operativo que se está ejecutando en ese momento. Para especificar una arquitectura de SO diferente, consulte dotnet tool install, --arch option. Para más información, consulte la incidencia de GitHub dotnet/AspNetCore.Docs #29262.

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

  • Actualice el delimitador 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 de inicialización no se ejecuta.

Recursos adicionales