Learn / eZ Publish / Adding custom security policy limitations to your modules

Adding custom security policy limitations to your modules

Introduction

As you might know, eZ Publish user access control is pretty precise and has a very fine granularity. Most of kernel modules allow indeed to limit access to themselves thanks to security policies we can assign to a user or to a user group. This is particularly the case for content module, fundamental within the CMS, thanks to its function limitations that can be configured in the admin interface. With these limitations, one can precisely define what a contributor has the right to do regarding content and functionalities.

Pre-requisites and target population

This tutorial is targeted to anyone who needs to implement security policy limitations for custom modules. Knowledge of eZ Publish module development is a must to fully understand the interest of this article. If you are not familiar to module and/or extension development ineZ Publish, you may read this excellent article about developing extensions. You also might read this interesting (old) tutorial about module development for eZ Publish (warning : this article was based on eZ Publish 3.x and PHP4, so read it with PHP5 style in mind). You can brush-up your knowledge of the basics of Access Control in eZ Publish by reviewing the online documentation on this subject.

 

Defining module functions to limit access to them

It is obviously possible to define security rules, configurable in the same way, for our own modules. All we need to do is to create a list of functions for our module, in 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();

Here we define a function for the module mymodule and we affect it to the view myview. This function will display in the corresponding list when creating a new security policy for module mymodule in the admin interface. You will find this kind of definition in most of modules contained in the kernel, or within extensions developped by eZ Systems or by the community (this is the case for NovenINIUpdate extension).

What I described above is sufficient in most cases, but what if we need to add more complex limitations like in content module (language, section, etc...) ?

 

Defining our own limitations

Definition

As you probably noticed in the module.php example above, variable $FunctionList['myfunction'] is an empty array, which means that the function doesn't have any limitation. We just need to fill it with right values to add some.

Example for a language level limitation :

<?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);

Here we tell eZ Publish that myfunction has a limitation named Language and that we need to fetch possible values list with eZContentLanguage::fetchLimitationList() method, with parameter false. This simply stands for an old school way to define a callback function, probably here since first releases of eZ Publish 3 (remind you first versions of PHP4 and all its limitations). For curious minds, the important stuffs can be found in eZPolicyLimitation::allValuesAsArrayWithNames(), from line 249 (in a 4.3.0 eZ Publish instance).

Here we call a class from the kernel to fill our limitation array, but it is of course possible to use a class from an extension. However, autoload system cannot be used here and an old good include_once is made in the backend. So, in order to use a class from an extension, we need to add another key to our $Language array :

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

This way, extension/myextension/classes/myclass.php class will be included.

What does our method MyClass:fetchLanguageLimitationList() have to return ? A quick look to eZContentLanguage::fetchLimitationList() shows us that it must be a simple array with each entry being an associative array itself containing id and name keys.

  • id will be the value stored in the database for the security policy limitation
  • name will simply be the label displayed in the admin interface

Here what should be our class 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;
    }
}
 

Adding the new policy to “Editor” role

Here is below a series of screenshots showing all the process :

 
 
 
 

Controlling access to our module

Now that our limitations are defined, we now need to filter access to our module depending on the rights that have been affected to a user. Indeed, this control is made in index.php for content module, and only for this one. As a consequence, we are forced to control the access ourselves. We can imagine changes in this regard in the future versions of eZ Publish.

Playing with eZJSCore

Manual access control of those limitations using the framework can easily become a real pain as the system is pretty complex. While a little refactoring would be nice on this part, workarounds exist that will make your life easier! Developer Andrè Rømcke have implemented in eZJSCore extension a simplified access control method in the shape of a template operator. This extension being now part of eZ Publish distribution, it would be a shame not to use it ! Besides, you can find a real good article presenting this extension on the community portal.

Here is the best way to proceed :

extension/myextension/modules/mymodule/myview.php :

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

// Or if you want to check using limitations as well using ezjscore 1.2 (comes with eZ Publish 4.4) and up
// In this case providing list of languages user must have access to
$userHasAccess = ezjscAccessTemplateFunctions::hasAccessToLimitation( 'mymodule', 'myfunction', array( 'Language' => $languageList ) );

Get the limitations list for current user

Unfortunately, older versions of eZJSCore then 1.1.1 / 1.2 doesn't have any method allowing us to get available limitations for current user, which could be very useful to display a combo box containing limitations granted to the user for example (available languages in our case).

To do this, we will need to write a method returning those limitations in a simplified way. Indeed, eZUser class does have hasAccessTo() method, but its result is absolutely unreadable and needs to be strongly simplified. We will thus write a complementary method returning simplified limitations.

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;
    }
}

This method returns an array containing result from eZUser::hasAccessTo(), with a new key : simplifiedLimitations. This key is also an array, containing the precious limitations.

In our example, for a user whom we would have affected a limitation Language allowing only fre-FR and eng-GB, this array would contain :

$limitations = MyClass::getSimplifiedUserAccess( 'mymodule', 'myfunction' );
print_r( $limitations['simplifiedLimitations'] );

// Result
Array
(
    [Language] => Array
        (
            [0] => fre-FR
            [1] => eng-GB
        )

)

