@wlfi-agent/cli 1.4.17 → 1.4.19
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.
- package/Cargo.lock +5 -0
- package/README.md +61 -28
- package/crates/vault-cli-admin/src/io_utils.rs +149 -1
- package/crates/vault-cli-admin/src/main.rs +639 -16
- package/crates/vault-cli-admin/src/shared_config.rs +18 -18
- package/crates/vault-cli-admin/src/tui/token_rpc.rs +190 -3
- package/crates/vault-cli-admin/src/tui/utils.rs +59 -0
- package/crates/vault-cli-admin/src/tui.rs +1205 -120
- package/crates/vault-cli-agent/Cargo.toml +1 -0
- package/crates/vault-cli-agent/src/io_utils.rs +163 -2
- package/crates/vault-cli-agent/src/main.rs +648 -32
- package/crates/vault-cli-daemon/Cargo.toml +4 -0
- package/crates/vault-cli-daemon/src/main.rs +617 -67
- package/crates/vault-cli-daemon/src/relay_sync.rs +776 -4
- package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +5 -0
- package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +32 -1
- package/crates/vault-daemon/src/persistence.rs +637 -100
- package/crates/vault-daemon/src/tests.rs +1013 -3
- package/crates/vault-daemon/src/tests_parts/part2.rs +99 -0
- package/crates/vault-daemon/src/tests_parts/part4.rs +11 -7
- package/crates/vault-domain/src/nonce.rs +4 -0
- package/crates/vault-domain/src/tests.rs +616 -0
- package/crates/vault-policy/src/engine.rs +55 -32
- package/crates/vault-policy/src/tests.rs +195 -0
- package/crates/vault-sdk-agent/src/lib.rs +415 -22
- package/crates/vault-signer/Cargo.toml +3 -0
- package/crates/vault-signer/src/lib.rs +266 -40
- package/crates/vault-transport-unix/src/lib.rs +653 -5
- package/crates/vault-transport-xpc/src/tests.rs +531 -3
- package/crates/vault-transport-xpc/tests/e2e_flow.rs +3 -0
- package/dist/cli.cjs +756 -194
- package/dist/cli.cjs.map +1 -1
- package/package.json +5 -2
- package/packages/cache/.turbo/turbo-build.log +20 -20
- package/packages/cache/coverage/clover.xml +529 -394
- package/packages/cache/coverage/coverage-final.json +2 -2
- package/packages/cache/coverage/index.html +21 -21
- package/packages/cache/coverage/src/client/index.html +1 -1
- package/packages/cache/coverage/src/client/index.ts.html +1 -1
- package/packages/cache/coverage/src/errors/index.html +1 -1
- package/packages/cache/coverage/src/errors/index.ts.html +12 -12
- package/packages/cache/coverage/src/index.html +1 -1
- package/packages/cache/coverage/src/index.ts.html +1 -1
- package/packages/cache/coverage/src/service/index.html +21 -21
- package/packages/cache/coverage/src/service/index.ts.html +769 -313
- package/packages/cache/dist/{chunk-QNK6GOTI.js → chunk-KC53LH5Z.js} +35 -2
- package/packages/cache/dist/chunk-KC53LH5Z.js.map +1 -0
- package/packages/cache/dist/{chunk-QF4XKEIA.cjs → chunk-UVU7VFE3.cjs} +35 -2
- package/packages/cache/dist/chunk-UVU7VFE3.cjs.map +1 -0
- package/packages/cache/dist/index.cjs +2 -2
- package/packages/cache/dist/index.js +1 -1
- package/packages/cache/dist/service/index.cjs +2 -2
- package/packages/cache/dist/service/index.js +1 -1
- package/packages/cache/node_modules/.bin/tsc +2 -2
- package/packages/cache/node_modules/.bin/tsserver +2 -2
- package/packages/cache/node_modules/.bin/tsup +2 -2
- package/packages/cache/node_modules/.bin/tsup-node +2 -2
- package/packages/cache/node_modules/.bin/vitest +4 -4
- package/packages/cache/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/cache/src/service/index.test.ts +165 -19
- package/packages/cache/src/service/index.ts +38 -1
- package/packages/config/.turbo/turbo-build.log +4 -4
- package/packages/config/dist/index.cjs +0 -17
- package/packages/config/dist/index.cjs.map +1 -1
- package/packages/config/src/index.ts +0 -17
- package/packages/rpc/.turbo/turbo-build.log +11 -11
- package/packages/rpc/dist/index.cjs +0 -17
- package/packages/rpc/dist/index.cjs.map +1 -1
- package/packages/rpc/src/index.js +1 -0
- package/packages/ui/node_modules/.bin/tsc +2 -2
- package/packages/ui/node_modules/.bin/tsserver +2 -2
- package/packages/ui/node_modules/.bin/tsup +2 -2
- package/packages/ui/node_modules/.bin/tsup-node +2 -2
- package/scripts/install-cli-launcher.mjs +37 -0
- package/scripts/install-rust-binaries.mjs +47 -0
- package/scripts/run-tests-isolated.mjs +210 -0
- package/src/cli.ts +310 -50
- package/src/lib/admin-reset.ts +101 -33
- package/src/lib/admin-setup.ts +285 -55
- package/src/lib/agent-auth-migrate.ts +5 -1
- package/src/lib/asset-broadcast.ts +15 -4
- package/src/lib/config-amounts.ts +6 -4
- package/src/lib/hidden-tty-prompt.js +1 -0
- package/src/lib/hidden-tty-prompt.ts +105 -0
- package/src/lib/keychain.ts +1 -0
- package/src/lib/local-admin-access.ts +4 -29
- package/src/lib/rust.ts +129 -33
- package/src/lib/signed-tx.ts +1 -0
- package/src/lib/sudo.ts +15 -5
- package/src/lib/wallet-profile.ts +3 -0
- package/src/lib/wallet-setup.ts +52 -0
- package/packages/cache/dist/chunk-QF4XKEIA.cjs.map +0 -1
- package/packages/cache/dist/chunk-QNK6GOTI.js.map +0 -1
|
@@ -1232,21 +1232,33 @@ impl KeyManagerDaemonApi for UnixDaemonClient {
|
|
|
1232
1232
|
#[cfg(test)]
|
|
1233
1233
|
mod tests {
|
|
1234
1234
|
use super::{
|
|
1235
|
-
assert_root_owned_daemon_socket_path,
|
|
1236
|
-
|
|
1235
|
+
assert_root_owned_daemon_socket_path, assert_trusted_daemon_socket_path,
|
|
1236
|
+
combined_allowed_peer_euids, ensure_socket_parent, handle_connection, read_frame,
|
|
1237
|
+
remove_existing_socket_file, rpc_access_level, socket_mode_for_allowed_peer_euids,
|
|
1237
1238
|
write_frame, RpcAccessLevel, UnixDaemonClient, UnixDaemonServer, UnixTransportError,
|
|
1238
|
-
WireRequest, WireResponse,
|
|
1239
|
+
WireDaemonError, WireRequest, WireResponse, MAX_WIRE_BODY_BYTES,
|
|
1239
1240
|
};
|
|
1241
|
+
use serde_json::to_vec;
|
|
1240
1242
|
use std::collections::BTreeSet;
|
|
1241
1243
|
use std::path::Path;
|
|
1242
1244
|
use std::sync::Arc;
|
|
1243
1245
|
use std::time::Duration;
|
|
1244
1246
|
use time::OffsetDateTime;
|
|
1247
|
+
use tokio::io::AsyncWriteExt;
|
|
1245
1248
|
use tokio::net::UnixStream;
|
|
1246
1249
|
use uuid::Uuid;
|
|
1247
|
-
use vault_daemon::{
|
|
1248
|
-
|
|
1250
|
+
use vault_daemon::{
|
|
1251
|
+
DaemonConfig, DaemonError, DaemonRpcRequest, DaemonRpcResponse, KeyManagerDaemonApi,
|
|
1252
|
+
InMemoryDaemon,
|
|
1253
|
+
};
|
|
1254
|
+
use vault_domain::{
|
|
1255
|
+
AdminSession, AgentAction, AgentCredentials, EntityScope, ManualApprovalStatus,
|
|
1256
|
+
NonceReleaseRequest, NonceReservationRequest, PolicyAttachment, PolicyType, RelayConfig,
|
|
1257
|
+
SignRequest, SpendingPolicy,
|
|
1258
|
+
};
|
|
1259
|
+
use vault_policy::{PolicyDecision, PolicyError};
|
|
1249
1260
|
use vault_signer::SoftwareSignerBackend;
|
|
1261
|
+
use vault_signer::{KeyCreateRequest, SignerError};
|
|
1250
1262
|
|
|
1251
1263
|
fn unique_socket_path(label: &str) -> std::path::PathBuf {
|
|
1252
1264
|
std::env::temp_dir().join(format!(
|
|
@@ -1257,6 +1269,19 @@ mod tests {
|
|
|
1257
1269
|
))
|
|
1258
1270
|
}
|
|
1259
1271
|
|
|
1272
|
+
fn short_test_root(label: &str) -> std::path::PathBuf {
|
|
1273
|
+
let base = if Path::new("/private/tmp").is_dir() {
|
|
1274
|
+
Path::new("/private/tmp")
|
|
1275
|
+
} else {
|
|
1276
|
+
Path::new("/tmp")
|
|
1277
|
+
};
|
|
1278
|
+
base.join(format!(
|
|
1279
|
+
"w-{}-{}",
|
|
1280
|
+
label,
|
|
1281
|
+
&uuid::Uuid::new_v4().simple().to_string()[..6]
|
|
1282
|
+
))
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1260
1285
|
fn singleton_allowed_set(euid: u32) -> BTreeSet<u32> {
|
|
1261
1286
|
let mut set = BTreeSet::new();
|
|
1262
1287
|
set.insert(euid);
|
|
@@ -1281,6 +1306,97 @@ mod tests {
|
|
|
1281
1306
|
}
|
|
1282
1307
|
}
|
|
1283
1308
|
|
|
1309
|
+
fn policy_all_per_tx(max_amount_wei: u128) -> SpendingPolicy {
|
|
1310
|
+
SpendingPolicy::new(
|
|
1311
|
+
0,
|
|
1312
|
+
PolicyType::PerTxMaxSpending,
|
|
1313
|
+
max_amount_wei,
|
|
1314
|
+
EntityScope::All,
|
|
1315
|
+
EntityScope::All,
|
|
1316
|
+
EntityScope::All,
|
|
1317
|
+
)
|
|
1318
|
+
.expect("policy")
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
fn admin_session(lease: vault_domain::Lease) -> AdminSession {
|
|
1322
|
+
AdminSession {
|
|
1323
|
+
vault_password: "vault-password".to_string(),
|
|
1324
|
+
lease,
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
fn sign_request(credentials: &AgentCredentials, amount_wei: u128) -> SignRequest {
|
|
1329
|
+
let action = AgentAction::TransferNative {
|
|
1330
|
+
chain_id: 1,
|
|
1331
|
+
to: "0x2222222222222222222222222222222222222222"
|
|
1332
|
+
.parse()
|
|
1333
|
+
.expect("recipient"),
|
|
1334
|
+
amount_wei,
|
|
1335
|
+
};
|
|
1336
|
+
let now = OffsetDateTime::now_utc();
|
|
1337
|
+
SignRequest {
|
|
1338
|
+
request_id: Uuid::new_v4(),
|
|
1339
|
+
agent_key_id: credentials.agent_key.id,
|
|
1340
|
+
agent_auth_token: credentials.auth_token.clone(),
|
|
1341
|
+
payload: to_vec(&action).expect("serialize action"),
|
|
1342
|
+
action,
|
|
1343
|
+
requested_at: now,
|
|
1344
|
+
expires_at: now + time::Duration::minutes(2),
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
fn nonce_reservation_request(
|
|
1349
|
+
credentials: &AgentCredentials,
|
|
1350
|
+
min_nonce: u64,
|
|
1351
|
+
) -> NonceReservationRequest {
|
|
1352
|
+
let now = OffsetDateTime::now_utc();
|
|
1353
|
+
NonceReservationRequest {
|
|
1354
|
+
request_id: Uuid::new_v4(),
|
|
1355
|
+
agent_key_id: credentials.agent_key.id,
|
|
1356
|
+
agent_auth_token: credentials.auth_token.clone(),
|
|
1357
|
+
chain_id: 1,
|
|
1358
|
+
min_nonce,
|
|
1359
|
+
exact_nonce: false,
|
|
1360
|
+
requested_at: now,
|
|
1361
|
+
expires_at: now + time::Duration::minutes(2),
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
fn nonce_release_request(
|
|
1366
|
+
credentials: &AgentCredentials,
|
|
1367
|
+
reservation_id: Uuid,
|
|
1368
|
+
) -> NonceReleaseRequest {
|
|
1369
|
+
let now = OffsetDateTime::now_utc();
|
|
1370
|
+
NonceReleaseRequest {
|
|
1371
|
+
request_id: Uuid::new_v4(),
|
|
1372
|
+
agent_key_id: credentials.agent_key.id,
|
|
1373
|
+
agent_auth_token: credentials.auth_token.clone(),
|
|
1374
|
+
reservation_id,
|
|
1375
|
+
requested_at: now,
|
|
1376
|
+
expires_at: now + time::Duration::minutes(2),
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
async fn spawn_one_shot_server(
|
|
1381
|
+
socket_path: std::path::PathBuf,
|
|
1382
|
+
response: WireResponse,
|
|
1383
|
+
) -> tokio::task::JoinHandle<()> {
|
|
1384
|
+
if let Some(parent) = socket_path.parent() {
|
|
1385
|
+
std::fs::create_dir_all(parent).expect("create server parent");
|
|
1386
|
+
}
|
|
1387
|
+
let _ = std::fs::remove_file(&socket_path);
|
|
1388
|
+
let listener = tokio::net::UnixListener::bind(&socket_path).expect("bind fake server");
|
|
1389
|
+
tokio::spawn(async move {
|
|
1390
|
+
let (mut stream, _) = listener.accept().await.expect("accept");
|
|
1391
|
+
let _: WireRequest = read_frame(&mut stream, Duration::from_secs(2))
|
|
1392
|
+
.await
|
|
1393
|
+
.expect("read request");
|
|
1394
|
+
write_frame(&mut stream, &response, Duration::from_secs(2))
|
|
1395
|
+
.await
|
|
1396
|
+
.expect("write response");
|
|
1397
|
+
})
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1284
1400
|
#[cfg(unix)]
|
|
1285
1401
|
fn socket_mode(path: &Path) -> u32 {
|
|
1286
1402
|
use std::os::unix::fs::PermissionsExt;
|
|
@@ -1361,6 +1477,114 @@ mod tests {
|
|
|
1361
1477
|
std::fs::remove_dir_all(&root).expect("cleanup");
|
|
1362
1478
|
}
|
|
1363
1479
|
|
|
1480
|
+
#[test]
|
|
1481
|
+
#[cfg(unix)]
|
|
1482
|
+
fn assert_trusted_daemon_socket_path_accepts_current_user_socket_and_rejects_bad_parents() {
|
|
1483
|
+
use std::os::unix::fs::symlink;
|
|
1484
|
+
use std::os::unix::net::UnixListener as StdUnixListener;
|
|
1485
|
+
|
|
1486
|
+
let err = assert_trusted_daemon_socket_path(Path::new(""))
|
|
1487
|
+
.expect_err("empty path must be rejected");
|
|
1488
|
+
assert!(err.contains("must not be empty"));
|
|
1489
|
+
|
|
1490
|
+
let err = assert_trusted_daemon_socket_path(Path::new("daemon.sock")).expect_err("must reject");
|
|
1491
|
+
assert!(err.contains("must have a parent directory"));
|
|
1492
|
+
|
|
1493
|
+
let root = short_test_root("trusted");
|
|
1494
|
+
let real_dir = root.join("real");
|
|
1495
|
+
std::fs::create_dir_all(&real_dir).expect("create real dir");
|
|
1496
|
+
let socket_path = real_dir.join("daemon.sock");
|
|
1497
|
+
let _listener = StdUnixListener::bind(&socket_path).expect("bind socket");
|
|
1498
|
+
assert_eq!(
|
|
1499
|
+
assert_trusted_daemon_socket_path(&socket_path).expect("trusted socket"),
|
|
1500
|
+
socket_path
|
|
1501
|
+
);
|
|
1502
|
+
|
|
1503
|
+
let linked_dir = root.join("linked");
|
|
1504
|
+
symlink(&real_dir, &linked_dir).expect("symlink dir");
|
|
1505
|
+
let err = assert_trusted_daemon_socket_path(&linked_dir.join("daemon.sock"))
|
|
1506
|
+
.expect_err("symlink parent must fail");
|
|
1507
|
+
assert!(err.contains("socket directory"));
|
|
1508
|
+
assert!(err.contains("must not be a symlink"));
|
|
1509
|
+
|
|
1510
|
+
std::fs::remove_file(root.join("linked")).expect("remove symlink");
|
|
1511
|
+
std::fs::remove_file(socket_path).expect("remove socket");
|
|
1512
|
+
std::fs::remove_dir_all(root).expect("cleanup");
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
#[tokio::test(flavor = "current_thread")]
|
|
1516
|
+
async fn write_and_read_frame_cover_protocol_and_serialization_errors() {
|
|
1517
|
+
let (mut writer, mut reader) = UnixStream::pair().expect("stream pair");
|
|
1518
|
+
let message = WireRequest {
|
|
1519
|
+
body_json: "{\"ok\":true}".to_string(),
|
|
1520
|
+
};
|
|
1521
|
+
write_frame(&mut writer, &message, Duration::from_secs(2))
|
|
1522
|
+
.await
|
|
1523
|
+
.expect("write frame");
|
|
1524
|
+
let decoded: WireRequest = read_frame(&mut reader, Duration::from_secs(2))
|
|
1525
|
+
.await
|
|
1526
|
+
.expect("read frame");
|
|
1527
|
+
assert_eq!(decoded.body_json, message.body_json);
|
|
1528
|
+
|
|
1529
|
+
let oversized = WireRequest {
|
|
1530
|
+
body_json: "x".repeat(MAX_WIRE_BODY_BYTES + 1),
|
|
1531
|
+
};
|
|
1532
|
+
let err = write_frame(&mut writer, &oversized, Duration::from_secs(2))
|
|
1533
|
+
.await
|
|
1534
|
+
.expect_err("oversized write");
|
|
1535
|
+
assert!(matches!(err, UnixTransportError::Protocol(message) if message.contains("wire body exceeds max bytes")));
|
|
1536
|
+
|
|
1537
|
+
let (mut invalid_writer, mut invalid_reader) = UnixStream::pair().expect("stream pair");
|
|
1538
|
+
invalid_writer
|
|
1539
|
+
.write_all(&((MAX_WIRE_BODY_BYTES as u32) + 1).to_be_bytes())
|
|
1540
|
+
.await
|
|
1541
|
+
.expect("write length");
|
|
1542
|
+
let err = read_frame::<WireRequest>(&mut invalid_reader, Duration::from_secs(2))
|
|
1543
|
+
.await
|
|
1544
|
+
.expect_err("oversized read");
|
|
1545
|
+
assert!(matches!(err, UnixTransportError::Protocol(message) if message.contains("wire frame exceeds max bytes")));
|
|
1546
|
+
|
|
1547
|
+
let (mut malformed_writer, mut malformed_reader) = UnixStream::pair().expect("stream pair");
|
|
1548
|
+
malformed_writer
|
|
1549
|
+
.write_all(&(4u32).to_be_bytes())
|
|
1550
|
+
.await
|
|
1551
|
+
.expect("write length");
|
|
1552
|
+
malformed_writer
|
|
1553
|
+
.write_all(b"nope")
|
|
1554
|
+
.await
|
|
1555
|
+
.expect("write body");
|
|
1556
|
+
let err = read_frame::<WireRequest>(&mut malformed_reader, Duration::from_secs(2))
|
|
1557
|
+
.await
|
|
1558
|
+
.expect_err("malformed read");
|
|
1559
|
+
assert!(matches!(err, UnixTransportError::Serialization(_)));
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
#[test]
|
|
1563
|
+
#[cfg(unix)]
|
|
1564
|
+
fn remove_existing_socket_file_covers_missing_socket_and_non_socket_paths() {
|
|
1565
|
+
use std::os::unix::net::UnixListener as StdUnixListener;
|
|
1566
|
+
|
|
1567
|
+
let root = short_test_root("rm");
|
|
1568
|
+
std::fs::create_dir_all(&root).expect("create root");
|
|
1569
|
+
|
|
1570
|
+
let missing = root.join("missing.sock");
|
|
1571
|
+
remove_existing_socket_file(&missing).expect("missing is a noop");
|
|
1572
|
+
|
|
1573
|
+
let file = root.join("file.sock");
|
|
1574
|
+
std::fs::write(&file, "not a socket").expect("write file");
|
|
1575
|
+
let err = remove_existing_socket_file(&file).expect_err("non-socket path");
|
|
1576
|
+
assert!(err.contains("is not a unix socket"));
|
|
1577
|
+
std::fs::remove_file(&file).expect("remove file");
|
|
1578
|
+
|
|
1579
|
+
let socket = root.join("daemon.sock");
|
|
1580
|
+
let _listener = StdUnixListener::bind(&socket).expect("bind socket");
|
|
1581
|
+
drop(_listener);
|
|
1582
|
+
remove_existing_socket_file(&socket).expect("remove socket");
|
|
1583
|
+
assert!(!socket.exists());
|
|
1584
|
+
|
|
1585
|
+
std::fs::remove_dir_all(root).expect("cleanup");
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1364
1588
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
1365
1589
|
async fn unix_round_trip_for_issue_lease() {
|
|
1366
1590
|
let socket_path = unique_socket_path("lease");
|
|
@@ -1398,6 +1622,154 @@ mod tests {
|
|
|
1398
1622
|
let _ = server_task.await;
|
|
1399
1623
|
}
|
|
1400
1624
|
|
|
1625
|
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
1626
|
+
async fn unix_round_trip_for_full_key_manager_api_surface() {
|
|
1627
|
+
let socket_path = unique_socket_path("full-surface");
|
|
1628
|
+
let daemon = Arc::new(
|
|
1629
|
+
InMemoryDaemon::new(
|
|
1630
|
+
"vault-password",
|
|
1631
|
+
SoftwareSignerBackend::default(),
|
|
1632
|
+
DaemonConfig::default(),
|
|
1633
|
+
)
|
|
1634
|
+
.expect("daemon"),
|
|
1635
|
+
);
|
|
1636
|
+
let server = UnixDaemonServer::bind(
|
|
1637
|
+
socket_path.clone(),
|
|
1638
|
+
singleton_allowed_set(nix::unistd::geteuid().as_raw()),
|
|
1639
|
+
)
|
|
1640
|
+
.await
|
|
1641
|
+
.expect("server");
|
|
1642
|
+
let server_task = tokio::spawn({
|
|
1643
|
+
let daemon = daemon.clone();
|
|
1644
|
+
async move {
|
|
1645
|
+
server
|
|
1646
|
+
.run_until_shutdown(daemon, async {
|
|
1647
|
+
std::future::pending::<()>().await;
|
|
1648
|
+
})
|
|
1649
|
+
.await
|
|
1650
|
+
}
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
1654
|
+
let client = UnixDaemonClient::new(socket_path.clone(), Duration::from_secs(2));
|
|
1655
|
+
|
|
1656
|
+
let lease = client.issue_lease("vault-password").await.expect("lease");
|
|
1657
|
+
let session = admin_session(lease);
|
|
1658
|
+
|
|
1659
|
+
let policy = policy_all_per_tx(100);
|
|
1660
|
+
client
|
|
1661
|
+
.add_policy(&session, policy.clone())
|
|
1662
|
+
.await
|
|
1663
|
+
.expect("add policy");
|
|
1664
|
+
|
|
1665
|
+
let policies = client.list_policies(&session).await.expect("list policies");
|
|
1666
|
+
assert_eq!(policies.len(), 1);
|
|
1667
|
+
assert!(policies[0].enabled);
|
|
1668
|
+
|
|
1669
|
+
let vault_key = client
|
|
1670
|
+
.create_vault_key(&session, KeyCreateRequest::Generate)
|
|
1671
|
+
.await
|
|
1672
|
+
.expect("create vault key");
|
|
1673
|
+
let exported = client
|
|
1674
|
+
.export_vault_private_key(&session, vault_key.id)
|
|
1675
|
+
.await
|
|
1676
|
+
.expect("export key");
|
|
1677
|
+
assert!(exported.is_some());
|
|
1678
|
+
|
|
1679
|
+
let mut credentials = client
|
|
1680
|
+
.create_agent_key(&session, vault_key.id, PolicyAttachment::AllPolicies)
|
|
1681
|
+
.await
|
|
1682
|
+
.expect("create agent key");
|
|
1683
|
+
|
|
1684
|
+
let evaluation = client
|
|
1685
|
+
.evaluate_for_agent(sign_request(&credentials, 1))
|
|
1686
|
+
.await
|
|
1687
|
+
.expect("evaluate");
|
|
1688
|
+
assert_eq!(evaluation.evaluated_policy_ids, vec![policy.id]);
|
|
1689
|
+
|
|
1690
|
+
let explanation = client
|
|
1691
|
+
.explain_for_agent(sign_request(&credentials, 1))
|
|
1692
|
+
.await
|
|
1693
|
+
.expect("explain");
|
|
1694
|
+
assert!(matches!(explanation.decision, PolicyDecision::Allow));
|
|
1695
|
+
|
|
1696
|
+
let signature = client
|
|
1697
|
+
.sign_for_agent(sign_request(&credentials, 1))
|
|
1698
|
+
.await
|
|
1699
|
+
.expect("sign");
|
|
1700
|
+
assert!(!signature.bytes.is_empty());
|
|
1701
|
+
|
|
1702
|
+
let reservation = client
|
|
1703
|
+
.reserve_nonce(nonce_reservation_request(&credentials, 7))
|
|
1704
|
+
.await
|
|
1705
|
+
.expect("reserve nonce");
|
|
1706
|
+
assert_eq!(reservation.nonce, 7);
|
|
1707
|
+
client
|
|
1708
|
+
.release_nonce(nonce_release_request(&credentials, reservation.reservation_id))
|
|
1709
|
+
.await
|
|
1710
|
+
.expect("release nonce");
|
|
1711
|
+
|
|
1712
|
+
let rotated_token = client
|
|
1713
|
+
.rotate_agent_auth_token(&session, credentials.agent_key.id)
|
|
1714
|
+
.await
|
|
1715
|
+
.expect("rotate token");
|
|
1716
|
+
credentials.auth_token = rotated_token;
|
|
1717
|
+
|
|
1718
|
+
let approvals = client
|
|
1719
|
+
.list_manual_approval_requests(&session)
|
|
1720
|
+
.await
|
|
1721
|
+
.expect("list approvals");
|
|
1722
|
+
assert!(approvals.is_empty());
|
|
1723
|
+
|
|
1724
|
+
let relay_config = client
|
|
1725
|
+
.set_relay_config(
|
|
1726
|
+
&session,
|
|
1727
|
+
Some("http://127.0.0.1:8787".to_string()),
|
|
1728
|
+
Some("https://relay.example".to_string()),
|
|
1729
|
+
)
|
|
1730
|
+
.await
|
|
1731
|
+
.expect("set relay config");
|
|
1732
|
+
assert_eq!(
|
|
1733
|
+
relay_config,
|
|
1734
|
+
RelayConfig {
|
|
1735
|
+
relay_url: Some("http://127.0.0.1:8787".to_string()),
|
|
1736
|
+
frontend_url: Some("https://relay.example".to_string()),
|
|
1737
|
+
daemon_id_hex: relay_config.daemon_id_hex.clone(),
|
|
1738
|
+
daemon_public_key_hex: relay_config.daemon_public_key_hex.clone(),
|
|
1739
|
+
}
|
|
1740
|
+
);
|
|
1741
|
+
let current_relay_config = client
|
|
1742
|
+
.get_relay_config(&session)
|
|
1743
|
+
.await
|
|
1744
|
+
.expect("get relay config");
|
|
1745
|
+
assert_eq!(current_relay_config.relay_url, relay_config.relay_url);
|
|
1746
|
+
assert_eq!(current_relay_config.frontend_url, relay_config.frontend_url);
|
|
1747
|
+
|
|
1748
|
+
client
|
|
1749
|
+
.disable_policy(&session, policy.id)
|
|
1750
|
+
.await
|
|
1751
|
+
.expect("disable policy");
|
|
1752
|
+
let disabled = client.list_policies(&session).await.expect("list disabled");
|
|
1753
|
+
assert_eq!(disabled.len(), 1);
|
|
1754
|
+
assert!(!disabled[0].enabled);
|
|
1755
|
+
|
|
1756
|
+
client
|
|
1757
|
+
.revoke_agent_key(&session, credentials.agent_key.id)
|
|
1758
|
+
.await
|
|
1759
|
+
.expect("revoke agent");
|
|
1760
|
+
let err = client
|
|
1761
|
+
.sign_for_agent(sign_request(&credentials, 1))
|
|
1762
|
+
.await
|
|
1763
|
+
.expect_err("revoked agent must fail");
|
|
1764
|
+
assert!(matches!(
|
|
1765
|
+
err,
|
|
1766
|
+
DaemonError::UnknownAgentKey(id) if id == credentials.agent_key.id
|
|
1767
|
+
) || matches!(err, DaemonError::AgentAuthenticationFailed));
|
|
1768
|
+
|
|
1769
|
+
server_task.abort();
|
|
1770
|
+
let _ = server_task.await;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1401
1773
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
1402
1774
|
async fn client_rejects_unexpected_daemon_euid() {
|
|
1403
1775
|
let socket_path = unique_socket_path("peer-euid");
|
|
@@ -1445,6 +1817,74 @@ mod tests {
|
|
|
1445
1817
|
let _ = server_task.await;
|
|
1446
1818
|
}
|
|
1447
1819
|
|
|
1820
|
+
#[tokio::test(flavor = "current_thread")]
|
|
1821
|
+
async fn call_rpc_surfaces_wire_daemon_errors_and_transport_fallbacks() {
|
|
1822
|
+
let socket_path = unique_socket_path("wire-error");
|
|
1823
|
+
let client = UnixDaemonClient::new(socket_path.clone(), Duration::from_secs(2));
|
|
1824
|
+
|
|
1825
|
+
let daemon_task = spawn_one_shot_server(
|
|
1826
|
+
socket_path.clone(),
|
|
1827
|
+
WireResponse {
|
|
1828
|
+
ok: false,
|
|
1829
|
+
body_json: serde_json::to_string(&WireDaemonError::UnknownLease)
|
|
1830
|
+
.expect("serialize daemon error"),
|
|
1831
|
+
},
|
|
1832
|
+
)
|
|
1833
|
+
.await;
|
|
1834
|
+
let err = client
|
|
1835
|
+
.call_rpc(DaemonRpcRequest::IssueLease {
|
|
1836
|
+
vault_password: "vault-password".to_string(),
|
|
1837
|
+
})
|
|
1838
|
+
.await
|
|
1839
|
+
.expect_err("daemon error");
|
|
1840
|
+
assert!(matches!(err, UnixTransportError::Daemon(DaemonError::UnknownLease)));
|
|
1841
|
+
daemon_task.await.expect("daemon task");
|
|
1842
|
+
|
|
1843
|
+
let transport_task = spawn_one_shot_server(
|
|
1844
|
+
socket_path,
|
|
1845
|
+
WireResponse {
|
|
1846
|
+
ok: false,
|
|
1847
|
+
body_json: "plain transport failure".to_string(),
|
|
1848
|
+
},
|
|
1849
|
+
)
|
|
1850
|
+
.await;
|
|
1851
|
+
let err = client
|
|
1852
|
+
.call_rpc(DaemonRpcRequest::IssueLease {
|
|
1853
|
+
vault_password: "vault-password".to_string(),
|
|
1854
|
+
})
|
|
1855
|
+
.await
|
|
1856
|
+
.expect_err("transport fallback");
|
|
1857
|
+
assert!(matches!(
|
|
1858
|
+
err,
|
|
1859
|
+
UnixTransportError::Daemon(DaemonError::Transport(message))
|
|
1860
|
+
if message == "plain transport failure"
|
|
1861
|
+
));
|
|
1862
|
+
transport_task.await.expect("transport task");
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
#[tokio::test(flavor = "current_thread")]
|
|
1866
|
+
async fn issue_lease_rejects_unexpected_response_types() {
|
|
1867
|
+
let socket_path = unique_socket_path("unexpected-response");
|
|
1868
|
+
let client = UnixDaemonClient::new(socket_path.clone(), Duration::from_secs(2));
|
|
1869
|
+
let response_task = spawn_one_shot_server(
|
|
1870
|
+
socket_path,
|
|
1871
|
+
WireResponse {
|
|
1872
|
+
ok: true,
|
|
1873
|
+
body_json: serde_json::to_string(&DaemonRpcResponse::Unit)
|
|
1874
|
+
.expect("serialize response"),
|
|
1875
|
+
},
|
|
1876
|
+
)
|
|
1877
|
+
.await;
|
|
1878
|
+
|
|
1879
|
+
let err = client
|
|
1880
|
+
.issue_lease("vault-password")
|
|
1881
|
+
.await
|
|
1882
|
+
.expect_err("wrong response type");
|
|
1883
|
+
assert!(matches!(err, DaemonError::Transport(message) if message == "unexpected response type"));
|
|
1884
|
+
|
|
1885
|
+
response_task.await.expect("response task");
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1448
1888
|
#[test]
|
|
1449
1889
|
fn rpc_access_level_classifies_admin_and_agent_requests() {
|
|
1450
1890
|
let admin_request = vault_daemon::DaemonRpcRequest::IssueLease {
|
|
@@ -1493,6 +1933,214 @@ mod tests {
|
|
|
1493
1933
|
);
|
|
1494
1934
|
}
|
|
1495
1935
|
|
|
1936
|
+
#[tokio::test(flavor = "current_thread")]
|
|
1937
|
+
async fn bind_rejects_empty_admin_and_agent_allowlists() {
|
|
1938
|
+
let socket_path = unique_socket_path("empty-allowlists");
|
|
1939
|
+
let err = match UnixDaemonServer::bind_with_allowed_peer_euids(
|
|
1940
|
+
socket_path.clone(),
|
|
1941
|
+
BTreeSet::new(),
|
|
1942
|
+
singleton_allowed_set(nix::unistd::geteuid().as_raw()),
|
|
1943
|
+
)
|
|
1944
|
+
.await
|
|
1945
|
+
{
|
|
1946
|
+
Ok(_) => panic!("empty admin allowlist must fail"),
|
|
1947
|
+
Err(err) => err,
|
|
1948
|
+
};
|
|
1949
|
+
assert!(matches!(err, UnixTransportError::Protocol(message) if message.contains("allowed admin peer euid set must not be empty")));
|
|
1950
|
+
|
|
1951
|
+
let err = match UnixDaemonServer::bind_with_allowed_peer_euids(
|
|
1952
|
+
socket_path,
|
|
1953
|
+
singleton_allowed_set(nix::unistd::geteuid().as_raw()),
|
|
1954
|
+
BTreeSet::new(),
|
|
1955
|
+
)
|
|
1956
|
+
.await
|
|
1957
|
+
{
|
|
1958
|
+
Ok(_) => panic!("empty agent allowlist must fail"),
|
|
1959
|
+
Err(err) => err,
|
|
1960
|
+
};
|
|
1961
|
+
assert!(matches!(err, UnixTransportError::Protocol(message) if message.contains("allowed agent peer euid set must not be empty")));
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
#[tokio::test(flavor = "current_thread")]
|
|
1965
|
+
async fn run_until_shutdown_honors_ready_shutdown_and_exposes_socket_path() {
|
|
1966
|
+
let socket_path = unique_socket_path("ready-shutdown");
|
|
1967
|
+
let server = UnixDaemonServer::bind(
|
|
1968
|
+
socket_path.clone(),
|
|
1969
|
+
singleton_allowed_set(nix::unistd::geteuid().as_raw()),
|
|
1970
|
+
)
|
|
1971
|
+
.await
|
|
1972
|
+
.expect("server");
|
|
1973
|
+
assert_eq!(server.socket_path(), socket_path.as_path());
|
|
1974
|
+
|
|
1975
|
+
let daemon = Arc::new(
|
|
1976
|
+
InMemoryDaemon::new(
|
|
1977
|
+
"vault-password",
|
|
1978
|
+
SoftwareSignerBackend::default(),
|
|
1979
|
+
DaemonConfig::default(),
|
|
1980
|
+
)
|
|
1981
|
+
.expect("daemon"),
|
|
1982
|
+
);
|
|
1983
|
+
server
|
|
1984
|
+
.run_until_shutdown(daemon, async {})
|
|
1985
|
+
.await
|
|
1986
|
+
.expect("shutdown must succeed");
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
#[test]
|
|
1990
|
+
fn wire_daemon_error_roundtrip_covers_all_variants() {
|
|
1991
|
+
let manual_approval_id = Uuid::new_v4();
|
|
1992
|
+
let unknown_vault_key = Uuid::new_v4();
|
|
1993
|
+
let unknown_agent_key = Uuid::new_v4();
|
|
1994
|
+
let unknown_policy = Uuid::new_v4();
|
|
1995
|
+
let unknown_approval = Uuid::new_v4();
|
|
1996
|
+
let unknown_reservation = Uuid::new_v4();
|
|
1997
|
+
|
|
1998
|
+
let cases = vec![
|
|
1999
|
+
(
|
|
2000
|
+
DaemonError::AuthenticationFailed,
|
|
2001
|
+
DaemonError::AuthenticationFailed.to_string(),
|
|
2002
|
+
),
|
|
2003
|
+
(DaemonError::UnknownLease, DaemonError::UnknownLease.to_string()),
|
|
2004
|
+
(DaemonError::InvalidLease, DaemonError::InvalidLease.to_string()),
|
|
2005
|
+
(
|
|
2006
|
+
DaemonError::TooManyActiveLeases,
|
|
2007
|
+
DaemonError::TooManyActiveLeases.to_string(),
|
|
2008
|
+
),
|
|
2009
|
+
(
|
|
2010
|
+
DaemonError::UnknownVaultKey(unknown_vault_key),
|
|
2011
|
+
DaemonError::UnknownVaultKey(unknown_vault_key).to_string(),
|
|
2012
|
+
),
|
|
2013
|
+
(
|
|
2014
|
+
DaemonError::UnknownAgentKey(unknown_agent_key),
|
|
2015
|
+
DaemonError::UnknownAgentKey(unknown_agent_key).to_string(),
|
|
2016
|
+
),
|
|
2017
|
+
(
|
|
2018
|
+
DaemonError::UnknownPolicy(unknown_policy),
|
|
2019
|
+
DaemonError::UnknownPolicy(unknown_policy).to_string(),
|
|
2020
|
+
),
|
|
2021
|
+
(
|
|
2022
|
+
DaemonError::UnknownManualApprovalRequest(unknown_approval),
|
|
2023
|
+
DaemonError::UnknownManualApprovalRequest(unknown_approval).to_string(),
|
|
2024
|
+
),
|
|
2025
|
+
(
|
|
2026
|
+
DaemonError::AgentAuthenticationFailed,
|
|
2027
|
+
DaemonError::AgentAuthenticationFailed.to_string(),
|
|
2028
|
+
),
|
|
2029
|
+
(
|
|
2030
|
+
DaemonError::PayloadActionMismatch,
|
|
2031
|
+
DaemonError::PayloadActionMismatch.to_string(),
|
|
2032
|
+
),
|
|
2033
|
+
(
|
|
2034
|
+
DaemonError::PayloadTooLarge { max_bytes: 1024 },
|
|
2035
|
+
DaemonError::PayloadTooLarge { max_bytes: 1024 }.to_string(),
|
|
2036
|
+
),
|
|
2037
|
+
(
|
|
2038
|
+
DaemonError::InvalidRequestTimestamps,
|
|
2039
|
+
DaemonError::InvalidRequestTimestamps.to_string(),
|
|
2040
|
+
),
|
|
2041
|
+
(DaemonError::RequestExpired, DaemonError::RequestExpired.to_string()),
|
|
2042
|
+
(
|
|
2043
|
+
DaemonError::RequestReplayDetected,
|
|
2044
|
+
DaemonError::RequestReplayDetected.to_string(),
|
|
2045
|
+
),
|
|
2046
|
+
(
|
|
2047
|
+
DaemonError::InvalidPolicyAttachment("attachment".to_string()),
|
|
2048
|
+
DaemonError::InvalidPolicyAttachment("attachment".to_string()).to_string(),
|
|
2049
|
+
),
|
|
2050
|
+
(
|
|
2051
|
+
DaemonError::InvalidNonceReservation("nonce".to_string()),
|
|
2052
|
+
DaemonError::InvalidNonceReservation("nonce".to_string()).to_string(),
|
|
2053
|
+
),
|
|
2054
|
+
(
|
|
2055
|
+
DaemonError::UnknownNonceReservation(unknown_reservation),
|
|
2056
|
+
DaemonError::UnknownNonceReservation(unknown_reservation).to_string(),
|
|
2057
|
+
),
|
|
2058
|
+
(
|
|
2059
|
+
DaemonError::MissingNonceReservation {
|
|
2060
|
+
chain_id: 1,
|
|
2061
|
+
nonce: 7,
|
|
2062
|
+
},
|
|
2063
|
+
DaemonError::MissingNonceReservation {
|
|
2064
|
+
chain_id: 1,
|
|
2065
|
+
nonce: 7,
|
|
2066
|
+
}
|
|
2067
|
+
.to_string(),
|
|
2068
|
+
),
|
|
2069
|
+
(
|
|
2070
|
+
DaemonError::InvalidPolicy("policy".to_string()),
|
|
2071
|
+
DaemonError::InvalidPolicy("policy".to_string()).to_string(),
|
|
2072
|
+
),
|
|
2073
|
+
(
|
|
2074
|
+
DaemonError::InvalidRelayConfig("relay".to_string()),
|
|
2075
|
+
DaemonError::InvalidRelayConfig("relay".to_string()).to_string(),
|
|
2076
|
+
),
|
|
2077
|
+
(
|
|
2078
|
+
DaemonError::ManualApprovalRequired {
|
|
2079
|
+
approval_request_id: manual_approval_id,
|
|
2080
|
+
relay_url: Some("https://relay.example".to_string()),
|
|
2081
|
+
frontend_url: Some("https://frontend.example".to_string()),
|
|
2082
|
+
},
|
|
2083
|
+
DaemonError::ManualApprovalRequired {
|
|
2084
|
+
approval_request_id: manual_approval_id,
|
|
2085
|
+
relay_url: Some("https://relay.example".to_string()),
|
|
2086
|
+
frontend_url: Some("https://frontend.example".to_string()),
|
|
2087
|
+
}
|
|
2088
|
+
.to_string(),
|
|
2089
|
+
),
|
|
2090
|
+
(
|
|
2091
|
+
DaemonError::ManualApprovalRejected {
|
|
2092
|
+
approval_request_id: manual_approval_id,
|
|
2093
|
+
},
|
|
2094
|
+
DaemonError::ManualApprovalRejected {
|
|
2095
|
+
approval_request_id: manual_approval_id,
|
|
2096
|
+
}
|
|
2097
|
+
.to_string(),
|
|
2098
|
+
),
|
|
2099
|
+
(
|
|
2100
|
+
DaemonError::ManualApprovalRequestNotPending {
|
|
2101
|
+
approval_request_id: manual_approval_id,
|
|
2102
|
+
status: ManualApprovalStatus::Approved,
|
|
2103
|
+
},
|
|
2104
|
+
DaemonError::ManualApprovalRequestNotPending {
|
|
2105
|
+
approval_request_id: manual_approval_id,
|
|
2106
|
+
status: ManualApprovalStatus::Approved,
|
|
2107
|
+
}
|
|
2108
|
+
.to_string(),
|
|
2109
|
+
),
|
|
2110
|
+
(
|
|
2111
|
+
DaemonError::Policy(PolicyError::NoAttachedPolicies),
|
|
2112
|
+
DaemonError::Policy(PolicyError::NoAttachedPolicies).to_string(),
|
|
2113
|
+
),
|
|
2114
|
+
(
|
|
2115
|
+
DaemonError::Signer(SignerError::InvalidPrivateKey),
|
|
2116
|
+
DaemonError::Signer(SignerError::InvalidPrivateKey).to_string(),
|
|
2117
|
+
),
|
|
2118
|
+
(
|
|
2119
|
+
DaemonError::PasswordHash("hash".to_string()),
|
|
2120
|
+
DaemonError::PasswordHash("hash".to_string()).to_string(),
|
|
2121
|
+
),
|
|
2122
|
+
(
|
|
2123
|
+
DaemonError::InvalidConfig("config".to_string()),
|
|
2124
|
+
DaemonError::InvalidConfig("config".to_string()).to_string(),
|
|
2125
|
+
),
|
|
2126
|
+
(
|
|
2127
|
+
DaemonError::Transport("transport".to_string()),
|
|
2128
|
+
DaemonError::Transport("transport".to_string()).to_string(),
|
|
2129
|
+
),
|
|
2130
|
+
(
|
|
2131
|
+
DaemonError::Persistence("persistence".to_string()),
|
|
2132
|
+
DaemonError::Persistence("persistence".to_string()).to_string(),
|
|
2133
|
+
),
|
|
2134
|
+
(DaemonError::LockPoisoned, DaemonError::LockPoisoned.to_string()),
|
|
2135
|
+
];
|
|
2136
|
+
|
|
2137
|
+
for (original, expected_message) in cases {
|
|
2138
|
+
let wire = WireDaemonError::from(original);
|
|
2139
|
+
let recovered = wire.into_daemon_error();
|
|
2140
|
+
assert_eq!(recovered.to_string(), expected_message);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
|
|
1496
2144
|
#[tokio::test(flavor = "current_thread")]
|
|
1497
2145
|
async fn handle_connection_rejects_admin_requests_from_agent_only_peers() {
|
|
1498
2146
|
let daemon = Arc::new(
|