Criar um ASP.NET Core Web com dados do usuário protegidos por autorização

Por Rick Anderson e Joe Audette

Consulte este pdf

Este tutorial mostra como criar um aplicativo Web ASP.NET Core web com dados do usuário protegidos por autorização. Ele exibe uma lista de contatos que os usuários autenticados (registrados) criaram. Há três grupos de segurança:

  • Os usuários registrados podem exibir todos os dados aprovados e podem editar/excluir seus próprios dados.
  • Os gerentes podem aprovar ou rejeitar dados de contato. Somente contatos aprovados são visíveis para os usuários.
  • Os administradores podem aprovar/rejeitar e editar/excluir quaisquer dados.

As imagens neste documento não corresponderão exatamente aos modelos mais recentes.

Na imagem a seguir, o usuário Rick ( rick@example.com ) está entre. Rick só pode exibir contatos aprovados e Editar / Excluir / Criar links para seus contatos. Somente o último registro, criado por Rick, exibe os links Editar e Excluir. Outros usuários não verão o último registro até que um gerente ou administrador mude o status para "Aprovado".

Captura de tela mostrando Rick se inscreveu

Na imagem a seguir, manager@contoso.com é assinado e na função do gerente:

Captura de tela mostrando manager@contoso.com a assinatura

A imagem a seguir mostra a exibição de detalhes de gerentes de um contato:

Exibição do gerente de um contato

Os botões Aprovar e Rejeitar só são exibidos para gerentes e administradores.

Na imagem a seguir, admin@contoso.com é assinado e na função do administrador:

Captura de tela mostrando admin@contoso.com a assinatura

O administrador tem todos os privilégios. Ela pode ler/editar/excluir qualquer contato e alterar o status dos contatos.

O aplicativo foi criado por scaffolding do seguinte Contact modelo:

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

O exemplo contém os seguintes manipuladores de autorização:

  • ContactIsOwnerAuthorizationHandler: garante que um usuário só possa editar seus dados.
  • ContactManagerAuthorizationHandler: permite que os gerentes aprovem ou rejeitem contatos.
  • ContactAdministratorsAuthorizationHandler: permite que os administradores aprovem ou rejeitem contatos e editem/excluam contatos.

Pré-requisitos

Este tutorial é avançado. Você deve estar familiarizado com:

O aplicativo inicial e concluído

Baixe o aplicativo concluído. Teste o aplicativo concluído para que você se familiarizar com seus recursos de segurança.

O aplicativo inicial

Baixe o aplicativo inicial.

Execute o aplicativo, toque no link ContactManager e verifique se você pode criar, editar e excluir um contato. Para criar o aplicativo inicial, consulte Criar o aplicativo inicial.

Proteger dados do usuário

As seções a seguir têm todas as principais etapas para criar o aplicativo de dados de usuário seguro. Você pode achar útil consultar o projeto concluído.

Ligar os dados de contato ao usuário

Use a ASP.NET ID de usuário para garantir que os usuários Identity possam editar seus dados, mas não dados de outros usuários. Adicione OwnerID e ContactStatus ao Contact modelo:

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

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

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

    public ContactStatus Status { get; set; }
}

public enum ContactStatus
{
    Submitted,
    Approved,
    Rejected
}

OwnerID é a ID do usuário da tabela AspNetUser no banco de Identity dados. O Status campo determina se um contato pode ser visualizado por usuários gerais.

Crie uma nova migração e atualize o banco de dados:

dotnet ef migrations add userID_Status
dotnet ef database update

Adicionar serviços de função ao Identity

Acrescentar AddRoles para adicionar serviços de função:

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>();

Exigir usuários autenticados

De definir a política de autenticação de fallback para exigir que os usuários sejam autenticados:

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();
    });

O código realçado anterior define a política de autenticação de fallback. A política de autenticação de fallback exige que todos os usuários sejam autenticados, exceto páginas, controladores ou métodos de ação com um atributo Razor de autenticação. Por exemplo, Páginas, controladores ou métodos de ação com ou use o atributo de autenticação aplicado em vez da Razor [AllowAnonymous] política de [Authorize(PolicyName="MyPolicy")] autenticação de fallback.

RequireAuthenticatedUser adiciona DenyAnonymousAuthorizationRequirement à instância atual, que impõe que o usuário atual seja autenticado.

