Open carlossosa opened 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);
}
}
<?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;
}
}
@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 :)
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.