Summary

API credentials are stored in State key value store, which does not allow them to be set from settings.php and is contrary to widely adopted practice

To do

  • Update config definitions to include SF consumer key, secret, and login URL Done!
  • Update references use config instead of state Done!
  • Create an update hook to copy the values from state to config Done!
  • Add instructions in README (done), project page, and example code in salesforce_example to demonstrate how to use

Original post

Right now I can't see how I would set and/or override this module's API credentials in environment-specific settings.php files, as is typically done. Instead of config or settings storage, it seems the creds are currently stored in state (in the database).

If the module stored the keys in settings we could do something like this:

// Sandbox Salesforce config.
$settings['salesforce.consumer_key'] = 'foo';
$settings['salesforce.consumer_secret'] = 'bar';
$settings['salesforce.login_url'] = 'https://test.salesforce.com';

So first: are you all achieving something like this in some way I'm currently unaware of?

If not, I hope to provide a patch. For reasons of security and automation I'd prefer to store our API keys as environment-specific environment variables and provide them to Drupal via settings.php. Ideally we would not have the Salesforce API credentials stored in the Drupal database at all.

Comments

chrisolof created an issue. See original summary.

aaronbauman’s picture

Status: Active » Fixed

They need to stay in state, because rest endpoints change per salesforce org (sandbox vs production).
But, you can override state vars from settings.php similar to config:

global $settings;
$settings['salesforce.consumer_key'] = '12345';
$settings['salesforce.consumer_secret'] = '67890';
chrisolof’s picture

Status: Fixed » Active

@aaronbauman

But, you can override state vars from settings.php similar to config...

I'm not finding this to be the case. If I set one of these in my settings.php file as follows:

$settings['salesforce.consumer_key'] = 'foo';

This has absolutely no effect on what comes back from a drush eval "var_export(\Drupal::state()->get('salesforce.consumer_key'));". What's in my database always comes back - and if I clear the stored value from the DB I get back NULL, regardless of what's in my settings.php file. This corresponds to what I see on the Salesforce authorize form, which I understand to use the same \Drupal::state()->get().

If instead I issue a drush eval "var_export(\Dr\Core\Site\Settings::get('salesforce.consumer_key'));", I do see the "foo" value come back.

Am I missing something? Is this actually working for you?

If not, my proposal is to shift these three API configuration items (salesforce.consumer_key, salesforce.consumer_secret, and salesforce.login_url) from state storage into a storage system that can be easily overridden per environment. Originally I was thinking of just having this stuff defined in settings.php (no form fields / no db storage). I think this could would work well, but might be disruptive to the existing installations that won't have anything defined in settings.php. I suppose we could look first for a setting and fall back to state when that doesn't exist. Looking at other modules I see a pretty big trend toward utilizing config (which does allow settings.php overrides - so this would work too). Config is nice in that a module update hook may be able to move the existing state values into the site's active config, and would still allow for setting the values in the authorize form, which users are accustomed to. You could use settings.php to get the job done, but you wouldn't have to.

Anyway just ideas - thoughts?

aaronbauman’s picture

Mmm, you're right.
Looks like $settings overrides the Drupal\Core\Site\Settings service.
I know there's a way to do this from settings.php but can't find an example - i'll keep looking and let you know

We can switch to using config once drupal core properly supports different config sets per environment.
(Open issue, I'm too lazy to actually find it)

aaronbauman’s picture

After researching, there is no way to override State from settings.php

Config doesn't work for users who want to keep their credentials out of YML (and therefore version control), and requires additional overhead to manage different values per environment.
Settings might be the best option, but since Settings are read-only they can't be exposed in a form.

I'm open to any solutions which will accommodate:

  • setting values through UI
  • settings / overriding values in settings.php
  • keeping values out of version control (and even better, out of the database too)

Maybe a primary source in Settings, with fallback to State would work?

chrisolof’s picture

Maybe a primary source in Settings, with fallback to State would work?

This certainly seems like the simplest solution in terms of implementation and it should have zero impact on existing installations.

My only hesitation with this approach is that every module I've analyzed that also deals with this where-to-store-the-api-keys space is using config. Here are some examples (think of any API-connected Drupal module you've used and you may find the same):

