41 votes

Le Contrôle d'accès dans ASP.NET MVC en fonction des paramètres d'entrée / de la couche de service?

Préambule: c'est un peu une question philosophique. Je suis à la recherche de plus pour la "bonne" façon de faire plutôt que de "une" façon de le faire.

Imaginons que j'ai quelques produits, et un ASP.NET application MVC effectuer CRUD sur ces produits:-

mysite.example/products/1
mysite.example/products/1/edit

Je suis en utilisant le modèle de référentiel, de sorte qu'il n'a pas d'importance où ces produits proviennent:-de

public interface IProductRepository
{
  IEnumberable<Product> GetProducts();
  ....
}

Aussi mon Référentiel décrit une liste d'Utilisateurs, et les produits dans lesquels ils sont des gestionnaires pour (beaucoup-beaucoup d'entre les Utilisateurs et les Produits). Ailleurs dans la demande, un Super-Admin consiste à effectuer des opérations CRUD sur les Utilisateurs et la gestion de la relation entre les Utilisateurs et les Produits qu'ils sont autorisés à gérer.

Personne n'est autorisé à afficher n'importe quel produit, mais seuls les utilisateurs qui sont désignés comme des "admins" pour un produit particulier sont autorisés à invoquer par exemple, l'action d'Édition.

Comment doit - je aller sur la mise en œuvre que dans ASP.NET MVC? À moins que j'ai raté quelque chose, je ne peux pas utiliser le haut-ASP.NET Autoriser en tant qu'attribut d'abord, j'avais besoin d'un rôle différent pour chaque produit, et la deuxième je ne sais pas quel rôle pour vérifier jusqu'à ce que j'ai récupéré mon Produit à partir du Référentiel.

Bien évidemment, on peut généraliser à partir de ce scénario à la plupart du contenu-scénarios de gestion - par exemple, les Utilisateurs sont autorisés à modifier leurs propres Messages du Forum. StackOverflow les utilisateurs ne sont autorisés à modifier leurs propres questions, à moins qu'ils ai 2000 ou plus rep...

La solution la plus simple, comme un exemple, serait quelque chose comme:-

public class ProductsController
{
  public ActionResult Edit(int id)
  {
    Product p = ProductRepository.GetProductById(id);
    User u = UserService.GetUser(); // Gets the currently logged in user
    if (ProductAdminService.UserIsAdminForProduct(u, p))
    {
      return View(p);
    }
    else
    {
      return RedirectToAction("AccessDenied");
    }
  }
}

Mes questions:

  • Une partie de ce code devra être répété - imagine qu'il y a plusieurs opérations (mise à Jour, Supprimer, SetStock, l'Ordre, la CreateOffer) en fonction de l'Utilisateur-Produits de la relation. Vous devez copier-coller plusieurs fois.
  • Ce n'est pas très testable - ce que vous avez à se moquer de mon compte quatre objets pour chaque test.
  • Il ne semble pas vraiment comme le contrôleur de "travail" pour vérifier si l'utilisateur est autorisé à effectuer l'action. Je préfère de loin un plus enfichable (par exemple AOP grâce à des attributs) de la solution. Cependant, serait-ce nécessairement que vous avez à SÉLECTIONNER le produit deux fois (une fois dans le AuthorizationFilter, et de nouveau dans le Contrôleur)?
  • Serait-il mieux de retourner une 403 si l'utilisateur n'est pas autorisé à faire cette demande? Si oui, comment pourrais-je aller sur le faire?

Je vais probablement garder cette mise à jour comme je l'ai trouver des idées moi-même, mais je suis très impatient d'entendre la vôtre!

Merci à l'avance!

Modifier

Juste pour ajouter un peu de détail ici. La question que je vais avoir, c'est que je veux la règle d'entreprise "Seuls les utilisateurs autorisés peuvent modifier les produits" à être contenues dans un seul et unique endroit. J'ai l'impression que le même code qui détermine si un utilisateur peut GET ou POST pour le Modifier l'action devrait également être chargé de déterminer si pour rendre le lien "Modifier" sur l'Index ou les Détails de ce point de vue. Peut-être que ce n'est pas possible/pas possible, mais j'ai l'impression que ça devrait être...

Edit 2

