doctrine / DoctrineMongoDBBundle

Integrates Doctrine MongoDB ODM with Symfony
http://symfony.com/doc/current/bundles/DoctrineMongoDBBundle/index.html
MIT License
377 stars 229 forks source link

How to pass driver_option "autoEncryption" to MongoDB\Client #742

Open carlossosa opened 2 years ago

carlossosa commented 2 years ago

The last few days I have been looking a way (or workaround) to pass the "autoEncryption" but nothing. The only option accepted by the Bundle under "driver_options" is "context" which is considered deprecated. I'll try to use a CompilerPass to override the "doctrine_mongodb.odm.default_connection" with this option.

My request is ( if the resources and the time allows it) to support this driver option. I'll try to learn more about the Bundle and create Pull request to support this option.

carlossosa commented 2 years ago

Helo,

I created a public gist with this workaround ( perhaps not the most "fancy" way but it works) but still needs testing. I post this here to help anyone who has a similar problem.

# services.yaml
    # Resolver
    App\DependencyInjection\MongoBinaryEnvVarProcessor:
        tags:
            - { name: container.env_var_processor, priority: -1 }
# config.yaml
app:
  autoEncryption:
    keyVaultNamespace: "%env(MONGODB_DB)%.keyVault"
    kmsProviders:
      aws:
        accessKeyId: "%env(AWS_KEY)%"
        secretAccessKey: "%env(AWS_SECRET)%"
    schemaMap:
      "%env(MONGODB_DB)%.clients":
        bsonType: "object"
        encryptMetadata:
          keyId: ["%env(mongoBinary:base64:MONGO_KEY_ID)%"]
          algorithm: "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
        properties:
          ssn:
            encrypt: true
<?php

namespace App;

use App\DependencyInjection\AppExtension;
use App\DependencyInjection\Compiler\MongoODMConfigurePass;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel
{
    use MicroKernelTrait;

    protected function build(ContainerBuilder $container)
    {
        $container->registerExtension(new AppExtension());
        $container->addCompilerPass(new MongoODMConfigurePass());
    }
}
<?php

namespace App\DependencyInjection;

use MongoDB\BSON\Binary;
use Symfony\Component\DependencyInjection\EnvVarProcessorInterface;

class MongoBinaryEnvVarProcessor implements EnvVarProcessorInterface
{

    public function getEnv(string $prefix, string $name, \Closure $getEnv)
    {
        $binary = $getEnv( $name);

        if ( $binary === null) {
            return null;
        }

        return new Binary($binary, Binary::TYPE_UUID);
    }

    public static function getProvidedTypes()
    {
        return [
            'mongoBinary' => "string",
        ];
    }
}
<?php

namespace App\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;

class AppExtension extends Extension
{
    protected array $config = [];

    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();

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

    public function getConfig(): array
    {
        return $this->config;
    }
}
<?php

