Merge branch 'feature/security-gui-5647'

This commit is contained in:
Eric Lippmann 2014-11-19 16:31:36 +01:00
commit 0862602fcf
15 changed files with 524 additions and 89 deletions

View File

@ -24,14 +24,17 @@ class ConfigController extends ActionController
public function init()
{
$this->view->tabs = Widget::create('tabs')->add('index', array(
'title' => 'Application',
'title' => $this->translate('Application'),
'url' => 'config'
))->add('authentication', array(
'title' => 'Authentication',
'title' => $this->translate('Authentication'),
'url' => 'config/authentication'
))->add('resources', array(
'title' => 'Resources',
'title' => $this->translate('Resources'),
'url' => 'config/resource'
))->add('permissions', array(
'title' => $this->translate('Permissions'),
'url' => 'permissions'
));
}

View File

@ -0,0 +1,150 @@
<?php
// {{{ICINGA_LICENSE_HEADER}}}
// {{{ICINGA_LICENSE_HEADER}}}
use Icinga\Application\Config;
use Icinga\Forms\ConfirmRemovalForm;
use Icinga\Forms\Security\RoleForm;
use Icinga\Web\Controller\ActionController;
use Icinga\Web\Notification;
use Icinga\Web\Widget;
class PermissionsController extends ActionController
{
public function init()
{
$this->view->tabs = Widget::create('tabs')->add('index', array(
'title' => $this->translate('Application'),
'url' => 'config'
))->add('authentication', array(
'title' => $this->translate('Authentication'),
'url' => 'config/authentication'
))->add('resources', array(
'title' => $this->translate('Resources'),
'url' => 'config/resource'
))->add('permissions', array(
'title' => $this->translate('Permissions'),
'url' => 'permissions'
));
}
public function indexAction()
{
$this->view->tabs->activate('permissions');
$this->view->roles = Config::app('roles', true);
}
public function newAction()
{
$role = new RoleForm(array(
'onSuccess' => function (RoleForm $role) {
$name = $role->getElement('name')->getValue();
$values = $role->getValues();
try {
$role->add($name, $values);
} catch (InvalidArgumentException $e) {
$role->addError($e->getMessage());
return false;
}
if ($role->save()) {
Notification::success(t('Role created'));
return true;
}
return false;
}
));
$role
->setSubmitLabel($this->translate('Create Role'))
->setIniConfig(Config::app('roles', true))
->setRedirectUrl('security')
->handleRequest();
$this->view->form = $role;
}
public function updateAction()
{
$name = $this->_request->getParam('role');
if (empty($name)) {
throw new Zend_Controller_Action_Exception(
sprintf($this->translate('Required parameter \'%s\' missing'), 'role'),
400
);
}
$role = new RoleForm();
$role->setSubmitLabel($this->translate('Update Role'));
try {
$role
->setIniConfig(Config::app('roles', true))
->load($name);
} catch (InvalidArgumentException $e) {
throw new Zend_Controller_Action_Exception(
$e->getMessage(),
400
);
}
$role
->setOnSuccess(function (RoleForm $role) use ($name) {
$oldName = $name;
$name = $role->getElement('name')->getValue();
$values = $role->getValues();
try {
$role->update($name, $values, $oldName);
} catch (InvalidArgumentException $e) {
$role->addError($e->getMessage());
return false;
}
if ($role->save()) {
Notification::success(t('Role updated'));
return true;
}
return false;
})
->setRedirectUrl('security')
->handleRequest();
$this->view->name = $name;
$this->view->form = $role;
}
public function removeAction()
{
$name = $this->_request->getParam('role');
if (empty($name)) {
throw new Zend_Controller_Action_Exception(
sprintf($this->translate('Required parameter \'%s\' missing'), 'role'),
400
);
}
$role = new RoleForm();
try {
$role
->setIniConfig(Config::app('roles', true))
->load($name);
} catch (InvalidArgumentException $e) {
throw new Zend_Controller_Action_Exception(
$e->getMessage(),
400
);
}
$confirmation = new ConfirmRemovalForm(array(
'onSuccess' => function (ConfirmRemovalForm $confirmation) use ($name, $role) {
try {
$role->remove($name);
} catch (InvalidArgumentException $e) {
Notification::error($e->getMessage());
return false;
}
if ($role->save()) {
Notification::success(t('Role removed'));
return true;
}
return false;
}
));
$confirmation
->setSubmitLabel($this->translate('Remove Role'))
->setRedirectUrl('security')
->handleRequest();
$this->view->name = $name;
$this->view->form = $confirmation;
}
}