Le démarrage d'une prime sur celui-ci. J'ai reçu des bons et des réponses utiles, mais rien de ce que je me sens à l'aise "accepter". Gardez à l'esprit que je suis à la recherche d'un bien propre méthode pour garder la logique métier qui détermine si oui ou non le lien "Modifier" sur l'index de la vue sera affiché au même endroit, qui détermine si oui ou non une demande de Produits/Modifier/1 est autorisé ou non. J'aimerais garder la pollution dans ma méthode d'action pour une absoloute minimum. Idéalement, je suis à la recherche d'un attribut de base de la solution, mais j'accepte que, peut-être impossible.

29voto

Mark Seemann Points 102767

Tout d'abord, je pense que vous avez déjà à moitié compris, parce que vous avez dit que

que d'abord j'avais besoin d'un rôle différent pour chaque produit, et la deuxième je ne sais pas quel rôle pour vérifier jusqu'à ce que j'ai récupéré mon Produit à partir du Référentiel

J'ai vu de nombreuses tentatives visant à faire de la sécurité basée sur les rôles faire quelque chose, il n'a jamais été l'intention de faire, mais vous êtes déjà passé ce point, donc c'est cool :)

L'alternative à la sécurité basée sur les rôles est de l'ACL basée sur la sécurité, et je pense que c'est ce que vous avez besoin ici.

Vous aurez toujours besoin de récupérer la liste de contrôle d'accès d'un produit et de vérifier si l'utilisateur a le droit d'autorisation pour le produit. Il en est ainsi du contexte et de l'interaction lourd que je pense que purement déclarative approche est à la fois trop rigide et trop implicite (c'est à dire vous ne pouvez pas réaliser combien de lectures de base de données sont nécessaires à l'ajout d'un attribut à un peu de code).

Je pense que de tels scénarios sont mieux modélisées par une classe qui encapsule l'ACL logique, vous permettant de Requête de décision ou de faire une Affirmation basée sur le contexte actuel, à quelque chose comme ceci:

var p = this.ProductRepository.GetProductById(id);
var user = this.GetUser();
var permission = new ProductEditPermission(p);

Si vous voulez juste pour savoir si l'utilisateur peut modifier le produit, vous pouvez émettre une Requête:

bool canEdit = permission.IsGrantedTo(user);

Si vous voulez juste pour s'assurer que l'utilisateur a le droit de continuer, vous pouvez émettre une Affirmation:

permission.Demand(user);

Ceci devrait alors lancer une exception si l'autorisation n'est pas accordée.

Tout cela suppose que le Produit de la classe (la variable p) est associé à un ACL, comme ceci:

public class Product
{
    public IEnumerable<ProductAccessRule> AccessRules { get; }

    // other members...
}

Vous voudrez peut-être jeter un oeil à Système.De sécurité.AccessControl.FileSystemSecurity pour trouver de l'inspiration sur la modélisation des Acl.

Si l'utilisateur actuel est le même que le Fil de discussion.CurrentPrincipal (ce qui est le cas dans ASP.NET MVC, IIRC), vous pouvez simplyfy l'autorisation ci-dessus méthodes:

bool canEdit = permission.IsGranted();

ou

permission.Demand();

parce que l'utilisateur serait implicite. Vous pouvez prendre un coup d'oeil au Système.De sécurité.Les autorisations.PrincipalPermission pour l'inspiration.

16voto

David Glenn Points 12819

À partir de ce que vous décrivez, il semble que vous besoin d'une certaine forme de contrôle d'accès d'utilisateur plutôt que de rôle en fonction des autorisations. Si c'est le cas, alors il doit être mis en œuvre tout au long de votre logique métier. Votre scénario sonne comme vous pouvez le mettre en œuvre dans votre couche de service.

Fondamentalement, vous avez à mettre en œuvre toutes les fonctions de votre ProductRepository du point de vue de l'utilisateur en cours et les produits sont marqués avec les autorisations de l'utilisateur.

Il semble plus difficile qu'il ne l'est en réalité. Tout d'abord vous avez besoin d'un jeton de l'utilisateur de l'interface qui contient les informations de l'utilisateur de l'uid et le rôle de la liste (si vous souhaitez utiliser des rôles). Vous pouvez utiliser IPrincipal ou créer votre propre en suivant les lignes de

