Class: Mysigner::Config
- Inherits:
-
Object
- Object
- Mysigner::Config
- Defined in:
- lib/mysigner/config.rb
Constant Summary collapse
- CONFIG_DIR =
File.('~/.mysigner').freeze
- CONFIG_FILE =
File.join(CONFIG_DIR, 'config.yml').freeze
- KEY_FILE =
File.join(CONFIG_DIR, '.encryption_key').freeze
- KEYCHAIN_SERVICE =
'com.mysigner.cli'- KEYCHAIN_ACCOUNT =
'config_encryption_key'- ENV_API_TOKEN =
Environment variable names for CI/CD support
'MYSIGNER_API_TOKEN'- ENV_API_URL =
'MYSIGNER_API_URL'- ENV_EMAIL =
'MYSIGNER_EMAIL'- ENV_ORG_ID =
'MYSIGNER_ORG_ID'- ENV_LOCAL_ONLY =
'MYSIGNER_LOCAL_ONLY'
Instance Attribute Summary collapse
-
#api_url ⇒ Object
Returns the value of attribute api_url.
-
#current_organization_id ⇒ Object
Returns the value of attribute current_organization_id.
-
#encryption_enabled ⇒ Object
Returns the value of attribute encryption_enabled.
-
#local_only ⇒ Object
Returns the value of attribute local_only.
-
#organizations ⇒ Object
readonly
Returns the value of attribute organizations.
-
#user_email ⇒ Object
Returns the value of attribute user_email.
Class Method Summary collapse
-
.env_configured? ⇒ Boolean
Check if all required env vars are set for CI/CD mode.
-
.from_env ⇒ Object
Create a Config from environment variables (for CI/CD).
-
.local_only? ⇒ Boolean
Local-only mode at the Config level: cascade ENV → file.
-
.local_only_from_file? ⇒ Boolean
Lightweight check that reads only ~/.mysigner/config.yml’s ‘local_only:` key without invoking #load (which decrypts tokens via the Keychain).
Instance Method Summary collapse
-
#api_token(org_id = nil) ⇒ Object
Get API token for current organization (or specific org).
-
#clear ⇒ Object
Clear all configuration.
-
#disable_encryption! ⇒ Object
Disable encryption and decrypt all tokens.
-
#display ⇒ Object
Display config (with masked tokens).
-
#enable_encryption! ⇒ Object
Enable encryption and re-encrypt all tokens.
-
#encrypted_config? ⇒ Boolean
Check if config uses encryption.
-
#exists? ⇒ Boolean
Check if config file exists.
-
#fetch_encryption_key ⇒ Object
Public accessor for the per-machine 32-byte AES-256-GCM key.
-
#from_env? ⇒ Boolean
Whether this config was loaded from environment variables.
-
#has_token_for_org?(org_id) ⇒ Boolean
Check if we have a token for a specific organization.
-
#initialize ⇒ Config
constructor
A new instance of Config.
-
#load ⇒ Object
Load configuration from file.
-
#local_only? ⇒ Boolean
Instance-level predicate.
-
#org_name(org_id = nil) ⇒ Object
Get organization name.
- #organization_id ⇒ Object
-
#organization_ids ⇒ Object
Get all organization IDs.
-
#remove_token_for_org(org_id) ⇒ Object
Remove token for specific organization.
-
#save ⇒ Object
Save configuration to file.
-
#save_token_for_org(org_id, org_name, token) ⇒ Object
Save token for a specific organization.
-
#to_h ⇒ Object
Get config as hash.
-
#valid? ⇒ Boolean
Check if configuration is complete (has required fields).
Constructor Details
#initialize ⇒ Config
Returns a new instance of Config.
31 32 33 34 35 36 37 38 39 40 |
# File 'lib/mysigner/config.rb', line 31 def initialize @api_url = nil @user_email = nil @current_organization_id = nil @organizations = {} @encryption_enabled = true # Enable by default for security @local_only = false @from_env = false load if exists? end |
Instance Attribute Details
#api_url ⇒ Object
Returns the value of attribute api_url.
28 29 30 |
# File 'lib/mysigner/config.rb', line 28 def api_url @api_url end |
#current_organization_id ⇒ Object
Returns the value of attribute current_organization_id.
28 29 30 |
# File 'lib/mysigner/config.rb', line 28 def current_organization_id @current_organization_id end |
#encryption_enabled ⇒ Object
Returns the value of attribute encryption_enabled.
28 29 30 |
# File 'lib/mysigner/config.rb', line 28 def encryption_enabled @encryption_enabled end |
#local_only ⇒ Object
Returns the value of attribute local_only.
28 29 30 |
# File 'lib/mysigner/config.rb', line 28 def local_only @local_only end |
#organizations ⇒ Object (readonly)
Returns the value of attribute organizations.
29 30 31 |
# File 'lib/mysigner/config.rb', line 29 def organizations @organizations end |
#user_email ⇒ Object
Returns the value of attribute user_email.
28 29 30 |
# File 'lib/mysigner/config.rb', line 28 def user_email @user_email end |
Class Method Details
.env_configured? ⇒ Boolean
Check if all required env vars are set for CI/CD mode
43 44 45 46 |
# File 'lib/mysigner/config.rb', line 43 def self.env_configured? ENV.fetch(ENV_API_TOKEN, nil) && !ENV[ENV_API_TOKEN].empty? && ENV.fetch(ENV_ORG_ID, nil) && !ENV[ENV_ORG_ID].empty? end |
.from_env ⇒ Object
Create a Config from environment variables (for CI/CD)
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/mysigner/config.rb', line 49 def self.from_env config = allocate config.instance_variable_set(:@encryption_enabled, false) config.instance_variable_set(:@from_env, true) config.instance_variable_set(:@local_only, false) org_id = ENV.fetch(ENV_ORG_ID, nil) token = ENV.fetch(ENV_API_TOKEN, nil) config.instance_variable_set(:@api_url, ENV[ENV_API_URL] || 'https://mysigner.dev') config.instance_variable_set(:@user_email, ENV.fetch(ENV_EMAIL, nil)) config.instance_variable_set(:@current_organization_id, org_id.to_i) config.instance_variable_set(:@organizations, { org_id.to_s => { 'name' => 'CI', 'token' => token } }) config end |
.local_only? ⇒ Boolean
Local-only mode at the Config level: cascade ENV → file. The CLI Helpers concern layers –local-only / –no-local-only on top.
74 75 76 |
# File 'lib/mysigner/config.rb', line 74 def self.local_only? truthy_env?(ENV_LOCAL_ONLY) || local_only_from_file? end |
.local_only_from_file? ⇒ Boolean
Lightweight check that reads only ~/.mysigner/config.yml’s ‘local_only:` key without invoking #load (which decrypts tokens via the Keychain). A user with no MySigner account still needs to flip this setting, so we never raise on a missing/corrupt or absent file — we just return false.
83 84 85 86 87 88 |
# File 'lib/mysigner/config.rb', line 83 def self.local_only_from_file? data = YAML.safe_load_file(CONFIG_FILE) data.is_a?(Hash) && data['local_only'] == true rescue Errno::ENOENT, Psych::SyntaxError false end |
Instance Method Details
#api_token(org_id = nil) ⇒ Object
Get API token for current organization (or specific org)
99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/mysigner/config.rb', line 99 def api_token(org_id = nil) org_id ||= @current_organization_id return nil if org_id.nil? org_data = @organizations[org_id.to_s] token = org_data&.dig('token') return nil if token.nil? # Decrypt if encrypted encrypted?(token) ? decrypt_token(token) : token end |
#clear ⇒ Object
Clear all configuration
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/mysigner/config.rb', line 198 def clear @api_url = nil @user_email = nil @current_organization_id = nil @organizations = {} @local_only = false File.delete(CONFIG_FILE) if exists? # On non-macOS the encryption key lives in a file fallback. Wipe it on # logout so a fresh login can mint a new key — otherwise the old key # would silently encrypt a new token that nobody else can decrypt. FileUtils.rm_f(KEY_FILE) # Phase 0: logout also purges the keystore cache so a shared machine # doesn't leave prior-user keystore blobs on disk. keystores_dir = File.('~/.mysigner/keystores') FileUtils.rm_rf(keystores_dir) true rescue StandardError => e raise ConfigError, "Failed to clear config: #{e.}" end |
#disable_encryption! ⇒ Object
Disable encryption and decrypt all tokens
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 |
# File 'lib/mysigner/config.rb', line 285 def disable_encryption! return true unless @encryption_enabled # Decrypt all tokens first @organizations.each_value do |org_data| token = org_data['token'] next if token.nil? || !encrypted?(token) org_data['token'] = decrypt_token(token) end @encryption_enabled = false save true end |
#display ⇒ Object
Display config (with masked tokens)
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 |
# File 'lib/mysigner/config.rb', line 244 def display current_org_name = org_name(@current_organization_id) || '(not set)' current_token = api_token(@current_organization_id) display_data = { api_url: @api_url || '(not set)', user_email: @user_email || '(not set)', current_organization: "#{current_org_name} (ID: #{@current_organization_id || 'not set'})", current_token: current_token ? mask_token(current_token) : '(not set)' } # Show all organizations if @organizations.any? display_data[:all_organizations] = @organizations.map do |org_id, org_data| token_status = org_data['token'] ? '✓' : '✗' "#{org_data['name']} (ID: #{org_id}) #{token_status}" end.join(', ') end display_data end |
#enable_encryption! ⇒ Object
Enable encryption and re-encrypt all tokens
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 |
# File 'lib/mysigner/config.rb', line 267 def enable_encryption! return true if @encryption_enabled @encryption_enabled = true # Re-encrypt all existing tokens @organizations.each_value do |org_data| token = org_data['token'] next if token.nil? || encrypted?(token) org_data['token'] = encrypt_token(token) end save true end |
#encrypted_config? ⇒ Boolean
Check if config uses encryption
302 303 304 |
# File 'lib/mysigner/config.rb', line 302 def encrypted_config? @organizations.values.any? { |org_data| encrypted?(org_data['token']) } end |
#exists? ⇒ Boolean
Check if config file exists
223 224 225 |
# File 'lib/mysigner/config.rb', line 223 def exists? File.exist?(CONFIG_FILE) end |
#fetch_encryption_key ⇒ Object
Public accessor for the per-machine 32-byte AES-256-GCM key. Exposed so sibling stores (e.g. LocalCredentials) can encrypt secrets under the same key without duplicating the keychain/file fallback. The key itself is created on first read and is stable across calls.
310 311 312 |
# File 'lib/mysigner/config.rb', line 310 def fetch_encryption_key get_or_create_encryption_key end |
#from_env? ⇒ Boolean
Whether this config was loaded from environment variables
68 69 70 |
# File 'lib/mysigner/config.rb', line 68 def from_env? @from_env end |
#has_token_for_org?(org_id) ⇒ Boolean
Check if we have a token for a specific organization
121 122 123 124 |
# File 'lib/mysigner/config.rb', line 121 def has_token_for_org?(org_id) token = api_token(org_id) !token.nil? && !token.empty? end |
#load ⇒ Object
Load configuration from file
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/mysigner/config.rb', line 150 def load return false unless exists? # mysigner-51 — safe_load_file rejects arbitrary Ruby object # instantiation in the YAML (`!ruby/object:Foo` etc.). The config # shape is just String/Integer/Boolean/Hash (api_url, user_email, # current_organization_id, local_only, organizations: {id => {name, # token}}), all in safe_load's default allowed set, so no # permitted_classes # extension is needed. Low risk (the file is 0600 and user-owned) # but cheap hardening against a future RCE if config write or read # ever moves outside the owner-only assumption. data = YAML.safe_load_file(CONFIG_FILE) @api_url = data['api_url'] @user_email = data['user_email'] @current_organization_id = data['current_organization_id'] @organizations = data['organizations'] || {} @local_only = data['local_only'] == true # Auto-detect encryption from config @encryption_enabled = encrypted_config? true rescue StandardError => e raise ConfigError, "Failed to load config: #{e.}" end |
#local_only? ⇒ Boolean
Instance-level predicate. Merges two surfaces:
- @local_only: set to true by Helpers#blank_local_only_config so a
sentinel config always answers true without touching ENV or disk.
- self.class.local_only?: the normal ENV → file cascade.
94 95 96 |
# File 'lib/mysigner/config.rb', line 94 def local_only? @local_only || self.class.local_only? end |
#org_name(org_id = nil) ⇒ Object
Get organization name
127 128 129 130 131 132 133 |
# File 'lib/mysigner/config.rb', line 127 def org_name(org_id = nil) org_id ||= @current_organization_id return nil if org_id.nil? org_data = @organizations[org_id.to_s] org_data&.dig('name') end |
#organization_id ⇒ Object
140 141 142 |
# File 'lib/mysigner/config.rb', line 140 def organization_id @current_organization_id end |
#organization_ids ⇒ Object
Get all organization IDs
136 137 138 |
# File 'lib/mysigner/config.rb', line 136 def organization_ids @organizations.keys.map(&:to_i) end |
#remove_token_for_org(org_id) ⇒ Object
Remove token for specific organization
145 146 147 |
# File 'lib/mysigner/config.rb', line 145 def remove_token_for_org(org_id) @organizations.delete(org_id.to_s) end |
#save ⇒ Object
Save configuration to file
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
# File 'lib/mysigner/config.rb', line 179 def save ensure_config_dir_exists data = { 'api_url' => @api_url, 'user_email' => @user_email, 'current_organization_id' => @current_organization_id, 'organizations' => @organizations, 'local_only' => @local_only } File.write(CONFIG_FILE, data.to_yaml) File.chmod(0o600, CONFIG_FILE) # Make file readable only by owner true rescue StandardError => e raise ConfigError, "Failed to save config: #{e.}" end |
#save_token_for_org(org_id, org_name, token) ⇒ Object
Save token for a specific organization
112 113 114 115 116 117 118 |
# File 'lib/mysigner/config.rb', line 112 def save_token_for_org(org_id, org_name, token) encrypted_token = @encryption_enabled ? encrypt_token(token) : token @organizations[org_id.to_s] = { 'name' => org_name, 'token' => encrypted_token } end |
#to_h ⇒ Object
Get config as hash
235 236 237 238 239 240 241 |
# File 'lib/mysigner/config.rb', line 235 def to_h { api_url: @api_url, user_email: @user_email, current_organization_id: @current_organization_id } end |
#valid? ⇒ Boolean
Check if configuration is complete (has required fields)
228 229 230 231 232 |
# File 'lib/mysigner/config.rb', line 228 def valid? !@api_url.nil? && !@api_url.empty? && !@current_organization_id.nil? && has_token_for_org?(@current_organization_id) end |