A política de autenticação de fallback:

  • É aplicado a todas as solicitações que não especificam explicitamente uma política de autenticação. Para solicitações atendidas pelo roteamento de ponto de extremidade, isso incluiria qualquer ponto de extremidade que não especifique um atributo de autorização. Para solicitações atendidas por outro middleware após o middleware de autorização, como arquivos estáticos, isso aplicaria a política a todas as solicitações.

Definir a política de autenticação de fallback para exigir que os usuários sejam autenticados protege páginas e controladores Razor recém-adicionados. Ter a autenticação necessária por padrão é mais seguro do que depender de novos controladores e Razor Páginas para incluir o [Authorize] atributo.

A AuthorizationOptions classe também contém AuthorizationOptions.DefaultPolicy . O DefaultPolicy é a política usada com o atributo quando nenhuma política é [Authorize] especificada. [Authorize] não contém uma política nomeada, ao contrário de [Authorize(PolicyName="MyPolicy")] .

Para obter mais informações sobre políticas, consulte Autorização baseada em políticas ASP.NET Core .

Uma maneira alternativa para controladores MVC e Páginas exigirem que todos os usuários sejam Razor autenticados é adicionar um filtro de autorização:

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));
    });

O código anterior usa um filtro de autorização, definindo a política de fallback usa roteamento de ponto de extremidade. Definir a política de fallback é a maneira preferencial de exigir que todos os usuários sejam autenticados.

Adicione AllowAnonymous às páginas e para que os usuários anônimos possam obter Index informações sobre o site antes de se Privacy registrarem:

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()
        {

        }
    }
}

Configurar a conta de teste

A SeedData classe cria duas contas: administrador e gerente. Use a ferramenta Gerenciador de Segredos para definir uma senha para essas contas. De definir a senha do diretório do projeto (o diretório que contém Program.cs):

dotnet user-secrets set SeedUserPW <PW>

Se uma senha forte não for especificada, uma exceção será lançada quando SeedData.Initialize for chamado.

Atualize Main para usar a senha de teste:

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>();
            });
}

Criar as contas de teste e atualizar os contatos

Atualize Initialize o método na classe para criar as contas de SeedData teste:

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)
{
    IdentityResult IR = null;
    var roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>();

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

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

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

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

Adicione a ID de usuário do administrador ContactStatus e aos contatos. Faça um dos contatos "Enviado" e um "Rejeitado". Adicione a ID de usuário e o status a todos os contatos. Apenas um contato é mostrado:

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

Criar manipuladores de autorização de proprietário, gerente e administrador

Crie uma ContactIsOwnerAuthorizationHandler classe na pasta Autorização. O ContactIsOwnerAuthorizationHandler verifica se o usuário que atua em um recurso possui o 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;
        }
    }
}

O ContactIsOwnerAuthorizationHandler contexto de chamadas. Terá êxito se o usuário autenticado atual for o proprietário do contato. Manipuladores de autorização em geral:

  • Chame context.Succeed quando os requisitos são atendidos.
  • Retornar Task.CompletedTask quando os requisitos não são atendidos. Retornar sem uma chamada anterior para ou , não é um êxito ou falha, ele permite que outros manipuladores de autorização Task.CompletedTask context.Success context.Fail executem.

Se você precisar falhar explicitamente, chame o contexto. Falhaem .

O aplicativo permite aos proprietários de contato editar/Excluir/criar seus próprios dados. ContactIsOwnerAuthorizationHandler Não precisa verificar a operação passada no parâmetro de requisito.

Criar um manipulador de autorização de gerente

Crie uma ContactManagerAuthorizationHandler classe na pasta Authorization . O ContactManagerAuthorizationHandler verifica se o usuário que está atuando no recurso é um gerente. Somente os gerentes podem aprovar ou rejeitar alterações de conteúdo (novas ou alteradas).

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

Criar um manipulador de autorização de administrador

Crie uma ContactAdministratorsAuthorizationHandler classe na pasta Authorization . O ContactAdministratorsAuthorizationHandler verifica se o usuário que está atuando no recurso é um administrador. O administrador pode realizar todas as operações.

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

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

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

            return Task.CompletedTask;
        }
    }
}

Registrar os manipuladores de autorização