As a result, in our module where we need to display a combo box with authorized languages, we would have :

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>
 

Result

 
 

Security policies on tabs in the admin interface

eZ Publish 4.3.0 introduced the ability to filter the display of tabs in the admin interface depending on users security policies. Here is how to proceed :

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=My Tab
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

# Simply add access control to your module's default function
PolicyList[]=mymodule/function

*/ ?>

Enjoy !

 

Conclusion

Here is a quick recap of each step :

  1. Define a module function and affect it to one of the module views
  2. Define a callback method in order to fetch available limitations for the function
  3. Check user access in your module view with eZ JS Core
  4. Fetch available limitations for current user with a helper static method
  5. Control user access for tabs display in admin interface

As an example, we could consider NovenINIUpdate extension (environment switch for INI settings). It has indeed a GUI in the admin interface. At the time of writing, every user that has access to the main module of this extension can switch to every declared environment (ie. dev, staging, production). We could add a limitation on the environment, allowing many users to only switch from/to dev and staging environment. Production switch would be given only to admin users.

In short, building custom limitations for security policies attached to custom modules in eZ Publish is not a piece of cake. Personally, it took me several days of patience, deeply digging into the kernel to understand those mechanisms I just exposed to you, so you do not need to search for hours yourself :)

However, despite the real complexity, this mechanism allows very fine grain granularity in user access control. It is also unique and native in eZ Publish for a long time now.To be widely used, it would need to be simplified within the kernel, as it has been done thanks to eZJSCore and the helper method we used in this tutorial.

I particularly would like to thank Nicolas Pastorino, Damien Pobel and Andrè Rømcke who helped me during my research :-).

Resources

This tutorial is an english translation of my original post on my blog, Lolart.net (french) : http://www.lolart.net/blog/ez-publish/des-limitations-pour-vos-politiques-de-securite

Useful Articles and documentation :

This article is available in PDF for offline reading : Jerome Vieilledent - Adding custom security policy limitations to your modules - eZ Publish Community

About the author : Jérôme Vieilledent

Jérôme is a completely self-educated web developer. He learnt PHP all by himself and started developing with eZ Publish in 2007. He works now as a technical manager and expert at SQLi Agency in Paris.

License