public interface IUserToken {
  public int Uid { get; }
  public bool IsInRole(string role);
}

Ensuite, dans votre controller, vous analysez le jeton de l'utilisateur dans votre Référentiel constructeur.

IProductRepository ProductRepository = new ProductRepository(User);  //using IPrincipal

Si vous utilisez FormsAuthentication et personnalisé IUserToken ensuite, vous pouvez créer un Wrapper autour de la IPrincipal de sorte que votre ProductRepository est créé comme:

IProductRepository ProductRepository = new ProductRepository(new IUserTokenWrapper(User));

Maintenant, tous vos IProductRepository les fonctions d'accès au jeton d'utilisateur pour vérifier les autorisations. Par exemple:

public Product GetProductById(productId) {
  Product product = InternalGetProductById(UserToken.uid, productId);
  if (product == null) {
    throw new NotAuthorizedException();
  }
  product.CanEdit = (
    UserToken.IsInRole("admin") || //user is administrator
    UserToken.Uid == product.CreatedByID || //user is creator
    HasUserPermissionToEdit(UserToken.Uid, productId)  //other custom permissions
    );
}

Si vous vous demandez-vous à propos de l'obtention d'une liste de tous les produits, de votre code d'accès aux données vous pouvez faire une requête fondée sur une autorisation. Dans votre cas, une jointure gauche pour voir si le plusieurs-à-plusieurs table contient les UserToken.Uid et le productId. Si le côté droit de la jointure est présent, vous savez que l'utilisateur a l'autorisation de ce produit et puis vous pouvez définir votre Produit.CanEdit booléenne.

En utilisant cette méthode, vous pouvez ensuite utiliser la suite, si vous le souhaitez, de votre point de Vue (où le Modèle de votre Produit).

<% if(Model.CanEdit) { %>
  <a href="http://stackoverflow.com/Products/1/Edit">Edit</a>
<% } %>

ou dans votre contrôleur

public ActionResult Get(int id) {
  Product p = ProductRepository.GetProductById(id);
  if (p.CanEdit) {
    return View("EditProduct");
  }
  else {
    return View("Product");
  }
}

L'avantage de cette méthode est que la sécurité est intégrée à votre couche de service (ProductRepository) de sorte qu'il n'est pas manipulé par vos contrôleurs et ne peut pas être contournée par vos contrôleurs.

Le point principal est que la sécurité est placé dans une logique d'entreprise et non pas dans votre contrôleur.

3voto

Runeborg Points 711

Le copier coller des solutions vraiment devenir fastidieux après un certain temps, et est-ce vraiment gênant pour maintenir. Je serais probablement aller avec un attribut personnalisé à faire ce que vous avez besoin. Vous pouvez utiliser l'excellent .NET Réflecteur de voir comment les AuthorizeAttribute est mis en œuvre et effectuer votre propre logique.

Ce qu'il n'hérite FilterAttribute et la mise en œuvre de IAuthorizationFilter. Je ne peux pas tester cela pour le moment, mais quelque chose comme cela devrait fonctionner.

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public class ProductAuthorizeAttribute : FilterAttribute, IAuthorizationFilter
{
	public void OnAuthorization(AuthorizationContext filterContext)
    {
		if (filterContext == null)
		{
			throw new ArgumentNullException("filterContext");
		}

		object productId;
		if (!filterContext.RouteData.Values.TryGetValue("productId", out productId))
		{
			filterContext.Result = new HttpUnauthorizedResult();
			return;
		}

		// Fetch product and check for accessrights

		if (user.IsAuthorizedFor(productId))
		{
			HttpCachePolicyBase cache = filterContext.HttpContext.Response.Cache;
			cache.SetProxyMaxAge(new TimeSpan(0L));
			cache.AddValidationCallback(new HttpCacheValidateHandler(this.Validate), null);
		}
		else
			filterContext.Result = new HttpUnauthorizedResult();
	}

	private void Validate(HttpContext context, object data, ref HttpValidationStatus validationStatus)
	{
		// The original attribute performs some validation in here as well, not sure it is needed though
		validationStatus = HttpValidationStatus.Valid;
	}
}

