Skip to content

Commit 4a08a66

Browse files
committed
#13 authorize search endpoint, add public mode
1 parent 346634c commit 4a08a66

File tree

7 files changed

+66
-53
lines changed

7 files changed

+66
-53
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ By far most changes relate to `atomic-server`, so if not specified, assume the c
77

88
- Add authentication to restrict read access. Works by signing requests with Private Keys. #13
99
- Refactor internal error model, Use correct HTTP status codes #11
10+
- Add `public-mode` to server, to keep performance maximum if you don't want authentication.
1011

1112
## v0.28.2
1213

lib/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ pub use atoms::Atom;
7777
pub use commit::Commit;
7878
#[cfg(feature = "db")]
7979
pub use db::Db;
80+
pub use errors::AtomicError;
81+
pub use errors::AtomicErrorType;
8082
pub use resources::Resource;
8183
pub use store::Store;
8284
pub use storelike::Storelike;

server/src/config.rs

+7-4
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ pub struct Opts {
3838
/// The IP address of the server
3939
#[clap(long, default_value = "0.0.0.0", env = "ATOMIC_IP")]
4040
pub ip: IpAddr,
41-
/// If we're using HTTPS or plaintext HTTP.
42-
/// Is disabled when using cert_init
41+
/// Use HTTPS instead of HTTP.
42+
/// Will get certificates from LetsEncrypt.
4343
#[clap(long, env = "ATOMIC_HTTPS")]
4444
pub https: bool,
4545
/// Endpoint where the front-end assets are hosted
@@ -55,12 +55,15 @@ pub struct Opts {
5555
/// Path for atomic data config directory. Defaults to "~/.config/atomic/""
5656
#[clap(long, env = "ATOMIC_CONFIG_DIR")]
5757
pub config_dir: Option<PathBuf>,
58-
/// When enabled, it allows POSTing to the /search endpoint
58+
/// CAUTION: Makes data public on the `/search` endpoint. When enabled, it allows POSTing to the /search endpoint and returns search results as single triples, without performing authentication checks. See https://github.com/joepio/atomic-data-rust/blob/master/server/rdf-search.md
5959
#[clap(long, env = "ATOMIC_RDF_SEARCH")]
6060
pub rdf_search: bool,
61-
/// When enabled, previous versions of resources are removed from the search index when updated.
61+
/// By default, Atomic-Server keeps previous verions of resources indexed in Search. When enabling this flag, previous versions of resources are removed from the search index when their values are updated.
6262
#[clap(long, env = "ATOMIC_REMOVE_PREVIOUS_SEARCH")]
6363
pub remove_previous_search: bool,
64+
/// CAUTION: Skip authentication checks, making all data public. Improves performance.
65+
#[clap(long, env = "ATOMIC_PUBLIC_MODE")]
66+
pub public_mode: bool,
6467
}
6568

6669
#[derive(Parser, Clone, Debug)]

server/src/errors.rs

+10-23
Original file line numberDiff line numberDiff line change
@@ -20,29 +20,7 @@ pub struct AtomicServerError {
2020
pub error_type: AppErrorType,
2121
}
2222

23-
impl AtomicServerError {
24-
#[allow(dead_code)]
25-
pub fn not_found(message: String) -> AtomicServerError {
26-
AtomicServerError {
27-
message: format!("Resource not found. {}", message),
28-
error_type: AppErrorType::NotFound,
29-
}
30-
}
31-
32-
pub fn unauthorized(message: String) -> AtomicServerError {
33-
AtomicServerError {
34-
message: format!("Unauthorized. {}", message),
35-
error_type: AppErrorType::Unauthorized,
36-
}
37-
}
38-
39-
pub fn other_error(message: String) -> AtomicServerError {
40-
AtomicServerError {
41-
message,
42-
error_type: AppErrorType::Other,
43-
}
44-
}
45-
}
23+
impl AtomicServerError {}
4624

4725
#[derive(Serialize)]
4826
pub struct AppErrorResponse {
@@ -145,6 +123,15 @@ impl From<acme_lib::Error> for AtomicServerError {
145123
}
146124
}
147125

126+
impl From<actix_web::Error> for AtomicServerError {
127+
fn from(error: actix_web::Error) -> Self {
128+
AtomicServerError {
129+
message: error.to_string(),
130+
error_type: AppErrorType::Other,
131+
}
132+
}
133+
}
134+
148135
impl From<atomic_lib::errors::AtomicError> for AtomicServerError {
149136
fn from(error: atomic_lib::errors::AtomicError) -> Self {
150137
println!("ERROR: {:?}", error);

server/src/handlers/resource.rs

+6-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
22
appstate::AppState, content_types::get_accept, content_types::ContentType,
3-
errors::AtomicServerResult, helpers::get_auth_headers,
3+
errors::AtomicServerResult, helpers::get_client_agent,
44
};
55
use actix_web::{web, HttpResponse};
66
use atomic_lib::Storelike;
@@ -13,11 +13,11 @@ pub async fn get_resource(
1313
data: web::Data<Mutex<AppState>>,
1414
req: actix_web::HttpRequest,
1515
) -> AtomicServerResult<HttpResponse> {
16-
let context = data.lock().unwrap();
16+
let appstate = data.lock().unwrap();
1717

1818
let headers = req.headers();
1919
let mut content_type = get_accept(headers);
20-
let base_url = &context.config.local_base_url;
20+
let base_url = &appstate.config.local_base_url;
2121
// Get the subject from the path, or return the home URL
2222
let subject = if let Some(subj_end) = path {
2323
let mut subj_end_string = subj_end.as_str();
@@ -40,13 +40,9 @@ pub async fn get_resource(
4040
String::from(base_url)
4141
};
4242

43-
let store = &context.store;
44-
45-
// Authentication check. If the user has no headers, continue with the Public Agent.
46-
let auth_header_values = get_auth_headers(headers, subject.clone())?;
47-
let for_agent =
48-
atomic_lib::authentication::get_agent_from_headers_and_check(auth_header_values, store)?;
43+
let store = &appstate.store;
4944

45+
let for_agent = get_client_agent(headers, &appstate, subject.clone())?;
5046
let mut builder = HttpResponse::Ok();
5147
log::info!("get_resource: {} as {}", subject, content_type.to_mime());
5248
builder.header("Content-Type", content_type.to_mime());
@@ -56,7 +52,7 @@ pub async fn get_resource(
5652
"Cache-Control",
5753
"no-store, no-cache, must-revalidate, private",
5854
);
59-
let resource = store.get_resource_extended(&subject, false, Some(for_agent))?;
55+
let resource = store.get_resource_extended(&subject, false, for_agent)?;
6056
match content_type {
6157
ContentType::Json => {
6258
let body = resource.to_json(store)?;

server/src/handlers/search.rs

+20-15
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ pub async fn search_query(
2323
params: web::Query<SearchQuery>,
2424
req: actix_web::HttpRequest,
2525
) -> AtomicServerResult<HttpResponse> {
26-
let context = data
26+
let appstate = data
2727
.lock()
2828
.expect("Failed to lock mutexguard in search_query");
2929

30-
let store = &context.store;
31-
let searcher = context.search_state.reader.searcher();
32-
let fields = crate::search::get_schema_fields(&context.search_state)?;
30+
let store = &appstate.store;
31+
let searcher = appstate.search_state.reader.searcher();
32+
let fields = crate::search::get_schema_fields(&appstate.search_state)?;
3333
let default_limit = 30;
3434
let limit = if let Some(l) = params.limit {
3535
if l > 0 {
@@ -46,7 +46,6 @@ pub async fn search_query(
4646
// Fuzzy searching is not possible when filtering by property
4747
should_fuzzy = false;
4848
}
49-
let return_subjects = !params.include.unwrap_or(false);
5049

5150
let mut subjects: Vec<String> = Vec::new();
5251
let mut atoms: Vec<StringAtom> = Vec::new();
@@ -67,7 +66,7 @@ pub async fn search_query(
6766
} else {
6867
// construct the query
6968
let query_parser = QueryParser::for_index(
70-
&context.search_state.index,
69+
&appstate.search_state.index,
7170
vec![
7271
fields.subject,
7372
// I don't think we need to search in the property
@@ -133,7 +132,7 @@ pub async fn search_query(
133132
.ok_or("Add a query param")?
134133
.to_string()
135134
);
136-
let mut results_resource = Resource::new(subject);
135+
let mut results_resource = Resource::new(subject.clone());
137136
results_resource.set_propval(urls::IS_A.into(), vec![urls::ENDPOINT].into(), store)?;
138137
results_resource.set_propval(urls::DESCRIPTION.into(), atomic_lib::Value::Markdown("Full text-search endpoint. You can use the keyword `AND` and `OR`, or use `\"` for advanced searches. ".into()), store)?;
139138
results_resource.set_propval(
@@ -147,22 +146,28 @@ pub async fn search_query(
147146
store,
148147
)?;
149148

150-
if return_subjects {
149+
if appstate.config.opts.rdf_search {
150+
// Always return all subjects, don't do authentication
151151
results_resource.set_propval(urls::ENDPOINT_RESULTS.into(), subjects.into(), store)?;
152152
} else {
153+
// Default case: return full resources, do authentication
153154
let mut resources: Vec<Resource> = Vec::new();
155+
156+
let for_agent = crate::helpers::get_client_agent(req.headers(), &appstate, subject)?;
154157
for s in subjects {
155-
// TODO: use authentication, allow for non-public search
156-
let r = store.get_resource_extended(&s, true, Some(atomic_lib::authentication::PUBLIC_AGENT.into()))
157-
.map_err(|e| format!("Failed to construct search results, because one of the Subjects cannot be returned. Try again with the `&subjects=true` query parameter. Error: {}", e))?;
158-
resources.push(r);
158+
log::info!("Subject in search result: {}", s);
159+
match store.get_resource_extended(&s, true, for_agent.clone()) {
160+
Ok(r) => resources.push(r),
161+
Err(_e) => {
162+
log::info!("Skipping result: {} : {}", s, _e);
163+
continue;
164+
}
165+
}
159166
}
160167
results_resource.set_propval(urls::ENDPOINT_RESULTS.into(), resources.into(), store)?;
161168
}
162-
163-
// let json_ad = atomic_lib::serialize::resources_to_json_ad(&resources)?;
164169
let mut builder = HttpResponse::Ok();
165-
// log::info!("Search q: {} hits: {}", &query.q, resources.len());
170+
// TODO: support other serialization options
166171
Ok(builder.body(results_resource.to_json_ad()?))
167172
}
168173

server/src/helpers.rs

+20-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use actix_web::http::HeaderMap;
44
use atomic_lib::authentication::AuthValues;
55

6-
use crate::errors::AtomicServerResult;
6+
use crate::{appstate::AppState, errors::AtomicServerResult};
77

88
// Returns None if the string is empty.
99
// Useful for parsing form inputs.
@@ -54,3 +54,22 @@ pub fn get_auth_headers(
5454
_missing => Err("Missing authentication headers. You need `x-atomic-public-key`, `x-atomic-signature`, `x-atomic-agent` and `x-atomic-timestamp` for authentication checks.".into()),
5555
}
5656
}
57+
58+
/// Checks for authentication headers and returns the agent's subject if everything is well.
59+
/// Skips these checks in public_mode.
60+
pub fn get_client_agent(
61+
headers: &HeaderMap,
62+
appstate: &AppState,
63+
requested_subject: String,
64+
) -> AtomicServerResult<Option<String>> {
65+
if appstate.config.opts.public_mode {
66+
return Ok(None);
67+
}
68+
// Authentication check. If the user has no headers, continue with the Public Agent.
69+
let auth_header_values = get_auth_headers(headers, requested_subject)?;
70+
let for_agent = atomic_lib::authentication::get_agent_from_headers_and_check(
71+
auth_header_values,
72+
&appstate.store,
73+
)?;
74+
Ok(Some(for_agent))
75+
}

0 commit comments

Comments
 (0)