Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement front end module permissions #6232

Merged
merged 23 commits into from Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions core-bundle/config/listener.yaml
Expand Up @@ -118,6 +118,12 @@ services:
arguments:
- '@translator'

contao.listener.data_container.frontend_module_permissions:
class: Contao\CoreBundle\EventListener\DataContainer\FrontendModulePermissionsListener
arguments:
- '@security.helper'
- '@database_connection'

contao.listener.data_container.layout_options:
class: Contao\CoreBundle\EventListener\DataContainer\LayoutOptionsListener
arguments:
Expand Down
6 changes: 6 additions & 0 deletions core-bundle/config/migrations.yaml
Expand Up @@ -64,3 +64,9 @@ services:
class: Contao\CoreBundle\Migration\Version502\AlwaysForwardMigration
arguments:
- '@database_connection'

contao.migration.version_503.frontend_modules:
class: Contao\CoreBundle\Migration\Version503\FrontendModulesMigration
arguments:
- '@database_connection'
- '@contao.framework'
5 changes: 5 additions & 0 deletions core-bundle/config/services.yaml
Expand Up @@ -857,6 +857,11 @@ services:
arguments:
- '@security.access.decision_manager'

contao.security.data_container.frontend_modules_voter:
class: Contao\CoreBundle\Security\Voter\DataContainer\FrontendModulesVoter
arguments:
- '@security.helper'

contao.security.data_container.image_size_access_voter:
class: Contao\CoreBundle\Security\Voter\DataContainer\ImageSizeAccessVoter
arguments:
Expand Down
3 changes: 2 additions & 1 deletion core-bundle/contao/classes/BackendUser.php
Expand Up @@ -20,6 +20,7 @@
* @property array $groups
* @property array $elements
* @property array $fields
* @property array $frontendModules
* @property array $pagemounts
* @property array $filemounts
* @property array $filemountIds
Expand Down Expand Up @@ -242,7 +243,7 @@ protected function setUserFromDb()

// Inherit permissions
$always = array('alexf');
$depends = array('modules', 'themes', 'elements', 'fields', 'pagemounts', 'alpty', 'filemounts', 'fop', 'forms', 'formp', 'imageSizes', 'amg');
$depends = array('modules', 'themes', 'elements', 'fields', 'frontendModules', 'pagemounts', 'alpty', 'filemounts', 'fop', 'forms', 'formp', 'imageSizes', 'amg');

