create-feltdb 0.5.6 → 0.5.7

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.
@@ -1,4 +1,4 @@
1
1
  // One release train keeps generated applications installable. The repository
2
2
  // validation script checks these values against every workspace manifest.
3
- export const FELTDB_PACKAGE_VERSION = '0.5.6';
3
+ export const FELTDB_PACKAGE_VERSION = '0.5.7';
4
4
  export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
@@ -513,6 +513,7 @@ dependencies = [
513
513
  "base64",
514
514
  "feltdb",
515
515
  "futures-core",
516
+ "hmac",
516
517
  "password-hash",
517
518
  "rand 0.8.7",
518
519
  "reqwest",
@@ -668,6 +669,15 @@ version = "0.5.2"
668
669
  source = "registry+https://github.com/rust-lang/crates.io-index"
669
670
  checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
670
671
 
672
+ [[package]]
673
+ name = "hmac"
674
+ version = "0.12.1"
675
+ source = "registry+https://github.com/rust-lang/crates.io-index"
676
+ checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
677
+ dependencies = [
678
+ "digest",
679
+ ]
680
+
671
681
  [[package]]
672
682
  name = "http"
673
683
  version = "1.5.0"
@@ -9,6 +9,7 @@ async-stream = "0.3"
9
9
  argon2 = "0.5"
10
10
  password-hash = "0.5"
11
11
  base64 = "0.22"
12
+ hmac = "0.12"
12
13
  feltdb = { path = "../feltdb" }
13
14
  futures-core = "0.3"
14
15
  rand = "0.8"
@@ -1,5 +1,8 @@
1
- use std::sync::{atomic::AtomicU64, Arc};
2
1
  use std::path::PathBuf;
2
+ use std::{
3
+ sync::{atomic::AtomicU64, Arc},
4
+ time::Instant,
5
+ };
3
6
 
4
7
  use feltdb::{
5
8
  FeltDb,
@@ -33,6 +36,7 @@ use crate::{
33
36
 
34
37
  #[derive(Clone)]
35
38
  pub struct AppState {
39
+ pub started_at: Instant,
36
40
  pub db: FeltDb,
37
41
  pub namespace: Arc<str>,
38
42
  pub ids: Arc<AtomicU64>,
@@ -0,0 +1,273 @@
1
+ use serde::{Deserialize, Serialize};
2
+ use std::time::{SystemTime, UNIX_EPOCH};
3
+
4
+ /// Authenticated principal derived exclusively from verified credentials.
5
+ /// Never derived from request body or untrusted sources.
6
+ /// Available to authorization evaluation and included in request context.
7
+ #[derive(Debug, Clone, Serialize, Deserialize)]
8
+ pub struct AuthenticatedPrincipal {
9
+ /// ID of the service key used for authentication.
10
+ /// Preserved separately from actorId for auditing.
11
+ pub service_key_id: String,
12
+
13
+ /// ID of the actor (user, service, workload, etc.) performing the operation.
14
+ pub actor_id: String,
15
+
16
+ /// Tenant ID from authenticated credentials.
17
+ pub tenant_id: String,
18
+
19
+ /// Roles granted to this principal.
20
+ pub roles: Vec<String>,
21
+
22
+ /// Authentication method used.
23
+ pub auth_method: AuthMethod,
24
+
25
+ /// When this principal was authenticated.
26
+ pub authenticated_at: u64,
27
+
28
+ /// Provenance tracking for audit.
29
+ pub provenance: AuthenticationProvenance,
30
+ }
31
+
32
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33
+ #[serde(rename_all = "lowercase")]
34
+ pub enum AuthMethod {
35
+ ApiKey,
36
+ Session,
37
+ Delegation,
38
+ }
39
+
40
+ #[derive(Debug, Clone, Serialize, Deserialize)]
41
+ pub struct AuthenticationProvenance {
42
+ /// Source of the credential (e.g., "bearer_token", "session_cookie", "delegation_token").
43
+ pub credential_source: String,
44
+
45
+ /// Hash of the credential for audit purposes (never the credential itself).
46
+ pub credential_hash: Option<String>,
47
+
48
+ /// Additional context about the authentication.
49
+ pub context: String,
50
+ }
51
+
52
+ impl AuthenticatedPrincipal {
53
+ /// Creates a new AuthenticatedPrincipal from API key authentication.
54
+ pub fn from_api_key(
55
+ service_key_id: String,
56
+ actor_id: String,
57
+ tenant_id: String,
58
+ roles: Vec<String>,
59
+ ) -> Self {
60
+ Self {
61
+ service_key_id,
62
+ actor_id,
63
+ tenant_id,
64
+ roles,
65
+ auth_method: AuthMethod::ApiKey,
66
+ authenticated_at: now(),
67
+ provenance: AuthenticationProvenance {
68
+ credential_source: "bearer_token".into(),
69
+ credential_hash: None,
70
+ context: "API key authentication".into(),
71
+ },
72
+ }
73
+ }
74
+
75
+ /// Creates a new AuthenticatedPrincipal from session authentication.
76
+ pub fn from_session(
77
+ service_key_id: String,
78
+ actor_id: String,
79
+ tenant_id: String,
80
+ roles: Vec<String>,
81
+ ) -> Self {
82
+ Self {
83
+ service_key_id,
84
+ actor_id,
85
+ tenant_id,
86
+ roles,
87
+ auth_method: AuthMethod::Session,
88
+ authenticated_at: now(),
89
+ provenance: AuthenticationProvenance {
90
+ credential_source: "session_cookie".into(),
91
+ credential_hash: None,
92
+ context: "Session authentication".into(),
93
+ },
94
+ }
95
+ }
96
+
97
+ /// Creates a new AuthenticatedPrincipal from delegation token.
98
+ pub fn from_delegation(
99
+ service_key_id: String,
100
+ actor_id: String,
101
+ tenant_id: String,
102
+ roles: Vec<String>,
103
+ delegation_context: String,
104
+ ) -> Self {
105
+ Self {
106
+ service_key_id,
107
+ actor_id,
108
+ tenant_id,
109
+ roles,
110
+ auth_method: AuthMethod::Delegation,
111
+ authenticated_at: now(),
112
+ provenance: AuthenticationProvenance {
113
+ credential_source: "delegation_token".into(),
114
+ credential_hash: None,
115
+ context: delegation_context,
116
+ },
117
+ }
118
+ }
119
+
120
+ /// Checks if this principal has a specific role.
121
+ pub fn has_role(&self, role: &str) -> bool {
122
+ self.roles.iter().any(|r| r == role || r == "*")
123
+ }
124
+
125
+ /// Checks if actor_id matches expected value, rejecting mismatches.
126
+ pub fn verify_actor_id(&self, expected: &str) -> Result<(), String> {
127
+ if self.actor_id != expected {
128
+ Err(format!(
129
+ "actor_id mismatch: authenticated as {} but operation requested {}",
130
+ self.actor_id, expected
131
+ ))
132
+ } else {
133
+ Ok(())
134
+ }
135
+ }
136
+
137
+ /// Checks if tenant_id matches expected value, rejecting mismatches.
138
+ pub fn verify_tenant_id(&self, expected: &str) -> Result<(), String> {
139
+ if self.tenant_id != expected {
140
+ Err(format!(
141
+ "tenant_id mismatch: authenticated for {} but operation requested {}",
142
+ self.tenant_id, expected
143
+ ))
144
+ } else {
145
+ Ok(())
146
+ }
147
+ }
148
+ }
149
+
150
+ fn now() -> u64 {
151
+ SystemTime::now()
152
+ .duration_since(UNIX_EPOCH)
153
+ .unwrap_or_default()
154
+ .as_secs()
155
+ }
156
+
157
+
158
+ #[cfg(test)]
159
+ mod tests {
160
+ use super::*;
161
+
162
+ #[test]
163
+ fn authenticated_principal_preserves_separation() {
164
+ let principal = AuthenticatedPrincipal::from_api_key(
165
+ "key_abc123".into(),
166
+ "user_xyz".into(),
167
+ "tenant_prod".into(),
168
+ vec!["read".into(), "write".into()],
169
+ );
170
+
171
+ assert_eq!(principal.service_key_id, "key_abc123");
172
+ assert_eq!(principal.actor_id, "user_xyz");
173
+ assert_eq!(principal.tenant_id, "tenant_prod");
174
+ assert_eq!(principal.auth_method, AuthMethod::ApiKey);
175
+ }
176
+
177
+ #[test]
178
+ fn verify_actor_id_rejects_mismatches() {
179
+ let principal = AuthenticatedPrincipal::from_api_key(
180
+ "key_abc123".into(),
181
+ "user_xyz".into(),
182
+ "tenant_prod".into(),
183
+ vec![],
184
+ );
185
+
186
+ assert!(principal.verify_actor_id("user_xyz").is_ok());
187
+ assert!(principal.verify_actor_id("user_other").is_err());
188
+ }
189
+
190
+ #[test]
191
+ fn verify_tenant_id_rejects_mismatches() {
192
+ let principal = AuthenticatedPrincipal::from_api_key(
193
+ "key_abc123".into(),
194
+ "user_xyz".into(),
195
+ "tenant_prod".into(),
196
+ vec![],
197
+ );
198
+
199
+ assert!(principal.verify_tenant_id("tenant_prod").is_ok());
200
+ assert!(principal.verify_tenant_id("tenant_other").is_err());
201
+ }
202
+
203
+ #[test]
204
+ fn has_role_works_correctly() {
205
+ let principal = AuthenticatedPrincipal::from_api_key(
206
+ "key_abc123".into(),
207
+ "user_xyz".into(),
208
+ "tenant_prod".into(),
209
+ vec!["admin".into(), "viewer".into()],
210
+ );
211
+
212
+ assert!(principal.has_role("admin"));
213
+ assert!(principal.has_role("viewer"));
214
+ assert!(!principal.has_role("editor"));
215
+ }
216
+
217
+ #[test]
218
+ fn wildcard_role_matches_all() {
219
+ let principal = AuthenticatedPrincipal::from_api_key(
220
+ "key_abc123".into(),
221
+ "user_xyz".into(),
222
+ "tenant_prod".into(),
223
+ vec!["*".into()],
224
+ );
225
+
226
+ assert!(principal.has_role("any_role"));
227
+ assert!(principal.has_role("admin"));
228
+ }
229
+
230
+ #[test]
231
+ fn from_session_creates_correct_principal() {
232
+ let principal = AuthenticatedPrincipal::from_session(
233
+ "key_session".into(),
234
+ "user_abc".into(),
235
+ "tenant_staging".into(),
236
+ vec!["viewer".into()],
237
+ );
238
+
239
+ assert_eq!(principal.auth_method, AuthMethod::Session);
240
+ assert_eq!(principal.provenance.credential_source, "session_cookie");
241
+ }
242
+
243
+ #[test]
244
+ fn from_delegation_creates_correct_principal() {
245
+ let principal = AuthenticatedPrincipal::from_delegation(
246
+ "key_delegated".into(),
247
+ "service_xyz".into(),
248
+ "tenant_prod".into(),
249
+ vec!["execute".into()],
250
+ "delegated from sherpa".into(),
251
+ );
252
+
253
+ assert_eq!(principal.auth_method, AuthMethod::Delegation);
254
+ assert_eq!(principal.provenance.credential_source, "delegation_token");
255
+ assert_eq!(principal.provenance.context, "delegated from sherpa");
256
+ }
257
+
258
+ #[test]
259
+ fn multiple_calls_to_verify_preserve_state() {
260
+ let principal = AuthenticatedPrincipal::from_api_key(
261
+ "key_abc123".into(),
262
+ "user_xyz".into(),
263
+ "tenant_prod".into(),
264
+ vec![],
265
+ );
266
+
267
+ // Verify multiple times to ensure no state mutation
268
+ assert!(principal.verify_actor_id("user_xyz").is_ok());
269
+ assert!(principal.verify_actor_id("user_xyz").is_ok());
270
+ assert!(principal.verify_tenant_id("tenant_prod").is_ok());
271
+ assert!(principal.verify_tenant_id("tenant_prod").is_ok());
272
+ }
273
+ }