api_keys 0.2.1 → 0.3.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 +4 -4
- data/.simplecov +36 -0
- data/AGENTS.md +5 -0
- data/Appraisals +17 -0
- data/CHANGELOG.md +9 -0
- data/CLAUDE.md +5 -0
- data/README.md +767 -3
- data/Rakefile +9 -4
- data/app/controllers/api_keys/keys_controller.rb +43 -11
- data/app/controllers/api_keys/security_controller.rb +8 -0
- data/app/views/api_keys/keys/_empty_state.html.erb +9 -0
- data/app/views/api_keys/keys/_form.html.erb +31 -2
- data/app/views/api_keys/keys/_key_actions.html.erb +20 -0
- data/app/views/api_keys/keys/_key_badges.html.erb +17 -0
- data/app/views/api_keys/keys/_key_row.html.erb +21 -35
- data/app/views/api_keys/keys/_key_status.html.erb +10 -0
- data/app/views/api_keys/keys/_keys_table.html.erb +2 -7
- data/app/views/api_keys/keys/_publishable_keys.html.erb +40 -0
- data/app/views/api_keys/keys/_secret_keys.html.erb +39 -0
- data/app/views/api_keys/keys/_show_token.html.erb +5 -1
- data/app/views/api_keys/keys/_token_display.html.erb +11 -0
- data/app/views/api_keys/keys/index.html.erb +40 -8
- data/app/views/api_keys/security/best_practices.html.erb +73 -47
- data/app/views/layouts/api_keys/application.html.erb +113 -7
- data/context7.json +4 -0
- data/gemfiles/rails_7.2.gemfile +21 -0
- data/gemfiles/rails_8.0.gemfile +21 -0
- data/gemfiles/rails_8.1.gemfile +21 -0
- data/lib/api_keys/configuration.rb +77 -0
- data/lib/api_keys/errors.rb +73 -0
- data/lib/api_keys/form_builder_extensions.rb +158 -0
- data/lib/api_keys/helpers/expiration_options.rb +131 -0
- data/lib/api_keys/helpers/token_session.rb +68 -0
- data/lib/api_keys/helpers/view_helpers.rb +216 -0
- data/lib/api_keys/models/api_key.rb +229 -17
- data/lib/api_keys/models/concerns/has_api_keys.rb +183 -3
- data/lib/api_keys/services/authenticator.rb +45 -2
- data/lib/api_keys/services/digestor.rb +6 -2
- data/lib/api_keys/tenant_resolution.rb +3 -1
- data/lib/api_keys/version.rb +1 -1
- data/lib/api_keys.rb +12 -0
- data/lib/generators/api_keys/add_key_types_generator.rb +68 -0
- data/lib/generators/api_keys/templates/add_key_types_to_api_keys.rb.erb +18 -0
- data/lib/generators/api_keys/templates/create_api_keys_table.rb.erb +9 -0
- data/lib/generators/api_keys/templates/initializer.rb +242 -120
- metadata +24 -58
|
@@ -1,70 +1,96 @@
|
|
|
1
1
|
<header>
|
|
2
|
-
<h1>API Key Security
|
|
3
|
-
<p>Protecting your API keys is crucial for maintaining the security
|
|
2
|
+
<h1>API Key Security</h1>
|
|
3
|
+
<p>Protecting your API keys is crucial for maintaining the security of your account and data.</p>
|
|
4
4
|
</header>
|
|
5
5
|
|
|
6
|
-
<article class="col-
|
|
6
|
+
<article class="col-8">
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
</section>
|
|
8
|
+
<% if key_types_feature_enabled? %>
|
|
9
|
+
<section class="api-keys-section">
|
|
10
|
+
<h2>Understanding Key Types</h2>
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
<h3>Secret Keys</h3>
|
|
13
|
+
<p>Secret keys provide full access to your account and should be treated like passwords.</p>
|
|
14
|
+
<ul>
|
|
15
|
+
<li><strong>Never expose</strong> in client-side code (browsers, mobile apps, desktop apps)</li>
|
|
16
|
+
<li><strong>Never commit</strong> to version control (Git, etc.)</li>
|
|
17
|
+
<li><strong>Store securely</strong> using environment variables or secrets management</li>
|
|
18
|
+
<li><strong>Can be revoked</strong> immediately if compromised</li>
|
|
19
|
+
</ul>
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
<h3>Publishable Keys</h3>
|
|
22
|
+
<p>Publishable keys are designed for client-side use with limited, safe permissions.</p>
|
|
23
|
+
<ul>
|
|
24
|
+
<li><strong>Safe to embed</strong> in browser JavaScript, mobile apps, and public code</li>
|
|
25
|
+
<li><strong>Limited access</strong> — cannot perform sensitive operations</li>
|
|
26
|
+
<li><strong>Always visible</strong> — you can view the full key anytime in your dashboard</li>
|
|
27
|
+
<li><strong>Cannot be revoked</strong> — designed to be long-lived identifiers</li>
|
|
28
|
+
</ul>
|
|
29
|
+
</section>
|
|
30
|
+
<% end %>
|
|
24
31
|
|
|
25
32
|
<section>
|
|
26
|
-
<
|
|
27
|
-
<p>Committing keys to your source code repository (like Git, Mercurial, etc.) is a common and dangerous mistake. Even in private repositories, accidental pushes or repository breaches can leak your keys.</p>
|
|
28
|
-
<p><strong>Solution:</strong> Store keys in environment variables or use a dedicated secrets management system. Access the key in your code via these secure methods.</p>
|
|
29
|
-
</section>
|
|
33
|
+
<h2>Essential Practices</h2>
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
<h3>Treat Secret Keys Like Passwords</h3>
|
|
36
|
+
<p>Your API keys grant access to your account. Handle them with the same care you would apply to your account password.</p>
|
|
37
|
+
|
|
38
|
+
<h3>Use Separate Keys for Different Purposes</h3>
|
|
39
|
+
<p>Create distinct keys for different applications and environments. If one key is compromised, you can revoke it without disrupting others.</p>
|
|
40
|
+
<p><em>Tip:</em> Use descriptive names like "Production Backend" or "Staging iOS App" to easily identify each key's purpose.</p>
|
|
41
|
+
|
|
42
|
+
<% unless key_types_feature_enabled? %>
|
|
43
|
+
<h3>Never Expose Keys in Client-Side Code</h3>
|
|
44
|
+
<p><strong>Never</strong> embed API keys in mobile apps, browser JavaScript, or desktop applications. Exposed keys can be easily extracted by malicious actors.</p>
|
|
45
|
+
<p><strong>Solution:</strong> Route API requests through your own backend server, which can securely store and use the API key.</p>
|
|
46
|
+
<% end %>
|
|
38
47
|
</section>
|
|
39
48
|
|
|
40
49
|
<section>
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
<
|
|
50
|
+
<h2>Secure Storage</h2>
|
|
51
|
+
|
|
52
|
+
<h3>Environment Variables</h3>
|
|
53
|
+
<p>The simplest secure method. Set an environment variable on your server:</p>
|
|
54
|
+
<pre><code># In your shell or deployment config
|
|
55
|
+
export YOUR_SERVICE_API_KEY="sk_..."
|
|
56
|
+
|
|
57
|
+
# Access in Ruby
|
|
58
|
+
ENV['YOUR_SERVICE_API_KEY']</code></pre>
|
|
59
|
+
|
|
60
|
+
<h3>Rails Encrypted Credentials</h3>
|
|
61
|
+
<p>For Rails applications, use encrypted credentials:</p>
|
|
62
|
+
<pre><code># Edit credentials
|
|
63
|
+
bin/rails credentials:edit
|
|
64
|
+
|
|
65
|
+
# Access in code
|
|
66
|
+
Rails.application.credentials.your_service_api_key</code></pre>
|
|
67
|
+
<p><%= link_to "Rails Security Guide", "https://guides.rubyonrails.org/security.html#custom-credentials", target: "_blank", rel: "noopener noreferrer", class: "text-primary" %></p>
|
|
68
|
+
|
|
69
|
+
<h3>Secrets Management Services</h3>
|
|
70
|
+
<p>For production environments, consider dedicated services like HashiCorp Vault, AWS Secrets Manager, or Google Secret Manager.</p>
|
|
44
71
|
</section>
|
|
45
72
|
|
|
46
73
|
<section>
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
74
|
+
<h2>Monitoring & Rotation</h2>
|
|
75
|
+
|
|
76
|
+
<h3>Monitor Usage</h3>
|
|
77
|
+
<p>Regularly check for unexpected activity spikes or requests from unusual locations, which could indicate a compromised key.</p>
|
|
78
|
+
|
|
79
|
+
<h3>Rotate Keys Periodically</h3>
|
|
80
|
+
<p>Generate new keys and revoke old ones on a regular schedule (e.g., every 90 days). This limits exposure if a key is leaked undetected.</p>
|
|
81
|
+
<p><em>Tip:</em> Create a new key first, update your application, verify it works, then revoke the old key.</p>
|
|
82
|
+
|
|
83
|
+
<h3>Revoke Immediately if Compromised</h3>
|
|
84
|
+
<p>If you suspect a key has been leaked, revoke it immediately from your dashboard.</p>
|
|
54
85
|
</section>
|
|
55
86
|
|
|
56
87
|
<section>
|
|
57
|
-
<
|
|
58
|
-
<p>Ensure all API requests are made over HTTPS
|
|
88
|
+
<h2>Always Use HTTPS</h2>
|
|
89
|
+
<p>Ensure all API requests are made over HTTPS. Transmitting keys over unencrypted HTTP exposes them to eavesdropping.</p>
|
|
59
90
|
</section>
|
|
60
91
|
|
|
61
92
|
<hr>
|
|
62
93
|
|
|
63
|
-
<p
|
|
64
|
-
|
|
65
|
-
<%# Link back to the keys index if appropriate %>
|
|
66
|
-
<% if defined?(api_keys.keys_path) %>
|
|
67
|
-
<p><%= link_to "Back to API Keys", api_keys.keys_path, class: "text-primary" %></p>
|
|
68
|
-
<% end %>
|
|
94
|
+
<p><%= link_to "Back to API Keys", api_keys.keys_path, class: "text-primary" %></p>
|
|
69
95
|
|
|
70
|
-
</article>
|
|
96
|
+
</article>
|
|
@@ -12,15 +12,67 @@
|
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<style>
|
|
15
|
+
/*
|
|
16
|
+
* API Keys CSS Variables
|
|
17
|
+
* Override these in your host application to customize the dashboard appearance.
|
|
18
|
+
* Example in your app's CSS:
|
|
19
|
+
* :root {
|
|
20
|
+
* --api-keys-primary-color: #your-brand-color;
|
|
21
|
+
* --api-keys-danger-color: #your-danger-color;
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
24
|
+
:root {
|
|
25
|
+
/* Colors */
|
|
26
|
+
--api-keys-primary-color: #007bff;
|
|
27
|
+
--api-keys-danger-color: #c23539;
|
|
28
|
+
--api-keys-success-color: #28a745;
|
|
29
|
+
--api-keys-warning-color: #ffc107;
|
|
30
|
+
--api-keys-muted-color: #6c757d;
|
|
31
|
+
|
|
32
|
+
/* Badge colors */
|
|
33
|
+
--api-keys-badge-secret-bg: #e7f1ff;
|
|
34
|
+
--api-keys-badge-secret-color: #004085;
|
|
35
|
+
--api-keys-badge-publishable-bg: #fef3cd;
|
|
36
|
+
--api-keys-badge-publishable-color: #856404;
|
|
37
|
+
--api-keys-badge-live-bg: #d4edda;
|
|
38
|
+
--api-keys-badge-live-color: #155724;
|
|
39
|
+
--api-keys-badge-test-bg: #f8d7da;
|
|
40
|
+
--api-keys-badge-test-color: #721c24;
|
|
41
|
+
|
|
42
|
+
/* Status colors */
|
|
43
|
+
--api-keys-status-active-color: green;
|
|
44
|
+
--api-keys-status-revoked-color: orange;
|
|
45
|
+
--api-keys-status-expired-color: red;
|
|
46
|
+
|
|
47
|
+
/* Spacing */
|
|
48
|
+
--api-keys-section-padding: 1.5em;
|
|
49
|
+
--api-keys-section-margin: 2em;
|
|
50
|
+
--api-keys-border-radius: 8px;
|
|
51
|
+
|
|
52
|
+
/* Typography */
|
|
53
|
+
--api-keys-font-family: inherit;
|
|
54
|
+
--api-keys-code-font-size: 0.8em;
|
|
55
|
+
}
|
|
56
|
+
|
|
15
57
|
body {
|
|
16
|
-
|
|
58
|
+
font-family: var(--api-keys-font-family);
|
|
17
59
|
}
|
|
18
60
|
|
|
19
61
|
@media (prefers-color-scheme: dark) {
|
|
62
|
+
:root {
|
|
63
|
+
--api-keys-badge-secret-bg: #1a365d;
|
|
64
|
+
--api-keys-badge-secret-color: #90cdf4;
|
|
65
|
+
--api-keys-badge-publishable-bg: #744210;
|
|
66
|
+
--api-keys-badge-publishable-color: #faf089;
|
|
67
|
+
--api-keys-badge-live-bg: #22543d;
|
|
68
|
+
--api-keys-badge-live-color: #9ae6b4;
|
|
69
|
+
--api-keys-badge-test-bg: #742a2a;
|
|
70
|
+
--api-keys-badge-test-color: #feb2b2;
|
|
71
|
+
}
|
|
72
|
+
|
|
20
73
|
body {
|
|
21
|
-
|
|
22
|
-
--bg-color:rgb(
|
|
23
|
-
--bg-secondary-color:rgb(34, 34, 34);
|
|
74
|
+
--bg-color: rgb(14, 14, 14);
|
|
75
|
+
--bg-secondary-color: rgb(34, 34, 34);
|
|
24
76
|
--font-color: #f5f5f5;
|
|
25
77
|
--color-grey: #ccc;
|
|
26
78
|
--color-darkGrey: #777;
|
|
@@ -29,7 +81,7 @@
|
|
|
29
81
|
|
|
30
82
|
code, pre {
|
|
31
83
|
color: var(--font-color);
|
|
32
|
-
font-size:
|
|
84
|
+
font-size: var(--api-keys-code-font-size);
|
|
33
85
|
}
|
|
34
86
|
|
|
35
87
|
.api-keys-align-center {
|
|
@@ -52,7 +104,7 @@
|
|
|
52
104
|
.api-keys-action-buttons button {
|
|
53
105
|
background: none;
|
|
54
106
|
padding: 0;
|
|
55
|
-
color:
|
|
107
|
+
color: var(--api-keys-danger-color);
|
|
56
108
|
}
|
|
57
109
|
|
|
58
110
|
.api-keys-button-text {
|
|
@@ -77,6 +129,38 @@
|
|
|
77
129
|
margin-top: 2.4em;
|
|
78
130
|
}
|
|
79
131
|
|
|
132
|
+
/* API Keys sections styling */
|
|
133
|
+
.api-keys-section {
|
|
134
|
+
margin-top: var(--api-keys-section-margin);
|
|
135
|
+
padding: var(--api-keys-section-padding);
|
|
136
|
+
border: 1px solid var(--color-grey, #ccc);
|
|
137
|
+
border-radius: var(--api-keys-border-radius);
|
|
138
|
+
background-color: var(--bg-secondary-color, #f9f9f9);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.api-keys-section h2 {
|
|
142
|
+
margin-top: 0;
|
|
143
|
+
margin-bottom: 0.5em;
|
|
144
|
+
font-size: 1.25em;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.api-keys-section-description {
|
|
148
|
+
margin-bottom: 1em;
|
|
149
|
+
color: var(--color-darkGrey, #666);
|
|
150
|
+
font-size: 0.9em;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.api-keys-info-text {
|
|
154
|
+
margin-bottom: 1em;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@media (prefers-color-scheme: dark) {
|
|
158
|
+
.api-keys-section {
|
|
159
|
+
background-color: var(--bg-secondary-color, rgb(34, 34, 34));
|
|
160
|
+
border-color: var(--color-darkGrey, #555);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
80
164
|
</style>
|
|
81
165
|
|
|
82
166
|
<nav class="nav container">
|
|
@@ -109,7 +193,29 @@
|
|
|
109
193
|
<%# Footer content %>
|
|
110
194
|
</footer>
|
|
111
195
|
|
|
112
|
-
|
|
196
|
+
<script>
|
|
197
|
+
// Minimal JS for API Keys dashboard - event delegation for show/copy tokens
|
|
198
|
+
document.addEventListener('click', function(e) {
|
|
199
|
+
// Show token (one-way: masked -> full, no hiding back)
|
|
200
|
+
if (e.target.matches('.btn-show-token')) {
|
|
201
|
+
var cell = e.target.closest('td');
|
|
202
|
+
cell.querySelector('.token-masked').style.display = 'none';
|
|
203
|
+
cell.querySelector('.token-full').style.display = 'inline';
|
|
204
|
+
e.target.style.display = 'none';
|
|
205
|
+
cell.querySelector('.btn-copy-token').style.display = 'inline';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Copy token to clipboard
|
|
209
|
+
if (e.target.matches('.btn-copy-token')) {
|
|
210
|
+
var token = e.target.getAttribute('data-token');
|
|
211
|
+
navigator.clipboard.writeText(token).then(function() {
|
|
212
|
+
var originalText = e.target.textContent;
|
|
213
|
+
e.target.textContent = 'Copied!';
|
|
214
|
+
setTimeout(function() { e.target.textContent = originalText; }, 2000);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
</script>
|
|
113
219
|
|
|
114
220
|
</body>
|
|
115
221
|
</html>
|
data/context7.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "rails", "~> 7.2.3"
|
|
7
|
+
|
|
8
|
+
group :development, :test do
|
|
9
|
+
gem "appraisal"
|
|
10
|
+
gem "minitest", "~> 6.0"
|
|
11
|
+
gem "minitest-mock"
|
|
12
|
+
gem "minitest-reporters"
|
|
13
|
+
gem "rack-test"
|
|
14
|
+
gem "simplecov", require: false
|
|
15
|
+
gem "sqlite3", ">= 2.1"
|
|
16
|
+
gem "bootsnap", require: false
|
|
17
|
+
gem "mocha", "~> 2.0"
|
|
18
|
+
gem "ostruct"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "rails", "~> 8.0"
|
|
7
|
+
|
|
8
|
+
group :development, :test do
|
|
9
|
+
gem "appraisal"
|
|
10
|
+
gem "minitest", "~> 6.0"
|
|
11
|
+
gem "minitest-mock"
|
|
12
|
+
gem "minitest-reporters"
|
|
13
|
+
gem "rack-test"
|
|
14
|
+
gem "simplecov", require: false
|
|
15
|
+
gem "sqlite3", ">= 2.1"
|
|
16
|
+
gem "bootsnap", require: false
|
|
17
|
+
gem "mocha", "~> 2.0"
|
|
18
|
+
gem "ostruct"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "rails", "~> 8.1.2"
|
|
7
|
+
|
|
8
|
+
group :development, :test do
|
|
9
|
+
gem "appraisal"
|
|
10
|
+
gem "minitest", "~> 6.0"
|
|
11
|
+
gem "minitest-mock"
|
|
12
|
+
gem "minitest-reporters"
|
|
13
|
+
gem "rack-test"
|
|
14
|
+
gem "simplecov", require: false
|
|
15
|
+
gem "sqlite3", ">= 2.1"
|
|
16
|
+
gem "bootsnap", require: false
|
|
17
|
+
gem "mocha", "~> 2.0"
|
|
18
|
+
gem "ostruct"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
gemspec path: "../"
|
|
@@ -55,6 +55,75 @@ module ApiKeys
|
|
|
55
55
|
# Debugging
|
|
56
56
|
attr_accessor :debug_logging
|
|
57
57
|
|
|
58
|
+
# Key Types & Environments (Stripe-style publishable/secret keys)
|
|
59
|
+
#
|
|
60
|
+
# @!attribute [rw] key_types
|
|
61
|
+
# @return [Hash] Key type definitions. Each key type has:
|
|
62
|
+
# - :prefix [String] Token prefix (e.g., "pk" → pk_test_)
|
|
63
|
+
# - :permissions [Array<String>, :all] Scope ceiling for this type
|
|
64
|
+
# - :revocable [Boolean] Whether keys can be revoked (default: true)
|
|
65
|
+
# - :limit [Integer, nil] Max keys per owner per environment (nil = unlimited)
|
|
66
|
+
# - :public [Boolean] If true AND revocable: false, store plaintext token in
|
|
67
|
+
# metadata so it can be viewed again in dashboard. Use ONLY for publishable
|
|
68
|
+
# keys that are designed to be embedded in distributed apps. (default: false)
|
|
69
|
+
# @example
|
|
70
|
+
# config.key_types = {
|
|
71
|
+
# publishable: { prefix: "pk", permissions: %w[read], revocable: false, public: true, limit: 1 },
|
|
72
|
+
# secret: { prefix: "sk", permissions: :all }
|
|
73
|
+
# }
|
|
74
|
+
#
|
|
75
|
+
# @!attribute [rw] environments
|
|
76
|
+
# @return [Hash] Environment definitions. Each environment has:
|
|
77
|
+
# - :prefix_segment [String, nil] Middle part of prefix (e.g., "test" → pk_test_)
|
|
78
|
+
# @example
|
|
79
|
+
# config.environments = { test: { prefix_segment: "test" }, live: { prefix_segment: "live" } }
|
|
80
|
+
#
|
|
81
|
+
# @!attribute [rw] current_environment
|
|
82
|
+
# @return [Proc, Symbol] Lambda or symbol returning current environment
|
|
83
|
+
# @example
|
|
84
|
+
# config.current_environment = -> { Rails.env.production? ? :live : :test }
|
|
85
|
+
#
|
|
86
|
+
# @!attribute [rw] strict_environment_isolation
|
|
87
|
+
# @return [Boolean] If true, keys only work in their matching environment
|
|
88
|
+
#
|
|
89
|
+
# @!attribute [rw] default_key_type
|
|
90
|
+
# @return [Symbol, nil] Default key type when not specified in create_api_key!
|
|
91
|
+
# @example
|
|
92
|
+
# config.default_key_type = :secret
|
|
93
|
+
#
|
|
94
|
+
# @!attribute [rw] dashboard_allow_cross_environment
|
|
95
|
+
# @return [Boolean] If true, dashboard shows keys from all environments.
|
|
96
|
+
# If false (default), dashboard only shows keys matching current_environment.
|
|
97
|
+
# @example
|
|
98
|
+
# config.dashboard_allow_cross_environment = false
|
|
99
|
+
attr_accessor :environments, :current_environment, :strict_environment_isolation,
|
|
100
|
+
:default_key_type, :dashboard_allow_cross_environment
|
|
101
|
+
|
|
102
|
+
# Custom writer for key_types that validates prefix uniqueness
|
|
103
|
+
attr_reader :key_types
|
|
104
|
+
|
|
105
|
+
# Sets the key types configuration with prefix collision validation.
|
|
106
|
+
# @param value [Hash] Key type definitions
|
|
107
|
+
# @raise [ArgumentError] If multiple key types share the same prefix
|
|
108
|
+
def key_types=(value)
|
|
109
|
+
validate_key_type_prefixes!(value) if value.is_a?(Hash) && value.any?
|
|
110
|
+
@key_types = value
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
# Validates that all key type prefixes are unique to prevent token collision
|
|
116
|
+
def validate_key_type_prefixes!(key_types_hash)
|
|
117
|
+
prefixes = key_types_hash.map { |_type, config| config[:prefix] }.compact
|
|
118
|
+
duplicates = prefixes.group_by(&:itself).select { |_k, v| v.size > 1 }.keys
|
|
119
|
+
|
|
120
|
+
if duplicates.any?
|
|
121
|
+
raise ArgumentError, "Key type prefixes must be unique. Duplicate prefix(es): #{duplicates.join(', ')}"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
public
|
|
126
|
+
|
|
58
127
|
# == Initialization ==
|
|
59
128
|
|
|
60
129
|
def initialize
|
|
@@ -127,6 +196,14 @@ module ApiKeys
|
|
|
127
196
|
|
|
128
197
|
# Tenant Resolution
|
|
129
198
|
@tenant_resolver = ->(api_key) { api_key.owner if api_key.respond_to?(:owner) }
|
|
199
|
+
|
|
200
|
+
# Key Types & Environments (default to empty/disabled for backwards compatibility)
|
|
201
|
+
@key_types = {} # Empty = feature disabled, legacy behavior
|
|
202
|
+
@environments = {} # Empty = no environment-based prefixes
|
|
203
|
+
@current_environment = -> { :default } # Default environment detection
|
|
204
|
+
@strict_environment_isolation = false # Don't enforce environment isolation by default
|
|
205
|
+
@default_key_type = nil # No default key type (must be specified explicitly)
|
|
206
|
+
@dashboard_allow_cross_environment = false # Dashboard shows only current environment's keys
|
|
130
207
|
end
|
|
131
208
|
end
|
|
132
209
|
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ApiKeys
|
|
4
|
+
module Errors
|
|
5
|
+
# Base error class for all ApiKeys errors
|
|
6
|
+
class BaseError < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Raised when attempting to revoke or destroy a non-revocable key
|
|
9
|
+
class KeyNotRevocableError < BaseError
|
|
10
|
+
def initialize(message = "This API key cannot be revoked or deleted")
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Raised when API key environment doesn't match current environment (strict isolation)
|
|
16
|
+
class EnvironmentMismatchError < BaseError
|
|
17
|
+
attr_reader :key_environment, :current_environment
|
|
18
|
+
|
|
19
|
+
def initialize(key_environment:, current_environment:)
|
|
20
|
+
@key_environment = key_environment
|
|
21
|
+
@current_environment = current_environment
|
|
22
|
+
super("API key environment (#{key_environment}) does not match current environment (#{current_environment})")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Raised when an invalid key type is specified
|
|
27
|
+
class InvalidKeyTypeError < BaseError
|
|
28
|
+
attr_reader :key_type, :valid_types
|
|
29
|
+
|
|
30
|
+
def initialize(key_type:, valid_types:)
|
|
31
|
+
@key_type = key_type
|
|
32
|
+
@valid_types = valid_types
|
|
33
|
+
super("Invalid key type '#{key_type}'. Valid types: #{valid_types.join(', ')}")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Raised when an invalid environment is specified
|
|
38
|
+
class InvalidEnvironmentError < BaseError
|
|
39
|
+
attr_reader :environment, :valid_environments
|
|
40
|
+
|
|
41
|
+
def initialize(environment:, valid_environments:)
|
|
42
|
+
@environment = environment
|
|
43
|
+
@valid_environments = valid_environments
|
|
44
|
+
super("Invalid environment '#{environment}'. Valid environments: #{valid_environments.join(', ')}")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Raised when key limit per type/environment is exceeded
|
|
49
|
+
class KeyLimitExceededError < BaseError
|
|
50
|
+
attr_reader :key_type, :environment, :limit
|
|
51
|
+
|
|
52
|
+
def initialize(key_type:, environment:, limit:)
|
|
53
|
+
@key_type = key_type
|
|
54
|
+
@environment = environment
|
|
55
|
+
@limit = limit
|
|
56
|
+
super("Maximum number of #{key_type} keys (#{limit}) reached for #{environment} environment")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Raised when key_types are configured but required database columns are missing
|
|
61
|
+
class MigrationRequiredError < BaseError
|
|
62
|
+
attr_reader :missing_columns
|
|
63
|
+
|
|
64
|
+
def initialize(missing_columns:)
|
|
65
|
+
@missing_columns = missing_columns
|
|
66
|
+
super(
|
|
67
|
+
"Key types are configured but required database columns are missing: #{missing_columns.join(', ')}. " \
|
|
68
|
+
"Run: rails generate api_keys:add_key_types && rails db:migrate"
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|