Rollbaserad och resursbaserad auktorisering

Exempelkod

Vår referensimplementering är ett ASP.NET Core program. I den här artikeln tittar vi på två allmänna metoder för auktorisering med hjälp av de auktoriserings-API:er som finns i ASP.NET Core.

  • Rollbaserad auktorisering. Auktorisera en åtgärd baserat på de roller som tilldelats en användare. Vissa åtgärder kräver till exempel en administratörsroll.
  • Resursbaserad auktorisering. Auktorisera en åtgärd baserat på en viss resurs. Till exempel har varje resurs en ägare. Ägaren kan ta bort resursen. andra användare kan inte.

En typisk app använder en blandning av båda. Om du till exempel vill ta bort en resurs måste användaren vara resursägare eller administratör.

Rollbaserad auktorisering

Tailspin Surveys-programmet definierar följande roller:

  • Administratör. Kan utföra alla CRUD-åtgärder på alla undersökningar som tillhör den klientorganisationen.
  • Skapare. Kan skapa nya undersökningar
  • Läsare. Kan läsa alla undersökningar som tillhör den klientorganisationen

Roller gäller för användare av programmet. I programmet Surveys är en användare antingen administratör, skapare eller läsare.

En diskussion om hur du definierar och hanterar roller finns i Programroller.

Oavsett hur du hanterar rollerna ser auktoriseringskoden ut ungefär så här. ASP.NET Core har en abstraktion som kallas auktoriseringsprinciper. Med den här funktionen definierar du auktoriseringsprinciper i kod och tillämpar sedan dessa principer på kontrollantåtgärder. Principen är frikopplad från kontrollanten.

Skapa principer

Om du vill definiera en princip skapar du först en klass som implementerar IAuthorizationRequirement . Det är enklast att härleda från AuthorizationHandler . I Handle metoden undersöker du relevanta anspråk.

Här är ett exempel från Programmet Tailspin Surveys:

public class SurveyCreatorRequirement : AuthorizationHandler<SurveyCreatorRequirement>, IAuthorizationRequirement
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SurveyCreatorRequirement requirement)
    {
        if (context.User.HasClaim(ClaimTypes.Role, Roles.SurveyAdmin) ||
            context.User.HasClaim(ClaimTypes.Role, Roles.SurveyCreator))
        {
            context.Succeed(requirement);
        }
        return Task.FromResult(0);
    }
}

Den här klassen definierar kravet på att en användare skapar en ny undersökning. Användaren måste ha rollen SurveyAdmin eller SurveyCreator.

I din startklass definierar du en namngiven princip som innehåller ett eller flera krav. Om det finns flera krav måste användaren uppfylla alla krav för att kunna auktorisera. Följande kod definierar två principer:

services.AddAuthorization(options =>
{
    options.AddPolicy(PolicyNames.RequireSurveyCreator,
        policy =>
        {
            policy.AddRequirements(new SurveyCreatorRequirement());
            policy.RequireAuthenticatedUser(); // Adds DenyAnonymousAuthorizationRequirement
            // By adding the CookieAuthenticationDefaults.AuthenticationScheme, if an authenticated
            // user is not in the appropriate role, they will be redirected to a "forbidden" page.
            policy.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
        });

    options.AddPolicy(PolicyNames.RequireSurveyAdmin,
        policy =>
        {
            policy.AddRequirements(new SurveyAdminRequirement());
            policy.RequireAuthenticatedUser();  
            policy.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
        });
});

Den här koden anger också autentiseringsschemat, vilket meddelar ASP.NET mellanprogram för autentisering ska köras om auktoriseringen misslyckas. I det här fallet anger vi mellanprogram för cookieautentisering eftersom mellanprogram för cookieautentisering kan omdirigera användaren till sidan "Förbjuden". Platsen för sidan Förbjuden anges i alternativet för AccessDeniedPath cookie-mellanprogram. Se AccessDeniedPath

Auktorisera kontrollantåtgärder

Slutligen, för att auktorisera en åtgärd i en MVC-kontrollant, anger du principen i Authorize attributet:

[Authorize(Policy = PolicyNames.RequireSurveyCreator)]
public IActionResult Create()
{
    var survey = new SurveyDTO();
    return View(survey);
}

I tidigare versioner ASP.NET skulle du ange egenskapen Roles för attributet :

// old way
[Authorize(Roles = "SurveyCreator")]

Detta stöds fortfarande i ASP.NET Core, men det har vissa nackdelar jämfört med auktoriseringsprinciper:

  • Den förutsätter en viss anspråkstyp. Principer kan söka efter vilken anspråkstyp som helst. Roller är bara en typ av anspråk.
  • Rollnamnet är hårdkodat i attributet . Med principer finns auktoriseringslogiken på ett och samma ställe, vilket gör det enklare att uppdatera eller till och med läsa in från konfigurationsinställningarna.
  • Principer möjliggör mer komplexa auktoriseringsbeslut (till exempel ålder > = 21) som inte kan uttryckas av enkla rollmedlemskap.

Resursbaserad auktorisering

