warg_protocol/operator/
mod.rs

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
15/// The currently supported operator protocol version.
16pub 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
125// Serialization
126
127impl 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}