Os serviços que usam Entity Framework Core devem ser registrados para injeção de dependência usando addscoped. o ContactIsOwnerAuthorizationHandler usa ASP.NET Core Identity , que se baseia em Entity Framework Core. Registre os manipuladores com a coleção de serviços para que fiquem disponíveis para o ContactsController através de injeção de dependência. Adicione o seguinte código ao 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 e ContactManagerAuthorizationHandler são adicionados como singletons. Eles são singletons porque não usam o EF e todas as informações necessárias estão no Context parâmetro do HandleRequirementAsync método.

Autorização de suporte

Nesta seção, você atualizará as Razor páginas e adicionará uma classe de requisitos de operações.

Examinar a classe de requisitos de operações de contato

Examine a ContactOperations classe. Essa classe contém os requisitos aos quais o aplicativo dá suporte:

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";
    }
}

Criar uma classe base para as páginas de contatos Razor

Crie uma classe base que contenha os serviços usados nas páginas de contatos Razor . A classe base coloca o código de inicialização em um local:

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

O código anterior:

  • Adiciona o IAuthorizationService serviço para acessar os manipuladores de autorização.
  • Adiciona o Identity UserManager serviço.
  • Adicione a ApplicationDbContext.

Atualizar o CREATEMODEL

Atualize o Construtor criar modelo de página para usar a DI_BasePageModel classe base:

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

Atualize o CreateModel.OnPostAsync método para:

  • Adicione a ID de usuário ao Contact modelo.
  • Chame o manipulador de autorização para verificar se o usuário tem permissão para criar contatos.
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");
}

Atualizar o IndexModel

Atualize o OnGetAsync método para que somente os contatos aprovados sejam mostrados para usuários gerais:

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();
    }
}

Atualizar o EditModel

Adicione um manipulador de autorização para verificar se o usuário possui o contato. Como a autorização de recursos está sendo validada, o [Authorize] atributo não é suficiente. O aplicativo não tem acesso ao recurso quando os atributos são avaliados. A autorização baseada em recursos deve ser imperativa. As verificações devem ser executadas depois que o aplicativo tem acesso ao recurso, seja carregando-o no modelo de página ou carregando-o dentro do próprio manipulador. Você acessa com frequência o recurso passando a chave 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");
    }
}

Atualizar o DeleteModel

Atualize o modelo de página de exclusão para usar o manipulador de autorização para verificar se o usuário tem a permissão Excluir no contato.

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");
    }
}

Injetar o serviço de autorização nas exibições

Atualmente, a interface do usuário mostra links de edição e exclusão de contatos que o usuário não pode modificar.

Insira o serviço de autorização no arquivo pages/_ViewImports. cshtml para que ele esteja disponível para todas as exibições:

@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

A marcação anterior adiciona várias using instruções.

Atualize os links Editar e excluir em pages/Contacts/index. cshtml para que eles sejam renderizados apenas para usuários com as permissões apropriadas:

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

Aviso

Ocultar links de usuários que não têm permissão para alterar dados não protege o aplicativo. Ocultar links torna o aplicativo mais amigável exibindo apenas links válidos. Os usuários podem invadir as URLs geradas para invocar operações de edição e exclusão nos dados que não possuem. A Razor página ou o controlador deve impor verificações de acesso para proteger os dados.

Atualizar detalhes

Atualize a exibição de detalhes para que os gerentes possam aprovar ou rejeitar contatos:

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

Atualize o modelo de página de detalhes:

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");
    }
}

Adicionar ou remover um usuário de uma função

Consulte este problema para obter informações sobre:

  • Removendo privilégios de um usuário. Por exemplo, ativar mudo de um usuário em um aplicativo de chat.
  • Adicionando privilégios a um usuário.

Diferenças entre desafio e proíba

Esse aplicativo define a política padrão para exigir usuários autenticados. O código a seguir permite usuários anônimos. Os usuários anônimos têm permissão para mostrar as diferenças entre o desafio versus proíba.

[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();
    }
}

No código anterior:

  • Quando o usuário não é autenticado, um ChallengeResult é retornado. Quando um ChallengeResult é retornado, o usuário é redirecionado para a página de entrada.
  • Quando o usuário é autenticado, mas não autorizado, um ForbidResult é retornado. Quando um ForbidResult é retornado, o usuário é redirecionado para a página acesso negado.

Testar o aplicativo concluído