namespace App\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $bsonTypes = [
            'string',
            'int',
            'float',
            'bool',
            'object',
            'array',
            'binary',
            'date',
            'timestamp',
            'regex',
            'dbPointer',
            'javascript',
            'symbol',
            'javascriptWithScope',
            'int64',
            'minKey',
            'maxKey',
            'numberLong',
        ];

        $algorithms = ['AEAD_AES_256_CBC_HMAC_SHA_512-Random','AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'];

        $tree = new TreeBuilder('app');
        $tree->getRootNode()
                ->children()
                    ->arrayNode('autoEncryption')
                        ->children()
                            ->scalarNode('keyVaultNamespace')->isRequired()->end()
                            ->arrayNode('kmsProviders')
                                ->isRequired()
                                ->requiresAtLeastOneElement()
                                ->useAttributeAsKey('name')
                                ->arrayPrototype()
                                    ->children()
                                        ->scalarNode('accessKeyId')->isRequired()->end()
                                        ->scalarNode('secretAccessKey')->isRequired()->end()
                                    ->end() // children
                                ->end() // arrayPrototype
                            ->end() // kmsProviders
                            ->arrayNode('schemaMap')
                                ->isRequired()
                                ->useAttributeAsKey('name')
                                ->arrayPrototype()
                                    ->children()
                                        ->enumNode('bsonType')->defaultValue('object')->values($bsonTypes)->end()
                                        ->arrayNode('encryptMetadata')
                                            ->children()
                                                ->arrayNode('keyId')->scalarPrototype()->end()->isRequired()->end()
                                                ->enumNode('algorithm')->values($algorithms)->defaultValue('AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic')->end()
                                            ->end() // children
                                        ->end() // encryptMetadata
                                        ->arrayNode('properties')
                                            ->isRequired()
                                            ->requiresAtLeastOneElement()
                                            ->useAttributeAsKey('name')
                                            ->arrayPrototype()
                                                ->children()
                                                    ->arrayNode('encrypt')
                                                        ->treatTrueLike(['keyId' => null])
                                                        ->children()
                                                            ->enumNode('bsonType')->defaultValue('string')->values($bsonTypes)->end()
                                                            ->enumNode('algorithm')->values($algorithms)->defaultValue('AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic')->end()
                                                            ->arrayNode('keyId')
                                                                ->validate()
                                                                    ->ifEmpty()->thenUnset()
                                                                ->end() // validate
                                                                ->scalarPrototype()->end() // scalarPrototype
                                                            ->end() // keyId
                                                        ->end() // children
                                                    ->end() // encrypt
                                                ->end() // children
                                            ->end() // arrayPrototype
                                        ->end() // array node
                                    ->end() // children
                                ->end() // arrayPrototype
                            ->end() // schemaMap
                        ->end() // end of mongoAutoEncryption
                    ->end() // mongoAutoEncryption
                ->end() // app
            ;

        return $tree;
    }
}
<?php

namespace App\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class MongoODMConfigurePass implements CompilerPassInterface
{

    public function process(ContainerBuilder $container)
    {
        $config = $container->getExtension('app')->getConfig();
        $defaultConnection = $container->getDefinition("doctrine_mongodb.odm.default_connection");

        $arg = $defaultConnection->getArgument(2);

        if (array_key_exists('autoEncryption', $config)) {
            $arg['autoEncryption'] = $config['autoEncryption'];
        }
        $defaultConnection->setArgument(2, $arg);
    }
}

This is an optional step but required when using AWS KMS, this command register the master key from AWS KMS into the MongoDB Vault collection

<?php

namespace App\Command\System;

use MongoDB\Client;
use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(name: "app:system:create-mongo-encrypt-key")]
class CreateMongoEncryptKeyCommand extends Command
{
    public function __construct(protected AbstractVault $vault)
    {
        parent::__construct();
    }

    protected function configure()
    {
        $this->addArgument('key-id', InputArgument::REQUIRED, 'Key ID');
        $this->addArgument('aws-region', InputArgument::OPTIONAL, 'AWS Region', 'us-east-1');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $io = new SymfonyStyle($input, $output);

        $kms = [
            "aws" => [
                "accessKeyId" => $_ENV['AWS_KEY'],
                "secretAccessKey" => $_ENV['AWS_SECRET'],
            ]
        ];

        $masterKey = [
            "key" => $input->getArgument("key-id"),
            "region" => $input->getArgument('aws-region'),
        ];

        $mongoClient = new Client($_ENV['MONGODB_URL']);
        $encryptClient = $mongoClient->getManager()->createClientEncryption([
            "keyVaultNamespace" => $_ENV['MONGODB_DB'].".keyVault",
            "kmsProviders" => $kms
        ]);

        $keyId = $encryptClient->createDataKey('aws', [
            "masterKey" => $masterKey
        ]);

        if ($this->vault->generateKeys()) {
            $io->success($this->vault->getLastMessage());
        }

        $this->vault->seal("MONGO_KEY_ID", base64_encode($keyId->getData()));

        $io->success($this->vault->getLastMessage() ?? 'Secret was successfully stored in the vault.');

        return self::SUCCESS;
    }
}
malarzm commented 2 years ago

@carlossosa thanks for posting your solution, I'm glad you're figured this out! If you have some time on your hands I'd love to have this ironed out and incorporated into the bundle. Feel free to hop on our Slack and ask if you'd need help :)