Problem/Motivation

Right now, the source database credentials provided to begin an upgrade process are persisted by saving them to every migration configuration entity (as part of the 'source' key) created from the templates. #2455573: Support migration groups would at least save them to one central place, the group, but storing the credentials to a Drupal database in plain text in configuration is... sub-optimal.

Some options:

  • The approach we used in Drupal 7 - encrypt the credentials and store them in configuration.
    • Advantages - simplicity, keeping the configuration entity self-contained (not needing to reach outside itself to figure out how to get its data).
    • Disadvantages - on general principle, keeping one database's credentials in another database (even encrypted) seems risky. A D7 issue with using drupal_private_key is when deploying the developed migration to another environment, the credentials can not be decrypted without changing that environment's drupal_private_key to that of the original environment.
  • Don't persist the credentials - provide them explicitly through the front end for every operation (the original migrate-manifest implementation).
    • Advantages - credentials not persisted, no worries about them getting stolen from the environment.
    • Disadvantages - bad UX unless you're doing a single one-time migration, can't do things like migrate-status without having to re-enter the credentials.
  • Store credentials in state. Whether deployability of the source database credentials is a good or bad thing may differ from project to project.
    • Advantages - the credentials would not be deployable with configuration.
    • Disadvantages - the credentials would not be deployable with configuration.
  • Add to settings.php, as the install process does for the host Drupal database credentials.
    • Advantages - same level of security as host Drupal database credentials.
    • Disadvantages - writing settings.php is gross.

Proposed resolution

?

Remaining tasks

Figure out both an optimal solution for migrate_upgrade, and general best practices for any migration handling potentially sensitive source data credentials.

User interface changes

API changes

Data model changes

Comments

mikeryan created an issue. See original summary.

webchick’s picture

Priority: Normal » Major
Issue tags: +Migrate critical

I'm going to raise this to a migrate critical, since it seems like it's required prior to calling Migrate "supported" in D8 core.

mikeryan’s picture

Issue summary: View changes
mikeryan’s picture

mikeryan’s picture

Project: Migrate Upgrade » Drupal core
Version: 8.x-1.x-dev » 8.0.x-dev
Component: Miscellaneous » migration system
Category: Task » Plan

Moving to core queue - establishing best practices is a general migration system issue, and there may be changes to the core API depending on what we decide here.

phenaproxima’s picture

Right off the bat I would rule out options #2 and #4. The UX of #2 is so abysmal that most users will probably hate us, and writing to settings.php is not merely gross, but super-annoying and potentially dangerous (permissions issues). In my mind, only #1 and #3 are viable options.

On balance, #3 (state) seems like the best option to me. It cannot be persisted across environments, true, but at least it won't be stored in plain text for all to see. I don't think it's much to ask for people to re-enter their DB credentials when switching between environments -- they only have to do it once per environment and there's a good chance that, most of the time, their credentials will differ across environments anyway. I think state offers us the best balance between UX and (such as it is) security concerns, and I'd be willing to sacrifice portability for those.

benjy’s picture

#3 isn't a bad option although we still end up with plain text credentials in the database.

I actually think a combination of #2 and #4 could be great. Eg, you can enter the details in the form, but, if the user has put an entry in their settings file then that takes precedence. We don't write to settings.php, they can simply be put there by a developer? That works well for developers who often have settings.local.php files anyway per environment.

mikeryan’s picture

I started playing with this a couple of weeks ago, then got distracted by other things (and apparently managed to blow away my feature branch). Anyway, what I was playing with was

  1. SQL source plugin has a database_state_key - the name of a State key containing minimally 'key', and potentially 'target' and 'database' keys as well, with the same meanings as the existing config (except we should require 'key', we should not default it to 'migrate').
  2. Where migrate_upgrade is now adding the key and database to the source plugin config, it would instead create a state key like 'drupal_6_upgrade' and store it there, and add the name of that key to the source plugin config.
  3. When the DB settings are in settings.php, only the key needs to be set.
  4. Keep the configuration ability as well, falling back to it when database_state_key is not set. The core upgrade tool will use state, but a general-purpose UI could ask "Should the database credentials be deployable?" and use config instead of state when the answer is yes.

In the lost branch this was basically working, the challenge was getting the tests working without the default key of 'migrate'...

phenaproxima’s picture