Se você ainda não definiu uma senha para contas de usuário propagadas, use a ferramenta Gerenciador de segredo para definir uma senha:

  • Escolha uma senha forte: Use oito ou mais caracteres e, pelo menos, um caractere, número e símbolo em letras maiúsculas. Por exemplo, Passw0rd! atende aos requisitos de senha forte.

  • Execute o seguinte comando na pasta do projeto, em que <PW> é a senha:

    dotnet user-secrets set SeedUserPW <PW>
    

Se o aplicativo tiver contatos:

  • Exclua todos os registros na Contact tabela.
  • Reinicie o aplicativo para propagar o banco de dados.

Uma maneira fácil de testar o aplicativo concluído é iniciar três navegadores diferentes (ou sessões Incognito/InPrivate). Em um navegador, registre um novo usuário (por exemplo, test@example.com ). Entre em cada navegador com um usuário diferente. Verifique as seguintes operações:

  • Os usuários registrados podem exibir todos os dados de contato aprovados.
  • Os usuários registrados podem editar/excluir seus próprios dados.
  • Os gerentes podem aprovar/rejeitar dados de contato. A Details exibição mostra os botões aprovar e rejeitar .
  • Os administradores podem aprovar/rejeitar e editar/excluir todos os dados.
Usuário Propagado pelo aplicativo Opções
test@example.com Não Edite/exclua os próprios dados.
manager@contoso.com Sim Aprovar/rejeitar e editar/excluir os próprios dados.
admin@contoso.com Sim Aprovar/rejeitar e editar/excluir todos os dados.

Crie um contato no navegador do administrador. Copie a URL para excluir e editar do contato do administrador. Cole esses links no navegador do usuário de teste para verificar se o usuário de teste não pode executar essas operações.

Criar o aplicativo inicial

  • Criar um Razor aplicativo de páginas chamado "ContactManager"

    • Crie o aplicativo com contas de usuário individuais.
    • Nomeie-o como "ContactManager" para que o namespace corresponda ao namespace usado no exemplo.
    • -uldespecifica LocalDB em vez do SQLite
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • Adicionar modelos/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; }
    }
    
  • Scaffold o Contact modelo.

  • Crie uma migração inicial e atualize o banco de dados:

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

se você tiver um bug com o dotnet aspnet-codegenerator razorpage comando, consulte este GitHub problema.

  • Atualize a âncora ContactManager no arquivo pages/Shared/_Layout. cshtml :
<a class="navbar-brand" asp-area="" asp-page="/Contacts/Index">ContactManager</a>
  • Testar o aplicativo Criando, editando e excluindo um contato

Propagar o banco de dados

Adicione a classe SeedData à pasta de dados :

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();
        }

    }
}

Chamada SeedData.Initialize de 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>();
                });
    }
}

Teste se o aplicativo propagau o banco de dados. Se houver linhas no BD de contato, o método de semente não será executado.

este tutorial mostra como criar um aplicativo web ASP.NET Core com dados de usuário protegidos por autorização. Ele exibe uma lista de contatos que usuários autenticados (registrados) criaram. Há três grupos de segurança:

  • Os usuários registrados podem exibir todos os dados aprovados e podem editar/excluir seus próprios dados.
  • Os gerentes podem aprovar ou rejeitar dados de contato. Somente contatos aprovados são visíveis para os usuários.
  • Os administradores podem aprovar/rejeitar e editar/excluir todos os dados.

Na imagem a seguir, o usuário Rick ( rick@example.com ) está conectado. Rick só pode exibir contatos aprovados e Editar / excluir / criar novos links para seus contatos. Somente o último registro, criado por Rick, exibe links de edição e exclusão . Outros usuários não verão o último registro até que um gerente ou Administrador altere o status para "aprovado".

Captura de tela mostrando Rick conectado

Na imagem a seguir, manager@contoso.com está conectado e na função do gerente:

Captura de tela mostrando a manager@contoso.com entrada

A imagem a seguir mostra a exibição de detalhes dos gerentes de um contato:

Exibição do gerente de um contato

Os botões aprovar e rejeitar são exibidos apenas para gerentes e administradores.

Na imagem a seguir, admin@contoso.com está conectado e na função do administrador:

Captura de tela mostrando a admin@contoso.com entrada

O administrador tem todos os privilégios. Ela pode ler/editar/excluir qualquer contato e alterar o status dos contatos.

