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

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

Open
carlossosa opened this issue Aug 3, 2022 · 2 comments
Open

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

carlossosa opened this issue Aug 3, 2022 · 2 comments
Labels

Comments

@carlossosa
Copy link

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.

@malarzm malarzm added this to the 4.6.0 milestone Aug 3, 2022
@malarzm malarzm added the Feature label Aug 3, 2022
@carlossosa
Copy link
Author

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
Copy link
Member

malarzm commented Aug 6, 2022

@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 :)

@franmomu franmomu modified the milestones: 4.6.0, 4.7.0 Jul 26, 2023
@alcaeus alcaeus removed this from the 4.7.0 milestone Dec 22, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants