Hi ya’ll! I had a question about using startup.jl in the home directory (not the project) to store credentials as environment variables.
I come from an R background, and I’m used to storing credentials in the .Renviron file as described here.
If a script involves credentials, those are retrieved using Sys.getenv("variable_name")–and if a user is missing these variables in their .Renviron file we can prompt them to fix that. This prevents passwords and such from being hard-coded in scripts and unintentionally making their way to GitHub or elsewhere.
When working with Julia interactively (eg. doing data analysis), I’ve been storing these credentials in my startup.jl file as environment variables and referencing them in a similar way. For example, if working with a database:
function query_db(;db_name = "")
user = ENV["DB_USERNAME"]
pass = ENV["DB_PASSWORD"]
...
end
I’m curious if this is a bad design pattern or if there is some other kind of “gotcha” associated with doing this.
I don’t think startup.jl is the best place since it is shared with all your projects. If you want to connect to a project specific database, you could start having conflicts. The usual practice in Julia, Python, and elsewhere, from what I’ve seen, is .env files.
DotEnv.jl is a nice package for reading .env files!
GitHub - JuliaPackaging/Preferences.jl: Project Preferences Package is a nice way to store project-specific config. Not sure if it’s really suitable for something sensitive like credentials, though. Would like to know other people’s opinions on the advantages of DotEnv.jl over a LocalPreferences.toml.
I looked into DotEnv.jl, I think I avoided it because the package hasn’t been updated in so long I thought maybe it had been abandoned/wasn’t supported anymore
Is the persistence across projects the only downside in your view? In the very specific use-case I’m thinking of that sparked this question (an internal analytics package), a limited set of variables shared amongst all projects is not necessarily a bad thing! For example, if you’re only using one database for all your projects then it makes sense for those credentials to be accessible across projects.
But I can see how this could get messy if credentials were highly project-specific. In which case .env files would definitely be the best way to go as far as I can tell.
In general, something simple like parsing a text file into a Dictionary is going to be extremely stable and not need updates for all of the Julia v1.X series. My guess is that it would work just fine!
If you want to make something accessible to all your projects using startup, I’d still use something like .env, .YAML, or something else to separate it from the startup.jl file itself.
It’s not unheard of to share your startup.jl file with someone so they can borrow your workflow ideas, so separating credentials from code makes it just that much harder to accidentally expose them!
That said, there is no real risk that I’m aware of to hard coding into startup.jl, so I think you can just do whatever feels right!
I’ll admit, .env is just me being stuck in my ways. I used it before Julia, so learning a Julia specific thing just hasn’t been a priority. That said, it does look very cool!
I guess one advantage is that .env is that it’s language agnostic so if you have a multilingual project, you don’t have to duplicate configurations. Starting up a python virtual environment with pipenv automatically picks up the .env file and loads them to the environment variables for you, which is nice.
If the credentials are more related to a local package than to a specific project/environment, I usually store them in the package’s Scratch space.
E.g.
using Scratch
import TOML
function set_token(token)
config_dir = @get_scratch!("config")
config_filename = joinpath(config_dir, "config.toml")
open(config_filename, "w") do f
TOML.print(f, Dict("token" => token))
end
# Set read/write permission for the user and no permissions for others.
chmod(config_filename, 0o600)
return
end
function get_token()
config_dir = @get_scratch!("config")
config_filename = joinpath(config_dir, "config.toml")
if !isfile(config_filename)
error("Set personal access token with `set_token`.")
end
config = TOML.parsefile(config_filename)
return config["token"]
end