O aplicativo foi criado por scaffolding o seguinte Contact modelo:

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

O exemplo contém os seguintes manipuladores de autorização:

  • ContactIsOwnerAuthorizationHandler: Garante que um usuário só possa editar seus dados.
  • ContactManagerAuthorizationHandler: Permite que os gerentes aprovem ou rejeitem contatos.
  • ContactAdministratorsAuthorizationHandler: Permite que os administradores aprovem ou rejeitem contatos e editem/excluam contatos.

Pré-requisitos

Este tutorial é avançado. Você deve estar familiarizado com:

O aplicativo iniciador e concluído

Baixe o aplicativo concluído . Teste o aplicativo concluído para que você se familiarize com seus recursos de segurança.

O aplicativo inicial

Baixe o aplicativo inicial .

Execute o aplicativo, toque no link do ContactManager e verifique se você pode criar, editar e excluir um contato.

Proteger dados do usuário

As seções a seguir têm todas as principais etapas para criar o aplicativo de dados de usuário seguro. Talvez você ache útil fazer referência ao projeto concluído.

Vincular os dados de contato ao usuário

Use o ASP.NET Identity ID de usuário para garantir que os usuários possam editar seus dados, mas não outros dados de usuários. Adicione OwnerID e ContactStatus ao Contact modelo:

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

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

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

    public ContactStatus Status { get; set; }
}

public enum ContactStatus
{
    Submitted,
    Approved,
    Rejected
}

OwnerID é a ID do usuário da AspNetUser tabela no banco de Identity dados. O Status campo determina se um contato é visível por usuários gerais.

Crie uma nova migração e atualize o banco de dados:

dotnet ef migrations add userID_Status
dotnet ef database update

Adicionar serviços de função a Identity

Acrescente AddRoles para adicionar serviços de função:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

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

Exigir usuários autenticados

Defina a política de autenticação padrão para exigir que os usuários sejam autenticados:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

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

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

Você pode recusar a autenticação na Razor página, controlador ou nível de método de ação com o [AllowAnonymous] atributo. Definir a política de autenticação padrão para exigir que os usuários sejam autenticados protege Razor páginas e controladores adicionados recentemente. Ter a autenticação exigida por padrão é mais seguro do que depender de novos controladores e Razor páginas para incluir o [Authorize] atributo.

Adicione AllowAnonymous às páginas de índice, sobre e de contato para que os usuários anônimos possam obter informações sobre o site antes de se registrarem.

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

namespace ContactManager.Pages
{
    [AllowAnonymous]
    public class IndexModel : PageModel
    {
        public void OnGet()
        {

        }
    }
}

Configurar a conta de teste

A SeedData classe cria duas contas: administrador e Gerenciador. Use a ferramenta Gerenciador de segredo para definir uma senha para essas contas. Defina a senha do diretório do projeto (o diretório que contém Program. cs):

dotnet user-secrets set SeedUserPW <PW>

Se uma senha forte não for especificada, uma exceção será lançada quando SeedData.Initialize for chamado.

Atualize Main para usar a senha de teste:

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

        using (var scope = host.Services.CreateScope())
        {
            var services = scope.ServiceProvider;
            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"];
            try
            {
                SeedData.Initialize(services, testUserPw).Wait();
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex.Message, "An error occurred seeding the DB.");
            }
        }

        host.Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

Criar as contas de teste e atualizar os contatos

Atualize o Initialize método na SeedData classe para criar as contas de teste:

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 };
        await userManager.CreateAsync(user, testUserPw);
    }

    return user.Id;
}

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

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

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

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

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

Adicione a ID de usuário do administrador e ContactStatus aos contatos. Faça um dos contatos "enviado" e um "rejeitado". Adicione a ID de usuário e o status a todos os contatos. Apenas um contato é mostrado:

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

Criar manipuladores de autorização de administrador, gerente e proprietário

Crie uma pasta de autorização e crie uma ContactIsOwnerAuthorizationHandler classe nela. O ContactIsOwnerAuthorizationHandler verifica se o usuário que está atuando em um recurso possui o recurso.

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

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.FromResult(0) if targeting a version of
                // .NET Framework older than 4.6:
                return Task.CompletedTask;
            }

            // If we're 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;
        }
    }
}