This is where we start to intersect with #2552791: MigrateSqlSource should use dependency injection. It should be Migrate Upgrade's job to persist the source DB credentials as it sees fit, and it should be the thing responsible for passing a fully initialized Connection object to the source plugins. Ideally, I'd like to see all of this done in the same patch.

mikeryan’s picture

Status: Active » Needs review
StatusFileSize
new4.1 KB

Here's an implementation of my proposal - the migrate_upgrade patch that uses this is at https://www.drupal.org/node/2574337#comment-10368365 (the tools in migrate_plus need no changes to work with it).

phenaproxima’s picture

  1. +++ b/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php
    @@ -58,20 +58,43 @@ public function __toString() {
    +        // @todo: Inject the state service.
    +        $database_state = \Drupal::state()->get($this->configuration['database_state_key']);
    

    Are we going to have a follow-up issue for injecting this dependency?

  2. +++ b/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php
    @@ -58,20 +58,43 @@ public function __toString() {
    +        if (isset($database_state['target'])) {
    +          $target = $database_state['target'];
    +        }
    +        else {
    +          $target = 'default';
    +        }
    +        if (isset($database_state['key'])) {
    +          $key = $database_state['key'];
    +        }
    +        else {
    +          $key = 'migrate';
    +        }
    

    Just for brevity's sake, can these use ternary syntax instead of if/else blocks?

  3. +++ b/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php
    @@ -58,20 +58,43 @@ public function __toString() {
    +        if (isset($this->configuration['target'])) {
    +          $target = $this->configuration['target'];
    +        }
    +        else {
    +          $target = 'default';
    +        }
    +        if (isset($this->configuration['key'])) {
    +          $key = $this->configuration['key'];
    +        }
    +        else {
    +          $key = 'migrate';
    +        }
    

    Ditto here.

    Actually, these two code blocks (using state or $this->configuration) are awfully similar -- seems to me they could be refactored into a single method.

mikeryan’s picture

StatusFileSize
new11.03 KB
new9.18 KB
phenaproxima’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php
    @@ -34,10 +37,31 @@
    +  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
    +    return new static(
    +      $configuration,
    +      $plugin_id,
    +      $plugin_definition,
    +      $migration,
    +      \Drupal::state()
    +    );
    

    \Drupal::state() should be $container->get('state').

  2. +++ b/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php
    @@ -61,47 +85,44 @@
    +   * @param $database_info
    

    Needs a type hint and documentation.

  3. +++ b/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php
    @@ -61,47 +85,44 @@
    +  protected function setUpDatabase($database_info) {
    

    Should be type hinted.

  4. +++ b/core/modules/migrate_drupal/src/Plugin/migrate/source/DrupalSqlBase.php
    @@ -51,8 +52,8 @@
    @@ -89,6 +90,7 @@ public static function create(ContainerInterface $container, array $configuratio
    
    @@ -89,6 +90,7 @@ public static function create(ContainerInterface $container, array $configuratio
           $plugin_id,
           $plugin_definition,
           $migration,
    +      \Drupal::state(),
           $container->get('entity.manager')
    

    $container->get('state')

  5. +++ b/core/modules/migrate_drupal/tests/src/Unit/source/d6/Drupal6SqlBaseTest.php
    @@ -68,8 +68,9 @@ class Drupal6SqlBaseTest extends MigrateTestCase {
    +    $state = $this->getmock('Drupal\Core\State\StateInterface');
    

    Er...should be getMock() :)

mikeryan’s picture

Status: Needs work » Needs review
StatusFileSize
new11.86 KB
new3.64 KB
phenaproxima’s picture

Status: Needs review » Reviewed & tested by the community

Hot diggity daffodil! This looks great to me. Presuming the PostrgeSQL and SQLite bots approve, methinks this is RTBC.

webchick’s picture

Status: Reviewed & tested by the community » Fixed

Great work. This solution indeed seems inline with how we handle sensitive data in other parts of the system.

Committed and pushed to 8.0.x. Thanks!

  • webchick committed 7fdb772 on 8.0.x
    Issue #2548977 by mikeryan, phenaproxima, benjy: How best to persist...

Status: Fixed » Needs work

The last submitted patch, 14: how_best_to_persist-2548977-14.patch, failed testing.

jeroent’s picture

Status: Needs work » Fixed

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.