agent_deploy_contract/
owners.rs1use std::collections::BTreeSet;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum OwnerKind {
8 Runtime,
9 Agent,
10 User,
11}
12
13impl OwnerKind {
14 pub fn parse(value: &str) -> Option<Self> {
15 match value {
16 "runtime" => Some(Self::Runtime),
17 "agent" => Some(Self::Agent),
18 "user" => Some(Self::User),
19 _ => None,
20 }
21 }
22
23 pub const fn as_str(self) -> &'static str {
24 match self {
25 Self::Runtime => "runtime",
26 Self::Agent => "agent",
27 Self::User => "user",
28 }
29 }
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
33#[serde(deny_unknown_fields, rename_all = "camelCase")]
34pub struct OwnerRef {
35 pub kind: OwnerKind,
36 pub id: String,
37}
38
39impl OwnerRef {
40 pub fn runtime(id: impl Into<String>) -> Self {
41 Self {
42 kind: OwnerKind::Runtime,
43 id: id.into(),
44 }
45 }
46
47 pub fn agent(id: impl Into<String>) -> Self {
48 Self {
49 kind: OwnerKind::Agent,
50 id: id.into(),
51 }
52 }
53
54 pub fn user(id: impl Into<String>) -> Self {
55 Self {
56 kind: OwnerKind::User,
57 id: id.into(),
58 }
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum OwnerValidationError {
64 Empty,
65 Duplicate,
66 Invalid,
67}
68
69pub fn normalize_owners(owners: Vec<OwnerRef>) -> Result<Vec<OwnerRef>, OwnerValidationError> {
70 if owners.is_empty() {
71 return Err(OwnerValidationError::Empty);
72 }
73 let mut set = BTreeSet::new();
74 for owner in owners {
75 let id = owner.id.trim();
76 if id.is_empty() || id.len() > 255 {
77 return Err(OwnerValidationError::Invalid);
78 }
79 if !set.insert(OwnerRef {
80 kind: owner.kind,
81 id: id.to_string(),
82 }) {
83 return Err(OwnerValidationError::Duplicate);
84 }
85 }
86 Ok(set.into_iter().collect())
87}
88
89pub fn owners_intersect(caller: &[OwnerRef], resource: &[OwnerRef]) -> bool {
90 let resource_set: BTreeSet<_> = resource.iter().collect();
91 caller.iter().any(|owner| resource_set.contains(owner))
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn normalize_rejects_empty_and_duplicates() {
100 assert!(matches!(
101 normalize_owners(vec![]),
102 Err(OwnerValidationError::Empty)
103 ));
104 assert!(matches!(
105 normalize_owners(vec![OwnerRef::user("u1"), OwnerRef::user("u1")]),
106 Err(OwnerValidationError::Duplicate)
107 ));
108 }
109
110 #[test]
111 fn intersect_requires_shared_identity() {
112 assert!(owners_intersect(
113 &[OwnerRef::user("u1")],
114 &[OwnerRef::user("u1"), OwnerRef::agent("agt_1")]
115 ));
116 assert!(!owners_intersect(
117 &[OwnerRef::runtime("rt-2")],
118 &[OwnerRef::runtime("rt-1")]
119 ));
120 }
121}