O ContactIsOwnerAuthorizationHandler contexto de chamadas . Com sucesso se o usuário autenticado atual for o proprietário do contato. Manipuladores de autorização geralmente:

  • Chame context.Succeed quando os requisitos forem atendidos.
  • Retornar Task.CompletedTask quando os requisitos não forem atendidos. Retornando Task.CompletedTask sem uma chamada anterior para context.Success ou context.Fail , não é um êxito ou falha, ele permite que outros manipuladores de autorização sejam executados.

Se você precisar falhar explicitamente, chame Context. Falha.

O aplicativo permite aos proprietários de contato editar/Excluir/criar seus próprios dados. ContactIsOwnerAuthorizationHandler Não precisa verificar a operação passada no parâmetro de requisito.

Criar um manipulador de autorização de gerente

Crie uma ContactManagerAuthorizationHandler classe na pasta Authorization . O ContactManagerAuthorizationHandler verifica se o usuário que está atuando no recurso é um gerente. Somente os gerentes podem aprovar ou rejeitar alterações de conteúdo (novas ou alteradas).

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

Criar um manipulador de autorização de administrador

Crie uma ContactAdministratorsAuthorizationHandler classe na pasta Authorization . O ContactAdministratorsAuthorizationHandler verifica se o usuário que está atuando no recurso é um administrador. O administrador pode realizar todas as operações.

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

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

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

            return Task.CompletedTask;
        }
    }
}

Registrar os manipuladores de autorização

Os serviços que usam Entity Framework Core devem ser registrados para injeção de dependência usando addscoped. o ContactIsOwnerAuthorizationHandler usa ASP.NET Core Identity , que se baseia em Entity Framework Core. Registre os manipuladores com a coleção de serviços para que fiquem disponíveis para o ContactsController através de injeção de dependência. Adicione o seguinte código ao final de ConfigureServices :

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

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

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

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

    services.AddSingleton<IAuthorizationHandler,
                          ContactAdministratorsAuthorizationHandler>();

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

ContactAdministratorsAuthorizationHandler e ContactManagerAuthorizationHandler são adicionados como singletons. Eles são singletons porque não usam o EF e todas as informações necessárias estão no Context parâmetro do HandleRequirementAsync método.

Autorização de suporte

Nesta seção, você atualizará as Razor páginas e adicionará uma classe de requisitos de operações.

Examinar a classe de requisitos de operações de contato

Examine a ContactOperations classe. Essa classe contém os requisitos aos quais o aplicativo dá suporte:

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";
    }
}

Criar uma classe base para as páginas de contatos Razor

Crie uma classe base que contenha os serviços usados nas páginas de contatos Razor . A classe base coloca o código de inicialização em um local:

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

O código anterior:

  • Adiciona o IAuthorizationService serviço para acessar os manipuladores de autorização.
  • Adiciona o Identity UserManager serviço.
  • Adicione a ApplicationDbContext.

Atualizar o CREATEMODEL

Atualize o Construtor criar modelo de página para usar a DI_BasePageModel classe base:

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

Atualize o CreateModel.OnPostAsync método para:

  • Adicione a ID de usuário ao Contact modelo.
  • Chame o manipulador de autorização para verificar se o usuário tem permissão para criar contatos.
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 new ChallengeResult();
    }

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

    return RedirectToPage("./Index");
}

Atualizar o IndexModel

Atualize o OnGetAsync método para que somente os contatos aprovados sejam mostrados para usuários gerais:

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();
    }
}

Atualizar o EditModel

Adicione um manipulador de autorização para verificar se o usuário possui o contato. Como a autorização de recursos está sendo validada, o [Authorize] atributo não é suficiente. O aplicativo não tem acesso ao recurso quando os atributos são avaliados. A autorização baseada em recursos deve ser imperativa. As verificações devem ser executadas depois que o aplicativo tem acesso ao recurso, seja carregando-o no modelo de página ou carregando-o dentro do próprio manipulador. Você acessa com frequência o recurso passando a chave 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 new ChallengeResult();
        }

        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 new ChallengeResult();
        }

        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");
    }

    private bool ContactExists(int id)
    {
        return Context.Contact.Any(e => e.ContactId == id);
    }
}

Atualizar o DeleteModel

Atualize o modelo de página de exclusão para usar o manipulador de autorização para verificar se o usuário tem a permissão Excluir no contato.

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 new ChallengeResult();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        Contact = await Context.Contact.FindAsync(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 new ChallengeResult();
        }

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

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

