Système de MP, 2 questions

Le problème exposé dans ce sujet a été résolu.

Bonjour,

Je code un système de MP dans le cadre d'un site utilisant Symfony 3.

J'aimerais savoir comment faire pour refuser proprement la'ccès à la page de discussion si l'utilisateur n'est pas participant dans celle-ci, i.e. on n'a pa le droit de lire les MP des autres.

Voici le début de mon contrôleur actuel :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php
class DiscussionController extends BaseController {
    /**
     * @Route("/{_locale}/discussions/{id}/{page}", name="discussionShow", defaults={"page"=1}, requirements={"_locale"="%locales%", "id"="\d+", "page"="\d+"})
     */
    public function showAction(Request $request, Discussion $discu, $page=1)     {
$this->denyAccessUnlessGranted('ROLE_USER');
$user = $this->getUser();
if (!$discu->isParticipant($user)) $this->denyAccessUnlessGranted('ROLE_ALWAYS_FAIL');
...
}
...
}

Comme le nom l'indique, le rôle ROLE_ALWAYS_FAIL n'existe pas et est juste là parce que je dois bien mettre quelque chose. Dans d'autres classes j'utilise parfois ROLE_NON_EXIST. Ca marche, je me prends bien une erreur 403, mais évidemment, c'est pas vraiment « pro » comme code. C'est étrange que la méthode deny n'existe pas…

J'aurais pu aussi balancer une exception bidon, ou retourner exprès autre chose qu'une Response, mais ça ne fait pas beaucoup plus « pro ».

J'ai trouvé les AccessDecisionManager et les Voter en fouillant dans la doc, mais ça me paraît excessivement lourd et compliqué à mettre en place pour une vérification aussi simple. Est-ce que c'est la seule façon correcte de faire ?

J'ai une autre question Symfony: où est-on supposé placer les classes qui sont là juste pour gérer des données de formulaire ? Les considère-t-on comme des entités même si elles ne sont jamais stockées en base telles quelles ?

Par exemple, toujours pour mon système de MP, j'ai une classe DiscussionNew qui sert juste à être remplie par un formulaire « nouvelle discussion ». Une fois le formulaire de nouveau topic rempli et validé, je crée des entités Discussion et Post à partir de là. C'est ces entités Discussion et Post qui sont stockées en base.

Est-ce que je dois tout de même considérer que c'est une entité, malgré qu'elle n'est jamais persistée telle quelle ?

Est-ce que je m'y prends mal, est-ce que ce n'est pas du tout la bonne façon de faire ?

Voici cette classe :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class DiscussionNew {
/**
*@Assert\NotBlank
*/
protected $text;

    /**
*@Assert\NotBlank
     */
protected  $title;

/**
*@Assert\NotNull
*/
protected $dest;

public function setRecipient (User $u) { $this->dest=$u; }
public function getRecipient () { return $this->dest; }
    public function setText ($text)     {         $this->text = $text;             return $this;     }
    public function getText ()     {         return $this->text;     }
   public function setTitle ($title)     {         $this->title = $title;             return $this;     }
    public function getTitle ()     {         return $this->title;     }
}

ET extrait du contrôleur qui récupère le formulaire :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php
$d = new \AppBundle\Entity\DiscussionNew();
$d->setRecipient($recipient);
$form = $this->createForm(\AppBundle\Form\DiscussionNewForm::class, $d);
$form->handleRequest($request);
if ($form->isValid()) {
...
$m = $this->getDoctrine()->getManager();
$m->persist($discussion);
$m->persist($post);
$m->flush();
return $this->redirectToRoute('discussionShow', array('id'=>$discussion->getId(), '_locale'=>$request->getLocale()));
}

Merci pour vos réponses !

Edit: ah ben désolé, on dirait que la coloration syntaxique pour PHP de fonctionne pas…

+0 -0

J'ai trouvé les AccessDecisionManager et les Voter en fouillant dans la doc, mais ça me paraît excessivement lourd et compliqué à mettre en place pour une vérification aussi simple. Est-ce que c'est la seule façon correcte de faire ?

Tu fais toi-même la vérification avec !$discu->isParticipant($user). Si ton problème est juste de savoir comment retourner une 403 tu peux simplement lever une AccessDeniedHttpException.

Est-ce que je dois tout de même considérer que c'est une entité, malgré qu'elle n'est jamais persistée telle quelle ?

Si elle n'est jamais persistée ce n'est pas une entité.

Est-ce que je m'y prends mal, est-ce que ce n'est pas du tout la bonne façon de faire ?