- Commerce stripe
- Commerce paypal
- S3 filesystem
- Mailchimp

Talking with others at DrupalCon it's clear config can be used to supply API keys to Drupal in a way that keeps the (real) keys out of version control and out of the database:

Without contrib:

1. Save and export dummy/placeholder values for the API keys - like "[value-supplied-via-config-override]" or "0000" - you get the idea. These non-sensitive values do get tracked in version control. Or if the API fields are optional you just don't put values into the fields.
2. Settings.php / settings.local.php is used to replace those placeholder/empty values with real ones (currently, awkwardly the overridden values or even the fact that overrides are in place will not show in the UI - but there's a fix in the works for this). See Configuration override system for details and why overridden values aren't shown as default values in config form fields.

With contrib:

While I haven't personally verified this to work, it sounds like folks are using Config ignore in combination with Settings.php / settings.local.php config overrides to completely keep sensitive configuration (like API keys/endpoints) out of their codebase. Again, I haven't verified, but it sounds like dummy/placeholder values aren't necessary because those sensitive items you've config-ignored are not included in the exported config files at all.

Anyway that's what's pulling me toward config here. I don't love it, but it is what the larger community appears to be doing - and it does allow for setting / updating the values through the UI, overriding the values in settings.php, and (in round-about way) keeping the values out of version control (if you know what you're doing).

Thoughts? Personally I'm on the fence...

aaronbauman’s picture

Title: Store API credentials in settings rather than state? » Store API credentials in config rather than state?
Issue summary: View changes

Yeah, as insufficient as it is, sounds like moving this back into config is the way to go.
If that's what other modules are doing, then presumably efforts to improve config management will be moving in that direction as well.

Updated issue summary with todos, but lemme know if i missed anything.

chrisolof’s picture

Issue summary: View changes

Added login URL to what should move into config (login URL also changes for us as we move from prod to staging & development environments - so having that in config means we can override that per environment too).

aaronbauman’s picture

Title: Store API credentials in config rather than state? » Move API credentials to config
Category: Feature request » Task

Changing this to task

chrisolof’s picture

Assigned: Unassigned » chrisolof
chrisolof’s picture

Assigned: chrisolof » Unassigned
Issue summary: View changes
Status: Active » Needs review
StatusFileSize
new14.76 KB

Patch attached. Working well for me, but certainly needs review.

Also wasn't sure where to include details about this in the salesforce_example module, but I've got a new section in the readme dedicated to this.

Anyway let me know what you think and if you have any questions about what's going on in here. Thanks!

Status: Needs review » Needs work

The last submitted patch, 11: salesforce-api-creds-to-config-2961514-11.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

chrisolof’s picture

Assigned: Unassigned » chrisolof

Whoops sorry it looks like I broke some tests - updated patch coming soon...

aaronbauman’s picture

Maybe a cache problem on config?
On these lines:

+    // Check that our config was updated:
+    $this->assertEqual($key, $config->get('consumer_key'));
+    $this->assertEqual($secret, $config->get('consumer_secret'));
+    $this->assertEqual($url, $config->get('login_url'));

try comparing to the results of RestClient::getConsumerKey(), etc

chrisolof’s picture

Status: Needs work » Needs review
StatusFileSize
new16.18 KB
new1.84 KB

Yeah it seemed like the config object in the test was stale. I think this will pass now.

aaronbauman’s picture

Status: Needs review » Needs work

This patch looks great.

Sorry, I should have caught this before: Drupal\salesforce_encrypt\Rest\RestClient needs to be updated too to use config instead of state.

I think this comprises getDecrypted and setDecrypted, as well as the injected dependencies via SalesforceEncryptServiceProvider.

Doesn't look like there's any test coverage for this, but i'm not sure that it's totally necessary. If you can figure out a basic test in < 5 minutes, then go fot it, otherwise just those changes. Thanks again for this patch!

