Des limitations pour vos politiques de sécurité

La gestion des droits utilisateur dans eZ Publish est, vous le savez sans doute, assez précise et autorise un contrôle d'accès avec une granularité très fine. En effet, la plupart des modules du kernel permettent d'en limiter l'accès au moyen de politiques de sécurité que l'on peut attribuer à un utilisateur ou à un groupe d'utilisateurs. C'est particulièrement le cas pour le module content, central au CMS, grâce à ses limitations de fonctions configurables dans le backoffice. Ces limitations nous offrent la possibilité de définir précisément le champ d'action d'un contributeur au niveau du contenu géré par eZ Publish.

Définir ses fonctions de module pour en limiter l'accès

Il est bien sûr possible de définir des règles de sécurité configurables de la même manière pour nos propres modules. Il suffit pour cela de créer une liste de fonctions pour notre module, dans le fichier module.php :

extension/myextension/modules/mymodule/module.php

<?php 
$Module = array('name' => 'mymodule');
 
$ViewList = array();
$ViewList['myview'] = array(
 'script'     => 'view.php',
 'params'     => array(),
 'functions'     => array( 'myfunction' )
);
 
$FunctionList['myfunction'] = array();

Ici nous définissons une fonction pour le module mymodule et nous l'affectons à la vue myview. Cette fonction apparaîtra dans la liste correspondante lors de la définition d'une nouvelle politique de sécurité pour le module mymodule.

Vous retrouverez ce genre de définition dans la plupart des modules du kernel, voire d'extensions développées par eZ Systems ou plus largement par la communauté (c'est le cas par exemple de NovenINIUpdate ).

Ce mode de fonctionnement suffit dans la plupart des cas, mais comment faire si l'on souhaite appliquer des limitations plus poussées comme pour le module content (langue, section, etc...) ?

Définir ses propres limitations

Comme vous l'avez sûrement remarqué, dans l'exemple de module.php ci-dessus, la variable $FunctionList['myfunction'] est un tableau vide, ce qui signifie que la fonction ne prévoit aucune limitation. Pour en ajouter, il suffit de le remplir avec les bonnes valeurs.

Exemple pour une limitation au niveau de la langue :

<?php 
$Module = array('name' => 'mymodule');
 
$ViewList = array();
$ViewList['myview'] = array(
 'script'     => 'view.php',
 'params'     => array(),
 'functions'     => array( 'myfunction' )
);
 
$Language = array(
    'name'=> 'Language',
    'values'=> array(),
    'path' => 'classes/',
    'file' => 'ezcontentlanguage.php',
    'class' => 'eZContentLanguage',
    'function' => 'fetchLimitationList',
    'parameter' => array( false )
);
 
$FunctionList['myfunction'] = array('Language' => $Language);

Nous disons ici à eZ Publish que myfunction possède une limitation du nom de Language et dont il faut aller chercher la liste des valeurs possibles via la méthodeeZContentLanguage::fetchLimitationList(), avec le paramètre false. Il s'agit simplement d'une bien vieille manière d'appeler une fonction de callback datant probablement des premières releases d'eZ Publish 3 (souvenez-vous des premières versions de PHP4 et de toutes ses limitations). Pour les curieux, tout se joue dans la méthode eZPolicyLimitation::allValuesAsArrayWithNames(), à partir de la ligne 249 (dans un eZ Publish 4.3.0).

Nous appelons ici une classe du kernel pour remplir notre tableau de limitation, mais il est bien sûr possible d'utiliser une classe se trouvant dans une extension. Cependant, le système d'autoload de classes n'est pas utilisé ici et un bon vieil include_once est effectué en backend. Aussi, afin d'utiliser une classe se situant dans une extension, il est nécessaire de rajouter une autre clé à notre tableau $Language :

$Language = array(
    'name' => 'Language',
    'values' => array(),
    'extension' => 'myextension',
    'path' => 'classes/',
    'file' => 'myclass.php',
    'class' => 'MyClass',
    'function' => 'fetchLanguageLimitationList',
    'parameter' => array()
);

De cette façon, la classe extension/myextension/classes/myclass.php sera inclue.