Le composant formulaire se fout totalement de savoir ce que fait ta classe, donc c'est totalement légitime de construire une classe propre au formulaire, ça a même un nom. Tu peux voir cette utilisation précise expliquée ici : http://verraes.net/2013/04/decoupling-symfony2-forms-from-entities/

Pour ce qui est de la coloration syntaxique essaie de préfixer ton code par la balise php ouvrante.

+1 -0

Hello,

Alors déjà ça serait taupe cool si tu pouvais colorer ton code et l'indenter ! https://zestedesavoir.com/tutoriels/249/rediger-sur-zds/

Ensuite la bonne pratique est loin de ce que propose MatTheCat et plus proche de ce dont tu parles Quentin.

En fait tu peux utiliser la fonction $this->denyAccessUnlessGranted('READ', $message). Et pour "voter", il va falloir que tu utilises le système des voteurs. Et là où c'est cool c'est que le voteur tu l'enregistres en service, et du coup tu peux lui passer les services que tu veux pour "voter". Même si dans ton cas il est possible que l'objet seul suffise.

Après tu peux aller plus loin en modifiant dynamiquement les roles de l'utilisateur en fonction d'un objet de la route… Mais attention cela est plus compliqué et potentiellement dangereux pour les performances de ton application.

Pour ta seconde question, les formulaires sont gérés par des FormType (où Form est le nom de ta classe de formulaire et Type est une convention de nommage, ces classes héritent de AbstractType… Mais tout cela est détaillé dans la doc !). Usuellement on les places dans le dossier AppBundle/Form/Type. Bien sûr toutes les fantaisies sont possibles. Après quand tu as des formulaires complexes c'est au cas par cas ! (mais faire un formulaire qui n'est pas gérable avec des classes de formulaire....

[edit] J'avais pas tout lu :-) .

Pour moi tu t'y prends mal, tu peux faire un type pour chaque chose. Et ton Type de message va être inclu dans ton Type de discussion. Quelque chose comme ça en soit:

1
2
3
4
5
6
<?php

public function buildForm(FormBuilder $builder, array $options)
{
    $builder->add('title', 'text')->add('message', new MessageType);
}
+0 -1

Pour ce qui est de la coloration syntaxique essaie de préfixer ton code par la balise php ouvrante.

C'est corrigé. Effectivement, c'est mieux.

En fait tu peux utiliser la fonction $this->denyAccessUnlessGranted('READ', $message). Et pour "voter", il va falloir que tu utilises le système des voteurs. Et là où c'est cool c'est que le voteur tu l'enregistres en service, et du coup tu peux lui passer les services que tu veux pour "voter". Même si dans ton cas il est possible que l'objet seul suffise.

Oui et donc, c'est l'unique seule vraie bonne façon correcte de faire ? Je dois enregistrer un service de voteur juste pour cette bête vérification de propriété ? N'est-ce pas un peu overkill, n'y a-t-il pas plus simple ?

ET si j'ai 10 façons de vérifier les accès pour 10 parties / types de ressources différentes de mon site, je dois donc enregistrer 10 services de voteurs différents ? Comment il fait après pour savoir lequel est le bon dans chaque cas ? Il instancie tout à chaque fois ? C'est pas lourd à force ?

Je comprends pas l'idée. LE but c'est de faire un méga « centre de décision » ? C'est quoi l'avantage par rapport à vérifier les choses directement dans les contrôleurs où on en a besoin ?

Pour la deuxième question je dois m'être mal exprimé. Evidemment, j'ai bien une classe par formulaire. LE faire directement dans le contrôleur ça devient vite ingérable, c'est pas réutilisable, et pour le validateur c'est casse-pieds quand on le fait si facilement avec les annotations. D'ailleurs ma classe de formulaire ressemble un peu à celle de Nek, hormis que je n'ai pas de MessageType, ça ne me sert à rien vu qu'on saisit du markdown dans une simple zone de texte (J'utilise un TextareaType et ça me suffit)

LE fond de la question, en fait, c'est de savoir où est-ce que je devrais ranger la classe qui est utilisée comme fournisseur de données du formulaire. Pour moi vu que ce n'est pas ces objets-là qui sont sauvegardés dans la base, ce n'est pas des entités, donc ça ne doit pas dans AppBundle\Entity. Ca fait fouillis de les mélanger avec les véritables entités je trouve. ET ça ne me plaît pas non plus de les mettre dans AppBundle\Form, vu que ce n'est pas des formulaires C'est seulement les donnés du formulaire (la data_class) par opposition au formulaire lui-même (le Machintype).

Merci.

+0 -1

Il ne s'agit pas d'être overkill. Tu peux bien entendu faire cela dans ton contrôleur.

