Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ Latest
* [#149](https://github.com/cleverage/ui-process-bundle/issues/149) Clean up composer.json: remove the unused `symfony/dotenv`, `symfony/runtime`, `ext-ctype` and `ext-iconv` requirements (applications relying on the bundle to install `symfony/dotenv` / `symfony/runtime` must require them, as the Symfony skeleton does); replace `doctrine/common` by `doctrine/collections` (only `ArrayCollection` is used); require `twig/twig`, `symfony/twig-bundle`, `psr/log`, `doctrine/persistence`, `doctrine/migrations` and `symfony/event-dispatcher-contracts`, used directly but only installed transitively; add `twig/extra-bundle` and `symfony/property-access` (used by the tests) to the dev requirements.
* [#150](https://github.com/cleverage/ui-process-bundle/issues/150) Require `symfony/monolog-bundle` and `monolog/monolog`, used directly by the bundle (log handlers, `monolog` configuration prepended, `monolog.logger` tags) but only installed through `cleverage/process-bundle`, which may stop requiring MonologBundle (cleverage/process-bundle#269).

## Fixes
* [#169](https://github.com/cleverage/ui-process-bundle/issues/169) Security: the configuration added by the bundle to the `security` extension (`process_user_provider` user provider, provider, login form, logout and API token authenticator of the `main` firewall) overrides the application one, it can now be disabled with the new `security.configure_firewall` option (default `true`, same behaviour as before; `false`: no `security` configuration is added, the application configures the provider and the firewall, e.g. a firewall dedicated to the UI). The `monolog`, `doctrine_migrations` and `security` configurations are skipped when the bundle is not enabled, instead of a `LogicException`. The documentation no longer says that these configurations are prepended (only the Messenger one is). Update documentation, add tests.

v3.2
------

Expand Down
6 changes: 4 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ See [routes](reference/03-users_and_security.md#routes).
### Security

The bundle automatically configures a user provider and the `main` firewall (login form, logout, API token
authenticator). You only need a password hasher for the bundle `User` entity, which is the Symfony default:
authenticator), overriding the provider, login form and logout of your `main` firewall. You only need a password hasher
for the bundle `User` entity, which is the Symfony default:

```yaml
# config/packages/security.yaml
Expand All @@ -81,7 +82,8 @@ security:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
```

See [users & security](reference/03-users_and_security.md).
To configure the security in your application instead (e.g. on a dedicated firewall), set
`clever_age_ui_process.security.configure_firewall` to `false`. See [users & security](reference/03-users_and_security.md).

### Database

Expand Down
8 changes: 6 additions & 2 deletions docs/reference/01-bundle_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Every key is optional. Default values are shown below.
clever_age_ui_process:
security:
roles: ['ROLE_ADMIN']
configure_firewall: true
logs:
store_in_database: true
database_level: Info # Debug when kernel.environment is "dev"
Expand Down Expand Up @@ -38,6 +39,7 @@ Options
| Key | Type | Default | Description |
|---------|------------|------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
| `roles` | `string[]` | `['ROLE_ADMIN']` | Roles that can be assigned to a user in the UI user edit form. `ROLE_USER` is always granted to every user (see [users & security](03-users_and_security.md)). |
| `configure_firewall` | `bool` | `true` | Add the `process_user_provider` user provider and configure the `main` firewall of the application (overriding its provider, login form and logout). `false`: no `security` configuration is added, configure it in your application (see [users & security](03-users_and_security.md#configuring-the-security-in-your-application)). Must be a literal boolean (no parameter nor environment variable). |

### logs

Expand Down Expand Up @@ -103,11 +105,13 @@ parameters:
Configuration added to other bundles
------------------------------------

The bundle prepends some configuration to other bundles, so that it works out of the box:
The bundle configures other bundles, so that it works out of the box. The Messenger configuration is prepended: your
application configuration overrides it. The other ones are added after your application configuration, so they override
it. Each one is skipped when the bundle is not enabled.

| Bundle | Configuration |
|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `monolog` | `pb_ui_file` and `pb_ui_orm` handlers (with their `*_filter` handlers) listening to the `cleverage_process` and `cleverage_process_task` channels. See [process executions & logs](04-process_executions_and_logs.md). |
| `doctrine_migrations` | Migrations path `CleverAge\UiProcessBundle\Migrations`. |
| `framework` | Messenger `execute_process` transport (`doctrine://default`, no retry) and routing of `ProcessExecuteMessage` to it. See [messenger](08-messenger.md). |
| `security` | `process_user_provider` user provider and the `main` firewall. See [users & security](03-users_and_security.md). |
| `security` | `process_user_provider` user provider and the `main` firewall, unless `security.configure_firewall` is `false`. See [users & security](03-users_and_security.md). |
57 changes: 55 additions & 2 deletions docs/reference/03-users_and_security.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Users & security
Security configuration
----------------------

The bundle prepends the following configuration to the `security` extension:
The bundle adds the following configuration to the `security` extension:

```yaml
security:
Expand All @@ -26,7 +26,13 @@ security:
clear_site_data: '*'
```

So your application firewall must be named `main`. The only required application configuration is a password
This configuration is added **after** the application one, so it overrides it: the `provider`, `form_login` and `logout`
of your `main` firewall are replaced by the bundle ones (e.g. the `provider: users_in_memory` of the default Symfony
recipe), and the API token authenticator is added to it. So your application firewall must be named `main`, and it is
dedicated to the UI. To keep the control of your security configuration, disable it, see
[configuring the security in your application](#configuring-the-security-in-your-application).

With this configuration, the only required application configuration is a password
hasher for the `User` entity (the Symfony default configuration is enough):

```yaml
Expand All @@ -38,6 +44,53 @@ security:

The UI pages are protected by `#[IsGranted]` attributes, no `access_control` rule is required.

### Configuring the security in your application

Set `clever_age_ui_process.security.configure_firewall` to `false` (it must be a literal boolean, not a parameter nor an
environment variable): the bundle no longer adds any `security` configuration, neither the user provider nor the
firewall. Configure them in your application, on the firewall and with the provider of your choice. The UI requires:

- a user provider of the `CleverAge\UiProcessBundle\Entity\User` entity, by `email`;
- a firewall covering the UI routes, with a login form on the `process_login` route and the logout on the
`process_logout` route;
- the `cleverage_ui_process.security.http_process_execution_authenticator` authenticator on the firewall covering the
`http_process_execute` route, for the [HTTP API](06-http_api.md).

For instance, with the same behaviour as the bundle configuration on a firewall dedicated to the UI:

```yaml
# config/packages/clever_age_ui_process.yaml
clever_age_ui_process:
security:
configure_firewall: false
```

```yaml
# config/packages/security.yaml
security:
providers:
process_user_provider:
entity:
class: CleverAge\UiProcessBundle\Entity\User
property: email
firewalls:
process:
pattern: ^/(process|http/process)
provider: process_user_provider
custom_authenticator: [cleverage_ui_process.security.http_process_execution_authenticator]
form_login:
login_path: process_login
check_path: process_login
logout:
path: process_logout
target: process_login
clear_site_data: '*'
main:
# ... your application firewall
```

The `pattern` depends on the prefix of the [routes](#routes) (here the default ones).

Roles
-----

Expand Down
91 changes: 66 additions & 25 deletions src/DependencyInjection/CleverAgeUiProcessExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use CleverAge\UiProcessBundle\Controller\Admin\UserCrudController;
use CleverAge\UiProcessBundle\Entity\User;
use CleverAge\UiProcessBundle\Message\ProcessExecuteMessage;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\LogicException;
Expand All @@ -37,7 +38,7 @@ public function load(array $configs, ContainerBuilder $container): void
$env = $container->getParameter('kernel.environment');
$configuration = new Configuration($env);
/** @var array{
* security: array{roles: list<string>},
* security: array{roles: list<string>, configure_firewall: bool},
* logs: array{store_in_database: bool, database_level: string, file_level: string, report_increment_level: string, database_max_days: ?int, file_max_days: ?int},
* design: array{logo_path: string},
* notification: array{enabled: bool, ...},
Expand Down Expand Up @@ -83,9 +84,68 @@ public function load(array $configs, ContainerBuilder $container): void
}

/**
* Update default configuration for migrations, twig form theme, assets ...
* Configure the other bundles: Monolog handlers, migrations path, Messenger transport and security.
*
* The Messenger configuration is prepended (the application configuration wins). The other ones are appended, so
* they override the application configuration: in particular the security one replaces the provider, login form
* and logout of the application "main" firewall, unless the "security.configure_firewall" option is false.
*/
public function prepend(ContainerBuilder $container): void
{
if ($container->hasExtension('monolog')) {
$this->configureMonolog($container);
}
if ($container->hasExtension('doctrine_migrations')) {
$container->loadFromExtension(
'doctrine_migrations',
[
'migrations_paths' => ['CleverAge\UiProcessBundle\Migrations' => __DIR__.'/../Migrations'],
]
);
}
$container->prependExtensionConfig(
'framework',
[
'messenger' => [
'transport' => [
[
'name' => 'execute_process',
'dsn' => 'doctrine://default',
'retry_strategy' => ['max_retries' => 0],
],
],
'routing' => [
ProcessExecuteMessage::class => 'execute_process',
],
],
]
);
if ($container->hasExtension('security') && $this->isFirewallConfigured($container)) {
$this->configureSecurity($container);
}
}

/**
* The "security.configure_firewall" option, read before the bundle configuration is processed: the last value
* defined wins, as when it is processed.
*/
private function isFirewallConfigured(ContainerBuilder $container): bool
{
$configureFirewall = true;
foreach ($container->getExtensionConfig($this->getAlias()) as $config) {
if (!\is_array($config['security'] ?? null) || !\array_key_exists('configure_firewall', $config['security'])) {
continue;
}
$configureFirewall = $config['security']['configure_firewall'];
if (!\is_bool($configureFirewall)) {
throw new InvalidConfigurationException(\sprintf('The "%s.security.configure_firewall" option must be a boolean, "%s" given: parameters and environment variables are not supported, as it is read before they are resolved.', $this->getAlias(), get_debug_type($configureFirewall)));
}
}

return $configureFirewall;
}

private function configureMonolog(ContainerBuilder $container): void
{
$container->loadFromExtension(
'monolog',
Expand All @@ -112,29 +172,10 @@ public function prepend(ContainerBuilder $container): void
],
]
);
$container->loadFromExtension(
'doctrine_migrations',
[
'migrations_paths' => ['CleverAge\UiProcessBundle\Migrations' => __DIR__.'/../Migrations'],
]
);
$container->prependExtensionConfig(
'framework',
[
'messenger' => [
'transport' => [
[
'name' => 'execute_process',
'dsn' => 'doctrine://default',
'retry_strategy' => ['max_retries' => 0],
],
],
'routing' => [
ProcessExecuteMessage::class => 'execute_process',
],
],
]
);
}

private function configureSecurity(ContainerBuilder $container): void
{
$container->loadFromExtension(
'security',
[
Expand Down
3 changes: 2 additions & 1 deletion src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ protected function addSecuritySection(ArrayNodeDefinition $node): void
->children()
->arrayNode('security')->addDefaultsIfNotSet()
->children()
->arrayNode('roles')->defaultValue(['ROLE_ADMIN'])->scalarPrototype()->end() // Roles displayed inside user edit form
->arrayNode('roles')->defaultValue(['ROLE_ADMIN'])->scalarPrototype()->end()->end() // Roles displayed inside user edit form
->booleanNode('configure_firewall')->defaultTrue()->end() // add the process_user_provider user provider and configure the main firewall (see CleverAgeUiProcessExtension::prepend()), false to configure the security of the UI in the application
->end()
->end()
->end()
Expand Down
48 changes: 48 additions & 0 deletions tests/DependencyInjection/CleverAgeUiProcessExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
Expand Down Expand Up @@ -228,6 +229,53 @@ public function testPrepend(): void
);
}

public function testPrependWithoutFirewallConfiguration(): void
{
$container = $this->createContainer('prod');
foreach (['monolog', 'doctrine_migrations', 'security'] as $alias) {
$container->registerExtension($this->createExtension($alias));
}
$container->loadFromExtension('security', ['firewalls' => ['main' => ['provider' => 'app_users']]]);
$container->registerExtension(new CleverAgeUiProcessExtension());
$container->loadFromExtension('clever_age_ui_process', ['security' => ['configure_firewall' => true]]);
$container->loadFromExtension('clever_age_ui_process', ['security' => ['configure_firewall' => false]]);

(new CleverAgeUiProcessExtension())->prepend($container);

// Only the application configuration: the last "configure_firewall" value wins
self::assertSame(
[['firewalls' => ['main' => ['provider' => 'app_users']]]],
$container->getExtensionConfig('security')
);
self::assertCount(1, $container->getExtensionConfig('monolog'));
self::assertCount(1, $container->getExtensionConfig('doctrine_migrations'));
}

public function testPrependWithAParameterAsFirewallConfiguration(): void
{
$container = $this->createContainer('prod');
$container->registerExtension($this->createExtension('security'));
$container->registerExtension(new CleverAgeUiProcessExtension());
$container->loadFromExtension('clever_age_ui_process', ['security' => ['configure_firewall' => '%env(bool:UI_FIREWALL)%']]);

$this->expectException(InvalidConfigurationException::class);
$this->expectExceptionMessage('The "clever_age_ui_process.security.configure_firewall" option must be a boolean, "string" given');

(new CleverAgeUiProcessExtension())->prepend($container);
}

public function testPrependWithoutTheConfiguredBundles(): void
{
$container = $this->createContainer('prod');

(new CleverAgeUiProcessExtension())->prepend($container);

self::assertSame([], $container->getExtensionConfig('monolog'));
self::assertSame([], $container->getExtensionConfig('doctrine_migrations'));
self::assertSame([], $container->getExtensionConfig('security'));
self::assertCount(1, $container->getExtensionConfig('framework'));
}

public function testPrependedFrameworkConfigurationComesFirst(): void
{
$container = $this->createContainer('prod');
Expand Down
Loading
Loading