aaronbauman’s picture

PS: planning to get this and #2899460: Handling of field properties into a 3.1 release within the next couple weeks.

chrisolof’s picture

Assigned: chrisolof » Unassigned
Status: Needs work » Needs review
StatusFileSize
new27.65 KB
new11.06 KB

Good catch. I reworked Drupal\salesforce_encrypt\Rest\RestClient so that it focuses more on becoming an encryption/decryption layer atop its parent Drupal\salesforce\Rest\RestClient, rather than doing direct storage and retrieval itself. It no longer cares where each value ends up or is sourced from (state vs config - that's left to the parent to decide), so the move to config no longer breaks it. Should make ongoing maintenance / adjustments easier as they'll cascade better from parent to child (rather than needing to be repeated in both).

Anyway this is working well for me in a test environment but it certainly needs review and more testing.

aaronbauman’s picture

Looks great, love the elegance of this approach too.
I'll try to give this a more thorough review by the end of this week.

Thanks again.

aaronbauman’s picture

This is in, thanks again for your work here.

Only change i had to make is to switch to RestClient::getConsumerKey and ::getConsumerSecret on the auth form (instead of getting it from config) so that it could be decrypted before populating the default input value.

alanburke’s picture

Just a quick note to say thanks for this.
It's working really well for us.

aaronbauman’s picture

Status: Needs review » Fixed

Glad to hear it.

Updated status.

chrisolof’s picture

Awesome! Thanks for bringing this in. We're using it across a number of environments and it seems to be working well.

One concern with the change to RestClient::getConsumerKey and ::getConsumerSecret for default values in the auth form:

RestClient::getConsumerKey and ::getConsumerSecret return immutable config, which means overrides come through. Populating the form with overrides (immutable config) means overrides can creep into config any time someone authorizes a site. It's basically what the new config system is trying to eliminate:

...Drupal 7 had the global $conf variable that was usually populated in settings.php with conditional override values for configuration. A big drawback of that system was that the overrides crept into actual configuration. When a configuration form that contained overridden values was saved, the conditional override got into the actual configuration storage.

- Configuration override system doc page

Suggestion: We show (mutable / non-overridden) config values here in the form, decrypted if necessary.

I'll try and follow with a patch here soon so you can see better what I'm proposing. I think it'll be a pretty simple/small change.

aaronbauman’s picture

Title: Move API credentials to config » Remove immutable config from default form values [was: Move API credentials to config]
Status: Fixed » Active

Ahh - yeah, that makes sense
Reopening

chrisolof’s picture

Status: Active » Needs review
StatusFileSize
new4.18 KB

Attached patch shows mutable / non-overridden config values in the auth form, decrypted when necessary. Needs review.

  • aaronbauman committed fd80671 on 8.x-3.x authored by chrisolof
    Issue #2961514 by chrisolof, aaronbauman: Remove immutable config from...
aaronbauman’s picture

Status: Needs review » Fixed

This is great, thanks. committed to fd80671

As a followup, i'm gonna move encryption profile assignment setting from State to Config

Status: Fixed » Closed (fixed)

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

kaypro4’s picture

Hey there, I think that this change is resulting in the consumer key and secret being blown away (and auth broken) in my deployed environments since we don't have those two keys in config in source.

I'm trying to track what happened here and how best to address. We track both config files (obviously) and settings in source so neither are great for storing the secret value. I could store it as an env var on the server but won't go there unless I have to.

I see some decrypt/encrypt code added as a result of this change so wondering if storing the secret encrypted in config in source is an option. But don't see any info in the readme on the workflow to encrypt the config value.

Sorry for the confusion, and thanks for any support!

Matt

aaronbauman’s picture

@kaypro4 sounds like you need to enable and set up salesforce_encrypt
get your keys set up, create an encryption profile, and assign it to your salesforce_encrypt config.
Then your config values - including consumer key, consumer secret, and identity - will be encrypted and safer to commit to version control