Skip to content

Commit

Permalink
Merge 8acf82f into 1347f3e
Browse files Browse the repository at this point in the history
  • Loading branch information
henrym2 committed Jul 24, 2020
2 parents 1347f3e + 8acf82f commit dbef1bc
Show file tree
Hide file tree
Showing 15 changed files with 475 additions and 72 deletions.
46 changes: 32 additions & 14 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,49 @@ public function getConfigTreeBuilder()
//!Work in progress, config tree to be constructed in Issues #7 and #3
$rootNode
->children()
->append($this->getReportConfig())
->arrayNode('coop')
->children()
->booleanNode('active')->defaultFalse()->end()
->end()
->arrayNode('defaults')
->append($this->getReportConfig())
->append($this->getCOEP())
->append($this->getCOOP())
->append($this->getFetchmetaData())
->end()
->append($this->getFetchmetaData())

->arrayNode('paths')
->useAttributeAsKey('path')
->normalizeKeys(false)
->prototype('array')
->append($this->getReportConfig())
->append($this->getCOEP())
->append($this->getCOOP())
->append($this->getFetchmetaData())
->end()
->end()
;

return $treeBuilder;
}

private function getCOOP(): ScalarNodeDefinition
private function getCOOP(): ArrayNodeDefinition
{
$node = new ScalarNodeDefinition('coop');
$node->defaultValue('same-origin');
$node = new ArrayNodeDefinition('coop');
$node
->children()
->booleanNode('active')->defaultTrue()->end()
->booleanNode('policy_overwrite')->defaultFalse()->end()
->scalarNode('policy')->defaultValue('same-origin')->end()
->end();
return $node;
}

private function getCOEP(): EnumNodeDefinition
private function getCOEP(): ArrayNodeDefinition
{
$node = new EnumNodeDefinition('coep');
$node->defaultValue('require-corp')
->values(['require-corp', 'report-only']);
$node = new ArrayNodeDefinition('coep');
$node
->children()
->booleanNode('active')->defaultTrue()->end()
->booleanNode('policy_overwrite')->defaultFalse()->end()
->scalarNode('policy')->defaultValue('require-corp')->end()
->end();
return $node;
}

Expand All @@ -51,7 +69,7 @@ private function getFetchmetaData()
$node = new ArrayNodeDefinition('fetch_metadata');
$node->children()
->booleanNode('active')->defaultFalse()->end()
->scalarNode('fetch_metadata_policy')->defaultNull()->end()
->scalarNode('policy')->defaultNull()->end()
->arrayNode('allowed_endpoints')->prototype('scalar')->defaultValue(array())->end()
->end();
return $node;
Expand Down
21 changes: 9 additions & 12 deletions DependencyInjection/IseWebSecurityExtension.php
Original file line number Diff line number Diff line change
@@ -1,36 +1,33 @@
<?php
namespace Ise\WebSecurityBundle\DependencyInjection;

use Exception;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Yaml\Yaml;

class IseWebSecurityExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
//!Work in progress as described in Configuration.php To be rebuild in #7 and #3
$configuration = new Configuration();

$config = $this->processConfiguration($configuration, $configs);

$container->setParameter('ise_security.coop.active', $config['coop']);
$container->setParameter('ise_security.fetch_metadata.active', $config['fetch_metadata']['active']);
$defaults = $config['defaults'];

$loader = new YamlFileLoader(
$container,
new FileLocator(__DIR__.'/../Resources/config')
);
$loader->load('services.yaml');

$fetchMetaDataSubscriber = $container->getDefinition("ise_fetch_metadata.subscriber");
if ($config['fetch_metadata']['fetch_metadata_policy'] !== null) {
$fetchMetaDataSubscriber->setArgument(1, new Reference($config['fetch_metadata']['fetch_metadata_policy']));
}

$fetchMetaDataDefaultPolicy = $container->getDefinition("ise_fetch_metadata.default_policy");
$fetchMetaDataDefaultPolicy->setArgument(1, $config['fetch_metadata']['allowed_endpoints']);

