Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autocomplete cli clap #214

Draft
wants to merge 16 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
List of changes for this repo, including `atomic-cli`, `atomic-server` and `atomic-lib`.
By far most changes relate to `atomic-server`, so if not specified, assume the changes are relevant only for the server.

## v0.29.0

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

## v0.28.2

- Full-text search endpoint, powered by Tantify #40
Expand Down
16 changes: 13 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ license = "MIT"
name = "atomic-cli"
readme = "README.md"
repository = "https://github.com/joepio/atomic-data-rust"
version = "0.28.1"
version = "0.29.0"

[dependencies]
atomic_lib = {version = "0.28.1", path = "../lib", features = ["config", "rdf"]}
atomic_lib = {version = "0.29.0", path = "../lib", features = ["config", "rdf"]}
clap = "2.33.3"
colored = "2.0.0"
dirs = "3.0.1"
Expand Down
4 changes: 3 additions & 1 deletion cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ impl Context<'_> {
}

/// Reads config files for writing data, or promps the user if they don't yet exist
fn set_agent_config() -> AtomicResult<Config> {
fn set_agent_config() -> CLIResult<Config> {
let agent_config_path = atomic_lib::config::default_config_file_path()?;
match atomic_lib::config::read_config(&agent_config_path) {
Ok(found) => Ok(found),
Expand Down Expand Up @@ -328,3 +328,5 @@ fn validate(context: &mut Context) {
let reportstring = context.store.validate().to_string();
println!("{}", reportstring);
}

pub type CLIResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
6 changes: 3 additions & 3 deletions cli/src/new.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! Creating a new resource. Provides prompting logic
use crate::Context;
use crate::{CLIResult, Context};
use atomic_lib::mapping;
use atomic_lib::{
datatype::DataType,
Expand Down Expand Up @@ -44,7 +44,7 @@ fn prompt_instance<'a>(
context: &'a Context,
class: &Class,
preffered_shortname: Option<String>,
) -> AtomicResult<(Resource, Option<String>)> {
) -> CLIResult<(Resource, Option<String>)> {
// Not sure about the best way t
// The Path is the thing at the end of the URL, from the domain
// Here I set some (kind of) random numbers.
Expand Down Expand Up @@ -123,7 +123,7 @@ fn prompt_field(
property: &Property,
optional: bool,
context: &Context,
) -> AtomicResult<Option<String>> {
) -> CLIResult<Option<String>> {
let mut input: Option<String> = None;
let msg_appendix: &str = if optional {
" (optional)"
Expand Down
4 changes: 2 additions & 2 deletions cli/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ pub fn get_path(context: &mut Context) -> AtomicResult<()> {

// Returns a URL or Value
let store = &mut context.store;
let path = store.get_path(&path_string, Some(&context.mapping.lock().unwrap()))?;
let path = store.get_path(&path_string, Some(&context.mapping.lock().unwrap()), None)?;
let out = match path {
storelike::PathReturn::Subject(subject) => {
let resource = store.get_resource_extended(&subject, false)?;
let resource = store.get_resource_extended(&subject, false, None)?;
print_resource(context, &resource, subcommand_matches)?;
return Ok(());
}
Expand Down
2 changes: 1 addition & 1 deletion lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ license = "MIT"
name = "atomic_lib"
readme = "README.md"
repository = "https://github.com/joepio/atomic-data-rust"
version = "0.28.2"
version = "0.29.0"

[dependencies]
base64 = "0.13.0"
Expand Down
79 changes: 79 additions & 0 deletions lib/src/authentication.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//! Check signatures in authentication headers, find the correct agent. Authorization is done in Hierarchies

use crate::{commit::check_timestamp, errors::AtomicResult, Storelike};

/// Set of values extracted from the request.
/// Most are coming from headers.
pub struct AuthValues {
// x-atomic-public-key
pub public_key: String,
// x-atomic-timestamp
pub timestamp: i64,
// x-atomic-signature
// Base64 encoded public key from `subject_url timestamp`
pub signature: String,
pub requested_subject: String,
pub agent_subject: String,
}

/// Checks if the signature is valid for this timestamp.
/// Does not check if the agent has rights to access the subject.
pub fn check_auth_signature(subject: &str, auth_header: &AuthValues) -> AtomicResult<()> {
let agent_pubkey = base64::decode(&auth_header.public_key)?;
let message = format!("{} {}", subject, &auth_header.timestamp);
let peer_public_key =
ring::signature::UnparsedPublicKey::new(&ring::signature::ED25519, agent_pubkey);
let signature_bytes = base64::decode(&auth_header.signature)?;
peer_public_key
.verify(message.as_bytes(), &signature_bytes)
.map_err(|_e| {
format!(
"Incorrect signature for auth headers. This could be due to an error during signing or serialization of the commit. Compare this to the serialized message in the client: {}",
message,
)
})?;
Ok(())
}

/// Get the Agent's subject from headers
/// Checks if the auth headers are correct, whether signature matches the public key, whether the timestamp is valid.
/// by default, returns the public agent
pub fn get_agent_from_headers_and_check(
auth_header_values: Option<AuthValues>,
store: &impl Storelike,
) -> AtomicResult<String> {
let mut for_agent = crate::urls::PUBLIC_AGENT.to_string();
if let Some(auth_vals) = auth_header_values {
// If there are auth headers, check 'em, make sure they are valid.
check_auth_signature(&auth_vals.requested_subject, &auth_vals)?;
// check if the timestamp is valid
check_timestamp(auth_vals.timestamp)?;
// check if the public key belongs to the agent
let agent = store.get_resource(&auth_vals.agent_subject)?;
let found_public_key = agent.get(crate::urls::PUBLIC_KEY)?;
if found_public_key.to_string() != auth_vals.public_key {
return Err(
"The public key in the auth headers does not match the public key in the agent"
.to_string()
.into(),
);
} else {
for_agent = auth_vals.agent_subject;
}
};
Ok(for_agent)
}

// fn get_agent_from_value_index() {
// let map = store.get_prop_subject_map(&auth_vals.public_key)?;
// let agents = map.get(crate::urls::PUBLIC_KEY).ok_or(format!(
// "No agents for this public key: {}",
// &auth_vals.public_key
// ))?;
// // TODO: This is unreliable, as this will break if multiple atoms with the same public key exist.
// if agents.len() > 1 {
// return Err("Multiple agents for this public key".into());
// } else if let Some(found) = agents.iter().next() {
// for_agent = Some(found.to_string());
// }
// }
62 changes: 51 additions & 11 deletions lib/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,70 @@
//! Functions for interacting with an Atomic Server
use url::Url;

use crate::{errors::AtomicResult, parse::parse_json_ad_resource, Resource, Storelike};
use crate::{
agents::Agent, commit::sign_message, errors::AtomicResult, parse::parse_json_ad_resource,
Resource, Storelike,
};

/// Fetches a resource, makes sure its subject matches.
/// Checks the datatypes for the Values.
/// Ignores all atoms where the subject is different.
/// WARNING: Calls store methods, and is called by store methods, might get stuck in a loop!
pub fn fetch_resource(subject: &str, store: &impl Storelike) -> AtomicResult<Resource> {
let body = fetch_body(subject, crate::parse::JSON_AD_MIME)?;
pub fn fetch_resource(
subject: &str,
store: &impl Storelike,
for_agent: Option<Agent>,
) -> AtomicResult<Resource> {
let body = fetch_body(subject, crate::parse::JSON_AD_MIME, for_agent)?;
let resource = parse_json_ad_resource(&body, store)
.map_err(|e| format!("Error parsing body of {}. {}", subject, e))?;
Ok(resource)
}

/// Fetches a URL, returns its body
pub fn fetch_body(url: &str, content_type: &str) -> AtomicResult<String> {
/// Returns the various x-atomic authentication headers, includign agent signature
pub fn get_authentication_headers(url: &str, agent: &Agent) -> AtomicResult<Vec<(String, String)>> {
let mut headers = Vec::new();
let now = crate::datetime_helpers::now().to_string();
let message = format!("{} {}", url, now);
let signature = sign_message(
&message,
agent
.private_key
.as_ref()
.ok_or("No private key in agent")?,
&agent.public_key,
)?;
headers.push(("x-atomic-public-key".into(), agent.public_key.to_string()));
headers.push(("x-atomic-signature".into(), signature));
headers.push(("x-atomic-timestamp".into(), now));
headers.push(("x-atomic-agent".into(), agent.subject.to_string()));
Ok(headers)
}

/// Fetches a URL, returns its body.
/// Uses the store's Agent agent (if set) to sign the request.
pub fn fetch_body(url: &str, content_type: &str, for_agent: Option<Agent>) -> AtomicResult<String> {
if !url.starts_with("http") {
return Err(format!("Could not fetch url '{}', must start with http.", url).into());
}
if let Some(agent) = for_agent {
get_authentication_headers(url, &agent)?;
}
let resp = ureq::get(url)
.set("Accept", content_type)
.timeout_read(2000)
.call();
if resp.status() != 200 {
return Err(format!("Could not fetch url '{}'. Status: {}", url, resp.status()).into());
};
let status = resp.status();
let body = resp
.into_string()
.map_err(|e| format!("Could not parse response {}: {}", url, e))?;
.map_err(|e| format!("Could not parse HTTP response for {}: {}", url, e))?;
if status != 200 {
return Err(format!(
"Could not fetch url '{}'. Status: {}. Body: {}",
url, status, body
)
.into());
};
Ok(body)
}

Expand All @@ -50,7 +86,11 @@ pub fn fetch_tpf(
if let Some(val) = q_value {
url.query_pairs_mut().append_pair("value", val);
}
let body = fetch_body(url.as_str(), "application/ad+json")?;
let body = fetch_body(
url.as_str(),
"application/ad+json",
store.get_default_agent().ok(),
)?;
crate::parse::parse_json_ad_array(&body, store, false)
}

Expand Down Expand Up @@ -97,7 +137,7 @@ mod test {
#[ignore]
fn fetch_resource_basic() {
let store = crate::Store::init().unwrap();
let resource = fetch_resource(crate::urls::SHORTNAME, &store).unwrap();
let resource = fetch_resource(crate::urls::SHORTNAME, &store, None).unwrap();
let shortname = resource.get(crate::urls::SHORTNAME).unwrap();
assert!(shortname.to_string() == "shortname");
}
Expand Down
Loading