Mais que doit retourner ma méthode MyClass:fetchLanguageLimitationList() ? Un petit coup d'oeil à eZContentLanguage::fetchLimitationList() nous montre qu'il doit s'agir d'un tableau simple dont chaque entrée est elle-même un tableau associatif contenant les clés id et name.

  • id sera la valeur enregistrée dans la base de données pour la limitation sur la politique de sécurité
  • name sera simplement le libellé affiché pour la sélection dans le Backoffice.

Voici donc à quoi devra ressembler notre classe MyClass :

class MyClass
{
    /**
     * Fetches the array with names and IDs of the languages used on the site. This method is used by the permission system.
     *
     * @return Array with names and IDs of the languages used on the site.
     * @static
     */
    public static function fetchLanguageLimitationList()
    {
     $langList = eZINI::instance( 'site.ini' )->variable( 'RegionalSettings', 'SiteLanguageList' );
     $aResult = array();
     foreach($langList as $lang)
     {
      if($lang)
      {
       $aResult[] = array(
        'id' => $lang,
        'name' => $lang
       );
      }
     }
 
     return $aResult;
    }
}

Contrôler les accès dans notre module

Maintenant que les limitations sont définies, il reste cependant nécessaire de filtrer les accès à notre module en fonction des droits assignés à tel ou tel utilisateur. En effet, ce contrôle est effectué dans index.php pour le module content, et uniquement pour celui-ci. Nous sommes donc obligés de faire le gendarme nous-mêmes (ce point mérite d'ailleurs d'être optimisé pour les versions futures d'eZ Publish. Pour Fuji peut-être ?).

Tirer partie d'eZJSCore

Le contrôle manuel des limitations en utilisant le framework peut s'avérer un véritable chemin de croix tellement le système est complexe. J'ajouterais par ailleurs qu'il ne s'agit pas de la partie la plus propre du CMS et qu'un peu de nettoyage ne lui ferait pas de mal ! Heureusement, les développeurs Łukasz Serwatka et Andrè Rømcke ont implémenté dans l'extension eZJSCore une méthode de contrôle d'accès simplifiée sous la forme d'un opérateur de template. Cette extension faisant désormais partie de la distribution de base d'eZ Publish, il serait dommage de s'en priver !

Voici donc la meilleure manière de procéder :

extension/myextension/modules/mymodule/myview.php

$userHasAccess = ezjscAccessTemplateFunctions::hasAccessToLimitation( 'mymodule', 'myfunction' ); // Returns a boolean for current user

Récupérer la liste des limitations pour l'utilisateur courant

Malheureusement, eZJSCore ne possède pas de méthode permettant de récupérer les limitations disponibles pour l'utilisateur courant, ce qui peut être pourtant utile pour afficher par exemple une liste déroulante contenant les limitations accordées à l'utilisateur (dans notre cas, les langues disponibles).

Pour cela, il vous faudra écrire une méthode retournant ces limitations de manière simplifiée. En effet, la classe eZUser dispose de la méthode hasAccessTo() mais ce qu'elle retourne est absolument imbitable et demande à être fortement simplifié. Nous allons donc écrire une méthode complémentaire nous retournant des limitations simplifiées.

class MyClass
{
 /**
  * Shorthand method to check user access policy limitations for a given module/policy function.
  * Returns the same array as eZUser::hasAccessTo(), with "simplifiedLimitations".
  * 'simplifiedLimitations' array holds all the limitations names as defined in module.php.
  * If your limitation name is not defined as a key, then your user has full access to this limitation
  * @param string $module Name of the module
  * @param string $function Name of the policy function ($FunctionList element in module.php)
  * @return array
  */
 public static function getSimplifiedUserAccess( $module, $function )
 {
  $user = eZUser::currentUser();
  $userAccess = $user->hasAccessTo( $module, $function );
 
  $userAccess['simplifiedLimitations'] = array();
  if( $userAccess['accessWord'] == 'limited' )
  {
   foreach( $userAccess['policies'] as $policy )
   {
    foreach( $policy as $limitationName => $limitationList )
    {
     foreach( $limitationList as $limitationValue )
     {
      $userAccess['simplifiedLimitations'][$limitationName][] = $limitationValue;
     }
 
     $userAccess['simplifiedLimitations'][$limitationName] = array_unique($userAccess['simplifiedLimitations'][$limitationName]);
    }
   }
  }
  return $userAccess;
 }
}

Cette méthode nous retourne un tableau contenant le résultat de eZUser::hasAccessTo(), complété d'une clé simplifiedLimitations. Cette dernière est également un tableau contenant quant à lui les précieuses limitations.

Dans notre exemple, pour un utilisateur auquel nous aurions affecté une limitation Language autorisant uniquement fre-FR et eng-GB, ce tableau contiendrait :

$limitations = MyClass::getSimplifiedUserAccess( 'mymodule', 'myfunction' );
print_r( $limitations['simplifiedLimitations'] );
 
// Result
Array
(
    [Language] => Array
        (
            [0] => fre-FR
            [1] => eng-GB
        )
 
)

Dans notre module où nous devons afficher une liste déroulante des langues autorisées, nous aurions donc :

extension/myextension/modules/mymodule/myview.php

$tpl = eZTemplate::factory(); // Template init – from 4.3.0
 
$authorizedLang = eZINI::instance('site.ini')->variable( 'RegionalSettings', 'SiteLanguageList' ); // Default is all languages
$limitations = MyClass::getSimplifiedUserAccess( 'mymodule', 'myfunction' );
 
if( isset( $limitations['simplifiedLimitations']['Language'] ) ) // Found limitations on language. These will be the only available in the dropdown menu
 $authorizedLang = $limitations['simplifiedLimitations']['Language'];
 
$tpl->setVariable( 'languages', $authorizedLang );
 
$Result['content'] = $tpl->fetch( 'design:mydesignsubdir/myview.tpl' );

extension/myextension/design/standard/templates/mydesignsubdir/myview.tpl

<select name="LanguageSelection">
{foreach $languages as $language}
 <option value=”{$language}”>{$language}</option>
{/foreach}
</select>

Conclusion

En résumé, mettre en oeuvre des limitations pour des politiques de sécurité au sein de modules personnalisés dans eZ Publish n'est pas une mince affaire. A titre personnel, il m'aura fallu plusieurs jours de fouille dans le kernel et beaucoup de patience pour comprendre les mécanismes des politiques de sécurité et ainsi vous exposer ce tutoriel.

En outre et à la lumière de ce qui précède, il est clair que ce système nécessiterait un sérieux rafraîchissement pour les prochaines versions d'eZ Publish... La roadmap de Fuji est-elle toujours ouverte ? ;-)

Je tiens particulièrement à remercier Nicolas Pastorino , Damien Pobel et André Rømcke qui ont largement contribué à me mettre sur la voie :-).

Cadeau bonus

Pour tous ceux qui ont eu le courage de lire ce long billet jusqu'au bout et qui souhaitent savoir comment faire pour appliquer les règles de sécurité sur les onglets dans le nouveau Backoffice (eZ Publish 4.3.0 et supérieur), voici comment faire :

extension/myextension/settings/menu.ini.append.php

<?php /* #?ini charset="utf-8"?
 
[NavigationPart]
Part[mynavigationpart]=My NavigationPart description
 
[TopAdminMenu]
Tabs[]=mytab
 
[Topmenu_mytab]
NavigationPartIdentifier=mynavigationpart
Name=INI Config
Tooltip=My Tooltip
URL[]
URL[default]=mymodule/myview
Enabled[]
Enabled[default]=true
Enabled[browse]=false
Enabled[edit]=false
Shown[]
Shown[default]=true
Shown[navigation]=true
Shown[browse]=false
 
# Ajouter simplement le controle d'acces pour la fonction par defaut de votre module
PolicyList[]=mymodule/myfunction
 
*/ ?>

Enjoy !


Commentaires

plaque psoriasis (par plaque psoriasis)

This blog is great i love reading your posts. Keep up the great work! You know, a lot of people are hunting around for this info, you could help them greatly.