Il s'agit d'organisation dans ton code ;) . En outre si pour le moment le droit de vision te semble simple il pourrait être plus complexe: si tu veux qu'un admin puisse y avoir accès par exemple.

L'avantage principal de faire cela dans un « centre de décision » c'est que tu peux tester n'importe quel objet facilement, et surtout tu ne répètes pas le test pour chaque action (/contrôleur) !

Pour ton type je pense que tu peux quand même le faire avec tes deux entités en utilisant ton TextareaType. Il faut que tu utilises les property path !

Usuellement on range dans AppBundle\Form\Type car tu pourrais avoir plusieurs classes en rapport avec tes forms. Comme par exemple des FormExtension.

C'est sympa de me dénigrer mais il faudrait lire entièrement les posts avant.

L'OP utilise denyAccessUnlessGranted avec un rôle inexistant dans le seul but de retourner une 403. Pour retourner une 403 il suffit de lever une AccessDeniedHttpException. Après s'il veut être overkill comme tu dis c'est à lui d'en décider.

Et il ne parle pas des classes de type de formulaire mais de la data_class. Comme je l'ai déjà indiqué une telle classe s'appelle une commande, donc si seuls les formulaires les utilisent on peut les placer dans un dossier AppBundle\Form\Command.

Hello,

Oui juste jeter une AccessDeniedHttpException suffit amplement à ce qu'il veut faire (même si bon, ce serait plutôt une 401 dans ce cas mébonbref).

Pour les forms, oui avoir un type apr formulaire est souvent adapté. Voir ce qu'a dit Nek a ce sujet. Et pour le data_class, les opinions varient énormément en fait… Soit on fait des commandes comme l'a indiqué MatTheCat, soit on manipule directement les "entités" qui sont en fait des values objects en soit (c'est ce que je préfère personnellement, ça m'évite de devoir manipuler mon entité par la suite si jamais j'ai besoin de sauvegarder ces données dans ma base)

BTW

1
2
3
4
5
6
<?php

public function buildForm(FormBuilder $builder, array $options)
{
    $builder->add('title', 'text')->add('message', new MessageType);
}

Nek

Ne marchera pas sous Sf3, faut just donner le fqcn du type maintenant plutôt qu'une instance de type (MessageType::class). :}

+0 -0

A noter que la classe Controller du FrameworkBundle met à disposition une méthode createAccessDeniedException qui retourne une instance de Symfony\Component\Security\Core\Exception\AccessDeniedException qu'il ne reste qu'à lever. Et c'est bien une 403 qu'il faut retourner ; si la requête a passé les firewalls de Symfony c'est que l'utilisateur est authentifié.

Une 403 est une opération normalement "interdite", pas "pas assez d'authorisation". E.g : un sens interdit, t'as beau être le président de l'univers, sera toujours un sens interdit. Une 401 est un manque d'authentification (tu n'es pas assez authentifié pour accéder à la ressource)

+0 -1

L'authentification est binaire, tu es soit authentifié, soit pas authentifié. Si tu accèdes à une page qui nécessite une authentification sans ou avec des identifiants faux alors tu prends une 401, cf. https://tools.ietf.org/html/rfc7235#section-3.1

Si tu n'as pas pris une 401 mais que le serveur ne veut pas que tu accèdes à la ressource pour une raison ou une autre (en l'occurrence parce qu'on a pas accès à une discussion sans être participant) alors tu prends une 403, cf. https://tools.ietf.org/html/rfc7231#section-6.5.3

Si on suit ton exemple ça ne rimerait à rien de retourner une 403 puisque la ressource ne serait jamais accessible. En réalité tu peux passer si tu es "authentifié" en tant que véhicule d’intérêt général prioritaire. Si tu es un usager classique tu prends une 403. Si tu es un piéton tu n'as pas pu accéder à la route suite à une 401.

+0 -0

Je cite les deux liens que tu as mis en évidence :

401 :

The 401 (Unauthorized) status code indicates that the request has not been applied because it lacks valid authentication credentials for the target resource