// HOOK: Take custom permissions
if (!empty($GLOBALS['TL_PERMISSIONS']) && \is_array($GLOBALS['TL_PERMISSIONS']))
Expand Down
19 changes: 0 additions & 19 deletions core-bundle/contao/dca/tl_content.php
Expand Up @@ -748,7 +748,6 @@
'module' => array
(
'inputType' => 'select',
'options_callback' => array('tl_content', 'getModules'),
'eval' => array('mandatory'=>true, 'chosen'=>true, 'submitOnChange'=>true, 'tl_class'=>'w50 wizard'),
'wizard' => array
(
Expand Down Expand Up @@ -1403,24 +1402,6 @@ public function editModule(DataContainer $dc)
return ' <a href="' . StringUtil::specialcharsUrl($href) . '" title="' . StringUtil::specialchars($title) . '" onclick="Backend.openModalIframe({\'title\':\'' . StringUtil::specialchars(str_replace("'", "\\'", $title)) . '\',\'url\':this.href});return false">' . Image::getHtml('alias.svg', $title) . '</a>';
}

/**
* Get all modules and return them as array
*
* @return array
*/
public function getModules()
{
$arrModules = array();
$objModules = Database::getInstance()->execute("SELECT m.id, m.name, t.name AS theme FROM tl_module m LEFT JOIN tl_theme t ON m.pid=t.id ORDER BY t.name, m.name");

while ($objModules->next())
{
$arrModules[$objModules->theme][$objModules->id] = $objModules->name . ' (ID ' . $objModules->id . ')';
}

return $arrModules;
}

/**
* Dynamically set the ace syntax
*
Expand Down
6 changes: 5 additions & 1 deletion core-bundle/contao/dca/tl_module.php
Expand Up @@ -580,13 +580,17 @@ public function checkPermission()
*/
public function getModules()
{
$security = System::getContainer()->get('security.helper');
$groups = array();

foreach ($GLOBALS['FE_MOD'] as $k=>$v)
{
foreach (array_keys($v) as $kk)
{
$groups[$k][] = $kk;
if ($security->isGranted(ContaoCorePermissions::USER_CAN_ACCESS_FRONTEND_MODULE_TYPE, $kk))
{
$groups[$k][] = $kk;
}
}
}

Expand Down
12 changes: 10 additions & 2 deletions core-bundle/contao/dca/tl_user.php
Expand Up @@ -118,8 +118,8 @@
'admin' => '{name_legend},username,name,email;{backend_legend:hide},language,uploader,showHelp,thumbnails,useRTE,useCE,doNotCollapse;{theme_legend:hide},backendTheme;{password_legend:hide},password,pwChange;{admin_legend},admin;{account_legend},disable,start,stop',
'default' => '{name_legend},username,name,email;{backend_legend:hide},language,uploader,showHelp,thumbnails,useRTE,useCE,doNotCollapse;{theme_legend:hide},backendTheme;{password_legend:hide},password,pwChange;{admin_legend},admin;{groups_legend},groups,inherit;{account_legend},disable,start,stop',
'group' => '{name_legend},username,name,email;{backend_legend:hide},language,uploader,showHelp,thumbnails,useRTE,useCE,doNotCollapse;{theme_legend:hide},backendTheme;{password_legend:hide},password,pwChange;{admin_legend},admin;{groups_legend},groups,inherit;{account_legend},disable,start,stop',
'extend' => '{name_legend},username,name,email;{backend_legend:hide},language,uploader,showHelp,thumbnails,useRTE,useCE,doNotCollapse;{theme_legend:hide},backendTheme;{password_legend:hide},password,pwChange;{admin_legend},admin;{groups_legend},groups,inherit;{modules_legend},modules,themes;{elements_legend},elements,fields;{pagemounts_legend},pagemounts,alpty;{filemounts_legend},filemounts,fop;{imageSizes_legend},imageSizes;{forms_legend},forms,formp;{amg_legend},amg;{account_legend},disable,start,stop',
'custom' => '{name_legend},username,name,email;{backend_legend:hide},language,uploader,showHelp,thumbnails,useRTE,useCE,doNotCollapse;{theme_legend:hide},backendTheme;{password_legend:hide},password,pwChange;{admin_legend},admin;{groups_legend},groups,inherit;{modules_legend},modules,themes;{elements_legend},elements,fields;{pagemounts_legend},pagemounts,alpty;{filemounts_legend},filemounts,fop;{imageSizes_legend},imageSizes;{forms_legend},forms,formp;{amg_legend},amg;{account_legend},disable,start,stop'
'extend' => '{name_legend},username,name,email;{backend_legend:hide},language,uploader,showHelp,thumbnails,useRTE,useCE,doNotCollapse;{theme_legend:hide},backendTheme;{password_legend:hide},password,pwChange;{admin_legend},admin;{groups_legend},groups,inherit;{modules_legend},modules,themes,frontendModules;{elements_legend},elements,fields;{pagemounts_legend},pagemounts,alpty;{filemounts_legend},filemounts,fop;{imageSizes_legend},imageSizes;{forms_legend},forms,formp;{amg_legend},amg;{account_legend},disable,start,stop',
'custom' => '{name_legend},username,name,email;{backend_legend:hide},language,uploader,showHelp,thumbnails,useRTE,useCE,doNotCollapse;{theme_legend:hide},backendTheme;{password_legend:hide},password,pwChange;{admin_legend},admin;{groups_legend},groups,inherit;{modules_legend},modules,themes,frontendModules;{elements_legend},elements,fields;{pagemounts_legend},pagemounts,alpty;{filemounts_legend},filemounts,fop;{imageSizes_legend},imageSizes;{forms_legend},forms,formp;{amg_legend},amg;{account_legend},disable,start,stop'
),

// Fields
Expand Down Expand Up @@ -289,6 +289,14 @@
'eval' => array('multiple'=>true, 'helpwizard'=>true),
'sql' => "blob NULL"
),
'frontendModules' => array
(
'filter' => true,
'inputType' => 'checkbox',
'reference' => &$GLOBALS['TL_LANG']['FMD'],
'eval' => array('multiple'=>true, 'helpwizard'=>true, 'collapseUncheckedGroups'=>true),
'sql' => "blob NULL"
),
'pagemounts' => array
(
'inputType' => 'pageTree',
Expand Down
11 changes: 10 additions & 1 deletion core-bundle/contao/dca/tl_user_group.php
Expand Up @@ -64,7 +64,7 @@
// Palettes
'palettes' => array
(
'default' => '{title_legend},name;{modules_legend},modules,themes;{elements_legend},elements,fields;{pagemounts_legend},pagemounts,alpty;{filemounts_legend},filemounts,fop;{imageSizes_legend},imageSizes;{forms_legend},forms,formp;{amg_legend},amg;{alexf_legend:hide},alexf;{account_legend},disable,start,stop',
'default' => '{title_legend},name;{modules_legend},modules,themes,frontendModules;{elements_legend},elements,fields;{pagemounts_legend},pagemounts,alpty;{filemounts_legend},filemounts,fop;{imageSizes_legend},imageSizes;{forms_legend},forms,formp;{amg_legend},amg;{alexf_legend:hide},alexf;{account_legend},disable,start,stop',
),

// Fields
Expand Down Expand Up @@ -125,6 +125,15 @@
'eval' => array('multiple'=>true, 'helpwizard'=>true),
'sql' => "blob NULL"
),
'frontendModules' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_user']['frontendModules'],
'filter' => true,
'inputType' => 'checkbox',
'reference' => &$GLOBALS['TL_LANG']['FMD'],
'eval' => array('multiple'=>true, 'helpwizard'=>true, 'collapseUncheckedGroups'=>true),
'sql' => "blob NULL"
),
'pagemounts' => array
(
'label' => &$GLOBALS['TL_LANG']['tl_user']['pagemounts'],
Expand Down
6 changes: 6 additions & 0 deletions core-bundle/contao/languages/en/tl_user.xlf
Expand Up @@ -134,6 +134,12 @@
<trans-unit id="tl_user.fields.1">
<source>Here you can select the allowed form field types.</source>
</trans-unit>
<trans-unit id="tl_user.frontendModules.0">
<source>Front end modules</source>
</trans-unit>
<trans-unit id="tl_user.frontendModules.1">
<source>Here you can select the allowed front end module types.</source>
</trans-unit>
<trans-unit id="tl_user.pagemounts.0">
<source>Pagemounts</source>
</trans-unit>
Expand Down
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\CoreBundle\EventListener\DataContainer;

use Contao\BackendUser;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
use Contao\CoreBundle\Security\ContaoCorePermissions;
use Doctrine\DBAL\Connection;
use Symfony\Bundle\SecurityBundle\Security;

class FrontendModulePermissionsListener
{
public function __construct(
private readonly Security $security,
private readonly Connection $connection,
) {
}

#[AsCallback('tl_module', 'config.onload')]
public function setDefaultType(): void
{
$user = $this->security->getUser();

if (!$user instanceof BackendUser) {
return;
}

if ($user->isAdmin || empty($user->frontendModules)) {
return;
}

if (!\in_array($GLOBALS['TL_DCA']['tl_module']['fields']['type']['sql']['default'] ?? null, $user->frontendModules, true)) {
$GLOBALS['TL_DCA']['tl_module']['fields']['type']['default'] = $user->frontendModules[0];
}
}

#[AsCallback(table: 'tl_user', target: 'fields.frontendModules.options')]
#[AsCallback(table: 'tl_user_group', target: 'fields.frontendModules.options')]
public function frontendModuleOptions(): array
{
return array_map('array_keys', $GLOBALS['FE_MOD']);
}

#[AsCallback('tl_content', 'fields.module.options')]
public function allowedFrontendModuleOptions(): array
{
$options = [];

$modules = $this->connection->fetchAllAssociative('
SELECT m.id, m.name, m.type, t.name AS theme
FROM tl_module m
LEFT JOIN tl_theme t ON m.pid=t.id
ORDER BY t.name, m.name
');

foreach ($modules as $module) {
if (!$this->security->isGranted(ContaoCorePermissions::USER_CAN_ACCESS_FRONTEND_MODULE_TYPE, $module['type'])) {
continue;
}

$options[$module['theme']][$module['id']] = sprintf('%s (ID %s)', $module['name'], $module['id']);
}

return $options;
}
}
64 changes: 64 additions & 0 deletions core-bundle/src/Migration/Version503/FrontendModulesMigration.php
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\CoreBundle\Migration\Version503;

use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\CoreBundle\Migration\AbstractMigration;
use Contao\CoreBundle\Migration\MigrationResult;
use Doctrine\DBAL\Connection;

/**
* @internal
*/
class FrontendModulesMigration extends AbstractMigration
{
public function __construct(
private readonly Connection $connection,
private readonly ContaoFramework $framework,
) {
}

public function shouldRun(): bool
{
$schemaManager = $this->connection->createSchemaManager();

if (!$schemaManager->tablesExist(['tl_user_group'])) {
return false;
}

$columns = $schemaManager->listTableColumns('tl_user_group');

return !isset($columns['frontendmodules']);
}

public function run(): MigrationResult
{
$this->framework->initialize();

$this->connection->executeStatement('
ALTER TABLE
tl_user_group
ADD
frontendModules BLOB DEFAULT NULL
');

$this->connection->executeStatement(
'UPDATE tl_user_group SET frontendModules = :frontendModules',
[
'frontendModules' => serialize(array_keys(array_merge(...array_values($GLOBALS['FE_MOD'])))),
],
);

return $this->createResult(true);
}
}
14 changes: 13 additions & 1 deletion core-bundle/src/Security/ContaoCorePermissions.php
Expand Up @@ -27,11 +27,17 @@ final class ContaoCorePermissions
public const USER_CAN_EDIT_PAGE_HIERARCHY = 'contao_user.can_edit_page_hierarchy';

/**
* Access is granted if the current user can can delete the given page.
* Access is granted if the current user can delete the given page.
* Subject must be a page ID, a PageModel or a tl_page record as array.
*/
public const USER_CAN_DELETE_PAGE = 'contao_user.can_delete_page';

/**
* Access is granted if the current user can delete the given fron end module.
* Subject must be a module ID, a FrontendModule or a tl_module record as array.
*/
public const USER_CAN_DELETE_FRONTEND_MODULE = 'contao_user.can_delete_frontend_module';

/**
* Access is granted if the current user can edit articles of the given page.
* Subject must be a page ID, a PageModel or a tl_page record as array.
Expand Down Expand Up @@ -104,6 +110,12 @@ final class ContaoCorePermissions
*/
public const USER_CAN_ACCESS_ELEMENT_TYPE = 'contao_user.elements';

/**
* Access is granted if the current user can access the front end module type.
* Subject must be a front end module type (e.g. "navigation").
*/
public const USER_CAN_ACCESS_FRONTEND_MODULE_TYPE = 'contao_user.frontendModules';

/**
* Access is granted if the current user can access the form field type.
* Subject can be a content element type (e.g. "hidden") or null to
Expand Down
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace Contao\CoreBundle\Security\Voter\DataContainer;

use Contao\BackendUser;
use Contao\CoreBundle\Security\DataContainer\CreateAction;
use Contao\CoreBundle\Security\DataContainer\DeleteAction;
use Contao\CoreBundle\Security\DataContainer\ReadAction;
use Contao\CoreBundle\Security\DataContainer\UpdateAction;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

class FrontendModulesVoter extends AbstractDataContainerVoter
{
protected function getTable(): string
{
return 'tl_module';
}

protected function hasAccess(TokenInterface $token, CreateAction|DeleteAction|ReadAction|UpdateAction $action): bool
{
$user = $token->getUser();

if (!$user instanceof BackendUser) {
return false;
}

if ($user->isAdmin || empty($user->frontendModules)) {
return true;
}

return $this->isAllowedModuleType($action, $user);
}

private function isAllowedModuleType(CreateAction|DeleteAction|ReadAction|UpdateAction $subject, BackendUser $user): bool
{
if ($subject instanceof CreateAction) {
$type = $subject->getNew()['type'];

if (null === $type) {
return true;
}
} else {
$type = $subject->getCurrent()['type'];
}

return \in_array($type, $user->frontendModules, true);
}
}