Skip to content

Commit 97781a5

Browse files
committed
#556 constrain URLs
1 parent db9a54e commit 97781a5

File tree

10 files changed

+62
-21
lines changed

10 files changed

+62
-21
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ See [STATUS.md](server/STATUS.md) to learn more about which features will remain
4646
- Add systemd instructions to readme #271
4747
- Improve check_append error #558
4848
- Fix errors on successful export / import #565
49+
- Most Collection routes now live under `/collections`, e.g. `/collections/agents` instead of `/agents`. #556
50+
- 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
4951

5052
## [v0.34.0] - 2022-10-31
5153

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
@@ -396,14 +396,24 @@ pub fn create_collection_resource_for_class(
396396
) -> AtomicResult<Resource> {
397397
let class = store.get_class(class_subject)?;
398398

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

406-
let mut collection = CollectionBuilder::class_collection(&class.subject, &pluralized, store);
410+
let path = if is_collections_collection {
411+
"/collections".to_string()
412+
} else {
413+
format!("/collections/{}", pluralized)
414+
};
415+
416+
let mut collection = CollectionBuilder::class_collection(&class.subject, &path, store);
407417

408418
collection.sort_by = match class_subject {
409419
urls::COMMIT => Some(urls::CREATED_AT.to_string()),
@@ -424,7 +434,7 @@ pub fn create_collection_resource_for_class(
424434
.ok_or("No self_url present in store, can't populate collections")?;
425435

426436
// Let the Collections collection be the top level item
427-
let parent = if class.subject == urls::COLLECTION {
437+
let parent = if is_collections_collection {
428438
drive.to_string()
429439
} else {
430440
drive

lib/src/commit.rs

+18
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ pub struct CommitOpts {
3939
pub update_index: bool,
4040
/// For who the right checks will be perormed. If empty, the signer of the Commit will be used.
4141
pub validate_for_agent: Option<String>,
42+
/// Checks if the URL of the parent is present in its Parent URL.
43+
pub validate_subject_url_parent: bool,
4244
}
4345

4446
/// A Commit is a set of changes to a Resource.
@@ -160,6 +162,20 @@ impl Commit {
160162
.apply_changes(resource_old.clone(), store, false)
161163
.map_err(|e| format!("Error applying changes to Resource {}. {}", self.subject, e))?;
162164

165+
// For new subjects, make sure that the parent of the resource is part of the URL of the new subject.
166+
if is_new && opts.validate_subject_url_parent {
167+
if let Ok(parent) = resource_new.get(urls::PARENT) {
168+
let parent_str = parent.to_string();
169+
if !self.subject.starts_with(&parent_str) {
170+
return Err(format!(
171+
"The parent '{}' is not part of the URL of the new subject '{}'.",
172+
parent_str, self.subject
173+
)
174+
.into());
175+
}
176+
}
177+
}
178+
163179
if opts.validate_rights {
164180
let validate_for = opts.validate_for_agent.as_ref().unwrap_or(&self.signer);
165181
if is_new {
@@ -374,6 +390,7 @@ impl Commit {
374390
validate_signature: false,
375391
validate_timestamp: false,
376392
validate_rights: false,
393+
validate_subject_url_parent: false,
377394
validate_previous_commit: false,
378395
validate_for_agent: None,
379396
update_index: false,
@@ -694,6 +711,7 @@ mod test {
694711
validate_previous_commit: true,
695712
validate_rights: false,
696713
validate_for_agent: None,
714+
validate_subject_url_parent: true,
697715
update_index: true,
698716
};
699717
}

lib/src/db/test.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,12 @@ fn destroy_resource_and_check_collection_and_commits() {
187187
#[test]
188188
fn get_extended_resource_pagination() {
189189
let store = Db::init_temp("get_extended_resource_pagination").unwrap();
190-
let subject = format!("{}commits?current_page=2", store.get_server_url());
190+
let num = 3;
191+
let subject = format!(
192+
"{}collections/commits?current_page={}",
193+
store.get_server_url(),
194+
num
195+
);
191196
// Should throw, because page 2 is out of bounds for default page size
192197
let _wrong_resource = store
193198
.get_resource_extended(&subject, false, None)
@@ -202,7 +207,7 @@ fn get_extended_resource_pagination() {
202207
.unwrap()
203208
.to_int()
204209
.unwrap();
205-
assert_eq!(cur_page, 2);
210+
assert_eq!(cur_page, num);
206211
assert_eq!(resource.get_subject(), &subject_with_page_size);
207212
}
208213

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

@@ -557,8 +558,8 @@ mod test {
557558
.import(include_str!("../test_files/local_id.json"), &parse_opts)
558559
.unwrap();
559560

560-
let reference_subject = generate_id_from_local_id(&importer, "reference");
561-
let my_subject = generate_id_from_local_id(&importer, "my-local-id");
561+
let reference_subject = generate_id_from_local_id(&importer, "parent");
562+
let my_subject = generate_id_from_local_id(&importer, "parent/my-local-id");
562563
let found = store.get_resource(&my_subject).unwrap();
563564
let found_ref = store.get_resource(&reference_subject).unwrap();
564565

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)