View File

@ -5,6 +5,7 @@
namespace Icinga\Forms;
use Exception;
use Zend_Form_Decorator_Abstract;
use Icinga\Web\Form;
use Icinga\Application\Config;
use Icinga\File\Ini\IniWriter;
@ -58,7 +59,8 @@ class ConfigForm extends Form
'viewScript' => 'showConfiguration.phtml',
'errorMessage' => $e->getMessage(),
'configString' => $writer->render(),
'filePath' => $this->config->getConfigFile()
'filePath' => $this->config->getConfigFile(),
'placement' => Zend_Form_Decorator_Abstract::PREPEND
));
return false;
}

View File

@ -0,0 +1,225 @@
<?php
// {{{ICINGA_LICENSE_HEADER}}}
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Forms\Security;
use InvalidArgumentException;
use LogicException;
use Icinga\Application\Icinga;
use Icinga\Forms\ConfigForm;
use Icinga\Util\String;
/**
* Form for managing roles
*/
class RoleForm extends ConfigForm
{
/**
* Provided permissions by currently loaded modules
*
* @var array
*/
protected $providedPermissions = array();
/**
* Provided restrictions by currently loaded modules
*
* @var array
*/
protected $providedRestrictions = array();
/**
* (non-PHPDoc)
* @see \Icinga\Web\Form::init() For the method documentation.
*/
public function init()
{
foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) {
foreach ($module->getProvidedPermissions() as $permission) {
/** @var object $permission */
$this->providedPermissions[$permission->name] = $permission->name . ': ' . $permission->description;
}
foreach ($module->getProvidedRestrictions() as $restriction) {
/** @var object $restriction */
$this->providedRestrictions[$restriction->name] = $restriction->name . ': ' . $restriction->description;
}
}
}
/**
* (non-PHPDoc)
* @see \Icinga\Web\Form::createElements() For the method documentation.
*/
public function createElements(array $formData = array())
{
$this->addElements(array(
array(
'text',
'name',
array(
'required' => true,
'label' => t('Role Name'),
'description' => t('The name of the role')
),
),
array(
'textarea',
'users',
array(
'label' => t('Users'),
'description' => t('Comma-separated list of users that are assigned to the role')
),
),
array(
'textarea',
'groups',
array(
'label' => t('Groups'),
'description' => t('Comma-separated list of groups that are assigned to the role')
),
),
array(
'multiselect',
'permissions',
array(
'label' => t('Permissions Set'),
'description' => t('The permissions to grant. You may select more than one permission'),
'multiOptions' => $this->providedPermissions
)
)
));
return $this;
}
/**
* Load a role
*
* @param string $name The name of the role
*
* @return $this
*
* @throws LogicException If the config is not set
* @see ConfigForm::setConfig() For setting the config.
*/
public function load($name)
{
if (! isset($this->config)) {
throw new LogicException(sprintf('Can\'t load role \'%s\'. Config is not set', $name));
}
if (! $this->config->hasSection($name)) {
throw new InvalidArgumentException(sprintf(
t('Can\'t load role \'%s\'. Role does not exist'),
$name
));
}
$role = $this->config->getSection($name)->toArray();
$role['permissions'] = ! empty($role['permissions'])
? String::trimSplit($role['permissions'])
: null;
$role['name'] = $name;
$this->populate($role);
return $this;
}
/**
* Add a role
*
* @param string $name The name of the role
* @param array $values
*
* @return $this
*
* @throws LogicException If the config is not set
* @throws InvalidArgumentException If the role to add already exists
* @see ConfigForm::setConfig() For setting the config.
*/
public function add($name, array $values)
{
if (! isset($this->config)) {
throw new LogicException(sprintf('Can\'t add role \'%s\'. Config is not set', $name));
}
if ($this->config->hasSection($name)) {
throw new InvalidArgumentException(sprintf(
t('Can\'t add role \'%s\'. Role already exists'),
$name
));
}
$this->config->setSection($name, $values);
return $this;
}
/**
* Remove a role
*
* @param string $name The name of the role
*
* @return $this
*
* @throws LogicException If the config is not set
* @throws InvalidArgumentException If the role does not exist
* @see ConfigForm::setConfig() For setting the config.
*/
public function remove($name)
{
if (! isset($this->config)) {
throw new LogicException(sprintf('Can\'t remove role \'%s\'. Config is not set', $name));
}
if (! $this->config->hasSection($name)) {
throw new InvalidArgumentException(sprintf(
t('Can\'t remove role \'%s\'. Role does not exist'),
$name
));
}
$this->config->removeSection($name);
return $this;
}
/**
* Update a role
*
* @param string $name The possibly new name of the role
* @param array $values
* @param string $oldName The name of the role to update
*
* @return $this
*
* @throws LogicException If the config is not set
* @throws InvalidArgumentException If the role to update does not exist
* @see ConfigForm::setConfig() For setting the config.
*/
public function update($name, array $values, $oldName)
{
if (! isset($this->config)) {
throw new LogicException(sprintf('Can\'t update role \'%s\'. Config is not set', $name));
}
if ($name !== $oldName) {
// The permission got a new name
$this->remove($oldName);
$this->add($name, $values);
} else {
if (! $this->config->hasSection($name)) {
throw new InvalidArgumentException(sprintf(
t('Can\'t update role \'%s\'. Role does not exist'),
$name
));
}
$this->config->setSection($name, $values);
}
return $this;
}
/**
* (non-PHPDoc)
* @see \Zend_Form::getValues() For the method documentation.
*/
public function getValues($suppressArrayNotation = false)
{
$permissions = $this->getElement('permissions')->getValue();
return array(
'users' => $this->getElement('users')->getValue(),
'groups' => $this->getElement('groups')->getValue(),
'permissions' => ! empty($permissions) ? implode(', ', $permissions) : null
);
}
}

