guardian-db 0.19.0

High-performance, local-first decentralized database built on Rust and Iroh
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
use crate::access_control::{
    manifest::CreateAccessControllerOptions, manifest::Manifest, manifest::ManifestParams,
};
use crate::address::Address;
use crate::guardian::error::{GuardianError, Result};
use crate::log::{access_control::LogEntry, identity_provider::IdentityProvider};
use crate::p2p::network::client::IrohClient;
use async_trait::async_trait;
use iroh_blobs::Hash;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{Span, debug, instrument, warn};

/// CBOR-serializable representation of the write-access list, persisted to Iroh.
#[derive(Debug, Serialize, Deserialize)]
struct CborWriteAccess {
    #[serde(rename = "write")]
    write: Vec<String>,
}

/// In-memory state of the controller: the list of keys allowed to write.
struct ControllerState {
    write_access: Vec<String>,
}

/// Main structure of the Iroh access controller.
///
/// Permissions consist of a single write-access list. They are kept in memory
/// and can be persisted to / loaded from Iroh as CBOR blobs.
pub struct IrohAccessController {
    client: Arc<IrohClient>,
    state: RwLock<ControllerState>,
    span: Span,
}

impl IrohAccessController {
    /// Returns the controller type identifier.
    pub fn get_type(&self) -> &'static str {
        "iroh"
    }

    /// This controller has no address of its own, so it returns `None`.
    pub fn address(&self) -> Option<Box<dyn Address>> {
        None
    }

    /// Decides whether a log entry may be appended: the entry's identity must
    /// be in the write-access list (or the list must contain the universal
    /// `"*"` key), in which case the identity is also verified.
    #[instrument(skip(self, entry, identity_provider, _additional_context))]
    pub async fn can_append(
        &self,
        entry: &dyn LogEntry,
        identity_provider: &dyn IdentityProvider,
        _additional_context: &dyn crate::log::access_control::CanAppendAdditionalContext,
    ) -> Result<()> {
        let state = self.state.read().await;
        let key = entry.get_identity().id();

        for allowed_key in state.write_access.iter() {
            if allowed_key == key || allowed_key == "*" {
                // If the key is authorized, verify the identity.
                return identity_provider
                    .verify_identity(entry.get_identity())
                    .await;
            }
        }

        Err(GuardianError::Store(
            "Key does not have write permission".to_string(),
        ))
    }

    /// Returns the keys authorized for the given role. For this controller,
    /// `admin` and `write` map to the same write-access list; any other role
    /// returns an empty list.
    pub async fn get_authorized_by_role(&self, role: &str) -> Result<Vec<String>> {
        let state = self.state.read().await;
        // 'admin' and 'write' are the same thing for this controller.
        if role == "admin" || role == "write" {
            Ok(state.write_access.clone())
        } else {
            Ok(vec![])
        }
    }

    /// Grants write access to `key_id`. Only the `write` capability is
    /// supported; the key is added only if not already present.
    #[instrument(skip(self))]
    pub async fn grant(&self, capability: &str, key_id: &str) -> Result<()> {
        if capability != "write" {
            return Err(GuardianError::Store(format!(
                "IrohAccessController only supports 'write' capability, got '{}'",
                capability
            )));
        }

        let mut state = self.state.write().await;
        if !state.write_access.contains(&key_id.to_string()) {
            state.write_access.push(key_id.to_string());
            debug!(target: "iroh_access_controller",
                capability = %capability,
                key_id = %key_id,
                total_keys = state.write_access.len(),
                "Permission granted successfully"
            );
        } else {
            debug!(target: "iroh_access_controller",
                capability = %capability,
                key_id = %key_id,
                "Permission already exists"
            );
        }
        Ok(())
    }

    /// Revokes write access from `key_id`. Only the `write` capability is
    /// supported.
    #[instrument(skip(self))]
    pub async fn revoke(&self, capability: &str, key_id: &str) -> Result<()> {
        if capability != "write" {
            return Err(GuardianError::Store(format!(
                "IrohAccessController only supports 'write' capability, got '{}'",
                capability
            )));
        }

        let mut state = self.state.write().await;
        let initial_len = state.write_access.len();
        state.write_access.retain(|k| k != key_id);

        if state.write_access.len() < initial_len {
            debug!(target: "iroh_access_controller",
                capability = %capability,
                key_id = %key_id,
                remaining_keys = state.write_access.len(),
                "Permission revoked successfully"
            );
        } else {
            debug!(target: "iroh_access_controller",
                capability = %capability,
                key_id = %key_id,
                "Permission not found for revocation"
            );
        }
        Ok(())
    }

    /// Loads the controller's permissions from Iroh at the given address.
    ///
    /// The address is a hex-encoded 32-byte hash pointing to the CBOR manifest;
    /// the manifest in turn references the CBOR blob holding the write-access
    /// list, which becomes the new internal state.
    #[instrument(skip(self), fields(address = %address))]
    pub async fn load(&self, address: &str) -> Result<()> {
        let state = self.state.read().await;
        debug!(target: "iroh_access_controller", address = %address, "Reading permissions from the Iroh access controller");
        drop(state); // Release the read lock before the write operations.

        // Parse the hex string into a Hash.
        let hash_bytes = hex::decode(address)
            .map_err(|e| GuardianError::InvalidHash(format!("Invalid hex hash: {}", e)))?;

        if hash_bytes.len() != 32 {
            return Err(GuardianError::InvalidHash(format!(
                "Hash must be 32 bytes, found {}",
                hash_bytes.len()
            )));
        }

        let mut hash_array = [0u8; 32];
        hash_array.copy_from_slice(&hash_bytes);
        let hash = Hash::from_bytes(hash_array);

        let client = self.client.clone();
        let hash_string = hex::encode(hash.as_bytes());

        // Spawn a blocking task to handle the non-Send Iroh operations
        let manifest_data = tokio::task::spawn_blocking(move || {
            // Use tokio runtime handle to run async code in blocking context
            let rt = tokio::runtime::Handle::current();
            rt.block_on(async move {
                // 1. Read the main CBOR manifest using cat_bytes.
                let manifest_data = client
                    .cat_bytes(&hash_string)
                    .await
                    .map_err(|e| crate::guardian::error::GuardianError::Io(e.to_string()))?;

                Ok::<Vec<u8>, crate::guardian::error::GuardianError>(manifest_data)
            })
        })
        .await
        .map_err(|e| GuardianError::Store(format!("Task join error: {}", e)))??;

        let manifest: Manifest = serde_cbor::from_slice(&manifest_data)?;

        // 2. Read the permissions content using the manifest's address.
        let access_data_hash = manifest.params.address();
        let client_clone = self.client.clone();
        let access_data_hash_string = access_data_hash.to_string();

        // Spawn another blocking task for the second Iroh operation
        let access_data_bytes = tokio::task::spawn_blocking(move || {
            let rt = tokio::runtime::Handle::current();
            rt.block_on(async move {
                let access_data_bytes = client_clone
                    .cat_bytes(&access_data_hash_string)
                    .await
                    .map_err(|e| crate::guardian::error::GuardianError::Io(e.to_string()))?;

                Ok::<Vec<u8>, crate::guardian::error::GuardianError>(access_data_bytes)
            })
        })
        .await
        .map_err(|e| GuardianError::Store(format!("Task join error: {}", e)))??;

        let write_access_data: CborWriteAccess = serde_cbor::from_slice(&access_data_bytes)?;

        // 3. Extract the permissions directly from the CBOR.
        let write_access = write_access_data.write;

        // 4. Update the internal state with the new permissions.
        let mut state = self.state.write().await;
        state.write_access = write_access;

        Ok(())
    }

    /// Persists the current write-access list to Iroh as a CBOR blob and
    /// returns the manifest options referencing it.
    #[instrument(skip(self))]
    pub async fn save(&self) -> Result<CreateAccessControllerOptions> {
        let state = self.state.read().await;
        let cbor_data = CborWriteAccess {
            write: state.write_access.clone(),
        };
        // Serialize the CBOR structure into bytes.
        let cbor_bytes = serde_cbor::to_vec(&cbor_data)?;

        let client = self.client.clone();
        // Spawn a blocking task to handle the non-Send Iroh operations
        let response = tokio::task::spawn_blocking(move || {
            // Use tokio runtime handle to run async code in blocking context
            let rt = tokio::runtime::Handle::current();
            rt.block_on(async move {
                // Store the bytes using Iroh.
                client.add_bytes(cbor_bytes).await
            })
        })
        .await
        .map_err(|e| GuardianError::Store(format!("Task join error: {}", e)))??;

        // Convert the hex hash string into a Hash.
        let hash_bytes = hex::decode(&response.hash)
            .map_err(|e| GuardianError::InvalidHash(format!("Error decoding hash: {}", e)))?;

        if hash_bytes.len() != 32 {
            return Err(GuardianError::InvalidHash(format!(
                "Invalid hash: expected 32 bytes, found {}",
                hash_bytes.len()
            )));
        }

        let mut hash_array = [0u8; 32];
        hash_array.copy_from_slice(&hash_bytes);
        let hash = Hash::from_bytes(hash_array);

        debug!(target: "iroh_access_controller", hash = %hex::encode(hash.as_bytes()), "Iroh access controller saved");
        // Build and return the parameters for the new manifest.
        Ok(CreateAccessControllerOptions::new(
            hash,
            false,
            "iroh".to_string(),
        ))
    }

    /// Closes the controller. This is a no-op since it is Iroh-based: the state
    /// lives in Iroh and there are no local resources to release.
    #[instrument(skip(self))]
    pub async fn close(&self) -> Result<()> {
        debug!(target: "iroh_access_controller", "Closing Iroh access controller");

        let state = self.state.read().await;
        debug!(target: "iroh_access_controller",
            write_access_count = state.write_access.len(),
            "Iroh access controller closed successfully"
        );

        Ok(())
    }

    /// Creates a new controller. If no `write` access is configured in the
    /// params, the provided `identity_id` is granted write access by default.
    #[instrument(skip(client, params), fields(identity_id = %identity_id))]
    pub fn new(
        client: Arc<IrohClient>,
        identity_id: String,
        mut params: CreateAccessControllerOptions,
    ) -> Result<Self> {
        if params.get_access("write").is_none() {
            params.set_access("write".to_string(), vec![identity_id]);
        }

        let initial_state = ControllerState {
            write_access: params.get_access("write").unwrap_or_default(),
        };

        Ok(Self {
            client,
            state: RwLock::new(initial_state),
            span: tracing::info_span!("iroh_access_controller", controller_type = "iroh"),
        })
    }

    /// Returns a reference to the span used for tracing context.
    pub fn span(&self) -> &Span {
        &self.span
    }
}

// AccessController trait implementation for IrohAccessController.
#[async_trait]
impl crate::access_control::traits::AccessController for IrohAccessController {
    fn get_type(&self) -> &str {
        "iroh"
    }

    async fn get_authorized_by_role(&self, role: &str) -> Result<Vec<String>> {
        let state = self.state.read().await;

        match role {
            "write" => Ok(state.write_access.clone()),
            "read" => Ok(state.write_access.clone()), // By default, whoever can write can read.
            "admin" => Ok(state.write_access.clone()), // By default, uses the same permissions.
            _ => Ok(Vec::new()),
        }
    }

    async fn grant(&self, capability: &str, key_id: &str) -> Result<()> {
        if capability != "write" {
            return Err(GuardianError::Store(format!(
                "IrohAccessController only supports 'write' capability, got '{}'",
                capability
            )));
        }

        let mut state = self.state.write().await;
        if !state.write_access.contains(&key_id.to_string()) {
            state.write_access.push(key_id.to_string());
        }
        Ok(())
    }

    async fn revoke(&self, capability: &str, key_id: &str) -> Result<()> {
        if capability != "write" {
            return Err(GuardianError::Store(format!(
                "IrohAccessController only supports 'write' capability, got '{}'",
                capability
            )));
        }

        let mut state = self.state.write().await;
        state.write_access.retain(|k| k != key_id);
        Ok(())
    }

    async fn load(&self, address: &str) -> Result<()> {
        self.load(address).await
    }

    async fn save(&self) -> Result<Box<dyn crate::access_control::manifest::ManifestParams>> {
        let options = self.save().await?;
        Ok(Box::new(options))
    }

    async fn close(&self) -> Result<()> {
        IrohAccessController::close(self).await
    }

    async fn can_append(
        &self,
        entry: &dyn crate::log::access_control::LogEntry,
        _identity_provider: &dyn crate::log::identity_provider::IdentityProvider,
        _additional_context: &dyn crate::log::access_control::CanAppendAdditionalContext,
    ) -> Result<()> {
        let state = self.state.read().await;
        let entry_identity = entry.get_identity();
        let entry_id = entry_identity.id();

        // Check whether the identity has write permission.
        if state.write_access.contains(&"*".to_string())
            || state.write_access.contains(&entry_id.to_string())
        {
            Ok(())
        } else {
            Err(GuardianError::Store(format!(
                "Access denied: identity {} not authorized for write operations",
                entry_id
            )))
        }
    }
}