Qui se traduit par le fait que tu n'as pas assez d'autorisation (to lack : manquer) pour accéder à la ressource (c'est ce que je dis). Quand tu dis que c'est binaire, c'est en effet "j'ai l'autorsation" (aka j'ai les pouvoirs pour), "je n'ai pas l'autorisation". Ça ne précise absolument pas qu'on ait ou pas déjà un degré d'autorisation (techniquement, quand tu tapes sur une ressources, même de manière anonyme, tu as déjà un degré d'autorisation, un degré "visiteur", "anonyme", … etc).

Alors que la 403 :

If authentication credentials were provided in the request, the server considers them insufficient to grant access. The client SHOULD NOT automatically repeat the request with the same credentials. The client MAY repeat the request with new or different credentials. However, a request might be forbidden for reasons unrelated to the credentials

Donc la 403 (qui est en fait assez floue et libre à diverses interprétations) peut inclure des cas de la 401 (changer d'auth peut aboutir à un autre status code, genre une 200). Mais je préfère m'en tenir à la définition stricte rapportée dans la dernière phrase : "However, a request might be forbidden for reasons unrelated to the credentials", c'est à dire qu'une 403 n'est pas forcément liée à l'auth.

Et plus encore :

An origin server that wishes to "hide" the current existence of a forbidden target resource MAY instead respond with a status code of 404 (Not Found)

Renvoyer une 404 est parfois adaptée si on veut cacher la ressource. Soit ici c'est aussi adapté :}

+0 -2

Tu confonds autorisation et authentification qui sont indépendantes l'une de l'autre.

Je répète une dernière fois :

  • 401 quand on accède sans authentification à une ressource en nécessitant une.
  • 403 quand on accède à une ressource sans les autorisations nécessaires.

Ici l'utilisateur est authentifié mais n'a pas l'autorisation d'afficher la conversation, donc 403.

+1 -2

Encore une fois, pas forcément (c'est une interprétation courante, et d'ailleurs le débat que nous avons ici est un débat assez courant sur la toile…).

Puisque te montrer point par point sur la RFC ce en quoi je peux avoir raison (c'est ça le pb des rfc, c'est que c'est libre à interprétations…), je me base sur ce schéma : http://i.stack.imgur.com/whhD1.png

On peut voir ici la 401 "non autorisé" (sans mention quelconques du degré d'autorisation actuel), et la 403 en "interdit". C'est à dire, qui que tu sois, tu n'auras pas accès à la ressource. C'est tout… Ce qui peut porter à confusion, c'est que le fait que ce soit "interdit" peut en effet sous entendre "on a pas assez d'auth", mais IMO ce n'est pas le cas.

+1 -2

C'est un débat uniquement parce que des gens ne comprennent pas les RFC et se basent uniquement sur le nom des statuts pour en déduire leur sens.

Toi qui traduit valid authentication credentials par "autorisation" et qui parle de "pas assez d'auth", c'est que tu n'as pas compris les RFC. Quand tu les comprendras tu verras qu'il n'y a aucune place pour l'interprétation.

En me basant sur plusieurs textes (dont celui-ci : http://stackoverflow.com/a/14713094/624544), ces deux termes sont, dans ce contexte très précis, les mêmes. Comme je l'ai dejà dit à plusieurs reprises, jeter une 403 est valide, mais une 401 est selon moi plus adaptée (on peut réessayer de voir la discussion en s'authentifiant avec un autre compte, celui qui est accrédité). Alors qu'une 403, le serveur n'écoute même pas et ne levera pas le petit doigt pour voir s'il peut accéder à ta requête.

Mais comme je l'ai dit, cette RFC (même expliquée mot à mot ici, je peux rien faire de plus, vraiment) est sujette diverses interprétations, bien qu'elle ait tenté de divesifier le sens depuis la rfc 2616 en ce que j'ai dit (rendre le 403 plus souple).

Bref, perso je trouve ce débat stérile, je continuerai de balancer mes 403 quand je jugerai approprié qu'une action est juste "interdite" (quelque soit le level) et une 401 quand le gus n'a juste pas les droits nécessaires (n'est donc "pas autorisé").

C'était parti d'une boutade à la base, et laissons-là ce qu'elle est censée être, c'est à dire une boutade.

+0 -2

En lisant ton "texte" il n'y a absolument rien qui indique ou même qui laisse penser que ces deux termes sont identiques, ce qui serait stupide puisqu'ils sont différents. Tu peux t'évertuer à penser qu'il y a débat tant que tu ne voudras pas comprendre la différence. Et tu peux effectivement faire n'importe quoi de ton côté, mais n'incite pas les autres à faire de même.

Et tu aurais pu essayer de lire les commentaires de la dite réponse… http://stackoverflow.com/questions/3297048/403-forbidden-vs-401-unauthorized-http-responses/14713094#comment18542775_6937030, http://stackoverflow.com/questions/3297048/403-forbidden-vs-401-unauthorized-http-responses/14713094#comment44205731_6937030, http://stackoverflow.com/questions/3297048/403-forbidden-vs-401-unauthorized-http-responses/14713094#comment23617824_6937030

Et d'autres encore…

+0 -2
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte