2011-05-18 1 views
1

J'ai un site ASP.NET MVC qui utilise le modèle de référentiel pour accéder et modifier des données. Mon interface de référentiel est transmise à chaque contrôleur via leurs constructeurs. J'utilise également Ninject pour injecter mon type de référentiel concret via DependencyResolver.SetResolver().Où dois-je mettre du code pour les autorisations d'accès sur mon site ASP.NET MVC?

Les utilisateurs de mon site doivent uniquement pouvoir accéder aux données qui leur sont assignées. Ce que j'essaie de comprendre est où dois-je vérifier que l'utilisateur actuel a l'autorisation d'effectuer la demande en cours? Par exemple, l'utilisateur peut soumettre un formulaire de confirmation de suppression à l'URL/Item/Delete/123, qui supprimera l'Item avec l'ID 123. Cependant, je ne veux pas qu'un utilisateur puisse manipuler cette URL et finissent par supprimer l'article d'un autre utilisateur.

Devrais-je ajouter le code de validation utilisateur aux contrôleurs de sorte que la première chose que fait chaque méthode d'action est de vérifier si l'utilisateur actuel possède même les données qu'il essaie de modifier? Cela semble ajouter trop d'intelligence au contrôleur qui devrait être plutôt mince.

Je pense qu'il serait plus logique d'ajouter cette logique à mon référentiel? Par exemple, je peux avoir un Repository.GetItem (int id, string utilisateur) plutôt que Repository.GetItem (int id), qui lancerait une exception à moins que "user" ne possède l'élément demandé. Sinon, je pensais que chaque instance de mon référentiel créé pourrait être assignée à un utilisateur spécifique quand elle est instanciée. Ce référentiel spécifique à l'utilisateur lèverait alors des exceptions s'il y a jamais une tentative d'accès ou de modification de données qui n'appartiennent pas à l'utilisateur actuel. Le contrôleur aurait alors simplement besoin d'attraper ces exceptions et de rediriger l'utilisateur vers une page d'erreur si une personne est interceptée.

Répondre

5

Je suis récemment tombé sur le même problème. J'ai fini par aller avec un ActionFilter personnalisé qui hérite de AuthorizeAttribute.

Il a fondamentalement la même fonctionnalité que Authorize (vérifie si un utilisateur appartient à au moins un des rôles énumérés) mais ajoute également la capacité de vérifier si un utilisateur "possède" les données particulières.

Voici le code à utiliser comme exemple. Si quelque chose n'est pas clair, s'il vous plaît commenter, et je vais essayer d'expliquer.

[Modifier - Basé sur la suggestion de Ryan, j'ai fait params UserRole[] un paramètre constructeur au lieu d'une propriété publique et a ajouté AllowAnyRolesIfNoneSpecified.]

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)] 
public class AccountAuthorizeAttribute : AuthorizeAttribute 
{ 
    private readonly UserRole[] _userRoles; 

    public bool MustBeInRoleOrPageOwner { get; set; } 
    public bool MustBeInRoleAndPageOwner { get; set; } 
    public bool MustBeInRoleAndNotPageOwner { get; set; } 
    public bool AllowAnyRolesIfNoneSpecified { get; set; } 
    public string AccessDeniedViewName { get; set; } 

    public AccountAuthorizeAttribute(params UserRole[] userRoles) 
    { 
     _userRoles = userRoles; 
     MustBeInRoleOrPageOwner = false; 
     MustBeInRoleAndPageOwner = false; 
     MustBeInRoleAndNotPageOwner = false; 
     AllowAnyRolesIfNoneSpecified = true; 
     AccessDeniedViewName = "AccessDenied"; 
    } 

