1use anyhow::{Context, Error};
2use prost::Message;
3use thiserror::Error;
4use warg_crypto::{hash::AnyHash, Decode, Encode, Signable};
5use warg_protobuf::protocol as protobuf;
6
7use crate::{pbjson_to_prost_timestamp, prost_to_pbjson_timestamp, registry::RecordId};
8
9mod model;
10mod state;
11
12pub use model::{OperatorEntry, OperatorRecord};
13pub use state::{LogState, NamespaceState, ValidationError};
14
15pub const OPERATOR_RECORD_VERSION: u32 = 0;
17
18impl Decode for model::OperatorRecord {
19 fn decode(bytes: &[u8]) -> Result<Self, Error> {
20 protobuf::OperatorRecord::decode(bytes)?.try_into()
21 }
22}
23
24impl TryFrom<protobuf::OperatorRecord> for model::OperatorRecord {
25 type Error = Error;
26
27 fn try_from(record: protobuf::OperatorRecord) -> Result<Self, Self::Error> {
28 let prev: Option<RecordId> = match record.prev {
29 Some(hash_string) => {
30 let digest: AnyHash = hash_string.parse()?;
31 Some(digest.into())
32 }
33 None => None,
34 };
35 let version = record.version;
36 let pbjson_timestamp = record.time.context(InvalidTimestampError)?;
37 let prost_timestamp = pbjson_to_prost_timestamp(pbjson_timestamp);
38 let timestamp = prost_timestamp.try_into()?;
39
40 let entries: Result<Vec<model::OperatorEntry>, Error> = record
41 .entries
42 .into_iter()
43 .map(|proto_entry| proto_entry.try_into())
44 .collect();
45 let entries = entries?;
46
47 Ok(model::OperatorRecord {
48 prev,
49 version,
50 timestamp,
51 entries,
52 })
53 }
54}
55
56#[derive(Error, Debug)]
57#[error("empty or invalid timestamp in record")]
58struct InvalidTimestampError;
59
60impl TryFrom<protobuf::OperatorEntry> for model::OperatorEntry {
61 type Error = Error;
62
63 fn try_from(entry: protobuf::OperatorEntry) -> Result<Self, Self::Error> {
64 use protobuf::operator_entry::Contents;
65 let output = match entry.contents.ok_or(EmptyContentError)? {
66 Contents::Init(init) => model::OperatorEntry::Init {
67 hash_algorithm: init.hash_algorithm.parse()?,
68 key: init.key.parse()?,
69 },
70 Contents::GrantFlat(grant_flat) => model::OperatorEntry::GrantFlat {
71 key: grant_flat.key.parse()?,
72 permissions: grant_flat
73 .permissions
74 .into_iter()
75 .map(TryInto::try_into)
76 .collect::<Result<_, _>>()?,
77 },
78 Contents::RevokeFlat(revoke_flat) => model::OperatorEntry::RevokeFlat {
79 key_id: revoke_flat.key_id.into(),
80 permissions: revoke_flat
81 .permissions
82 .into_iter()
83 .map(TryInto::try_into)
84 .collect::<Result<_, _>>()?,
85 },
86 Contents::DefineNamespace(define_namespace) => model::OperatorEntry::DefineNamespace {
87 namespace: define_namespace.namespace,
88 },
89 Contents::ImportNamespace(import_namespace) => model::OperatorEntry::ImportNamespace {
90 namespace: import_namespace.namespace,
91 registry: import_namespace.registry,
92 },
93 };
94 Ok(output)
95 }
96}
97
98#[derive(Error, Debug)]
99#[error("no content in entry")]
100struct EmptyContentError;
101
102impl TryFrom<i32> for model::Permission {
103 type Error = Error;
104
105 fn try_from(permission: i32) -> Result<Self, Self::Error> {
106 let proto_perm = protobuf::OperatorPermission::try_from(permission)
107 .map_err(|_| PermissionParseError { value: permission })?;
108 match proto_perm {
109 protobuf::OperatorPermission::Unspecified => {
110 Err(Error::new(PermissionParseError { value: permission }))
111 }
112 protobuf::OperatorPermission::Commit => Ok(model::Permission::Commit),
113 protobuf::OperatorPermission::DefineNamespace => Ok(model::Permission::DefineNamespace),
114 protobuf::OperatorPermission::ImportNamespace => Ok(model::Permission::ImportNamespace),
115 }
116 }
117}
118
119#[derive(Error, Debug)]
120#[error("the value {value} could not be parsed as a permission")]
121struct PermissionParseError {
122 value: i32,
123}
124
125impl Signable for model::OperatorRecord {
128 const PREFIX: &'static [u8] = b"WARG-OPERATOR-RECORD-SIGNATURE-V0";
129}
130
131impl Encode for model::OperatorRecord {
132 fn encode(&self) -> Vec<u8> {
133 let proto_record: protobuf::OperatorRecord = self.into();
134 proto_record.encode_to_vec()
135 }
136}
137
138impl<'a> From<&'a model::OperatorRecord> for protobuf::OperatorRecord {
139 fn from(record: &'a model::OperatorRecord) -> Self {
140 protobuf::OperatorRecord {
141 prev: record.prev.as_ref().map(|hash| hash.to_string()),
142 version: record.version,
143 time: Some(prost_to_pbjson_timestamp(record.timestamp.into())),
144 entries: record.entries.iter().map(|entry| entry.into()).collect(),
145 }
146 }
147}
148
149impl<'a> From<&'a model::OperatorEntry> for protobuf::OperatorEntry {
150 fn from(entry: &'a model::OperatorEntry) -> Self {
151 use protobuf::operator_entry::Contents;
152 let contents = match entry {
153 model::OperatorEntry::Init {
154 hash_algorithm,
155 key,
156 } => Contents::Init(protobuf::OperatorInit {
157 key: key.to_string(),
158 hash_algorithm: hash_algorithm.to_string(),
159 }),
160 model::OperatorEntry::GrantFlat { key, permissions } => {
161 Contents::GrantFlat(protobuf::OperatorGrantFlat {
162 key: key.to_string(),
163 permissions: permissions.iter().map(Into::into).collect(),
164 })
165 }
166 model::OperatorEntry::RevokeFlat {
167 key_id,
168 permissions,
169 } => Contents::RevokeFlat(protobuf::OperatorRevokeFlat {
170 key_id: key_id.to_string(),
171 permissions: permissions.iter().map(Into::into).collect(),
172 }),
173 model::OperatorEntry::DefineNamespace { namespace } => {
174 Contents::DefineNamespace(protobuf::OperatorDefineNamespace {
175 namespace: namespace.clone(),
176 })
177 }
178 model::OperatorEntry::ImportNamespace {
179 namespace,
180 registry,
181 } => Contents::ImportNamespace(protobuf::OperatorImportNamespace {
182 namespace: namespace.clone(),
183 registry: registry.clone(),
184 }),
185 };
186 let contents = Some(contents);
187 protobuf::OperatorEntry { contents }
188 }
189}
190
191impl<'a> From<&'a model::Permission> for i32 {
192 fn from(permission: &'a model::Permission) -> Self {
193 let proto_perm = match permission {
194 model::Permission::Commit => protobuf::OperatorPermission::Commit,
195 model::Permission::DefineNamespace => protobuf::OperatorPermission::DefineNamespace,
196 model::Permission::ImportNamespace => protobuf::OperatorPermission::ImportNamespace,
197 };
198 proto_perm.into()
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 use std::time::SystemTime;
207
208 use crate::ProtoEnvelope;
209 use warg_crypto::hash::HashAlgorithm;
210 use warg_crypto::signing::generate_p256_pair;
211
212 #[test]
213 fn test_envelope_roundtrip() {
214 let (alice_pub, alice_priv) = generate_p256_pair();
215 let (bob_pub, _bob_priv) = generate_p256_pair();
216
217 let record = model::OperatorRecord {
218 prev: None,
219 version: 0,
220 timestamp: SystemTime::now(),
221 entries: vec![
222 model::OperatorEntry::Init {
223 hash_algorithm: HashAlgorithm::Sha256,
224 key: alice_pub,
225 },
226 model::OperatorEntry::GrantFlat {
227 key: bob_pub.clone(),
228 permissions: vec![model::Permission::Commit],
229 },
230 model::OperatorEntry::RevokeFlat {
231 key_id: bob_pub.fingerprint(),
232 permissions: vec![model::Permission::Commit],
233 },
234 ],
235 };
236
237 let first_envelope =
238 ProtoEnvelope::signed_contents(&alice_priv, record).expect("Failed to sign envelope 1");
239
240 let bytes = first_envelope.to_protobuf();
241
242 let second_envelope: ProtoEnvelope<model::OperatorRecord> =
243 match ProtoEnvelope::from_protobuf(&bytes) {
244 Ok(value) => value,
245 Err(error) => panic!("Failed to create envelope 2: {:?}", error),
246 };
247
248 assert_eq!(first_envelope, second_envelope);
249 }
250}