Resursbaserad auktorisering sker när auktoriseringen är beroende av en specifik resurs som påverkas av en åtgärd. I Programmet Tailspin Surveys har varje undersökning en ägare och noll till många deltagare.

  • Ägaren kan läsa, uppdatera, ta bort, publicera och avpublicera undersökningen.
  • Ägaren kan tilldela deltagare till undersökningen.
  • Deltagare kan läsa och uppdatera undersökningen.

Observera att "ägare" och "deltagare" inte är programroller. De lagras per undersökning i programdatabasen. För att kontrollera om en användare kan ta bort en undersökning kontrollerar appen till exempel om användaren är ägare till undersökningen.

I ASP.NET Core du resursbaserad auktorisering genom att härleda från AuthorizationHandler och åsidosätta metoden Handle.

public class SurveyAuthorizationHandler : AuthorizationHandler<OperationAuthorizationRequirement, Survey>
{
    protected override void HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement operation, Survey resource)
    {
    }
}

Observera att den här klassen är starkt typad för undersökningsobjekt. Registrera klassen för DI vid start:

services.AddSingleton<IAuthorizationHandler>(factory =>
{
    return new SurveyAuthorizationHandler();
});

Om du vill utföra auktoriseringskontroller använder du gränssnittet IAuthorizationService, som du kan mata in i dina kontrollanter. Följande kod kontrollerar om en användare kan läsa en undersökning:

if (await _authorizationService.AuthorizeAsync(User, survey, Operations.Read) == false)
{
    return StatusCode(403);
}

Eftersom vi skickar ett Survey -objekt anropar det här anropet SurveyAuthorizationHandler .

I auktoriseringskoden är en bra metod att aggregera alla användarens rollbaserade och resursbaserade behörigheter och sedan kontrollera aggregeringsuppsättningen mot önskad åtgärd. Här är ett exempel från appen Surveys. Programmet definierar flera behörighetstyper:

  • Administratör
  • Deltagare
  • Creator
  • Ägare
  • Läsare

Programmet definierar också en uppsättning möjliga åtgärder för undersökningar:

  • Skapa
  • Läs
  • Uppdatera
  • Ta bort
  • Publicera
  • Avpublicera

Följande kod skapar en lista över behörigheter för en viss användare och undersökning. Observera att den här koden tittar på både användarens approller och fälten ägare/deltagare i undersökningen.

public class SurveyAuthorizationHandler : AuthorizationHandler<OperationAuthorizationRequirement, Survey>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, Survey resource)
    {
        var permissions = new List<UserPermissionType>();
        int surveyTenantId = context.User.GetSurveyTenantIdValue();
        int userId = context.User.GetSurveyUserIdValue();
        string user = context.User.GetUserName();

        if (resource.TenantId == surveyTenantId)
        {
            // Admin can do anything, as long as the resource belongs to the admin's tenant.
            if (context.User.HasClaim(ClaimTypes.Role, Roles.SurveyAdmin))
            {
                context.Succeed(requirement);
                return Task.FromResult(0);
            }

            if (context.User.HasClaim(ClaimTypes.Role, Roles.SurveyCreator))
            {
                permissions.Add(UserPermissionType.Creator);
            }
            else
            {
                permissions.Add(UserPermissionType.Reader);
            }

            if (resource.OwnerId == userId)
            {
                permissions.Add(UserPermissionType.Owner);
            }
        }
        if (resource.Contributors != null && resource.Contributors.Any(x => x.UserId == userId))
        {
            permissions.Add(UserPermissionType.Contributor);
        }

        if (ValidateUserPermissions[requirement](permissions))
        {
            context.Succeed(requirement);
        }
        return Task.FromResult(0);
    }
}

I ett program för flera klienter måste du se till att behörigheterna inte "läcker" till en annan klientorganisations data. I appen Surveys tillåts behörigheten Deltagare för alla klienter – du kan tilldela någon från en annan klientorganisation som deltagare. De andra behörighetstyperna är begränsade till resurser som tillhör användarens klientorganisation. För att framtvinga det här kravet kontrollerar koden klientorganisations-ID:t innan behörigheten beviljas. (Fältet TenantId tilldelas när undersökningen skapas.)

Nästa steg är att kontrollera åtgärden (till exempel läsa, uppdatera eller ta bort) mot behörigheterna. Appen Surveys implementerar det här steget med hjälp av en uppslagstabell med funktioner:

static readonly Dictionary<OperationAuthorizationRequirement, Func<List<UserPermissionType>, bool>> ValidateUserPermissions
    = new Dictionary<OperationAuthorizationRequirement, Func<List<UserPermissionType>, bool>>

    {
        { Operations.Create, x => x.Contains(UserPermissionType.Creator) },

        { Operations.Read, x => x.Contains(UserPermissionType.Creator) ||
                                x.Contains(UserPermissionType.Reader) ||
                                x.Contains(UserPermissionType.Contributor) ||
                                x.Contains(UserPermissionType.Owner) },

        { Operations.Update, x => x.Contains(UserPermissionType.Contributor) ||
                                x.Contains(UserPermissionType.Owner) },

        { Operations.Delete, x => x.Contains(UserPermissionType.Owner) },

        { Operations.Publish, x => x.Contains(UserPermissionType.Owner) },

        { Operations.UnPublish, x => x.Contains(UserPermissionType.Owner) }
    };

Nästa