Vous pourriez probablement aussi stocker le produit/de l'utilisateur que vous chercher dans l'filterContext.Le contrôleur.TempData de sorte que vous pouvez le récupérer dans le contrôleur, ou les stocker dans certains cache.

Edit: je viens de remarquer que la partie sur le lien modifier. La meilleure façon que je peux penser à est en donnant l'autorisation de la partie à partir de l'attribut et de faire un HttpHelper pour cela que vous pouvez utiliser dans votre point de vue.

1voto

jimr Points 171

J'ai tendance à penser que cette autorisation est une partie de votre logique métier (ou au moins à l'extérieur de votre contrôleur de logique de toute façon). Je suis d'accord avec kevingessner ci-dessus, en ce que la demande d'autorisation doit être le cadre de l'appel à l'extraction de l'élément. Dans son OnException méthode, vous pouvez afficher la page de connexion (ou ce que vous avez configurés dans le web.config) par quelque chose comme ceci:

if (...)
{
	Response.StatusCode = 401;
	Response.StatusDescription = "Unauthorized";
	HttpContext.Response.End();
}

Et au lieu de faire UserRepository.GetUserSomehowFromTheRequest() appelle à toutes les méthodes d'action, je le ferai une fois (en remplacement du Contrôleur.OnAuthorization méthode par exemple), puis coller les données quelque part dans votre contrôleur de classe de base pour une utilisation ultérieure (par exemple, une propriété).

1voto

tvanfosson Points 268301

Je pense que c'est irréaliste, et une violation de la séparation des préoccupations, de s'attendre à avoir contrôleur/modèle de contrôle de code que le point de vue qui rend. Le contrôleur/code modèle peut définir un indicateur, dans le modèle de vue, que la vue peut utiliser pour déterminer ce qu'il doit faire, mais je ne pense pas que vous devriez vous attendre une seule méthode pour être utilisé par le contrôleur, de modèle et de vue de contrôler à la fois l'accès et le rendu du modèle.

Ayant dit que tu pouvais à l'approche de ce de deux manières, à la fois impliquerait un modèle d'affichage qui contient des annotations utilisées par la vue en plus du modèle actuel. Dans le premier cas, vous pouvez utiliser un attribut de contrôle d'accès à l'action. Ce serait ma préférence, mais impliquerait la décoration de chaque méthode de manière indépendante, à moins que toutes les actions d'un contrôleur ont les mêmes attributs.

J'ai développé un "rôle ou propriétaire" de l'attribut pour cet effet. Il vérifie que l'utilisateur est dans un rôle particulier ou est le propriétaire des données produites par la méthode. La propriété, dans mon cas, est contrôlé par la présence d'une clé étrangère de la relation entre l'utilisateur et les données en question-qui est, vous avez un ProductOwner table et il doit y avoir une ligne contenant le produit, le propriétaire de la paire pour le produit et l'utilisateur actuel. Elle diffère de la normale AuthorizeAttribute en ce que lorsque le titre de propriété ou le rôle de la vérification échoue, l'utilisateur est dirigé vers une page d'erreur, pas la page de connexion. Dans ce cas, chaque méthode serait nécessaire de définir un indicateur dans le modèle de vue qui indique que le modèle peut être modifié.

Sinon, vous pouvez mettre en œuvre un code similaire dans la ActionExecuting/ActionExecuted méthodes du contrôleur (ou un contrôleur de base de sorte qu'il s'applique de façon uniforme dans tous les contrôleurs). Dans ce cas, vous avez besoin d'écrire du code pour détecter ce type d'action est en cours d'exécution afin de savoir si pour annuler l'action fondée sur la propriété du produit en question. La même méthode pourrait définir le drapeau pour indiquer que le modèle peut être modifié. Dans ce cas, vous auriez probablement besoin d'une hiérarchie de modèle de sorte que vous pourriez lancer le modèle comme un modèle modifiable de sorte que vous pouvez définir la propriété, indépendamment du type de modèle.

Cette option semble la plus couplé à moi que l'aide de l'attribut et sans doute plus compliqué. Dans le cas de l'attribut que vous pouvez le concevoir de telle sorte qu'il faut les divers table et les noms de propriété en tant qu'attributs de l'attribut et utilise la réflexion pour obtenir les données à partir de votre référentiel basé sur les propriétés de l'attribut.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X