This work is licensed under the Creative Commons – Share Alike license ( http://creativecommons.org/licenses/by-sa/3.0 ).

eZ debug

Timing: Jan 17 2025 21:05:23
Script start
Timing: Jan 17 2025 21:05:23
Module start 'content'
Timing: Jan 17 2025 21:05:23
Module end 'content'
Timing: Jan 17 2025 21:05:23
Script end

Main resources:

Total runtime0.3206 sec
Peak memory usage4,096.0000 KB
Database Queries213

Timing points:

CheckpointStart (sec)Duration (sec)Memory at start (KB)Memory used (KB)
Script start 0.00000.0055 596.3047180.8047
Module start 'content' 0.00550.1973 777.10941,053.8438
Module end 'content' 0.20280.1177 1,830.9531349.9844
Script end 0.3205  2,180.9375 

Time accumulators:

 Accumulator Duration (sec) Duration (%) Count Average (sec)
Ini load
Load cache0.00411.2809210.0002
Check MTime0.00150.4769210.0001
Mysql Total
Database connection0.00060.201210.0006
Mysqli_queries0.143544.75682130.0007
Looping result0.00180.56162110.0000
Template Total0.299993.620.1500
Template load0.00190.592120.0009
Template processing0.298092.957820.1490
Template load and register function0.00010.035010.0001
states
state_id_array0.00401.2608100.0004
state_identifier_array0.00351.0875110.0003
Override
Cache load0.00351.10022260.0000
Sytem overhead
Fetch class attribute name0.00240.7481170.0001
Fetch class attribute can translate value0.00080.252390.0001
class_abstraction
Instantiating content class attribute0.00000.0085170.0000
XML
Image XML parsing0.01233.824190.0014
General
dbfile0.01253.9027520.0002
String conversion0.00000.001930.0000
Note: percentages do not add up to 100% because some accumulators overlap

CSS/JS files loaded with "ezjscPacker" during request:

CacheTypePacklevelSourceFiles
CSS0extension/community/design/community/stylesheets/ext/jquery.autocomplete.css
extension/community_design/design/suncana/stylesheets/scrollbars.css
extension/community_design/design/suncana/stylesheets/tabs.css
extension/community_design/design/suncana/stylesheets/roadmap.css
extension/community_design/design/suncana/stylesheets/content.css
extension/community_design/design/suncana/stylesheets/star-rating.css
extension/community_design/design/suncana/stylesheets/syntax_and_custom_tags.css
extension/community_design/design/suncana/stylesheets/buttons.css
extension/community_design/design/suncana/stylesheets/tweetbox.css
extension/community_design/design/suncana/stylesheets/jquery.fancybox-1.3.4.css
extension/bcsmoothgallery/design/standard/stylesheets/magnific-popup.css
extension/sevenx/design/simple/stylesheets/star_rating.css
extension/sevenx/design/simple/stylesheets/libs/fontawesome/css/all.min.css
extension/sevenx/design/simple/stylesheets/main.v02.css
extension/sevenx/design/simple/stylesheets/main.v02.res.css
JS0extension/ezjscore/design/standard/lib/yui/3.17.2/build/yui/yui-min.js
extension/ezjscore/design/standard/javascript/jquery-3.7.0.min.js
extension/community_design/design/suncana/javascript/jquery.ui.core.min.js
extension/community_design/design/suncana/javascript/jquery.ui.widget.min.js
extension/community_design/design/suncana/javascript/jquery.easing.1.3.js
extension/community_design/design/suncana/javascript/jquery.ui.tabs.js
extension/community_design/design/suncana/javascript/jquery.hoverIntent.min.js
extension/community_design/design/suncana/javascript/jquery.popmenu.js
extension/community_design/design/suncana/javascript/jScrollPane.js
extension/community_design/design/suncana/javascript/jquery.mousewheel.js
extension/community_design/design/suncana/javascript/jquery.cycle.all.js
extension/sevenx/design/simple/javascript/jquery.scrollTo.js
extension/community_design/design/suncana/javascript/jquery.cookie.js
extension/community_design/design/suncana/javascript/ezstarrating_jquery.js
extension/community_design/design/suncana/javascript/jquery.initboxes.js
extension/community_design/design/suncana/javascript/app.js
extension/community_design/design/suncana/javascript/twitterwidget.js
extension/community_design/design/suncana/javascript/community.js
extension/community_design/design/suncana/javascript/roadmap.js
extension/community_design/design/suncana/javascript/ez.js
extension/community_design/design/suncana/javascript/ezshareevents.js
extension/sevenx/design/simple/javascript/main.js

Templates used to render the page:

UsageRequested templateTemplateTemplate loadedEditOverride
1node/view/full.tplfull/article.tplextension/sevenx/design/simple/override/templates/full/article.tplEdit templateOverride template
9content/datatype/view/ezxmltext.tpl<No override>extension/community_design/design/suncana/templates/content/datatype/view/ezxmltext.tplEdit templateOverride template
15content/datatype/view/ezxmltags/header.tpl<No override>design/standard/templates/content/datatype/view/ezxmltags/header.tplEdit templateOverride template
22content/datatype/view/ezxmltags/emphasize.tpl<No override>design/standard/templates/content/datatype/view/ezxmltags/emphasize.tplEdit templateOverride template
13content/datatype/view/ezxmltags/strong.tpl<No override>design/standard/templates/content/datatype/view/ezxmltags/strong.tplEdit templateOverride template
50content/datatype/view/ezxmltags/paragraph.tpl<No override>extension/ezwebin/design/ezwebin/templates/content/datatype/view/ezxmltags/paragraph.tplEdit templateOverride template
13content/datatype/view/ezxmltags/link.tpl<No override>design/standard/templates/content/datatype/view/ezxmltags/link.tplEdit templateOverride template
11content/datatype/view/ezxmltags/separator.tpl<No override>extension/community_design/design/suncana/templates/content/datatype/view/ezxmltags/separator.tplEdit templateOverride template
7content/datatype/view/ezxmltags/newpage.tpl<No override>extension/community/design/standard/templates/content/datatype/view/ezxmltags/newpage.tplEdit templateOverride template
10content/datatype/view/ezxmltags/literal.tpl<No override>extension/community/design/standard/templates/content/datatype/view/ezxmltags/literal.tplEdit templateOverride template
10content/datatype/view/ezxmltags/li.tpl<No override>design/standard/templates/content/datatype/view/ezxmltags/li.tplEdit templateOverride template
2content/datatype/view/ezxmltags/ul.tpl<No override>design/standard/templates/content/datatype/view/ezxmltags/ul.tplEdit templateOverride template
8content/datatype/view/ezxmltags/embed.tpl<No override>design/standard/templates/content/datatype/view/ezxmltags/embed.tplEdit templateOverride template
8content/view/embed.tplembed/image.tplextension/sevenx/design/simple/override/templates/embed/image.tplEdit templateOverride template
8content/datatype/view/ezimage.tpl<No override>extension/sevenx/design/simple/templates/content/datatype/view/ezimage.tplEdit templateOverride template
1content/datatype/view/ezxmltags/ol.tpl<No override>design/standard/templates/content/datatype/view/ezxmltags/ol.tplEdit templateOverride template
1content/datatype/view/ezxmltags/line.tpl<No override>design/standard/templates/content/datatype/view/ezxmltags/line.tplEdit templateOverride template
1content/datatype/view/ezxmltags/embed-inline.tpl<No override>design/standard/templates/content/datatype/view/ezxmltags/embed-inline.tplEdit templateOverride template
1content/view/embed-inline.tpl<No override>design/standard/templates/content/view/embed-inline.tplEdit templateOverride template
1pagelayout.tpl<No override>extension/sevenx/design/simple/templates/pagelayout.tplEdit templateOverride template
 Number of times templates used: 192
 Number of unique templates used: 20

Time used to render debug report: 0.0001 secs