cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

commit 80cb62136204c5d7003f7834b42de22a454e781c
parent 20f4659909b8d65f17cd8b40f19504df09679e95
Author: triesap <tyson@radroots.org>
Date:   Sat, 25 Apr 2026 09:20:29 +0000

signer: bind myc actor to user identity

Diffstat:
Msrc/runtime/signer.rs | 26+++++++++++++-------------
Mtests/listing.rs | 16++++++++++++----
Mtests/myc_status.rs | 35+++++++++++++++++++++++++++--------
Mtests/order.rs | 16++++++++++++----
4 files changed, 64 insertions(+), 29 deletions(-)

diff --git a/src/runtime/signer.rs b/src/runtime/signer.rs @@ -21,7 +21,7 @@ const SIGNER_BINDING_MODEL: &str = "session_authorized_remote_signer"; struct MycBindingResolution { view: SignerBindingStatusView, resolved_account_id: Option<String>, - resolved_signer_public_key_hex: Option<String>, + resolved_account_public_key_hex: Option<String>, } #[derive(Debug, Clone)] @@ -74,15 +74,15 @@ pub fn resolve_actor_write_authority( } } - let Some(resolved_signer_public_key_hex) = resolution.resolved_signer_public_key_hex else { + let Some(resolved_account_public_key_hex) = resolution.resolved_account_public_key_hex else { return Err(ActorWriteBindingError::Unconfigured( - "myc signer binding reported ready without a resolved signer identity".to_owned(), + "myc signer binding reported ready without a resolved user identity".to_owned(), )); }; - if !resolved_signer_public_key_hex.eq_ignore_ascii_case(actor_pubkey) { + if !resolved_account_public_key_hex.eq_ignore_ascii_case(actor_pubkey) { return Err(ActorWriteBindingError::Unconfigured(format!( - "configured myc signer binding resolves signer pubkey `{resolved_signer_public_key_hex}` instead of {actor_role} pubkey `{actor_pubkey}`" + "configured myc signer binding resolves user pubkey `{resolved_account_public_key_hex}` instead of {actor_role} pubkey `{actor_pubkey}`" ))); } @@ -308,7 +308,7 @@ fn resolve_myc_binding(config: &RuntimeConfig, myc: &MycStatusView) -> MycBindin ), }, resolved_account_id: None, - resolved_signer_public_key_hex: None, + resolved_account_public_key_hex: None, }; }; @@ -407,7 +407,7 @@ fn resolve_myc_binding(config: &RuntimeConfig, myc: &MycStatusView) -> MycBindin } if let Some(account_ref) = binding.managed_account_ref.as_deref() { - if session.signer_identity.id != account_ref { + if session.user_identity.id != account_ref { return binding_status( binding, "unauthorized", @@ -415,8 +415,8 @@ fn resolve_myc_binding(config: &RuntimeConfig, myc: &MycStatusView) -> MycBindin Some(1), None, format!( - "configured signer session `{session_ref}` resolves signer `{}` instead of managed account `{account_ref}`", - session.signer_identity.id + "configured signer session `{session_ref}` resolves user `{}` instead of managed account `{account_ref}`", + session.user_identity.id ), ); } @@ -435,7 +435,7 @@ fn resolve_myc_binding(config: &RuntimeConfig, myc: &MycStatusView) -> MycBindin if let Some(account_ref) = binding.managed_account_ref.as_deref() { let matching_sessions = signing_sessions .into_iter() - .filter(|session| session.signer_identity.id == account_ref) + .filter(|session| session.user_identity.id == account_ref) .collect::<Vec<_>>(); return resolve_matching_sessions(binding, account_ref, matching_sessions); } @@ -548,9 +548,9 @@ fn binding_status( Some(reason) }, }, - resolved_account_id: resolved_session.map(|session| session.signer_identity.id.clone()), - resolved_signer_public_key_hex: resolved_session - .map(|session| session.signer_identity.public_key_hex.clone()), + resolved_account_id: resolved_session.map(|session| session.user_identity.id.clone()), + resolved_account_public_key_hex: resolved_session + .map(|session| session.user_identity.public_key_hex.clone()), } } diff --git a/tests/listing.rs b/tests/listing.rs @@ -10,6 +10,7 @@ use std::thread::{self, JoinHandle}; use std::time::Duration; use assert_cmd::prelude::*; +use radroots_identity::RadrootsIdentity; use radroots_sql_core::{SqlExecutor, SqliteExecutor}; use serde_json::{Value, json}; use tempfile::tempdir; @@ -105,21 +106,28 @@ fn sample_myc_status_payload( public_identity: &Value, connection_id: &str, ) -> Value { + let signer_identity = + serde_json::to_value(RadrootsIdentity::generate().to_public()).expect("signer identity"); + let signer_account_id = signer_identity["id"] + .as_str() + .expect("signer id") + .to_owned(); + assert_ne!(signer_account_id, account_id); json!({ "status": "healthy", "ready": true, "reasons": [], "signer_backend": { "local_signer": { - "account_id": account_id, - "public_identity": public_identity, + "account_id": signer_account_id, + "public_identity": signer_identity.clone(), "availability": "SecretBacked" }, "remote_session_count": 1, "remote_sessions": [ { "connection_id": connection_id, - "signer_identity": public_identity, + "signer_identity": signer_identity, "user_identity": public_identity, "relays": ["wss://relay.one"], "permissions": "sign_event" @@ -1014,7 +1022,7 @@ managed_account_ref = "{mismatch_account_id}" assert_eq!(publish_json["state"], "unconfigured"); assert_eq!(publish_json["signer_mode"], "myc"); assert!(publish_json["reason"].as_str().is_some_and(|value| { - value.contains("configured myc signer binding resolves signer pubkey") + value.contains("configured myc signer binding resolves user pubkey") })); assert!(requests.lock().expect("requests").is_empty()); } diff --git a/tests/myc_status.rs b/tests/myc_status.rs @@ -195,9 +195,14 @@ fn signer_status_reports_ready_for_configured_myc_managed_account_binding() { dir.path(), successful_status_script(payload.to_string()).as_str(), ); - let managed_account_ref = payload["signer_backend"]["local_signer"]["account_id"] + let managed_account_ref = + payload["signer_backend"]["remote_sessions"][0]["user_identity"]["id"] + .as_str() + .expect("managed account ref"); + let provider_account_ref = payload["signer_backend"]["local_signer"]["account_id"] .as_str() - .expect("managed account ref"); + .expect("provider account ref"); + assert_ne!(managed_account_ref, provider_account_ref); let signer_session_ref = payload["signer_backend"]["remote_sessions"][0]["connection_id"] .as_str() .expect("signer session ref"); @@ -244,6 +249,18 @@ signer_session_ref = "{signer_session_ref}" assert_eq!(json["binding"]["managed_account_ref"], managed_account_ref); assert_eq!(json["binding"]["signer_session_ref"], signer_session_ref); assert_eq!(json["myc"]["remote_session_count"], 1); + assert_eq!( + json["myc"]["remote_sessions"][0]["user_identity"]["id"], + managed_account_ref + ); + assert_ne!( + json["myc"]["remote_sessions"][0]["signer_identity"]["id"], + json["myc"]["remote_sessions"][0]["user_identity"]["id"] + ); + assert_ne!( + json["myc"]["local_signer"]["account_id"], + json["myc"]["remote_sessions"][0]["user_identity"]["id"] + ); } #[test] @@ -371,9 +388,10 @@ fn signer_status_reports_unauthorized_for_session_without_sign_event_permission( dir.path(), successful_status_script(payload.to_string()).as_str(), ); - let managed_account_ref = payload["signer_backend"]["local_signer"]["account_id"] - .as_str() - .expect("managed account ref"); + let managed_account_ref = + payload["signer_backend"]["remote_sessions"][0]["user_identity"]["id"] + .as_str() + .expect("managed account ref"); let signer_session_ref = payload["signer_backend"]["remote_sessions"][0]["connection_id"] .as_str() .expect("signer session ref"); @@ -426,9 +444,10 @@ fn signer_status_reports_unavailable_for_missing_bound_session() { dir.path(), successful_status_script(payload.to_string()).as_str(), ); - let managed_account_ref = payload["signer_backend"]["local_signer"]["account_id"] - .as_str() - .expect("managed account ref"); + let managed_account_ref = + payload["signer_backend"]["remote_sessions"][0]["user_identity"]["id"] + .as_str() + .expect("managed account ref"); write_user_config( dir.path(), format!( diff --git a/tests/order.rs b/tests/order.rs @@ -10,6 +10,7 @@ use std::thread::{self, JoinHandle}; use std::time::Duration; use assert_cmd::prelude::*; +use radroots_identity::RadrootsIdentity; use radroots_sql_core::{SqlExecutor, SqliteExecutor}; use serde_json::{Value, json}; use tempfile::tempdir; @@ -193,21 +194,28 @@ fn sample_myc_status_payload( public_identity: &Value, connection_id: &str, ) -> Value { + let signer_identity = + serde_json::to_value(RadrootsIdentity::generate().to_public()).expect("signer identity"); + let signer_account_id = signer_identity["id"] + .as_str() + .expect("signer id") + .to_owned(); + assert_ne!(signer_account_id, account_id); json!({ "status": "healthy", "ready": true, "reasons": [], "signer_backend": { "local_signer": { - "account_id": account_id, - "public_identity": public_identity, + "account_id": signer_account_id, + "public_identity": signer_identity.clone(), "availability": "SecretBacked" }, "remote_session_count": 1, "remote_sessions": [ { "connection_id": connection_id, - "signer_identity": public_identity, + "signer_identity": signer_identity, "user_identity": public_identity, "relays": ["wss://relay.one"], "permissions": "sign_event" @@ -1468,7 +1476,7 @@ managed_account_ref = "{mismatch_account_id}" assert_eq!(submit_json["state"], "unconfigured"); assert_eq!(submit_json["signer_mode"], "myc"); assert!(submit_json["reason"].as_str().is_some_and(|value| { - value.contains("configured myc signer binding resolves signer pubkey") + value.contains("configured myc signer binding resolves user pubkey") })); assert!(requests.lock().expect("requests lock").is_empty()); }