Skip to content

Commit 2202883

Browse files
committed
#556 constrain URLs
1 parent 9b9ad2f commit 2202883

File tree

10 files changed

+58
-22
lines changed

10 files changed

+58
-22
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ See [STATUS.md](server/STATUS.md) to learn more about which features will remain
6565
- Add systemd instructions to readme #271
6666
- Improve check_append error #558
6767
- Fix errors on successful export / import #565
68+
- Most Collection routes now live under `/collections`, e.g. `/collections/agents` instead of `/agents`. #556
69+
- 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
6870

6971
## [v0.34.0] - 2022-10-31
7072

lib/src/atomic_url.rs

Lines changed: 2 additions & 2 deletions
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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,14 +393,24 @@ pub fn create_collection_resource_for_class(
393393
) -> AtomicResult<Resource> {
394394
let class = store.get_class(class_subject)?;
395395

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

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

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

423433
// Let the Collections collection be the top level item
424-
let parent = if class.subject == urls::COLLECTION {
434+
let parent = if is_collections_collection {
425435
drive.to_string()
426436
} else {
427437
drive

lib/src/commit.rs

Lines changed: 18 additions & 0 deletions
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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ fn destroy_resource_and_check_collection_and_commits() {
192192
fn get_extended_resource_pagination() {
193193
let store = Db::init_temp("get_extended_resource_pagination").unwrap();
194194
let subject = format!(
195-
"{}commits?current_page=2&page_size=99999",
195+
"{}collections/commits?current_page=2&page_size=99999",
196196
store.get_server_url()
197197
);
198198
let for_agent = &ForAgent::Public;
@@ -212,7 +212,7 @@ fn get_extended_resource_pagination() {
212212
.unwrap()
213213
.to_int()
214214
.unwrap();
215-
assert_eq!(cur_page, 2);
215+
assert_eq!(cur_page, num);
216216
assert_eq!(resource.get_subject(), &subject_with_page_size);
217217
}
218218

lib/src/parse.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,8 @@ fn parse_json_ad_map_to_resource(
347347
validate_timestamp: false,
348348
validate_rights: parse_opts.for_agent != ForAgent::Sudo,
349349
validate_previous_commit: false,
350-
validate_for_agent: Some(parse_opts.for_agent.to_string()),
350+
validate_for_agent: parse_opts.for_agent.clone(),
351+
validate_subject_url_parent: true,
351352
update_index: true,
352353
};
353354

@@ -573,8 +574,8 @@ mod test {
573574
.import(include_str!("../test_files/local_id.json"), &parse_opts)
574575
.unwrap();
575576

576-
let reference_subject = generate_id_from_local_id(&importer, "reference");
577-
let my_subject = generate_id_from_local_id(&importer, "my-local-id");
577+
let reference_subject = generate_id_from_local_id(&importer, "parent");
578+
let my_subject = generate_id_from_local_id(&importer, "parent/my-local-id");
578579
let found = store.get_resource(&my_subject).unwrap();
579580
let found_ref = store.get_resource(&reference_subject).unwrap();
580581

lib/src/resources.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ impl Resource {
347347
validate_for_agent: agent.subject.into(),
348348
// TODO: auto-merge should work before we enable this https://github.com/atomicdata-dev/atomic-server/issues/412
349349
validate_previous_commit: false,
350+
validate_subject_url_parent: true,
350351
update_index: true,
351352
};
352353
let commit_response = commit.apply_opts(store, &opts)?;
@@ -375,6 +376,8 @@ impl Resource {
375376
validate_for_agent: agent.subject.into(),
376377
// https://github.com/atomicdata-dev/atomic-server/issues/412
377378
validate_previous_commit: false,
379+
// This is contentious: https://github.com/atomicdata-dev/atomic-data-rust/issues/556
380+
validate_subject_url_parent: false,
378381
update_index: true,
379382
};
380383
let commit_response = commit.apply_opts(store, &opts)?;
@@ -683,6 +686,7 @@ mod test {
683686
validate_signature: true,
684687
validate_timestamp: true,
685688
validate_rights: false,
689+
validate_subject_url_parent: true,
686690
validate_previous_commit: true,
687691
validate_for_agent: None,
688692
update_index: true,

lib/test_files/local_id.json

Lines changed: 5 additions & 5 deletions
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

Lines changed: 2 additions & 1 deletion
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-server/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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ async fn server_tests() {
7979
assert!(body.as_str().contains("html"));
8080

8181
// Should 200 (public)
82-
let req =
83-
test::TestRequest::with_uri("/properties").insert_header(("Accept", "application/ad+json"));
82+
let req = test::TestRequest::with_uri("/collections/properties")
83+
.insert_header(("Accept", "application/ad+json"));
8484
let resp = test::call_service(&app, req.to_request()).await;
8585
assert_eq!(
8686
resp.status().as_u16(),
@@ -109,8 +109,8 @@ async fn server_tests() {
109109
drive.save(store).unwrap();
110110

111111
// Should 401 (Unauthorized)
112-
let req =
113-
test::TestRequest::with_uri("/properties").insert_header(("Accept", "application/ad+json"));
112+
let req = test::TestRequest::with_uri("/collections/properties")
113+
.insert_header(("Accept", "application/ad+json"));
114114
let resp = test::call_service(&app, req.to_request()).await;
115115
assert_eq!(
116116
resp.status().as_u16(),
@@ -119,7 +119,7 @@ async fn server_tests() {
119119
);
120120

121121
// Get JSON-AD
122-
let req = build_request_authenticated("/properties", &appstate);
122+
let req = build_request_authenticated("/collections/properties", &appstate);
123123
let resp = test::call_service(&app, req.to_request()).await;
124124
let body = get_body(resp);
125125
println!("DEBUG: {:?}", body);
@@ -130,7 +130,7 @@ async fn server_tests() {
130130
);
131131

132132
// Get JSON-LD
133-
let req = build_request_authenticated("/properties", &appstate)
133+
let req = build_request_authenticated("/collections/properties", &appstate)
134134
.insert_header(("Accept", "application/ld+json"));
135135
let resp = test::call_service(&app, req.to_request()).await;
136136
assert!(resp.status().is_success(), "setup not returning JSON-LD");
@@ -141,7 +141,7 @@ async fn server_tests() {
141141
);
142142

143143
// Get turtle
144-
let req = build_request_authenticated("/properties", &appstate)
144+
let req = build_request_authenticated("/collections/properties", &appstate)
145145
.insert_header(("Accept", "text/turtle"));
146146
let resp = test::call_service(&app, req.to_request()).await;
147147
assert!(resp.status().is_success());

0 commit comments

Comments
 (0)