$configProvider = $container->getDefinition('ise_config.provider');
$configProvider->setArgument('$defaults', $defaults);
$configProvider->setArgument('$paths', $config['paths']);
}
}
21 changes: 12 additions & 9 deletions EventSubscriber/FetchMetadataRequestSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@

namespace Ise\WebSecurityBundle\EventSubscriber;

use Ise\WebSecurityBundle\Policies\FetchMetadataPolicyInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Ise\WebSecurityBundle\Options\ConfigProviderInterface;
use Ise\WebSecurityBundle\Policies\FetchMetadataPolicyProvider;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class FetchMetadataRequestSubscriber implements EventSubscriberInterface
{
private $fetchMetadataPolicy;
public function __construct(FetchMetadataPolicyInterface $fetchMetadataPolicy, ContainerInterface $container)
private $fetchMetadataPolicyProvider;
private $configProvider;
public function __construct(FetchMetadataPolicyProvider $fetchMetadataPolicyProvider, ConfigProviderInterface $configProvider)
{
$this->active = $container->getParameter('ise_security.fetch_metadata.active');
$this->fetchMetadataPolicy = $fetchMetadataPolicy;
$this->fetchMetadataPolicyProvider = $fetchMetadataPolicyProvider;
$this->configProvider = $configProvider;
}
public static function getSubscribedEvents()
{
Expand All @@ -30,8 +30,11 @@ public static function getSubscribedEvents()
public function requestEvent(RequestEvent $event): void
{
$request = $event->getRequest();
if ($this->active) {
if (!$this->fetchMetadataPolicy->applyPolicy($request)) {
$options = $this->configProvider->getPathConfig($request);
$fetchMetadataPolicy = $this->fetchMetadataPolicyProvider->getFetchMetadataPolicy($options['fetch_metadata']);

if ($options['fetch_metadata']['active']) {
if (!$fetchMetadataPolicy->applyPolicy($request)) {
$response = new Response('', Response::HTTP_FORBIDDEN);
$response->headers->set('Vary', 'sec-fetch-site, sec-fetch-dest, sec-fetch-mode');
$event->setResponse($response);
Expand Down
24 changes: 16 additions & 8 deletions EventSubscriber/ResponseSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@

namespace Ise\WebSecurityBundle\EventSubscriber;

use Ise\WebSecurityBundle\Options\ConfigProviderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class ResponseSubscriber implements EventSubscriberInterface
{
private $active;
public function __construct(ContainerInterface $container)
private $configProvider;

public function __construct(ConfigProviderInterface $configProvider)
{
$this->active = $container->getParameter('ise_security.coop.active');
$this->configProvider = $configProvider;
}

public static function getSubscribedEvents()
Expand All @@ -28,12 +30,18 @@ public function responseEvent(ResponseEvent $event)
{
//!WIP, to be broken out into policy handler class
$response = $event->getResponse();
if ($this->active) {
$response->headers->set("Cross-Origin-Resource-Policy", "same-origin");
$response->headers->set("Content-Security-Policy-Report-Only", 'default-src');
$response->headers->set('Cross-Origin-Opener-Policy', 'same-origin');
$response->headers->set('Cross-Origin-Embedder-Policy', 'require-corp');
$request = $event->getRequest();

$options = $this->configProvider->getPathConfig($request);

if ($options['coop']['active']) {
$response->headers->set('Cross-Origin-Opener-Policy', $options['coop']['policy']);
}

if ($options['coep']['active']) {
$response->headers->set('Cross-Origin-Embedder-Policy', $options['coep']['policy']);
}

$event->setResponse($response);
}
}
52 changes: 52 additions & 0 deletions Options/ConfigProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace Ise\WebSecurityBundle\Options;

use Symfony\Component\HttpFoundation\Request;

/**
* ConfigurationProvider
* Class implements the ConfigProviderInterface, parses paths and defaults config provided by the container to provide configuration for the current request route.
*/
class ConfigProvider implements ConfigProviderInterface
{
/**
* Paths configuration, populated via container injection
*
* @var [mixed]
*/
private $paths;
/**
* Defaults configuration, merged with per path config to ensure defaults are overwritten
*
* @var [mixed]
*/
private $defaults;

public function __construct($defaults = [], $paths = [])
{
$this->defaults = $defaults;
$this->paths = $paths;
}
/**
* getPathConfig parses the request uri and attempt to match it against a path config, where the path config is used as a regex to match against the uri.
* If no config is found, the default config is returned.
* If a config is found, then the default and the path config is merged and returned in $options.
* Path config is order dependant. IF '^/api' comes before '^/' in the config, then '^/api' will be applied and '^/' disregarded provided the first matches.
* @param Request $request The request that is to be configured
* @return array The Config to be applied to the Request.
*/
public function getPathConfig(Request $request): array
{
$uri = $request->getPathInfo() ?: '/';
foreach ($this->paths as $pathReg => $options) {
//Check if there is a config that matches the ui
if (preg_match('{'.$pathReg.'}i', $uri)) {
$options = array_merge($this->defaults, $options);
return $options;
}
}
//Return the defaults if no path configs are found
return $this->defaults;
}
}
13 changes: 13 additions & 0 deletions Options/ConfigProviderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Ise\WebSecurityBundle\Options;

use Symfony\Component\HttpFoundation\Request;

/**
* ConfigProviderInterface for creating and defining Configuration providers.
*/
interface ConfigProviderInterface
{
public function getPathConfig(Request $request): array;
}
4 changes: 2 additions & 2 deletions Policies/FetchMetadataDefaultPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ class FetchMetadataDefaultPolicy implements FetchMetadataPolicyInterface
{
/**
* CorsEndpoints denotes the allowed cross origin endpoints as part of the Default Fetch metadata policy
* @var array
* @var array [string]
* Example: [ '/images', '/api', '/health' ]
*/
private $corsEndpoints;

public function __construct($corsEndpoints = [])
public function __construct($corsEndpoints)
{
$this->corsEndpoints = $corsEndpoints;
}
Expand Down
29 changes: 29 additions & 0 deletions Policies/FetchMetadataPolicyProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Ise\WebSecurityBundle\Policies;

use InvalidArgumentException;

/** Factory Class for handling dynamic injection of Fetch Metadata Policies.
* Fetch Metadata policies are injected based on the pathConfig for a particular request. If no policy is configured the default Policy is returned.
* If a configured class does not implement the FetchMetadataPolicyInterface, then an Exception is thrown.
* @param array $pathConfig Array of path configuration generated by a ConfigurationProvider
* @return FetchMetadataPolicyInterface
*/
class FetchMetadataPolicyProvider
{
public function getFetchMetadataPolicy(array $pathConfig): FetchMetadataPolicyInterface
{
if (isset($pathConfig['policy'])) {
$policy = new $pathConfig['policy'];
if (is_subclass_of($policy, FetchMetadataPolicyInterface::class)) {
return $policy;
} else {
throw new InvalidArgumentException("Policy ".$pathConfig['policy']." does not implement FetchMetadataPolicyInterface and may not be a valid policy");
}
} else {
$allowedOrigins = $pathConfig["allowed_origins"] ?? [];
return new FetchMetadataDefaultPolicy($allowedOrigins);
}
}
}
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ To install the bundle on a project, add the following lines to your composer.jso
]
```

Due to a lack of Symfony Flex recipe to do so automatically. In your projects `/config/packages` folder, create `ise_web_security.yaml` and populate it with the yaml config detailed below.

## Config

>WIP, Config will change over time
Expand All @@ -41,9 +43,23 @@ the majority of the features.
```yaml
ise_web_security:
coop: true
fetch_metadata:
defaults:
coop:
active: false
coep:
active: true
policy: 'require-corp'
fetch_metadata:
active: true
paths:
'^/user':
coop:
active: true
policy: 'same-origin'
coep:
active: false
fetch_metadata:
active: false
```

## 🤝 Contributing
Expand Down

0 comments on commit dbef1bc

Please sign in to comment.