Skip to content

Commit 4abd382

Browse files
committed
#556 constrain URLs
1 parent 1cef100 commit 4abd382

File tree

11 files changed

+59
-21
lines changed

11 files changed

+59
-21
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ See [STATUS.md](server/STATUS.md) to learn more about which features will remain
5454
- Add systemd instructions to readme #271
5555
- Improve check_append error #558
5656
- Fix errors on successful export / import #565
57+
- Most Collection routes now live under `/collections`, e.g. `/collections/agents` instead of `/agents`. #556
58+
- Constrain new URLs. Commits for new Resources are now only valid if their parent is part of the current URL. So it's no longer possible to create `/some-path/new-resource` if `new-resource` is its parent is not in its URL. #556
5759

5860
## [v0.34.0] - 2022-10-31
5961

lib/src/atomic_url.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ impl AtomicUrl {
3636
pub fn set_route(&self, route: Routes) -> Self {
3737
let path = match route {
3838
Routes::AllVersions => "/all-versions".to_string(),
39-
Routes::Agents => "/agents".to_string(),
39+
Routes::Agents => "/collections/agents".to_string(),
4040
Routes::Collections => "/collections".to_string(),
41-
Routes::Commits => "/commits".to_string(),
41+
Routes::Commits => "/collections/commits".to_string(),
4242
Routes::CommitsUnsigned => "/commits-unsigned".to_string(),
4343
Routes::Endpoints => "/endpoints".to_string(),
4444
Routes::Import => "/import".to_string(),

lib/src/collections.rs

+12-2
Original file line numberDiff line numberDiff line change
@@ -392,14 +392,24 @@ pub fn create_collection_resource_for_class(
392392
) -> AtomicResult<Resource> {
393393
let class = store.get_class(class_subject)?;
394394

395+
// We use the `Collections` collection as the parent for all collections.
396+
// This also influences their URLs.
397+
let is_collections_collection = class.subject == urls::COLLECTION;
398+
395399
// Pluralize the shortname
396400
let pluralized = match class.shortname.as_ref() {
397401
"class" => "classes".to_string(),
398402
"property" => "properties".to_string(),
399403
other => format!("{}s", other),
400404
};
401405

402-
let mut collection = CollectionBuilder::class_collection(&class.subject, &pluralized, store);
406+
let path = if is_collections_collection {
407+
"/collections".to_string()
408+
} else {
409+
format!("/collections/{}", pluralized)
410+
};
411+
412+
let mut collection = CollectionBuilder::class_collection(&class.subject, &path, store);
403413

404414
collection.sort_by = match class_subject {
405415
urls::COMMIT => Some(urls::CREATED_AT.to_string()),
@@ -420,7 +430,7 @@ pub fn create_collection_resource_for_class(
420430
.ok_or("No self_url present in store, can't populate collections")?;
421431

422432
// Let the Collections collection be the top level item
423-
let parent = if class.subject == urls::COLLECTION {
433+
let parent = if is_collections_collection {
424434
drive.to_string()
425435
} else {
426436
drive

lib/src/commit.rs

+18
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ pub struct CommitOpts {
4646
pub update_index: bool,
4747
/// For who the right checks will be perormed. If empty, the signer of the Commit will be used.
4848
pub validate_for_agent: Option<String>,
49+
/// Checks if the URL of the parent is present in its Parent URL.
50+
pub validate_subject_url_parent: bool,
4951
}
5052

5153
/// A Commit is a set of changes to a Resource.
@@ -167,6 +169,20 @@ impl Commit {
167169
.apply_changes(resource_old.clone(), store, false)
168170
.map_err(|e| format!("Error applying changes to Resource {}. {}", self.subject, e))?;
169171

172+
// For new subjects, make sure that the parent of the resource is part of the URL of the new subject.
173+
if is_new && opts.validate_subject_url_parent {
174+
if let Ok(parent) = resource_new.get(urls::PARENT) {
175+
let parent_str = parent.to_string();
176+
if !self.subject.starts_with(&parent_str) {
177+
return Err(format!(
178+
"The parent '{}' is not part of the URL of the new subject '{}'.",
179+
parent_str, self.subject
180+
)
181+
.into());
182+
}
183+
}
184+
}
185+
170186
if opts.validate_rights {
171187
let validate_for = opts.validate_for_agent.as_ref().unwrap_or(&self.signer);
172188
if is_new {
@@ -381,6 +397,7 @@ impl Commit {
381397
validate_signature: false,
382398
validate_timestamp: false,
383399
validate_rights: false,
400+
validate_subject_url_parent: false,
384401
validate_previous_commit: false,
385402
validate_for_agent: None,
386403
update_index: false,
@@ -699,6 +716,7 @@ mod test {
699716
validate_previous_commit: true,
700717
validate_rights: false,
701718
validate_for_agent: None,
719+
validate_subject_url_parent: true,
702720
update_index: true,
703721
};
704722
}

lib/src/db/test.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ fn destroy_resource_and_check_collection_and_commits() {
188188
fn get_extended_resource_pagination() {
189189
let store = Db::init_temp("get_extended_resource_pagination").unwrap();
190190
let subject = format!(
191-
"{}commits?current_page=2&page_size=99999",
191+
"{}collections/commits?current_page=2&page_size=99999",
192192
store.get_server_url()
193193
);
194194
if store.get_resource_extended(&subject, false, None).is_ok() {
@@ -204,7 +204,7 @@ fn get_extended_resource_pagination() {
204204
.unwrap()
205205
.to_int()
206206
.unwrap();
207-
assert_eq!(cur_page, 2);
207+
assert_eq!(cur_page, num);
208208
assert_eq!(resource.get_subject(), &subject_with_page_size);
209209
}
210210

lib/src/parse.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ fn parse_json_ad_map_to_resource(
338338
validate_rights: parse_opts.for_agent.is_some(),
339339
validate_previous_commit: false,
340340
validate_for_agent: parse_opts.for_agent.clone(),
341+
validate_subject_url_parent: true,
341342
update_index: true,
342343
};
343344

@@ -563,8 +564,8 @@ mod test {
563564
.import(include_str!("../test_files/local_id.json"), &parse_opts)
564565
.unwrap();
565566

566-
let reference_subject = generate_id_from_local_id(&importer, "reference");
567-
let my_subject = generate_id_from_local_id(&importer, "my-local-id");
567+
let reference_subject = generate_id_from_local_id(&importer, "parent");
568+
let my_subject = generate_id_from_local_id(&importer, "parent/my-local-id");
568569
let found = store.get_resource(&my_subject).unwrap();
569570
let found_ref = store.get_resource(&reference_subject).unwrap();
570571

lib/src/plugins/add_pubkey.rs

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub fn request_email_add_pubkey() -> Endpoint {
2121
description: "Requests an email to add a new PublicKey to an Agent.".to_string(),
2222
shortname: "request-pubkey-reset".to_string(),
2323
handle: Some(handle_request_email_pubkey),
24+
handle_post: None,
2425
}
2526
}
2627

@@ -31,6 +32,7 @@ pub fn confirm_add_pubkey() -> Endpoint {
3132
description: "Confirms a token to add a new Public Key.".to_string(),
3233
shortname: "request-pubkey-reset".to_string(),
3334
handle: Some(handle_confirm_add_pubkey),
35+
handle_post: None,
3436
}
3537
}
3638

lib/src/resources.rs

+4
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ impl Resource {
331331
validate_for_agent: Some(agent.subject),
332332
// TODO: auto-merge should work before we enable this https://github.com/atomicdata-dev/atomic-data-rust/issues/412
333333
validate_previous_commit: false,
334+
validate_subject_url_parent: true,
334335
update_index: true,
335336
};
336337
let commit_response = commit.apply_opts(store, &opts)?;
@@ -359,6 +360,8 @@ impl Resource {
359360
validate_for_agent: Some(agent.subject),
360361
// https://github.com/atomicdata-dev/atomic-data-rust/issues/412
361362
validate_previous_commit: false,
363+
// This is contentious: https://github.com/atomicdata-dev/atomic-data-rust/issues/556
364+
validate_subject_url_parent: false,
362365
update_index: true,
363366
};
364367
let commit_response = commit.apply_opts(store, &opts)?;
@@ -665,6 +668,7 @@ mod test {
665668
validate_signature: true,
666669
validate_timestamp: true,
667670
validate_rights: false,
671+
validate_subject_url_parent: true,
668672
validate_previous_commit: true,
669673
validate_for_agent: None,
670674
update_index: true,

lib/test_files/local_id.json

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
[
22
{
3-
"https://atomicdata.dev/properties/localId": "reference",
3+
"https://atomicdata.dev/properties/localId": "parent",
44
"https://atomicdata.dev/properties/write": [
5-
"my-local-id"
5+
"parent/my-local-id"
66
],
77
"https://atomicdata.dev/properties/name": "My referenced resource"
88
},
99
{
10-
"https://atomicdata.dev/properties/localId": "my-local-id",
10+
"https://atomicdata.dev/properties/localId": "parent/my-local-id",
1111
"https://atomicdata.dev/properties/name": "My resource that refers",
12-
"https://atomicdata.dev/properties/parent": "reference",
12+
"https://atomicdata.dev/properties/parent": "parent",
1313
"https://atomicdata.dev/properties/write": [
14-
"reference"
14+
"parent"
1515
]
1616
}
1717
]

server/src/handlers/commit.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub async fn post_commit(
1313
let mut builder = HttpResponse::Ok();
1414
let incoming_commit_resource = parse_json_ad_commit_resource(&body, store)?;
1515
let incoming_commit = Commit::from_resource(incoming_commit_resource)?;
16-
if store.is_external_subject(&incoming_commit.subject)? {
16+
if store.is_external_subject(&incoming_commit.subject)? {
1717
return Err("Subject of commit is external, and should be sent to its origin domain. This store can not own this resource. See https://github.com/atomicdata-dev/atomic-data-rust/issues/509".into());
1818
}
1919
let opts = CommitOpts {
@@ -24,6 +24,7 @@ if store.is_external_subject(&incoming_commit.subject)? {
2424
// https://github.com/atomicdata-dev/atomic-data-rust/issues/412
2525
validate_previous_commit: false,
2626
validate_for_agent: Some(incoming_commit.signer.to_string()),
27+
validate_subject_url_parent: true,
2728
update_index: true,
2829
};
2930
let commit_response = incoming_commit.apply_opts(store, &opts)?;

server/src/tests.rs

+7-7
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ async fn server_tests() {
7878
assert!(body.as_str().contains("html"));
7979

8080
// Should 200 (public)
81-
let req =
82-
test::TestRequest::with_uri("/properties").insert_header(("Accept", "application/ad+json"));
81+
let req = test::TestRequest::with_uri("/collections/properties")
82+
.insert_header(("Accept", "application/ad+json"));
8383
let resp = test::call_service(&app, req.to_request()).await;
8484
assert_eq!(resp.status().as_u16(), 200, "resource should be public");
8585

@@ -104,8 +104,8 @@ async fn server_tests() {
104104
drive.save(store).unwrap();
105105

106106
// Should 401 (Unauthorized)
107-
let req =
108-
test::TestRequest::with_uri("/properties").insert_header(("Accept", "application/ad+json"));
107+
let req = test::TestRequest::with_uri("/collections/properties")
108+
.insert_header(("Accept", "application/ad+json"));
109109
let resp = test::call_service(&app, req.to_request()).await;
110110
assert_eq!(
111111
resp.status().as_u16(),
@@ -114,7 +114,7 @@ async fn server_tests() {
114114
);
115115

116116
// Get JSON-AD
117-
let req = build_request_authenticated("/properties", &appstate);
117+
let req = build_request_authenticated("/collections/properties", &appstate);
118118
let resp = test::call_service(&app, req.to_request()).await;
119119
let body = get_body(resp);
120120
println!("DEBUG: {:?}", body);
@@ -125,7 +125,7 @@ async fn server_tests() {
125125
);
126126

127127
// Get JSON-LD
128-
let req = build_request_authenticated("/properties", &appstate)
128+
let req = build_request_authenticated("/collections/properties", &appstate)
129129
.insert_header(("Accept", "application/ld+json"));
130130
let resp = test::call_service(&app, req.to_request()).await;
131131
assert!(resp.status().is_success(), "setup not returning JSON-LD");
@@ -136,7 +136,7 @@ async fn server_tests() {
136136
);
137137

138138
// Get turtle
139-
let req = build_request_authenticated("/properties", &appstate)
139+
let req = build_request_authenticated("/collections/properties", &appstate)
140140
.insert_header(("Accept", "text/turtle"));
141141
let resp = test::call_service(&app, req.to_request()).await;
142142
assert!(resp.status().is_success());

0 commit comments

Comments
 (0)