View File

@ -0,0 +1,45 @@
<div class="controls">
<?= $tabs ?>
</div>
<div class="content">
<div>
<h1><?= $this->translate('Permissions') ?></h1>
<?php /** @var \Icinga\Application\Config $roles */ if ($roles->isEmpty()): ?>
<?= $this->translate('No permissions found.') ?>
<?php else: ?>
<table class="action" data-base-target="_next">
<thead>
<tr>
<th><?= $this->translate('Name') ?></th>
<th><?= $this->translate('Permissions') ?></th>
<th><?= $this->translate('Users') ?></th>
<th><?= $this->translate('Groups') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($roles as $name => $role): /** @var object $role */ ?>
<tr>
<td>
<?= $this->escape($name) ?>
<div class="hidden">
<a href="<?= $this->url('permissions/update', array('role' => $name)) ?>"></a>
</div>
</td>
<td><?= $this->escape($role->permissions) ?></td>
<td><?= $this->escape($role->users) ?></td>
<td><?= $this->escape($role->groups) ?></td>
<td>
<a href="<?= $this->url('permissions/remove', array('role' => $name)) ?>">
<?= $this->translate('Remove role') ?>
</a>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
<?php endif ?>
<a data-base-target="_next" href="<?= $this->href('permissions/new') ?>">
<?= $this->translate('New Role') ?>
</a>
</div>
</div>

View File

@ -0,0 +1,4 @@
<div class="content">
<h1><?= $this->translate('New Role') ?></h1>
<?= $form ?>
</div>

View File

