Module: Pandoru::SecretStore
- Defined in:
- lib/pandoru/secret_store.rb
Overview
Portable secret storage: keeps the Pandora credential out of any plaintext file by delegating to the host OS’s native secret service. There’s no solid cross-platform Ruby gem for this, so we shell out to each platform’s tool, the way fastlane et al. do:
macOS → security (Keychain)
Linux → secret-tool (libsecret / Secret Service: GNOME Keyring, KWallet)
Windows → Credential Manager (not yet wired — see Adapters::Windows)
The credential is stored as a single JSON blob { “username”, “password” } under one fixed key, so retrieval is uniform across backends and we never need to enumerate the store to discover the username. Everything degrades gracefully: with no working backend, fetch returns nil and the resolver falls through to the config file.
Defined Under Namespace
Modules: Adapters
Constant Summary collapse
- SERVICE =
'pandoru'- DEFAULT_RUNNER =
Shells out, returning [stdout, success?]. Injectable so adapters can be unit-tested without touching a real keychain.
lambda do |cmd, stdin_data| opts = { err: File::NULL } opts[:stdin_data] = stdin_data if stdin_data out, status = Open3.capture2(*cmd, **opts) [out, status.success?] rescue Errno::ENOENT ['', false] end
Class Method Summary collapse
-
.adapter(runner: DEFAULT_RUNNER) ⇒ Object
The first available adapter for this host, or a Null adapter.
- .available?(adapter: adapter()) ⇒ Boolean
- .backend_name(adapter: adapter()) ⇒ Object
- .delete(service: SERVICE, adapter: adapter()) ⇒ Object
-
.fetch(service: SERVICE, adapter: adapter()) ⇒ Object
- username, password
-
from the store, or nil if absent/unreadable.
- .present?(value) ⇒ Boolean
- .store(username, password, service: SERVICE, adapter: adapter()) ⇒ Object
Class Method Details
.adapter(runner: DEFAULT_RUNNER) ⇒ Object
The first available adapter for this host, or a Null adapter.
36 37 38 39 40 |
# File 'lib/pandoru/secret_store.rb', line 36 def adapter(runner: DEFAULT_RUNNER) [Adapters::MacOS, Adapters::SecretTool, Adapters::Windows] .map { |klass| klass.new(runner: runner) } .find(&:available?) || Adapters::Null.new(runner: runner) end |
.available?(adapter: adapter()) ⇒ Boolean
42 43 44 |
# File 'lib/pandoru/secret_store.rb', line 42 def available?(adapter: adapter()) !adapter.is_a?(Adapters::Null) end |
.backend_name(adapter: adapter()) ⇒ Object
46 47 48 |
# File 'lib/pandoru/secret_store.rb', line 46 def backend_name(adapter: adapter()) adapter.name end |
.delete(service: SERVICE, adapter: adapter()) ⇒ Object
69 70 71 |
# File 'lib/pandoru/secret_store.rb', line 69 def delete(service: SERVICE, adapter: adapter()) adapter.delete(service) end |
.fetch(service: SERVICE, adapter: adapter()) ⇒ Object
- username, password
-
from the store, or nil if absent/unreadable.
51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/pandoru/secret_store.rb', line 51 def fetch(service: SERVICE, adapter: adapter()) raw = adapter.read(service) return nil if raw.nil? || raw.strip.empty? data = JSON.parse(raw) username = data['username'] password = data['password'] return nil unless present?(username) && present?(password) [username, password] rescue JSON::ParserError nil end |
.present?(value) ⇒ Boolean
73 74 75 |
# File 'lib/pandoru/secret_store.rb', line 73 def present?(value) !value.nil? && !value.to_s.strip.empty? end |
.store(username, password, service: SERVICE, adapter: adapter()) ⇒ Object
65 66 67 |
# File 'lib/pandoru/secret_store.rb', line 65 def store(username, password, service: SERVICE, adapter: adapter()) adapter.write(service, JSON.generate('username' => username, 'password' => password)) end |