Class: Anthropic::Credentials::ConfigProvider
- Inherits:
-
Object
- Object
- Anthropic::Credentials::ConfigProvider
- Defined in:
- lib/anthropic/credentials/config_provider.rb,
sig/anthropic/credentials.rbs
Overview
Abstract base for access-token providers backed by a config object, whether read from disk (CredentialsFile) or supplied in memory (InMemoryConfig).
Subclasses must implement:
load_config(private) -- parse and cache the config hash into@config, populate@base_url(validated viarequire_https!), and resolve@credentials_pathif applicable. Idempotent.config_source(private) -- a string used in error messages to identify the configuration's origin (a file path or a sentinel).
Direct Known Subclasses
Instance Method Summary collapse
-
#auth_block ⇒ Hash
The authentication block from the config.
-
#bind_base_url(base_url) ⇒ void
Sets the owning client's
base_urlas a fallback for token exchange. -
#build_workload_delegate(auth) ⇒ WorkloadIdentity
Builds a WorkloadIdentity delegate for OIDC federation.
-
#call(force_refresh: false) ⇒ AccessToken
Returns an access token, performing token exchange if necessary.
-
#call_oidc_federation(auth, force_refresh:) ⇒ AccessToken
Handles OIDC federation authentication.
-
#call_user_oauth(_auth, force_refresh:) ⇒ AccessToken
Handles user OAuth authentication.
-
#coerce_expires_at(value) ⇒ Integer?
Coerces an expires_at value to an Integer.
- #config_source ⇒ #to_s abstract
-
#extra_headers ⇒ Hash{String => String}
Returns headers derived from the config (e.g.,
workspace_id). -
#fill(target, key, env_var) ⇒ void
Fills a single field from an environment variable if not already set.
-
#fill_missing_from_env(config, auth) ⇒ void
Fills missing config fields from environment variables.
-
#initialize ⇒ ConfigProvider
constructor
A new instance of ConfigProvider.
-
#load_config ⇒ Hash
abstract
The loaded config hash with symbol keys.
-
#read_credentials ⇒ Hash
Reads the credentials file.
-
#read_credentials_if_exists ⇒ Hash?
Reads the credentials file if it exists.
-
#resolve_base_url(config) ⇒ String
The resolved base URL.
-
#resolve_identity_token_provider(auth) ⇒ #call
Resolves the identity token provider from the auth config.
-
#resolved_base_url ⇒ String?
Returns the
base_urlfrom the config, if set. -
#write_credentials(data) ⇒ void
Atomically writes to the credentials file.
Constructor Details
#initialize ⇒ ConfigProvider
Returns a new instance of ConfigProvider.
16 17 18 19 20 21 22 |
# File 'lib/anthropic/credentials/config_provider.rb', line 16 def initialize @config = nil @credentials_path = nil @base_url = nil @bound_base_url = nil @workload_delegate = nil end |
Instance Method Details
#auth_block ⇒ Hash
Returns the authentication block from the config.
110 111 112 113 |
# File 'lib/anthropic/credentials/config_provider.rb', line 110 def auth_block config = load_config config[:authentication] end |
#bind_base_url(base_url) ⇒ void
This method returns an undefined value.
Sets the owning client's base_url as a fallback for token exchange.
59 60 61 62 63 64 65 66 67 |
# File 'lib/anthropic/credentials/config_provider.rb', line 59 def bind_base_url(base_url) bound = base_url.to_s.chomp("/") Anthropic::Config.require_https!(bound, field: "base_url") @bound_base_url = bound return unless @config @base_url = resolve_base_url(@config) Anthropic::Config.require_https!(@base_url, field: "#{config_source}: base_url") end |
#build_workload_delegate(auth) ⇒ WorkloadIdentity
Builds a WorkloadIdentity delegate for OIDC federation.
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
# File 'lib/anthropic/credentials/config_provider.rb', line 182 def build_workload_delegate(auth) federation_rule_id = auth[:federation_rule_id] organization_id = @config[:organization_id] unless federation_rule_id && organization_id raise Anthropic::Errors::ConfigurationError, "Config file with authentication.type #{AUTH_TYPE_OIDC_FEDERATION.inspect} must include " \ "'authentication.federation_rule_id' and top-level 'organization_id': #{config_source}" end provider = resolve_identity_token_provider(auth) delegate = WorkloadIdentity.new( identity_token_provider: provider, federation_rule_id: federation_rule_id, organization_id: organization_id, service_account_id: auth[:service_account_id], workspace_id: @config[:workspace_id], scope: auth[:scope] ) delegate.bind_base_url(@base_url) delegate end |
#call(force_refresh: false) ⇒ AccessToken
Returns an access token, performing token exchange if necessary.
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/anthropic/credentials/config_provider.rb', line 30 def call(force_refresh: false) auth = auth_block auth_type = auth[:type] case auth_type when AUTH_TYPE_OIDC_FEDERATION call_oidc_federation(auth, force_refresh: force_refresh) when AUTH_TYPE_USER_OAUTH call_user_oauth(auth, force_refresh: force_refresh) else raise Anthropic::Errors::ConfigurationError, "Unknown authentication.type #{auth_type.inspect} at #{config_source}. " \ "Expected #{AUTH_TYPE_OIDC_FEDERATION.inspect} or #{AUTH_TYPE_USER_OAUTH.inspect}." end end |
#call_oidc_federation(auth, force_refresh:) ⇒ AccessToken
Handles OIDC federation authentication.
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/anthropic/credentials/config_provider.rb', line 120 def call_oidc_federation(auth, force_refresh:) @workload_delegate ||= build_workload_delegate(auth) return @workload_delegate.call(force_refresh: force_refresh) unless @credentials_path cached = read_credentials_if_exists if !force_refresh && cached access_token = cached[:access_token] expires_at = cached[:expires_at] if access_token && expires_at begin expires_at_int = Integer(expires_at) if Time.now.to_i < expires_at_int - MANDATORY_REFRESH_SECONDS return AccessToken.new(token: access_token.to_s, expires_at: expires_at_int) end rescue ArgumentError, TypeError # rubocop:disable Lint/SuppressedException end end end token = @workload_delegate.call(force_refresh: force_refresh) begin cached_strings = cached&.transform_keys(&:to_s) || {} write_credentials( cached_strings.merge( "version" => "1.0", "type" => "oauth_token", "access_token" => token.token, "expires_at" => token.expires_at ) ) rescue StandardError # cache write failures are non-fatal; return the freshly minted token regardless end token end |
#call_user_oauth(_auth, force_refresh:) ⇒ AccessToken
Handles user OAuth authentication.
165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/anthropic/credentials/config_provider.rb', line 165 def call_user_oauth(_auth, force_refresh:) # rubocop:disable Lint/UnusedMethodArgument creds = read_credentials access_token = creds[:access_token] unless access_token raise Anthropic::Errors::ConfigurationError, "Credentials file at #{@credentials_path} is missing 'access_token'." end expires_at = coerce_expires_at(creds[:expires_at]) AccessToken.new(token: access_token, expires_at: expires_at) end |
#coerce_expires_at(value) ⇒ Integer?
Coerces an expires_at value to an Integer.
371 372 373 374 375 376 377 378 379 |
# File 'lib/anthropic/credentials/config_provider.rb', line 371 def coerce_expires_at(value) return nil if value.nil? Integer(value) rescue ArgumentError, TypeError raise Anthropic::Errors::ConfigurationError, "Credentials file at #{@credentials_path} has invalid 'expires_at' #{value.inspect}; " \ "expected an integer Unix timestamp in seconds." end |
#config_source ⇒ #to_s
Subclass must return a human-readable identifier of the config origin used in error messages (a file path or a sentinel string).
105 106 107 |
# File 'lib/anthropic/credentials/config_provider.rb', line 105 def config_source raise NotImplementedError, "#{self.class} must implement #config_source" end |
#extra_headers ⇒ Hash{String => String}
Returns headers derived from the config (e.g., workspace_id).
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/anthropic/credentials/config_provider.rb', line 72 def extra_headers config = load_config headers = {} auth_type = config[:authentication][:type] # For federation profiles workspace_id is sent in the jwt-bearer # exchange body, not as a request header (the minted token is already # workspace-scoped, so the header would be ignored). if auth_type != AUTH_TYPE_OIDC_FEDERATION workspace_id = config[:workspace_id] headers["anthropic-workspace-id"] = workspace_id.to_s if workspace_id end headers end |
#fill(target, key, env_var) ⇒ void
This method returns an undefined value.
Fills a single field from an environment variable if not already set.
424 425 426 427 428 429 |
# File 'lib/anthropic/credentials/config_provider.rb', line 424 def fill(target, key, env_var) value = target[key] return unless value.nil? || (value.is_a?(String) && value.empty?) v = ENV[env_var] target[key] = v if v && !v.empty? end |
#fill_missing_from_env(config, auth) ⇒ void
This method returns an undefined value.
Fills missing config fields from environment variables.
398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 |
# File 'lib/anthropic/credentials/config_provider.rb', line 398 def fill_missing_from_env(config, auth) fill(config, :base_url, ENV_BASE_URL) fill(config, :organization_id, ENV_ORGANIZATION_ID) fill(config, :workspace_id, ENV_WORKSPACE_ID) auth_type = auth[:type] if auth_type == AUTH_TYPE_OIDC_FEDERATION fill(auth, :federation_rule_id, ENV_FEDERATION_RULE_ID) fill(auth, :service_account_id, ENV_SERVICE_ACCOUNT_ID) fill(auth, :scope, ENV_SCOPE) unless auth[:identity_token] v = ENV[ENV_IDENTITY_TOKEN_FILE] auth[:identity_token] = {source: "file", path: v} if v && !v.empty? end elsif auth_type == AUTH_TYPE_USER_OAUTH fill(auth, :scope, ENV_SCOPE) end end |
#load_config ⇒ Hash
Subclass must populate and cache @config (symbolized hash),
set @base_url (and validate it via Anthropic::Config.require_https!
when applicable), and set @credentials_path if the config references
one. Must be idempotent: a no-op when @config is already set.
Returns the loaded config hash with symbol keys.
96 97 98 |
# File 'lib/anthropic/credentials/config_provider.rb', line 96 def load_config raise NotImplementedError, "#{self.class} must implement #load_config" end |
#read_credentials ⇒ Hash
Reads the credentials file. Re-reads on every call -- daemons rotate it.
On Unix, verifies the file is not group/world-readable. World-readable credentials files are refused outright; group-readable files log a warning but are accepted. The check is skipped on Windows where POSIX mode bits don't carry the same meaning.
267 268 269 270 271 272 273 274 275 276 277 278 279 280 |
# File 'lib/anthropic/credentials/config_provider.rb', line 267 def read_credentials raw = @credentials_path.read JSON.parse(raw, symbolize_names: true) rescue Anthropic::Errors::ConfigurationError raise rescue JSON::ParserError => e raise Anthropic::Errors::ConfigurationError, "Credentials file at #{@credentials_path} is not valid JSON: #{e.}" rescue StandardError => e raise Anthropic::Errors::ConfigurationError, "Credentials file at #{@credentials_path} could not be read: #{e.}" end |
#read_credentials_if_exists ⇒ Hash?
Reads the credentials file if it exists.
330 331 332 333 334 335 336 |
# File 'lib/anthropic/credentials/config_provider.rb', line 330 def read_credentials_if_exists return nil unless @credentials_path&.exist? read_credentials rescue Anthropic::Errors::ConfigurationError nil end |
#resolve_base_url(config) ⇒ String
Returns the resolved base URL.
383 384 385 386 387 388 389 390 391 |
# File 'lib/anthropic/credentials/config_provider.rb', line 383 def resolve_base_url(config) if config[:base_url] config[:base_url].to_s.chomp("/") elsif @bound_base_url @bound_base_url else DEFAULT_BASE_URL end end |
#resolve_identity_token_provider(auth) ⇒ #call
Resolves the identity token provider from the auth config.
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 |
# File 'lib/anthropic/credentials/config_provider.rb', line 211 def resolve_identity_token_provider(auth) identity_token_cfg = auth[:identity_token] return IdentityTokenFile.new unless identity_token_cfg if identity_token_cfg.is_a?(Hash) source = identity_token_cfg[:source] case source when "file" identity_token_path = identity_token_cfg[:path] unless identity_token_path && !identity_token_path.empty? raise Anthropic::Errors::ConfigurationError, "identity_token source 'file' requires a non-empty path; " \ "#{config_source} has identity_token=#{identity_token_cfg.inspect}." end IdentityTokenFile.new(identity_token_path) when "env" env_var_name = identity_token_cfg[:value] unless env_var_name && !env_var_name.empty? raise Anthropic::Errors::ConfigurationError, "identity_token source 'env' requires a non-empty value; " \ "#{config_source} has identity_token=#{identity_token_cfg.inspect}." end -> { content = ENV[env_var_name] if content.nil? raise Anthropic::Errors::ConfigurationError, "Identity token environment variable #{env_var_name} is not set" end content = content.strip if content.empty? raise Anthropic::Errors::ConfigurationError, "Identity token environment variable #{env_var_name} is empty" end content } else raise Anthropic::Errors::ConfigurationError, "identity_token source #{source.inspect} is not supported; only 'file' and 'env' are implemented" end else identity_token_path = identity_token_cfg.to_s IdentityTokenFile.new(identity_token_path) end end |
#resolved_base_url ⇒ String?
Returns the base_url from the config, if set.
49 50 51 52 53 |
# File 'lib/anthropic/credentials/config_provider.rb', line 49 def resolved_base_url config = load_config raw = config[:base_url] raw ? raw.to_s.chomp("/") : nil end |
#write_credentials(data) ⇒ void
This method returns an undefined value.
Atomically writes to the credentials file.
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 |
# File 'lib/anthropic/credentials/config_provider.rb', line 342 def write_credentials(data) temp_path = nil parent = @credentials_path.parent parent.mkpath parent.chmod(0o700) temp_path = @credentials_path.dirname.join(".#{@credentials_path.basename}.tmp#{SecureRandom.hex(8)}") File.open(temp_path, "w", 0o600) do |f| f.write(JSON.pretty_generate(data)) f.fsync end File.rename(temp_path, @credentials_path) ensure unless temp_path.nil? begin File.unlink(temp_path) rescue StandardError # best-effort cleanup; the temp file may already be gone (successful rename or concurrent unlink) end end end |