ask-auth 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +182 -0
- data/lib/ask/auth/providers/database.rb +46 -0
- data/lib/ask/auth/providers/env.rb +35 -0
- data/lib/ask/auth/providers/file.rb +47 -0
- data/lib/ask/auth/providers/oauth.rb +79 -0
- data/lib/ask/auth/providers/rails_credentials.rb +27 -0
- data/lib/ask/auth/version.rb +7 -0
- data/lib/ask/auth.rb +99 -0
- data/lib/ask-auth.rb +3 -0
- metadata +107 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b1cfb133ab080dc2840a380b4039d783fa982a676c05b3b10cb597264d0da899
|
|
4
|
+
data.tar.gz: 042e1afabf24c3543eef2cab5e54c8918e97c5bbccf489c8af4f1fd1211e3739
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ee48024db5d7ffaf38afa8244017202c42b917aa8233bd600f0895bc100e32ac5d1089067666eb6ee8fb159d59c88e31236daf9fdca4ef085078a2ecb0f07e9e
|
|
7
|
+
data.tar.gz: 9d6ba6e6f408b522f57da8793638812105dd3c0b691f24a98ff29ff316e31b82078d9ea84f77a1a71be48e8f360b1edb22f07b674138b29c1aa227e0b585c33d
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kaka Ruto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# ask-auth
|
|
2
|
+
|
|
3
|
+
**Credential resolution for the ask-rb ecosystem.**
|
|
4
|
+
|
|
5
|
+
A single API for resolving credentials across all ask-rb gems. Service gems call `Ask::Auth.resolve(:github_token)` — they never touch env vars, files, or OAuth flows directly. The resolution chain walks configured providers in order and returns the first match.
|
|
6
|
+
|
|
7
|
+
Zero external dependencies for the core. Optional ActiveRecord integration for database-backed token storage.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Add this line to your Gemfile:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem "ask-auth"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or install it directly:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
gem install ask-auth
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
require "ask-auth"
|
|
27
|
+
|
|
28
|
+
# Simple — works everywhere, no config needed
|
|
29
|
+
token = Ask::Auth.resolve(:github_token)
|
|
30
|
+
# => "ghp_abc123..."
|
|
31
|
+
|
|
32
|
+
# With a user context (for per-user providers like Database)
|
|
33
|
+
token = Ask::Auth.resolve(:openai_api_key, user: current_user)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
By default, the resolution chain checks these providers in order:
|
|
37
|
+
|
|
38
|
+
1. **Env** — environment variables
|
|
39
|
+
2. **File** — `~/.ask/credentials.yml`
|
|
40
|
+
3. **RailsCredentials** — `Rails.application.credentials` (if Rails is loaded)
|
|
41
|
+
4. **Database** — ActiveRecord-backed token storage (if ActiveRecord is loaded)
|
|
42
|
+
5. **OAuth** — interactive PKCE flow (returns nil by default — requires explicit authorization)
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
Customize the provider chain with `Ask::Auth.configure`:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
Ask::Auth.configure do |c|
|
|
50
|
+
c.providers = [
|
|
51
|
+
Ask::Auth::Providers::Env.new,
|
|
52
|
+
Ask::Auth::Providers::File.new(path: "~/.myapp/creds.yml"),
|
|
53
|
+
Ask::Auth::Providers::Database.new(model: AccessToken)
|
|
54
|
+
]
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Once configured, the resolution chain is frozen and thread-safe.
|
|
59
|
+
|
|
60
|
+
## Providers
|
|
61
|
+
|
|
62
|
+
### Env (`Ask::Auth::Providers::Env`)
|
|
63
|
+
|
|
64
|
+
Resolves credentials from environment variables by convention. Tries multiple naming styles:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
Ask::Auth.resolve(:github_token)
|
|
68
|
+
# Checks (in order): ENV["GITHUB_TOKEN"], ENV["GITHUBTOKEN"], ENV["github_token"]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
No configuration needed.
|
|
72
|
+
|
|
73
|
+
### File (`Ask::Auth::Providers::File`)
|
|
74
|
+
|
|
75
|
+
Reads credentials from a YAML file:
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
# ~/.ask/credentials.yml
|
|
79
|
+
github_token: ghp_abc123...
|
|
80
|
+
openai_api_key: sk-...
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
Ask::Auth::Providers::File.new
|
|
85
|
+
Ask::Auth::Providers::File.new(path: "~/.custom/credentials.yml")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The file is created with `0600` permissions when written (the provider is read-only; use your editor or a setup script).
|
|
89
|
+
|
|
90
|
+
### RailsCredentials (`Ask::Auth::Providers::RailsCredentials`)
|
|
91
|
+
|
|
92
|
+
Wraps `Rails.application.credentials`. Converts `snake_case` names to dot-separated paths:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
Ask::Auth.resolve(:github_token)
|
|
96
|
+
# Looks up: Rails.application.credentials.github.token
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Safely returns nil when Rails is not loaded.
|
|
100
|
+
|
|
101
|
+
### Database (`Ask::Auth::Providers::Database`)
|
|
102
|
+
|
|
103
|
+
ActiveRecord-backed token storage per user. Expects a model with `user_id`, `name`, `token`, `expires_at`, and `refresh_token` columns.
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
# app/models/credential.rb
|
|
107
|
+
class Credential < ApplicationRecord
|
|
108
|
+
belongs_to :user
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
Ask::Auth::Providers::Database.new
|
|
114
|
+
Ask::Auth::Providers::Database.new(model: AccessToken)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Handles token expiry automatically: calls `refresh!` when a token has expired and a `refresh_token` is available.
|
|
118
|
+
|
|
119
|
+
### OAuth (`Ask::Auth::Providers::OAuth`)
|
|
120
|
+
|
|
121
|
+
PKCE OAuth flow for interactive credential authorization. This provider does not resolve automatically — it provides the authorization interface:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
provider = Ask::Auth::Providers::OAuth.new(
|
|
125
|
+
client_id: "your-client-id",
|
|
126
|
+
authorize_url: "https://provider.com/oauth/authorize",
|
|
127
|
+
token_url: "https://provider.com/oauth/token"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Step 1: Generate the authorization URL
|
|
131
|
+
url = provider.authorize_url(user: current_user)
|
|
132
|
+
|
|
133
|
+
# Step 2: Exchange the code for tokens
|
|
134
|
+
provider.authorize!(user: current_user, code: params[:code])
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The full token exchange flow requires configuration. The PKCE utility methods (`generate_code_verifier`, `generate_code_challenge`) are ready for use.
|
|
138
|
+
|
|
139
|
+
## Custom Providers
|
|
140
|
+
|
|
141
|
+
Any object that responds to `call(name, user:)` can be a provider:
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
Ask::Auth.configure do |c|
|
|
145
|
+
c.providers = [
|
|
146
|
+
Ask::Auth::Providers::Env.new,
|
|
147
|
+
->(name, user: nil) { user&.api_key_for(name) }
|
|
148
|
+
]
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Error Handling
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
begin
|
|
156
|
+
token = Ask::Auth.resolve(:missing_key)
|
|
157
|
+
rescue Ask::Auth::MissingCredential => e
|
|
158
|
+
puts e.message
|
|
159
|
+
# "No credential found for :missing_key. Set MISSING_KEY in your environment..."
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
begin
|
|
163
|
+
# At usage time
|
|
164
|
+
raise Ask::Auth::InvalidCredential.new(:github_token, "rate limited")
|
|
165
|
+
rescue Ask::Auth::InvalidCredential => e
|
|
166
|
+
puts e.message
|
|
167
|
+
# "Credential :github_token is rate limited..."
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Development
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
git clone https://github.com/ask-rb/ask-auth
|
|
175
|
+
cd ask-auth
|
|
176
|
+
bin/setup
|
|
177
|
+
bundle exec rake test
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
module Auth
|
|
5
|
+
module Providers
|
|
6
|
+
# ActiveRecord-backed token storage per user.
|
|
7
|
+
#
|
|
8
|
+
# Expects a model with +user_id+, +name+, +token+, +expires_at+, +refresh_token+.
|
|
9
|
+
# Handles expiry: if the token has expired and a refresh token is available,
|
|
10
|
+
# calls +refresh!+ using the stored refresh token.
|
|
11
|
+
#
|
|
12
|
+
# Only used when a model is configured or available. Safely returns nil otherwise.
|
|
13
|
+
class Database
|
|
14
|
+
# The model class used for credential storage.
|
|
15
|
+
attr_reader :model
|
|
16
|
+
|
|
17
|
+
def initialize(model: default_model)
|
|
18
|
+
@model = model
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(name, user: nil)
|
|
22
|
+
return nil unless @model
|
|
23
|
+
return nil unless user.respond_to?(:id)
|
|
24
|
+
|
|
25
|
+
record = @model.respond_to?(:find_by) ? @model.find_by(user_id: user.id, name: name.to_s) : nil
|
|
26
|
+
return nil unless record
|
|
27
|
+
|
|
28
|
+
if record.respond_to?(:expired?) && record.expired?
|
|
29
|
+
return nil unless record.respond_to?(:refresh!)
|
|
30
|
+
|
|
31
|
+
record.refresh!
|
|
32
|
+
record.reload if record.respond_to?(:reload)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
record.respond_to?(:token) ? record.token : record
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def default_model
|
|
41
|
+
defined?(::ActiveRecord) && defined?(::Credential) ? ::Credential : nil
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
module Auth
|
|
5
|
+
module Providers
|
|
6
|
+
# Resolves credentials from environment variables.
|
|
7
|
+
#
|
|
8
|
+
# Conventions tested (in order):
|
|
9
|
+
# resolve(:github_token) -> ENV["GITHUB_TOKEN"], ENV["GITHUBTOKEN"], ENV["github_token"]
|
|
10
|
+
#
|
|
11
|
+
# No configuration needed — just a convention.
|
|
12
|
+
class Env
|
|
13
|
+
# Returns the credential value from ENV, or nil if not found.
|
|
14
|
+
def call(name, user: nil)
|
|
15
|
+
conventions(name).each do |key|
|
|
16
|
+
value = ENV[key.to_s]
|
|
17
|
+
return value unless value.nil?
|
|
18
|
+
end
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def conventions(name)
|
|
25
|
+
name = name.to_s
|
|
26
|
+
[
|
|
27
|
+
name.upcase,
|
|
28
|
+
name.upcase.delete("_"),
|
|
29
|
+
name
|
|
30
|
+
].uniq
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Ask
|
|
7
|
+
module Auth
|
|
8
|
+
module Providers
|
|
9
|
+
# Reads credentials from a YAML file (default: +~/.ask/credentials.yml+).
|
|
10
|
+
#
|
|
11
|
+
# Ask::Auth::Providers::File.new(path: "~/.ask/credentials.yml")
|
|
12
|
+
#
|
|
13
|
+
# The file is automatically created (with 0600 permissions) when writing,
|
|
14
|
+
# but this provider is read-only by design.
|
|
15
|
+
class File
|
|
16
|
+
DEFAULT_PATH = "~/.ask/credentials.yml"
|
|
17
|
+
|
|
18
|
+
def initialize(path: DEFAULT_PATH)
|
|
19
|
+
@path = path
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns the credential value from the YAML file, or nil.
|
|
23
|
+
def call(name, user: nil)
|
|
24
|
+
data = load_file
|
|
25
|
+
return nil unless data
|
|
26
|
+
|
|
27
|
+
value = data[name.to_s] || data[name.to_sym]
|
|
28
|
+
value
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The path this provider reads from.
|
|
32
|
+
attr_reader :path
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def load_file
|
|
37
|
+
expanded = ::File.expand_path(@path)
|
|
38
|
+
return nil unless ::File.exist?(expanded)
|
|
39
|
+
|
|
40
|
+
YAML.safe_load(::File.read(expanded), permitted_classes: [Symbol]) || {}
|
|
41
|
+
rescue Psych::SyntaxError
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
|
|
7
|
+
module Ask
|
|
8
|
+
module Auth
|
|
9
|
+
module Providers
|
|
10
|
+
# PKCE OAuth flow for interactive credential authorization.
|
|
11
|
+
#
|
|
12
|
+
# This provider implements the OAuth interface. The full interactive flow
|
|
13
|
+
# can be deferred — the interface is wired for integration and the PKCE
|
|
14
|
+
# utility methods are ready.
|
|
15
|
+
#
|
|
16
|
+
# provider = Ask::Auth::Providers::OAuth.new(storage: database_provider)
|
|
17
|
+
# url = provider.authorize_url(user: current_user)
|
|
18
|
+
# # redirect user to url, then:
|
|
19
|
+
# provider.authorize!(user: current_user, code: params[:code])
|
|
20
|
+
class OAuth
|
|
21
|
+
def initialize(storage: nil, client_id: nil, authorize_url: nil, token_url: nil)
|
|
22
|
+
@storage = storage
|
|
23
|
+
@client_id = client_id
|
|
24
|
+
@authorize_url = authorize_url
|
|
25
|
+
@token_url = token_url
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns nil (no automatic resolution) — OAuth requires interactive flow.
|
|
29
|
+
def call(name, user: nil)
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Generate a PKCE code verifier (128-char alphanumeric string).
|
|
34
|
+
def generate_code_verifier
|
|
35
|
+
SecureRandom.alphanumeric(128)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Generate a PKCE code challenge (SHA256 base64 digest of verifier).
|
|
39
|
+
def generate_code_challenge(verifier)
|
|
40
|
+
::Base64.urlsafe_encode64(
|
|
41
|
+
OpenSSL::Digest.digest("SHA256", verifier),
|
|
42
|
+
padding: false
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns the authorization URL to redirect the user to.
|
|
47
|
+
def authorize_url(user:, verifier: nil, state: nil)
|
|
48
|
+
verifier ||= generate_code_verifier
|
|
49
|
+
state ||= SecureRandom.hex(16)
|
|
50
|
+
challenge = generate_code_challenge(verifier)
|
|
51
|
+
|
|
52
|
+
# Store state and verifier for this user (requires a storage provider)
|
|
53
|
+
if @storage && user
|
|
54
|
+
@storage.call(:oauth_state, user: user)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
uri = URI.parse(@authorize_url || "https://example.com/oauth/authorize")
|
|
58
|
+
uri.query = URI.encode_www_form(
|
|
59
|
+
response_type: "code",
|
|
60
|
+
client_id: @client_id || "YOUR_CLIENT_ID",
|
|
61
|
+
redirect_uri: "urn:ietf:wg:oauth:2.0:oob",
|
|
62
|
+
scope: "",
|
|
63
|
+
state: state,
|
|
64
|
+
code_challenge: challenge,
|
|
65
|
+
code_challenge_method: "S256"
|
|
66
|
+
)
|
|
67
|
+
uri.to_s
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Exchange an authorization code for tokens.
|
|
71
|
+
# +user+:: The user to associate the credential with
|
|
72
|
+
# +code+:: The authorization code from the redirect
|
|
73
|
+
def authorize!(user:, code:)
|
|
74
|
+
raise NotImplementedError, "Token exchange requires a configured token_url and client_id"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
module Auth
|
|
5
|
+
module Providers
|
|
6
|
+
# Resolves credentials from Rails encrypted credentials.
|
|
7
|
+
#
|
|
8
|
+
# Convention: +resolve(:github_token)+ looks up +Rails.application.credentials.github.token+
|
|
9
|
+
# (dot-separated path from the credential name).
|
|
10
|
+
#
|
|
11
|
+
# Safely returns nil when Rails is not loaded.
|
|
12
|
+
class RailsCredentials
|
|
13
|
+
def call(name, user: nil)
|
|
14
|
+
return nil unless defined?(::Rails) && ::Rails.application.respond_to?(:credentials)
|
|
15
|
+
|
|
16
|
+
parts = name.to_s.split("_")
|
|
17
|
+
value = parts.reduce(::Rails.application.credentials) do |obj, part|
|
|
18
|
+
break nil unless obj.respond_to?(part)
|
|
19
|
+
obj.public_send(part)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
value
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/ask/auth.rb
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "auth/version"
|
|
4
|
+
require_relative "auth/providers/env"
|
|
5
|
+
require_relative "auth/providers/file"
|
|
6
|
+
require_relative "auth/providers/rails_credentials"
|
|
7
|
+
require_relative "auth/providers/database"
|
|
8
|
+
require_relative "auth/providers/oauth"
|
|
9
|
+
|
|
10
|
+
module Ask
|
|
11
|
+
# Credential resolution for the ask-rb ecosystem.
|
|
12
|
+
#
|
|
13
|
+
# Resolves credentials by walking a configured chain of providers (Env → File →
|
|
14
|
+
# RailsCredentials → Database → OAuth) and returning the first match.
|
|
15
|
+
#
|
|
16
|
+
# Ask::Auth.resolve(:github_token)
|
|
17
|
+
# Ask::Auth.resolve(:openai_api_key, user: current_user)
|
|
18
|
+
#
|
|
19
|
+
module Auth
|
|
20
|
+
class MissingCredential < KeyError
|
|
21
|
+
def initialize(name)
|
|
22
|
+
super("No credential found for #{name.inspect}. " \
|
|
23
|
+
"Set #{name.to_s.upcase} in your environment, add it to ~/.ask/credentials.yml, " \
|
|
24
|
+
"or configure a provider.")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class InvalidCredential < RuntimeError
|
|
29
|
+
def initialize(name, reason = "invalid or expired")
|
|
30
|
+
super("Credential #{name.inspect} is #{reason}. " \
|
|
31
|
+
"Please update your token and try again.")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
def configure
|
|
37
|
+
config = Configuration.new
|
|
38
|
+
yield config
|
|
39
|
+
@configuration = config
|
|
40
|
+
@configuration.freeze
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def configuration
|
|
44
|
+
@configuration ||= default_configuration
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def reset_configuration!
|
|
48
|
+
@configuration = nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Walk providers in order and return the first non-nil credential.
|
|
52
|
+
# +name+:: Symbol or String identifying the credential (e.g. +:github_token+)
|
|
53
|
+
# +user+:: Optional user record for per-user providers (Database, OAuth)
|
|
54
|
+
def resolve(name, user: nil)
|
|
55
|
+
name = name.to_s.strip
|
|
56
|
+
return nil if name.empty?
|
|
57
|
+
|
|
58
|
+
configuration.providers.each do |provider|
|
|
59
|
+
value = provider.call(name, user: user)
|
|
60
|
+
next if value.nil?
|
|
61
|
+
|
|
62
|
+
normalized = normalize(value)
|
|
63
|
+
return normalized unless normalized.nil?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
raise MissingCredential, name
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def default_configuration
|
|
72
|
+
Configuration.new(
|
|
73
|
+
providers: [
|
|
74
|
+
Providers::Env.new,
|
|
75
|
+
Providers::File.new,
|
|
76
|
+
Providers::RailsCredentials.new,
|
|
77
|
+
Providers::Database.new,
|
|
78
|
+
Providers::OAuth.new
|
|
79
|
+
]
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Blank normalization: empty strings and whitespace-only normalize to nil
|
|
84
|
+
def normalize(value)
|
|
85
|
+
value = value.to_s.strip
|
|
86
|
+
value.empty? ? nil : value
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Holds configuration for the resolution chain.
|
|
91
|
+
class Configuration
|
|
92
|
+
attr_accessor :providers
|
|
93
|
+
|
|
94
|
+
def initialize(providers: [])
|
|
95
|
+
@providers = providers
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
data/lib/ask-auth.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ask-auth
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kaka Ruto
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: base64
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.2'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.2'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: minitest
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5.25'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '5.25'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: mocha
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.1'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.1'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rake
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '13.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '13.0'
|
|
68
|
+
description: Env, file, Rails credentials, database, and OAuth providers. Zero external
|
|
69
|
+
dependencies.
|
|
70
|
+
email:
|
|
71
|
+
- kaka@myrrlabs.com
|
|
72
|
+
executables: []
|
|
73
|
+
extensions: []
|
|
74
|
+
extra_rdoc_files: []
|
|
75
|
+
files:
|
|
76
|
+
- LICENSE
|
|
77
|
+
- README.md
|
|
78
|
+
- lib/ask-auth.rb
|
|
79
|
+
- lib/ask/auth.rb
|
|
80
|
+
- lib/ask/auth/providers/database.rb
|
|
81
|
+
- lib/ask/auth/providers/env.rb
|
|
82
|
+
- lib/ask/auth/providers/file.rb
|
|
83
|
+
- lib/ask/auth/providers/oauth.rb
|
|
84
|
+
- lib/ask/auth/providers/rails_credentials.rb
|
|
85
|
+
- lib/ask/auth/version.rb
|
|
86
|
+
homepage: https://github.com/ask-rb/ask-auth
|
|
87
|
+
licenses:
|
|
88
|
+
- MIT
|
|
89
|
+
metadata: {}
|
|
90
|
+
rdoc_options: []
|
|
91
|
+
require_paths:
|
|
92
|
+
- lib
|
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
94
|
+
requirements:
|
|
95
|
+
- - ">="
|
|
96
|
+
- !ruby/object:Gem::Version
|
|
97
|
+
version: '3.2'
|
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0'
|
|
103
|
+
requirements: []
|
|
104
|
+
rubygems_version: 4.0.3
|
|
105
|
+
specification_version: 4
|
|
106
|
+
summary: Credential resolution for the ask-rb ecosystem
|
|
107
|
+
test_files: []
|