    protected void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus) 
    { 
     validationStatus = OnCacheAuthorization(new HttpContextWrapper(context)); 
    } 

    public override void OnAuthorization(AuthorizationContext filterContext) 
    { 
     if (!filterContext.HttpContext.User.Identity.IsAuthenticated) 
     { 
      ShowLogOnPage(filterContext); 
      return; 
     } 
     using (var dbContext = new MainDbContext()) 
     { 
      var accountService = new AccountService(dbContext); 
      var emailAddress = filterContext.HttpContext.User.Identity.Name; 
      if (IsUserInRole(accountService, emailAddress)) 
      { 
       var isPageOwner = IsUserPageOwner(filterContext, dbContext, accountService, emailAddress); 
       if (MustBeInRoleAndPageOwner && !isPageOwner || MustBeInRoleAndNotPageOwner && isPageOwner) 
        ShowAccessDeniedPage(filterContext); 
      } 
      else 
      { 
       if (!MustBeInRoleOrPageOwner) 
        ShowAccessDeniedPage(filterContext); 
       else if (!IsUserPageOwner(filterContext, dbContext, accountService, emailAddress)) 
        ShowAccessDeniedPage(filterContext); 
      } 
     } 
    } 

    private bool IsUserInRole(AccountService accountService, string emailAddress) 
    { 
     if (_userRoles.Length == 0 && AllowAnyRolesIfNoneSpecified) return true; 
     return accountService.IsUserInRole(emailAddress, _userRoles); 
    } 

    protected virtual bool IsUserPageOwner(
     AuthorizationContext filterContext, MainDbContext dbContext, AccountService accountService, string emailAddress) 
    { 
     var id = GetRouteId(filterContext); 
     return IsUserPageOwner(dbContext, accountService, emailAddress, id); 
    } 

    protected int GetRouteId(AuthorizationContext filterContext) 
    { 
     return Convert.ToInt32(filterContext.RouteData.Values["id"]); 
    } 

    private bool IsUserPageOwner(MainDbContext dbContext, AccountService accountService, string emailAddress, int id) 
    { 
     return accountService.IsUserPageOwner(emailAddress, id); 
    } 

    private void ShowLogOnPage(AuthorizationContext filterContext) 
    { 
     filterContext.Result = new HttpUnauthorizedResult(); 
    } 

    private void ShowAccessDeniedPage(AuthorizationContext filterContext) 
    { 
     filterContext.Result = new ViewResult { ViewName = "ErrorPages/" + AccessDeniedViewName }; 
    } 

    private void PreventPageFromBeingCached(AuthorizationContext filterContext) 
    { 
     var cachePolicy = filterContext.HttpContext.Response.Cache; 
     cachePolicy.SetProxyMaxAge(new TimeSpan(0)); 
     cachePolicy.AddValidationCallback(CacheValidateHandler, null); 
    } 
} 

quelques notes:

Pour éviter « des chaînes magiques ", J'ai utilisé un tableau de valeurs enum UserRole au lieu d'une seule chaîne. De plus, je l'ai construit pour gérer plusieurs scénarios, je suis tombé sur:

  • L'utilisateur doit être dans un rôle ou le propriétaire de la page/données (par exemple, un administrateur peut modifier les données de personne et tout le monde peut modifier leur propre données)
  • L'utilisateur doit être dans un rôle et le propriétaire de la page/données (par ex., un utilisateur ne peut éditer que sa propre page/données - habituellement utilisé sans aucune restriction de rôle)
  • L'utilisateur doit être dans un rôle et pas le propriétaire de la page/données (par exemple, un administrateur peut éditer quelqu'un page/données sauf la sienne - disons, pour empêcher un administrateur de supprimer son propre compte)
  • Aucun utilisateur n'est autorisé à voir cette page, AllowAnyRolesIfNoneSpecified = false (par exemple, vous avez une méthode de contrôleur pour une page qui n'existe pas mais vous devez inclure la méthode parce que votre contrôleur hérite d'une classe de base qui a cette méthode)

Voici un exemple de déclaration d'attribut:

[AccountAuthorize(UserRole.Admin, MustBeInRoleAndNotPageOwner = true)] 
public override ActionResult DeleteConfirmed(int id) 
{ 
    ... 
} 

(Cela signifie un administrateur peut supprimer un compte, mais lui-même.)

+2

+1. Juste une suggestion. Vous pouvez utiliser 'params UserRole [] roles' dans votre constructeur pour échapper à cette horrible syntaxe de tableau dans votre exemple d'utilisation. – Ryan

+0

@Ryan, excellente suggestion. J'étais frustré par la syntaxe laide lorsque j'ai écrit ce code pour la première fois, mais je ne savais pas à l'époque que vous pouviez mélanger les paramètres et les propriétés du constructeur dans les déclarations d'attributs. Il se tourne vous pouvez, et le résultat est beaucoup plus propre. Je viens de coller le nouveau code dans ma réponse. Merci! – devuxer

Questions connexes