Injetar o serviço de autorização nas exibições

Atualmente, a interface do usuário mostra links de edição e exclusão de contatos que o usuário não pode modificar.

Insira o serviço de autorização no arquivo views/_ViewImports. cshtml para que ele esteja disponível para todas as exibições:

@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

A marcação anterior adiciona várias using instruções.

Atualize os links Editar e excluir em pages/Contacts/index. cshtml para que eles sejam renderizados apenas para usuários com as permissões apropriadas:

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

Aviso

Ocultar links de usuários que não têm permissão para alterar dados não protege o aplicativo. Ocultar links torna o aplicativo mais amigável exibindo apenas links válidos. Os usuários podem invadir as URLs geradas para invocar operações de edição e exclusão nos dados que não possuem. A Razor página ou o controlador deve impor verificações de acesso para proteger os dados.

Atualizar detalhes

Atualize a exibição de detalhes para que os gerentes possam aprovar ou rejeitar contatos:

        @*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-success">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>

Atualize o modelo de página de detalhes:

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 new ChallengeResult();
        }

        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 new ChallengeResult();
        }
        contact.Status = status;
        Context.Contact.Update(contact);
        await Context.SaveChangesAsync();

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

Adicionar ou remover um usuário de uma função

Consulte este problema para obter informações sobre:

  • Removendo privilégios de um usuário. Por exemplo, ativar mudo de um usuário em um aplicativo de chat.
  • Adicionando privilégios a um usuário.

Testar o aplicativo concluído

Se você ainda não definiu uma senha para contas de usuário propagadas, use a ferramenta Gerenciador de segredo para definir uma senha:

  • Escolha uma senha forte: Use oito ou mais caracteres e, pelo menos, um caractere, número e símbolo em letras maiúsculas. Por exemplo, Passw0rd! atende aos requisitos de senha forte.

  • Execute o seguinte comando na pasta do projeto, em que <PW> é a senha:

    dotnet user-secrets set SeedUserPW <PW>
    
  • Remover e atualizar o banco de dados

    dotnet ef database drop -f
    dotnet ef database update  
    
  • Reinicie o aplicativo para propagar o banco de dados.

Uma maneira fácil de testar o aplicativo concluído é iniciar três navegadores diferentes (ou sessões Incognito/InPrivate). Em um navegador, registre um novo usuário (por exemplo, test@example.com ). Entre em cada navegador com um usuário diferente. Verifique as seguintes operações:

  • Os usuários registrados podem exibir todos os dados de contato aprovados.
  • Os usuários registrados podem editar/excluir seus próprios dados.
  • Os gerentes podem aprovar/rejeitar dados de contato. A Details exibição mostra os botões aprovar e rejeitar .
  • Os administradores podem aprovar/rejeitar e editar/excluir todos os dados.
Usuário Propagado pelo aplicativo Opções
test@example.com Não Edite/exclua os próprios dados.
manager@contoso.com Sim Aprovar/rejeitar e editar/excluir os próprios dados.
admin@contoso.com Sim Aprovar/rejeitar e editar/excluir todos os dados.

Crie um contato no navegador do administrador. Copie a URL para excluir e editar do contato do administrador. Cole esses links no navegador do usuário de teste para verificar se o usuário de teste não pode executar essas operações.

Criar o aplicativo inicial

  • Criar um Razor aplicativo de páginas chamado "ContactManager"

    • Crie o aplicativo com contas de usuário individuais.
    • Nomeie-o como "ContactManager" para que o namespace corresponda ao namespace usado no exemplo.
    • -uldespecifica LocalDB em vez do SQLite
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • Adicionar modelos/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; }
    }
    
  • Scaffold o Contact modelo.

  • Crie uma migração inicial e atualize o banco de dados:

    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
    
  • Atualize a âncora ContactManager no arquivo pages/_Layout. cshtml :

    <a asp-page="/Contacts/Index" class="navbar-brand">ContactManager</a>
    
  • Testar o aplicativo Criando, editando e excluindo um contato

Propagar o banco de dados

Adicione a classe SeedData à pasta de dados .

Chamada SeedData.Initialize de Main :

public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateWebHostBuilder(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 IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

Teste se o aplicativo propagau o banco de dados. Se houver linhas no BD de contato, o método de semente não será executado.

Recursos adicionais