@ -0,0 +1,4 @@
<div class="content">
<h1><?= sprintf($this->translate('Remove Role %s'), $name) ?></h1>
<?= $form ?>
</div>

View File

@ -0,0 +1,4 @@
<div class="content">
<h1><?= sprintf($this->translate('Update Role %s'), $name) ?></h1>
<?= $form ?>
</div>

View File

@ -1,6 +1,5 @@
<div>
<h4><?= $this->translate('Saving Configuration Failed'); ?></h4>
<br>
<p>
<?= sprintf(
$this->translate('The file %s couldn\'t be stored. (Error: "%s")'),
@ -25,4 +24,4 @@
<pre>
<code><?= $this->escape($configString); ?></code>
</pre>
</p>
</p>

View File

@ -404,10 +404,9 @@ class Manager
}
/**
* Return an array containing all loaded modules
* Get the currently loaded modules
*
* @return array
* @see Module
* @return Module[]
*/
public function getLoadedModules()
{

View File

@ -5,6 +5,7 @@
namespace Icinga\Authentication;
use Icinga\Application\Config;
use Icinga\Application\Logger;
use Icinga\Exception\NotReadableError;
use Icinga\Data\ConfigObject;
use Icinga\User;
@ -43,72 +44,46 @@ class AdmissionLoader
}
/**
* Get user permissions
* Get user permissions and restrictions
*
* @param User $user
* @param User $user
*
* @return array
*/
public function getPermissions(User $user)
public function getPermissionsAndRestrictions(User $user)
{
$permissions = array();
$restrictions = array();
$username = $user->getUsername();
try {
$config = Config::app('permissions');
$roles = Config::app('roles');
} catch (NotReadableError $e) {
Logger::error(
'Can\'t get permissions for user \'%s\'. An exception was thrown:',
$user->getUsername(),
'Can\'t get permissions and restrictions for user \'%s\'. An exception was thrown:',
$username,
$e
);
return $permissions;
return array($permissions, $restrictions);
}
$username = $user->getUsername();
$userGroups = $user->getGroups();
foreach ($config as $section) {
if (! empty($section->permissions)
&& $this->match($username, $userGroups, $section)
) {
foreach ($roles as $role) {
if ($this->match($username, $userGroups, $role)) {
$permissions = array_merge(
$permissions,
array_diff(String::trimSplit($section->permissions), $permissions)
array_diff(String::trimSplit($role->permissions), $permissions)
);
$restrictionsFromRole = $role->toArray();
unset($restrictionsFromRole['users']);
unset($restrictionsFromRole['groups']);
unset($restrictionsFromRole['permissions']);
foreach ($restrictionsFromRole as $name => $restriction) {
if (! isset($restrictions[$name])) {
$restrictions[$name] = array();
}
$restrictions[$name][] = $restriction;
}
}
}
return $permissions;
}
/**
* Get user restrictions
*
* @param User $user
*
* @return array
*/
public function getRestrictions(User $user)
{
$restrictions = array();
try {
$config = Config::app('restrictions');
} catch (NotReadableError $e) {
Logger::error(
'Can\'t get restrictions for user \'%s\'. An exception was thrown:',
$user->getUsername(),
$e
);
return $restrictions;
}
$username = $user->getUsername();
$userGroups = $user->getGroups();
foreach ($config as $section) {
if (! empty($section->restriction)
&& $this->match($username, $userGroups, $section)
) {
$restrictions = array_merge(
$restrictions,
array_diff(String::trimSplit($section->restriction), $restrictions)
);
}
}
return $restrictions;
return array($permissions, $restrictions);
}
}

View File

@ -107,8 +107,9 @@ class Manager
}
$user->setGroups($groups);
$admissionLoader = new AdmissionLoader();
$user->setPermissions($admissionLoader->getPermissions($user));
$user->setRestrictions($admissionLoader->getRestrictions($user));
list($permissions, $restrictions) = $admissionLoader->getPermissionsAndRestrictions($user);
$user->setPermissions($permissions);
$user->setRestrictions($restrictions);
$this->user = $user;
if ($persist) {
$this->persistCurrentUser();

View File

@ -61,12 +61,14 @@ class IniWriter extends Zend_Config_Writer_FileAbstract
{
if (file_exists($this->_filename)) {
$oldconfig = new Zend_Config_Ini($this->_filename);
$content = file_get_contents($this->_filename);
} else {
$oldconfig = new Zend_Config(array());
$content = '';
}
$newconfig = $this->_config;
$editor = new IniEditor(@file_get_contents($this->_filename), $this->options);
$editor = new IniEditor($content, $this->options);
$this->diffConfigs($oldconfig, $newconfig, $editor);
$this->updateSectionOrder($newconfig, $editor);
return $editor->getText();

View File

@ -46,7 +46,7 @@ class Form extends Zend_Form
/**
* The callback to call instead of Form::onSuccess()
*
* @var Callback
* @var callable
*/
protected $onSuccess;
@ -122,29 +122,11 @@ class Form extends Zend_Form
);
/**
* Create a new form
*
* Accepts an additional option `onSuccess' which is a callback that is called instead of this
* form's method. It is called using the following signature: (Form $form).
*
* @see Zend_Form::__construct()
*
* @throws LogicException In case `onSuccess' is not callable
* (non-PHPDoc)
* @see \Zend_Form::construct() For the method documentation.
*/
public function __construct($options = null)
{
if (is_array($options) && isset($options['onSuccess'])) {
$this->onSuccess = $options['onSuccess'];
unset($options['onSuccess']);
} elseif (isset($options->onSuccess)) {
$this->onSuccess = $options->onSuccess;
unset($options->onSuccess);
}
if ($this->onSuccess !== null && false === is_callable($this->onSuccess)) {
throw new LogicException('The option `onSuccess\' is not callable');
}
// Zend's plugin loader reverses the order of added prefix paths thus trying our paths first before trying
// Zend paths
$this->addPrefixPaths(array(

View File

@ -2,14 +2,54 @@
// {{{ICINGA_LICENSE_HEADER}}}
// {{{ICINGA_LICENSE_HEADER}}}
/* @var $this \Icinga\Application\Modules\Module */
/** @var $this \Icinga\Application\Modules\Module */
$this->providePermission(
'monitoring/command/*',
$this->translate('Allow all commands')
);
$this->providePermission(
'monitoring/command/schedule*',
$this->translate('Allow all scheduling checks and downtimes')
);
$this->providePermission(
'monitoring/command/schedule-check',
$this->translate('Allow scheduling host and service checks')
);
$this->providePermission(
'monitoring/command/schedule-downtime',
$this->translate('Allow scheduling host and service downtimes')
);
$this->providePermission(
'monitoring/command/acknowledge-problem',
$this->translate('Allow acknowledging host and service problems')
);
$this->providePermission(
'monitoring/command/add-comment',
$this->translate('Allow commenting on hosts and services')
);
$this->providePermission(
'monitoring/command/remove*',
$this->translate('Allow removing problem acknowledgements, host and service comments and downtimes')
);
$this->providePermission(
'monitoring/command/remove-acknowledgement',
$this->translate('Allow removing problem acknowledgements')
);
$this->providePermission(
'monitoring/command/remove-comment',
$this->translate('Allow removing host and service comments')
);
$this->providePermission(
'monitoring/command/remove-downtime',
$this->translate('Allow removing host and service downtimes')
);
$this->provideRestriction(
'monitoring/filter',
$this->translate('Restrict views to the hosts and services that match the filter')
);
// TODO: We need to define a useful permission set for this module, the
// list provided here is just an example
$this->providePermission('commands/all', 'Allow to send all commands');
$this->providePermission('commands/safe', 'Allow to to send a subset of "safe" commands');
$this->providePermission('log', 'Allow full log access');
$this->provideRestriction('filter', 'Filter accessible object');
$this->provideConfigTab('backends', array(
'title' => 'Backends',
'url' => 'config'