actor.rs (8359B)
1 //! Actor provenance and authorization context. 2 //! 3 //! This module binds identity-owned public values to event-owned author roles. 4 //! It describes host selection and provenance but does not select accounts, 5 //! acquire keys, or prove that a host granted a role. 6 7 use radroots_event::contract::AuthorRole; 8 use radroots_identity::{AccountId, PublicKey}; 9 10 use crate::{Error, error::Kind}; 11 12 #[cfg(not(feature = "std"))] 13 use alloc::collections::BTreeSet; 14 #[cfg(feature = "std")] 15 use std::collections::BTreeSet; 16 17 /// Why a host supplied an actor to a signing operation. 18 /// 19 /// Account-backed variants carry the canonical public [`AccountId`]. The 20 /// account identifier must represent the same public bytes as the actor key. 21 #[non_exhaustive] 22 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 23 pub enum ActorSource { 24 /// The host resolved its explicitly selected local account. 25 LocalAccount(AccountId), 26 /// The caller supplied a public key without selecting an account. 27 ExplicitPublicKey, 28 /// The host resolved an account backed by a remote signer. 29 RemoteSigner(AccountId), 30 /// A service account was selected by explicit host policy. 31 Service(AccountId), 32 } 33 34 impl ActorSource { 35 /// Returns the selected account identifier for account-backed provenance. 36 #[must_use] 37 pub const fn account_id(self) -> Option<AccountId> { 38 match self { 39 Self::LocalAccount(account_id) 40 | Self::RemoteSigner(account_id) 41 | Self::Service(account_id) => Some(account_id), 42 Self::ExplicitPublicKey => None, 43 } 44 } 45 } 46 47 /// Declarative host selection for resolving an actor. 48 /// 49 /// Resolution is deliberately outside this package; this value carries no 50 /// database, keyring, UI, or process-global selection authority. 51 #[non_exhaustive] 52 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 53 pub enum ActorSelector { 54 /// Resolve the account already selected by explicit host policy. 55 SelectedAccount, 56 /// Resolve one canonical public account. 57 Account(AccountId), 58 /// Use one explicit canonical public key. 59 PublicKey(PublicKey), 60 /// Resolve the public key frozen into the authored event plan. 61 PlanAuthorPublicKey, 62 } 63 64 /// Inputs a host must satisfy when resolving an actor for an authored plan. 65 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 66 pub struct ActorResolutionRequest { 67 selector: ActorSelector, 68 required_role: AuthorRole, 69 expected_public_key: PublicKey, 70 } 71 72 impl ActorResolutionRequest { 73 /// Creates an actor-resolution request from canonical public values. 74 #[must_use] 75 pub const fn new( 76 selector: ActorSelector, 77 required_role: AuthorRole, 78 expected_public_key: PublicKey, 79 ) -> Self { 80 Self { 81 selector, 82 required_role, 83 expected_public_key, 84 } 85 } 86 87 /// Returns the host-owned selection instruction. 88 #[must_use] 89 pub const fn selector(&self) -> ActorSelector { 90 self.selector 91 } 92 93 /// Returns the event-contract role the resolved actor must hold. 94 #[must_use] 95 pub const fn required_role(&self) -> AuthorRole { 96 self.required_role 97 } 98 99 /// Returns the exact public key frozen into the plan. 100 #[must_use] 101 pub const fn expected_public_key(&self) -> PublicKey { 102 self.expected_public_key 103 } 104 } 105 106 /// An actor-role claim and its public provenance. 107 /// 108 /// Construction validates that account-backed provenance and the public key 109 /// describe the same identity. Role authorization against an event contract 110 /// is performed by the signing boundary before invoking a signer. 111 #[derive(Clone, Debug, PartialEq, Eq)] 112 pub struct Actor { 113 public_key: PublicKey, 114 roles: BTreeSet<AuthorRole>, 115 source: ActorSource, 116 } 117 118 impl Actor { 119 /// Creates actor provenance from canonical public values. 120 pub fn new<I>(public_key: PublicKey, source: ActorSource, roles: I) -> Result<Self, Error> 121 where 122 I: IntoIterator<Item = AuthorRole>, 123 { 124 if let Some(account_id) = source.account_id() 125 && account_id.as_bytes() != public_key.as_bytes() 126 { 127 return Err(Error::new(Kind::InvalidArgument)); 128 } 129 Ok(Self { 130 public_key, 131 roles: roles.into_iter().collect(), 132 source, 133 }) 134 } 135 136 /// Parses a canonical public key and creates validated actor provenance. 137 pub fn from_public_key_hex<I>( 138 public_key: &str, 139 source: ActorSource, 140 roles: I, 141 ) -> Result<Self, Error> 142 where 143 I: IntoIterator<Item = AuthorRole>, 144 { 145 let public_key = 146 PublicKey::from_hex(public_key).map_err(|_| Error::new(Kind::InvalidArgument))?; 147 Self::new(public_key, source, roles) 148 } 149 150 /// Returns the canonical expected author key. 151 #[must_use] 152 pub const fn public_key(&self) -> PublicKey { 153 self.public_key 154 } 155 156 /// Returns the claimed event-author roles. 157 #[must_use] 158 pub const fn roles(&self) -> &BTreeSet<AuthorRole> { 159 &self.roles 160 } 161 162 /// Returns the host-supplied actor provenance. 163 #[must_use] 164 pub const fn source(&self) -> ActorSource { 165 self.source 166 } 167 168 /// Returns the selected public account, when provenance is account-backed. 169 #[must_use] 170 pub const fn account_id(&self) -> Option<AccountId> { 171 self.source.account_id() 172 } 173 174 /// Reports whether the actor claims the required event-author role. 175 #[must_use] 176 pub fn satisfies(&self, required_role: AuthorRole) -> bool { 177 required_role == AuthorRole::Any || self.roles.contains(&required_role) 178 } 179 } 180 181 #[cfg(test)] 182 mod tests { 183 use super::*; 184 185 const ALICE: &str = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"; 186 const BOB: &str = "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af"; 187 188 fn public_key(value: &str) -> PublicKey { 189 PublicKey::from_hex(value).expect("valid public key") 190 } 191 192 fn account_id(value: &str) -> AccountId { 193 AccountId::from_hex(value).expect("valid account ID") 194 } 195 196 #[test] 197 fn roles_and_explicit_provenance_are_preserved() { 198 let actor = Actor::new( 199 public_key(ALICE), 200 ActorSource::ExplicitPublicKey, 201 [AuthorRole::Farmer, AuthorRole::Seller], 202 ) 203 .expect("actor"); 204 205 assert_eq!(actor.public_key(), public_key(ALICE)); 206 assert_eq!(actor.source(), ActorSource::ExplicitPublicKey); 207 assert_eq!(actor.account_id(), None); 208 assert!(actor.satisfies(AuthorRole::Any)); 209 assert!(actor.satisfies(AuthorRole::Farmer)); 210 assert!(actor.satisfies(AuthorRole::Seller)); 211 assert!(!actor.satisfies(AuthorRole::Buyer)); 212 } 213 214 #[test] 215 fn account_provenance_requires_the_same_public_identity() { 216 for source in [ 217 ActorSource::LocalAccount(account_id(ALICE)), 218 ActorSource::RemoteSigner(account_id(ALICE)), 219 ActorSource::Service(account_id(ALICE)), 220 ] { 221 let actor = Actor::new(public_key(ALICE), source, [AuthorRole::Service]) 222 .expect("matching account provenance"); 223 assert_eq!(actor.account_id(), Some(account_id(ALICE))); 224 } 225 226 let error = Actor::new( 227 public_key(ALICE), 228 ActorSource::LocalAccount(account_id(BOB)), 229 [AuthorRole::Farmer], 230 ) 231 .expect_err("mismatched account must fail"); 232 assert_eq!(error.kind(), Kind::InvalidArgument); 233 } 234 235 #[test] 236 fn invalid_public_key_text_is_rejected() { 237 let error = 238 Actor::from_public_key_hex("not-a-public-key", ActorSource::ExplicitPublicKey, []) 239 .expect_err("invalid public key must fail"); 240 assert_eq!(error.kind(), Kind::InvalidArgument); 241 } 242 243 #[test] 244 fn resolution_request_preserves_selection_role_and_expected_key() { 245 let request = ActorResolutionRequest::new( 246 ActorSelector::Account(account_id(ALICE)), 247 AuthorRole::Seller, 248 public_key(ALICE), 249 ); 250 251 assert_eq!( 252 request.selector(), 253 ActorSelector::Account(account_id(ALICE)) 254 ); 255 assert_eq!(request.required_role(), AuthorRole::Seller); 256 assert_eq!(request.expected_public_key(), public_key(ALICE)); 257 } 258 }