Last active
May 1, 2025 18:42
-
-
Save pdaoust/30a517aa79eb8c4cb816fa71f2dd1995 to your computer and use it in GitHub Desktop.
A simpler approach to validation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// I'm undecided whether this could actually _contain_ the context -- | |
// like, could EntryCrudReference be EntryCrudReference(Action, Entry)? | |
// We wouldn't want the whole source chain in `SourceChain` | |
// (tho the last entry might be helpful) | |
// and we don't know what's at the link base so we can't deliver that. | |
pub enum ValidationContext { | |
// Store* ops don't have any context except themselves. | |
// I originally named this `Record` but then realised that | |
// the record being validated isn't really part of the context, | |
// if we define 'context' as 'other stuff I have local access to'. | |
None, | |
SourceChain, | |
LinkBase, | |
LinkBaseAndCreateReference, | |
EntryCrudReference, | |
} | |
pub RecordWithContext { | |
action: Action, | |
entry: Option<Entry>, | |
context: ValidationContext, | |
} | |
pub trait SimplifyOp { | |
pub fn simplify() -> RecordWithContext; | |
} | |
impl SimplifyOp for Op { | |
pub fn simplify(&self) -> ExternResult<RecordWithContext> { | |
Ok(match self { | |
Op::StoreRecord(StoreRecord { record }) => RecordWithContext { | |
action: record.action(), | |
entry: record.entry.as_option(), | |
context: ValidationContext::None, | |
}, | |
Op::StoreEntry(StoreEntry { action, entry }) => RecordWithContext { | |
action: action.into(), | |
entry: Some(entry), | |
context: ValidationContext::None, | |
} | |
Op::RegisterUpdate(RegisterUpdate { update, new_entry }) => RecordWithContext { | |
action: Action::Update(update.hashed.content), | |
entry: new_entry, | |
context: ValidationContext::EntryCrudReference(must_get_action(update.hashed.content.original_action_address)), | |
}, | |
Op::RegisterUpdate(RegisterDelete { delete }) => RecordWithContext { | |
action: Action::Delete(delete.hashed.content), | |
entry: None, | |
context: ValidationContext::EntryCrudReference, | |
}, | |
Op::RegisterAgentActivity(RegisterAgentActivity { action, cached_entry }) => RecordWithContext { | |
action: action.hashed.content, | |
entry: cached_entry, | |
context: ValidationContext::SourceChain, | |
}, | |
Op::RegisterCreateLink(RegisterCreateLink { create_link }) => RecordWithContext { | |
action: Action::CreateLink(create_link.hashed.content), | |
entry: None, | |
context: ValidationContext::LinkBase, | |
}, | |
Op::RegisterDeleteLink(RegisterDeleteLink { delete_link, create_link }) => RecordWithContext { | |
action: Action::DeleteLink(delete_link.hashed.content), | |
entry: None, | |
context: ValidationContext::LinkBaseAndCreateReference, | |
} | |
}) | |
} | |
} | |
// ----------- testing out my toy code | |
// feels only somewhat awkward | |
#[hdk_extern] | |
pub fn validate(op: Op) -> ExternResult<ValidateCallbackResult> { | |
let { action, entry, context } = op.simplify(); | |
// If I have local access to the source chain, I want to do some really basic and user-unfriendly rate limiting. | |
if context == ValidationContext::SourceChain { | |
match action { | |
Action::Dna(_) | |
| Action::AgentValidationPkg(_) | |
| Action::Create(Create { entry_type: EntryType::AgentPubKey, .. }) | |
| Action::Create(Create { entry_type: EntryType::CapClaim, .. }) | |
| Action::Create(Create { entry_type: EntryType::CapGrant, .. }) | |
| Action::InitZomesComplete(_) | |
| Action::CloseChain(_) | |
| Action::OpenChain(_) => { /* don't rate-limit these action types */} | |
_ => { | |
let prev_action = must_get_action(action.prev_action.unwrap())?; | |
// This won't compile but whatever | |
if (action.timestamp() - prev_action.timestamp() < Duration::new(30, 0)) { | |
// No buckets!!! What a terrible UX! | |
return Ok(ValidateCallbackResult::Invalid("Went over rate limit")); | |
} | |
} | |
} | |
} | |
// If I have local access to the full record, I want to validate the record content. | |
if let Action::Create(Create { entry_type: EntryType::App(app_entry_def), .. }) | |
| Action::Update(Update { entry_type: EntryType::App(app_entry_def), .. }) = action { | |
// Imaginary function that I wish we had | |
match app_entry_def.into_unit_entry_type<UnitEntryTypes>() { | |
UnitEntryTypes::Message => { | |
match context { | |
ValidationContext::None => { | |
// Do record validation! | |
// is the message 280 characters or less? | |
} | |
ValidationContext::SourceChain | ValidationContext::EntryCrudReference => { /* save work by not validating in these contexts */ } | |
_ => unreachable!("Link base contexts don't apply to entry CRUD"); | |
} | |
} | |
_ => { }, | |
}; | |
} | |
// If I have local access to the entry being deleted/updated or the link being deleted, | |
// I want to enforce write privileges. | |
if context == ValidationContext::EntryCrudReference | |
|| context == ValidationContext::LinkBaseAndCreateReference { | |
if let Action::Update(Update { original_action_address: old_action_hash, .. }) | |
|| Action::Delete(Delete { deletes_address: old_action_hash, .. }) | |
|| Action::DeleteLink(DeleteLink { link_add_address: old_action_hash, .. }) = action { | |
// TODO: Do we know that this is a local get for all the above actions in the above contexts? | |
let old_action = must_get_action(old_action_hash)?; | |
// In this DNA, all CRUD must be on things written by the same author. | |
if (old_action.author() != action.author()) { | |
return Ok(ValidateCallbackResult::Invalid("Can't edit or delete other people's stuff")); | |
} | |
} else { | |
unreachable!("If it's a CRUD context, why would we have a non-CRUD action?"); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment