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

do not allow EnvironmentVariableAccess in initializers #1229

Merged
merged 1 commit into from
Jan 11, 2024

Conversation

markokajzer
Copy link
Contributor

@markokajzer markokajzer commented Jan 9, 2024

The description of the cop states Do not access ENV directly after initialization.. However in some ways, the application is already initialized when we get to config/**/*.rb:

  1. config_for(:service) is already available to read config from service.yml
  2. Rails.application.credentials is already available to read application credentials / secrets

I think it could make sense to push people to move their config in either of these two places. If they want to access their config / secrets in a Ruby file, let's say, config/initializers/service.rb they can use credentials or config_for, no need to access ENV directly again.

Also see https://guides.rubyonrails.org/initialization.html.

I understand this is a bit nuanced, so if this is not desired, that's completely fine ✌️


Before submitting the PR make sure the following are checked:

  • The PR relates to only one subject with a clear title and description in grammatically correct, complete sentences.
  • Wrote good commit messages.
  • Commit message starts with [Fix #issue-number] (if the related issue exists).
  • Feature branch is up-to-date with master (if not - rebase it).
  • Squashed related commits together.
  • Added tests.
  • Ran bundle exec rake default. It executes all tests and runs RuboCop on its own code.
  • Added an entry (file) to the changelog folder named {change_type}_{change_description}.md if the new code introduces user-observable changes. See changelog entry format for details.
  • If this is a new cop, consider making a corresponding update to the Rails Style Guide.

@koic
Copy link
Member

koic commented Jan 10, 2024

config/**/*.rb seems to be too broad of a match. For example, in config/boot.rb, it appears that the Rails constant cannot yet be resolved. i.e., uninitialized constant Rails (NameError)

@markokajzer
Copy link
Contributor Author

markokajzer commented Jan 10, 2024

Hmmm... Guess I was a bit too quick.

How about we limit the include to config/initializers? seems like boot.rb, environment.rb, application.rb have already run at that point and Rails has been loaded, see https://guides.rubyonrails.org/configuring.html#using-initializer-files.

After loading the framework and any gems in your application

Would that be an option?

@markokajzer markokajzer changed the title do not allow EnvironmentVariableAccess in Ruby files within config do not allow EnvironmentVariableAccess in initializers Jan 10, 2024
config/default.yml Outdated Show resolved Hide resolved
@koic
Copy link
Member

koic commented Jan 10, 2024

I have left two comments. Can you update these and squash the commits?

@markokajzer
Copy link
Contributor Author

markokajzer commented Jan 10, 2024

Updated, squashed, and added specs for the new cases 👍

@koic koic merged commit 8bcd6fa into rubocop:master Jan 11, 2024
13 checks passed
@koic
Copy link
Member

koic commented Jan 11, 2024

Thanks!

@dogweather
Copy link

dogweather commented Jun 29, 2024

I'm confused: With this rule enabled, how can a Rails app read env vars at all?

E.g., in my initializers, I ENV.fetch() env vars and create constants that I access throughout my app. This seems to be the safest way to do it.

A couple people (or maybe just one person?) has said that the Rails configuration lib can read env vars and check them at boot time. But no one has provided code showing this, so the rule was set disabled by default. I'm skeptical the lib does this: I couldn't find mention of this check-env-var-at-boot feature in the Guide.

In a nutshell, this rule seemed to be on shaky ground before this change. And now that all initializers are off-limits to ENV access under the rule, how should environment variables be access when enabled?

@markokajzer
Copy link
Contributor Author

markokajzer commented Jul 1, 2024

From the PR description:

However in some ways, the application is already initialized when we get to config/**/*.rb:

config_for(:service) is already available to read config from service.yml
Rails.application.credentials is already available to read application credentials / secrets

So what you can do is

# In config/slack.yml
default: &default
  client_id: <%= ENV["SLACK_CLIENT_ID"] %>
  client_secret: <%= ENV["SLACK_CLIENT_SECRET"] %>
# In application.rb
module MyApp
  class Application
    config.slack = config_for(:slack)
  end
end

# In rest of the app
Rails.application.config.slack.client_id

Similarly, secrets should be in credentials.yml [docs], so given

# In config/credentials.yml.enc
secret_key_base: 3b7cd72...
some_api_key: SOMEKEY
system:
  access_key_id: 1234AB

You can do

# In rest of the app
Rails.application.secrets.some_api_key
Rails.application.secrets.system.access_key_id

That way

  1. Secrets are in credentials.yml.enc
  2. Config is in config/*.yml
    • Direct ENV access only happens here (and in config/*.rb since those files are excluded)
    • You can use ENV.fetch here since it's erb templates
  3. Access to secrets happens via Rails.application.secrets
  4. Access to config happens via Rails.application.config

If you cannot or don't want to use the Rails secrets/credential mechanisms for whatever reason (we don't), you can put everything in Rails.application.config instead.

Hope that helps!

@dogweather
Copy link

dogweather commented Jul 2, 2024

Thanks @markokajzer. So if I understand it correctly, under this rule, ENV access can only happen in the config/*.yml files.

@taylorthurlow
Copy link
Contributor

taylorthurlow commented Jul 3, 2024

I'm not sure I fully understand the argument about the app already being semi-initialized at the point it starts loading the initializers directory. The idea of that makes sense, but I'm not sure how we extend that logic to mean that the initializers are an inappropriate place to read from ENV.

My understanding is that the entire reason we say "don't mess with ENV post-initialization" is because missing configuration wouldn't be caught at boot (obviously bad). This is the exact reason I read directly from ENV from within initializers in the first place.

I'm willing to be wrong here, but I'm really not thrilled by the idea of adding an extra YML file (where there was not one before) for each separate configuration I already have set up, which is just a bunch of boilerplate (and another layer of indirection) for mapping ENV variable names to configuration names.

@markokajzer
Copy link
Contributor Author

markokajzer commented Jul 4, 2024

I'm willing to be wrong here, but I'm really not thrilled by the idea of adding an extra YML file

Unfortunately, that is literally the suggested approach from the Rails docs.

Rails changed this a couple of times throughout the versions and years, and other people from the community dislike the different configuration options for similar reasons. Here's an example from searls.

don't mess with ENV post-initialization

i don't think it was ever about "messing" with anything. after all it was just access to ENV. one could argue why the rule existed in the first place if accessing ENV is okay in some places 🤷

to me, this was about enforcing one way of doing this (now everything is pushed towards config/*.yml instead of spreading access to ENV around the place like it usually happens), and recommending the suggested approach from the Rails docs to developers. personally I think a linter should enforce things to be done in a certain, consistent way, just like formatter.

similarly, if our env variable is an actual secret, maybe we can put it in Rails credentials, where it will be encrypted automatically and actually safe, in contrast to just having ENV which leaves it to the developer.

there are other options as well

  1. changing the error message (i agree it might not fit completely any more)
  2. adding links to the docs in error messages
  3. exclude config/initializers from the rule for your project, e.g. when initialization happens mostly there
  4. disable the rule completely, e.g. if initialization happens in a couple of places and/or ENV access if okay for you (this description would fit the projects i have to work on)

nevertheless, i'm just trying to explain some points here. I'm more then willing to take the L here and revert this.
your feedback is heard 👍

@taylorthurlow
Copy link
Contributor

I appreciate the extra thoughts & time, thanks @markokajzer. This certainly is far from a hill worth dying on as far as I'm concerned, and I think I am aligned on the idea that the linter should have an opinion on the correct way to do things like this, and I really can't argue with the idea that it's how the Rails guide wants you to do it.

The link you included is helpful as it's the same issue I'm having, and I also happen to be using standardrb, so looks like this won't be something I have to mess with configuration wise, either.

In the end, the discussion is what's important - if I could suggest anything extra, for what does appear to be an immediately-noticed change & resulting disagreement, maybe a small disclaimer in the Rubocop docs which has a very short rationalization for the choice? Even if it were just to explain that this is the "correct" way to do it as far as the Rails guides are concerned, and that enabling the cop by default is preferable to having no cop at all.

Cheers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants