Skip to content

Commit 6db5aa3

Browse files
blog: metadata and author for Fairtale's post (#377)
Signed-off-by: David Dal Busco <david.dalbusco@outlook.com>
1 parent 780c423 commit 6db5aa3

2 files changed

Lines changed: 74 additions & 40 deletions

File tree

blog/2025-04-10-data-validation-in-juno-best-practices-and-security.md

Lines changed: 65 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,49 @@
11
---
2+
slug: data-validation-in-juno-best-practices-and-security
3+
title: "Data Validation in Juno: Best Practices and Security Considerations"
4+
authors: [fairtale]
5+
tags: [programming, development, assertion, validation]
6+
image: https://images.unsplash.com/photo-1525011268546-bf3f9b007f6a?q=80&w=1000&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D
27
draft: true
38
---
49

10+
![](https://images.unsplash.com/photo-1591117207239-788bf8de6c3b?q=80&w=2946&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D)
511

6-
# Data Validation in Juno: Best Practices and Security Considerations
12+
_Photo by [Johann Walter Bantz](https://unsplash.com/fr/@1walter2)_
13+
14+
---
715

816
## Why Data Validation Matters in Decentralized Apps
917

1018
Data validation is always important. However, web3 comes with its own set of challenges which makes validation an even more important part of building trustworthy apps:
1119

1220
1. **No Central Administrator**: Unlike traditional systems, decentralized apps have no admin backdoor to fix data issues
13-
3. **Limited Data Access**: Developers often can't directly access or examine user data due to encryption and/or privacy
14-
2. **Data Immutability**: Once written to the blockchain, data can be difficult or impossible to modify
21+
2. **Limited Data Access**: Developers often can't directly access or examine user data due to encryption and/or privacy
22+
3. **Data Immutability**: Once written to the blockchain, data can be difficult or impossible to modify
1523
4. **Client-Side Vulnerability**: Front-end validation can be bypassed by determined users (like in web2)
1624
5. **Security Risks**: Invalid or malicious data can compromise application integrity and user trust
1725

1826
Getting validation right from the start is not just a best practice—it's essential for the secure and reliable operation of your application.
1927

28+
---
29+
2030
## Available Approaches
2131

2232
Juno offers three main approaches for data validation:
2333

2434
1. **Hooks (on_set_doc)**
2535
2. **Custom Endpoints**
26-
3. **Assertion Hooks (assert_set_doc)** <--- Recommended approach
36+
3. **Assertion Hooks (assert_set_doc)** #---- Recommended approach
2737

2838
Let's explore each approach with simple examples:
2939

40+
---
41+
3042
### on_set_doc Hooks
3143

3244
`on_set_doc` is a Hook that is triggered after a document has been written to the database. It offers a way to execute custom logic whenever data is added or updated to a collection using the set_doc function.
3345

34-
This allows for many use-cases, even for certain types of validation, but this hook runs *after* the data has already been written.
46+
This allows for many use-cases, even for certain types of validation, but this hook runs _after_ the data has already been written.
3547

3648
```rust
3749
// Example of validation and cleanup in on_set_doc
@@ -42,7 +54,7 @@ async fn on_set_doc(context: OnSetDocContext) -> Result<(), String> {
4254
let key = context.data.key;
4355
let doc = &context.data.data.after; // Reference to the full document after update
4456
let user_data: UserData = decode_doc_data(&doc.data)?; // Decoded custom data from the document
45-
57+
4658
// Step 2: Validate the data
4759
if user_data.username.len() < 3 {
4860
// Step 3: If validation fails, delete the document using low-level store function
@@ -54,17 +66,16 @@ async fn on_set_doc(context: OnSetDocContext) -> Result<(), String> {
5466
version: Some(doc.version), // Use the version from our doc reference
5567
}
5668
).await?;
57-
69+
5870
// Log the error instead of returning it to avoid trapping
5971
ic_cdk::print("Username must be at least 3 characters");
6072
return Ok(());
6173
}
62-
74+
6375
Ok(())
6476
}
6577
```
6678

67-
6879
**Issues:**
6980

7081
- The on_set_doc hook only executes AFTER data is already written to the database, which is not ideal for validation.
@@ -75,6 +86,8 @@ async fn on_set_doc(context: OnSetDocContext) -> Result<(), String> {
7586

7687
There are also other Juno hooks, but in general, they provide a way to execute custom logic whenever data is added, modified, or deleted from a Juno datastore collection.
7788

89+
---
90+
7891
### Custom Endpoints using Serverless Functions
7992

8093
Custom Endpoints are Juno serverless functions that expose new API endpoints through Candid (the Internet Computer's interface description language). They provide a validation layer through custom API routes before data reaches Juno's datastore, allowing for complex multi-step operations with custom validation logic.
@@ -98,12 +111,12 @@ async fn create_user(key: String, user_data: UserData) -> Result<(), String> {
98111
if !user_data.username.chars().all(|c| c.is_alphanumeric()) {
99112
return Err("Username must contain only letters and numbers".to_string());
100113
}
101-
114+
102115
// Step 2: Create and store document
103116
// First encode our data into a blob that Juno can store into the 'data' field
104117
let encoded_data = encode_doc_data(&user_data)
105118
.map_err(|e| format!("Failed to encode user data: {}", e))?;
106-
119+
107120
// Create a SetDoc instance - this is the required format for setting documents in Juno
108121
// SetDoc contains only what we want to store - Juno handles all metadata:
109122
// - created_at/updated_at timestamps
@@ -114,7 +127,7 @@ async fn create_user(key: String, user_data: UserData) -> Result<(), String> {
114127
description: None, // Optional field for filtering/searching
115128
version: None // None for new docs, Some(version) for updates
116129
};
117-
130+
118131
// Use set_doc_store to save the document
119132
// This is Juno's low-level storage function that:
120133
// 1. Takes ownership of the document (caller's Principal)
@@ -147,6 +160,8 @@ The common workaround is to restrict the datastore collection to "controller" ac
147160
- Requires building a custom permission system from scratch
148161
- Splits validation logic from data storage
149162

163+
---
164+
150165
### assert_set_doc Hooks (Recommended)
151166

152167
The `assert_set_doc` hook runs BEFORE any data is written to the database, allowing you to validate and reject invalid submissions immediately. This is the most secure validation method in Juno as it integrates directly with the core data storage mechanism.
@@ -171,16 +186,16 @@ fn assert_set_doc(context: AssertSetDocContext) -> Result<(), String> {
171186
// Access username from the document
172187
let data = context.data.data.proposed.data.as_object()
173188
.ok_or("Invalid data format")?;
174-
189+
175190
let username = data.get("username")
176191
.and_then(|v| v.as_str())
177192
.ok_or("Username is required")?;
178-
193+
179194
// Validate username
180195
if username.len() < 3 {
181196
return Err("Username must be at least 3 characters".to_string());
182197
}
183-
198+
184199
Ok(())
185200
},
186201
_ => Ok(())
@@ -201,6 +216,8 @@ fn assert_set_doc(context: AssertSetDocContext) -> Result<(), String> {
201216
- Allows users to use setDoc as intended in Juno
202217
- Can return custom error messages to the frontend
203218

219+
---
220+
204221
## Hook Execution Flow
205222

206223
Here's the sequence of events during a document write operation:
@@ -213,6 +230,8 @@ Here's the sequence of events during a document write operation:
213230
4. `on_set_doc` hook runs (post-processing)
214231
5. Operation completes
215232

233+
---
234+
216235
## When and How to Use Each Approach
217236

218237
### Use assert_set_doc Hooks For
@@ -242,6 +261,8 @@ Here's the sequence of events during a document write operation:
242261
- Batch processing
243262
- Rate limiting
244263

264+
---
265+
245266
## Best Practices Summary
246267

247268
1. **Use assert_set_doc for Validation**: Always validate data before storage
@@ -252,6 +273,8 @@ Here's the sequence of events during a document write operation:
252273
6. **Implement Error Handling**: Provide clear feedback for validation failures
253274
7. **Maintain Audit Trails**: Log validation events for security analysis
254275

276+
---
277+
255278
## Production Use-Case Examples
256279

257280
Below are more detailed, production-ready examples for each validation approach:
@@ -280,12 +303,12 @@ fn validate_user_document(context: &AssertSetDocContext) -> Result<(), String> {
280303
// Decode and validate the user data structure
281304
let user_data: UserData = decode_doc_data(&context.data.data.proposed.data)
282305
.map_err(|e| format!("Invalid user data format: {}", e))?;
283-
306+
284307
// Validate username format (3-20 chars, alphanumeric + limited symbols)
285308
if !is_valid_username(&user_data.username) {
286309
return Err("Username must be 3-20 characters and contain only letters, numbers, and underscores".to_string());
287310
}
288-
311+
289312
// Check username uniqueness by searching existing documents
290313
let search_pattern = format!("username={};", user_data.username.to_lowercase());
291314
let existing_users = list_docs(
@@ -298,35 +321,35 @@ fn validate_user_document(context: &AssertSetDocContext) -> Result<(), String> {
298321
..Default::default()
299322
},
300323
);
301-
324+
302325
// If this is an update operation, exclude the current document
303326
let is_update = context.data.data.before.is_some();
304327
for (doc_key, _) in existing_users.items {
305328
if is_update && doc_key == context.data.key {
306329
continue;
307330
}
308-
331+
309332
return Err(format!("Username '{}' is already taken", user_data.username));
310333
}
311-
334+
312335
Ok(())
313336
}
314337

315338
fn validate_vote_document(context: &AssertSetDocContext) -> Result<(), String> {
316339
// Decode vote data
317340
let vote_data: VoteData = decode_doc_data(&context.data.data.proposed.data)
318341
.map_err(|e| format!("Invalid vote data format: {}", e))?;
319-
342+
320343
// Validate vote value constraints
321344
if vote_data.value < -1.0 || vote_data.value > 1.0 {
322345
return Err(format!("Vote value must be -1, 0, or 1 (got: {})", vote_data.value));
323346
}
324-
347+
325348
// Validate vote weight constraints
326349
if vote_data.weight < 0.0 || vote_data.weight > 1.0 {
327350
return Err(format!("Vote weight must be between 0.0 and 1.0 (got: {})", vote_data.weight));
328351
}
329-
352+
330353
// Validate tag exists
331354
let tag_params = ListParams {
332355
matcher: Some(ListMatcher {
@@ -335,30 +358,30 @@ fn validate_vote_document(context: &AssertSetDocContext) -> Result<(), String> {
335358
}),
336359
..Default::default()
337360
};
338-
361+
339362
let existing_tags = list_docs(String::from("tags"), tag_params);
340363
if existing_tags.items.is_empty() {
341364
return Err(format!("Tag not found: {}", vote_data.tag_key));
342365
}
343-
366+
344367
// Prevent self-voting
345368
if vote_data.author_key == vote_data.target_key {
346369
return Err("Users cannot vote on themselves".to_string());
347370
}
348-
371+
349372
Ok(())
350373
}
351374

352375
fn validate_tag_document(context: &AssertSetDocContext) -> Result<(), String> {
353376
// Decode tag data
354377
let tag_data: TagData = decode_doc_data(&context.data.data.proposed.data)
355378
.map_err(|e| format!("Invalid tag data format: {}", e))?;
356-
379+
357380
// Validate tag name format and uniqueness
358381
if !is_valid_tag_name(&tag_data.name) {
359382
return Err("Tag name must be 3-50 characters and contain only letters, numbers, and underscores".to_string());
360383
}
361-
384+
362385
// Check tag name uniqueness
363386
let search_pattern = format!("name={};", tag_data.name.to_lowercase());
364387
let existing_tags = list_docs(
@@ -371,34 +394,34 @@ fn validate_tag_document(context: &AssertSetDocContext) -> Result<(), String> {
371394
..Default::default()
372395
},
373396
);
374-
397+
375398
let is_update = context.data.data.before.is_some();
376399
for (doc_key, _) in existing_tags.items {
377400
if is_update && doc_key == context.data.key {
378401
continue;
379402
}
380403
return Err(format!("Tag name '{}' is already taken", tag_data.name));
381404
}
382-
405+
383406
// Validate description length
384407
if tag_data.description.len() > 1024 {
385408
return Err(format!(
386409
"Tag description cannot exceed 1024 characters (current length: {})",
387410
tag_data.description.len()
388411
));
389412
}
390-
413+
391414
// Validate time periods
392415
validate_time_periods(&tag_data.time_periods)?;
393-
416+
394417
// Validate vote reward
395418
if tag_data.vote_reward < 0.0 || tag_data.vote_reward > 1.0 {
396419
return Err(format!(
397420
"Vote reward must be between 0.0 and 1.0 (got: {})",
398421
tag_data.vote_reward
399422
));
400423
}
401-
424+
402425
Ok(())
403426
}
404427

@@ -412,7 +435,7 @@ fn validate_time_periods(periods: &[TimePeriod]) -> Result<(), String> {
412435
periods.len()
413436
));
414437
}
415-
438+
416439
// Last period must be "infinity" (999 months)
417440
let last_period = periods.last().unwrap();
418441
if last_period.months != 999 {
@@ -421,7 +444,7 @@ fn validate_time_periods(periods: &[TimePeriod]) -> Result<(), String> {
421444
last_period.months
422445
));
423446
}
424-
447+
425448
// Validate each period's configuration
426449
for (i, period) in periods.iter().enumerate() {
427450
// Validate multiplier range (0.05 to 10.0)
@@ -431,7 +454,7 @@ fn validate_time_periods(periods: &[TimePeriod]) -> Result<(), String> {
431454
i + 1, period.multiplier
432455
));
433456
}
434-
457+
435458
// Validate multiplier step increments (0.05)
436459
let multiplier_int = (period.multiplier * 100.0).round();
437460
let remainder = multiplier_int % 5.0;
@@ -441,7 +464,7 @@ fn validate_time_periods(periods: &[TimePeriod]) -> Result<(), String> {
441464
i + 1, period.multiplier
442465
));
443466
}
444-
467+
445468
// Validate month duration
446469
if period.months == 0 {
447470
return Err(format!(
@@ -450,13 +473,15 @@ fn validate_time_periods(periods: &[TimePeriod]) -> Result<(), String> {
450473
));
451474
}
452475
}
453-
476+
454477
Ok(())
455478
}
456479
```
457480

458481
Remember: Security is about preventing unauthorized or invalid operations, not just making them difficult. assert_set_doc hooks provide the only guaranteed way to validate all data operations in Juno's Datastore.
459482

483+
---
484+
460485
## Reference: Available Juno Hooks and Context Types
461486

462487
This section provides a comprehensive reference of all available Juno hooks and their corresponding context types.
@@ -504,6 +529,8 @@ use junobuild_satellite::{
504529
};
505530
```
506531

532+
---
533+
507534
### Where to find the hooks and assertions in your project
508535

509536
When you run `juno dev eject`, all available hooks and assertions are scaffolded in your `lib.rs` module. However, you can selectively enable only the features you need by disabling default features in your `Cargo.toml` and explicitly specifying the ones you want to use.
@@ -514,5 +541,3 @@ Example configuration for using only `on_set_doc` and `assert_set_doc`:
514541
[dependencies]
515542
junobuild-satellite = { version = "0.0.21", default-features = false, features = ["on_set_doc", "assert_set_doc"] }
516543
```
517-
518-

blog/authors.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,12 @@ sam-the-tutor:
2424
socials:
2525
x: samthetutor2
2626
github: sam-thetutor
27+
28+
fairtale:
29+
name: Fairtale
30+
url: https://x.com/fairtal3
31+
image_url: https://pbs.twimg.com/profile_images/1869034633737752576/buJ6oZQs_400x400.jpg
32+
page: true
33+
socials:
34+
x: fairtal3
35+
github: fairtale5

0 commit comments

Comments
 (0)