Add a lot of client support, clean up warnings, and clean up output for clients

This commit is contained in:
William Brown 2019-09-09 20:32:49 +10:00
parent d436291eff
commit 4ba9508a31
18 changed files with 596 additions and 375 deletions

View file

@ -9,10 +9,10 @@ log = "0.4"
env_logger = "0.6" env_logger = "0.6"
reqwest = "0.9" reqwest = "0.9"
rsidm_proto = { path = "../rsidm_proto" } rsidm_proto = { path = "../rsidm_proto" }
serde_json = "1.0"
[dev-dependencies] [dev-dependencies]
tokio = "0.1" tokio = "0.1"
actix = "0.7" actix = "0.7"
rsidm = { path = "../rsidmd" } rsidm = { path = "../rsidmd" }
futures = "0.1" futures = "0.1"
serde_json = "1.0"

View file

@ -1,8 +1,27 @@
// #![deny(warnings)] #![deny(warnings)]
#![warn(unused_extern_crates)] #![warn(unused_extern_crates)]
#[macro_use]
extern crate log;
use serde_json;
use reqwest; use reqwest;
use rsidm_proto::v1::{
AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, Entry,
OperationResponse, UserAuthToken, WhoamiResponse,
};
#[derive(Debug)]
pub enum ClientError {
Unauthorized,
Http(reqwest::StatusCode),
Transport(reqwest::Error),
AuthenticationFailed,
}
#[derive(Debug)]
pub struct RsidmClient { pub struct RsidmClient {
client: reqwest::Client, client: reqwest::Client,
addr: String, addr: String,
@ -20,10 +39,156 @@ impl RsidmClient {
} }
} }
fn auth_step_init(&self, ident: &str, appid: Option<&str>) -> Result<AuthState, ClientError> {
// TODO: Way to avoid formatting so much?
let auth_dest = format!("{}/v1/auth", self.addr);
let auth_init = AuthRequest {
step: AuthStep::Init(ident.to_string(), appid.map(|s| s.to_string())),
};
// Handle this!
let mut response = self
.client
.post(auth_dest.as_str())
.body(serde_json::to_string(&auth_init).expect("Generated invalid initstep?!"))
.send()
.map_err(|e| ClientError::Transport(e))?;
match response.status() {
reqwest::StatusCode::OK => {}
unexpect => return Err(ClientError::Http(unexpect)),
}
// Check that we got the next step
let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap();
Ok(r.state)
}
// auth // auth
pub fn auth_anonymous(&self) -> Result<UserAuthToken, ClientError> {
let _state = match self.auth_step_init("anonymous", None) {
Ok(s) => s,
Err(e) => return Err(e),
};
// TODO: Avoid creating this so much?
let auth_dest = format!("{}/v1/auth", self.addr);
// Check state for auth continue contains anonymous.
let auth_anon = AuthRequest {
step: AuthStep::Creds(vec![AuthCredential::Anonymous]),
};
let mut response = self
.client
.post(auth_dest.as_str())
.body(serde_json::to_string(&auth_anon).unwrap())
.send()
.map_err(|e| ClientError::Transport(e))?;
match response.status() {
reqwest::StatusCode::OK => {}
unexpect => return Err(ClientError::Http(unexpect)),
}
// Check that we got the next step
let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap();
match r.state {
AuthState::Success(uat) => {
debug!("==> Authed as uat; {:?}", uat);
Ok(uat)
}
_ => Err(ClientError::AuthenticationFailed),
}
}
pub fn auth_simple_password(
&self,
ident: &str,
password: &str,
) -> Result<UserAuthToken, ClientError> {
// TODO: Way to avoid formatting so much?
let auth_dest = format!("{}/v1/auth", self.addr);
let _state = match self.auth_step_init(ident, None) {
Ok(s) => s,
Err(e) => return Err(e),
};
// Send the credentials required now
let auth_req = AuthRequest {
step: AuthStep::Creds(vec![AuthCredential::Password(password.to_string())]),
};
let mut response = self
.client
.post(auth_dest.as_str())
.body(serde_json::to_string(&auth_req).unwrap())
.send()
.map_err(|e| ClientError::Transport(e))?;
match response.status() {
reqwest::StatusCode::OK => {}
unexpect => return Err(ClientError::Http(unexpect)),
}
let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap();
match r.state {
AuthState::Success(uat) => {
debug!("==> Authed as uat; {:?}", uat);
Ok(uat)
}
_ => Err(ClientError::AuthenticationFailed),
}
}
// whoami // whoami
pub fn whoami(&self) -> Result<Option<(Entry, UserAuthToken)>, ClientError> {
let whoami_dest = format!("{}/v1/whoami", self.addr);
let mut response = self.client.get(whoami_dest.as_str()).send().unwrap();
// https://docs.rs/reqwest/0.9.15/reqwest/struct.Response.html
match response.status() {
// Continue to process.
reqwest::StatusCode::OK => {}
reqwest::StatusCode::UNAUTHORIZED => return Ok(None),
unexpect => return Err(ClientError::Http(unexpect)),
}
let r: WhoamiResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap();
Ok(Some((r.youare, r.uat)))
}
// search // search
// create // create
pub fn create(&self, entries: Vec<Entry>) -> Result<(), ClientError> {
let c = CreateRequest { entries: entries };
// TODO: Avoid formatting this so much!
let dest = format!("{}/v1/create", self.addr);
let mut response = self
.client
.post(dest.as_str())
.body(serde_json::to_string(&c).unwrap())
.send()
.map_err(|e| ClientError::Transport(e))?;
match response.status() {
reqwest::StatusCode::OK => {}
unexpect => return Err(ClientError::Http(unexpect)),
}
// TODO: What about errors
let _r: OperationResponse =
serde_json::from_str(response.text().unwrap().as_str()).unwrap();
Ok(())
}
// modify // modify
// //
} }

View file

@ -7,16 +7,15 @@ extern crate actix;
use actix::prelude::*; use actix::prelude::*;
extern crate rsidm; extern crate rsidm;
extern crate rsidm_client;
extern crate rsidm_proto; extern crate rsidm_proto;
extern crate serde_json; extern crate serde_json;
use rsidm_client::RsidmClient;
use rsidm::config::{Configuration, IntegrationTestConfig}; use rsidm::config::{Configuration, IntegrationTestConfig};
use rsidm::constants::UUID_ADMIN;
use rsidm::core::create_server_core; use rsidm::core::create_server_core;
use rsidm_proto::v1::{ use rsidm_proto::v1::Entry;
AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, Entry,
OperationResponse,
};
extern crate reqwest; extern crate reqwest;
@ -36,7 +35,8 @@ static ADMIN_TEST_PASSWORD: &'static str = "integration test admin password";
// Test external behaviorus of the service. // Test external behaviorus of the service.
fn run_test(test_fn: fn(reqwest::Client, &str) -> ()) { fn run_test(test_fn: fn(RsidmClient) -> ()) {
// ::std::env::set_var("RUST_LOG", "actix_web=debug,rsidm=debug");
let _ = env_logger::builder().is_test(true).try_init(); let _ = env_logger::builder().is_test(true).try_init();
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let port = PORT_ALLOC.fetch_add(1, Ordering::SeqCst); let port = PORT_ALLOC.fetch_add(1, Ordering::SeqCst);
@ -70,13 +70,10 @@ fn run_test(test_fn: fn(reqwest::Client, &str) -> ()) {
// later we could accept fixture as it's own future for re-use // later we could accept fixture as it's own future for re-use
// Setup the client, and the address we selected. // Setup the client, and the address we selected.
let client = reqwest::Client::builder()
.cookie_store(true)
.build()
.expect("Unexpected reqwest builder failure!");
let addr = format!("http://127.0.0.1:{}", port); let addr = format!("http://127.0.0.1:{}", port);
let rsclient = RsidmClient::new(addr.as_str());
test_fn(client, addr.as_str()); test_fn(rsclient);
// We DO NOT need teardown, as sqlite is in mem // We DO NOT need teardown, as sqlite is in mem
// let the tables hit the floor // let the tables hit the floor
@ -84,8 +81,8 @@ fn run_test(test_fn: fn(reqwest::Client, &str) -> ()) {
} }
#[test] #[test]
fn test_server_proto() { fn test_server_create() {
run_test(|client: reqwest::Client, addr: &str| { run_test(|rsclient: RsidmClient| {
let e: Entry = serde_json::from_str( let e: Entry = serde_json::from_str(
r#"{ r#"{
"attrs": { "attrs": {
@ -98,168 +95,54 @@ fn test_server_proto() {
) )
.unwrap(); .unwrap();
let c = CreateRequest { // Not logged in - should fail!
entries: vec![e], let res = rsclient.create(vec![e.clone()]);
user_uuid: UUID_ADMIN.to_string(), assert!(res.is_err());
};
let dest = format!("{}/v1/create", addr); let a_res = rsclient.auth_simple_password("admin", ADMIN_TEST_PASSWORD);
assert!(a_res.is_ok());
let mut response = client let res = rsclient.create(vec![e]);
.post(dest.as_str()) assert!(res.is_ok());
.body(serde_json::to_string(&c).unwrap())
.send()
.unwrap();
println!("{:?}", response);
let r: OperationResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap();
println!("{:?}", r);
// deserialise the response here
// check it's valid.
()
}); });
} }
#[test] #[test]
fn test_server_whoami_anonymous() { fn test_server_whoami_anonymous() {
run_test(|client: reqwest::Client, addr: &str| { run_test(|rsclient: RsidmClient| {
// First show we are un-authenticated. // First show we are un-authenticated.
let whoami_dest = format!("{}/v1/whoami", addr); let pre_res = rsclient.whoami();
let auth_dest = format!("{}/v1/auth", addr); // This means it was okay whoami, but no uat attached.
assert!(pre_res.unwrap().is_none());
let response = client.get(whoami_dest.as_str()).send().unwrap();
// https://docs.rs/reqwest/0.9.15/reqwest/struct.Response.html
println!("{:?}", response);
assert!(response.status() == reqwest::StatusCode::UNAUTHORIZED);
// Now login as anonymous // Now login as anonymous
let res = rsclient.auth_anonymous();
// Setup the auth initialisation assert!(res.is_ok());
let auth_init = AuthRequest {
step: AuthStep::Init("anonymous".to_string(), None),
};
let mut response = client
.post(auth_dest.as_str())
.body(serde_json::to_string(&auth_init).unwrap())
.send()
.unwrap();
assert!(response.status() == reqwest::StatusCode::OK);
// Check that we got the next step
let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap();
println!("==> AUTHRESPONSE ==> {:?}", r);
assert!(match &r.state {
AuthState::Continue(_all_list) => {
// Check anonymous is present? It will fail on next step if not ...
true
}
_ => false,
});
// Send the credentials required now
let auth_anon = AuthRequest {
step: AuthStep::Creds(vec![AuthCredential::Anonymous]),
};
let mut response = client
.post(auth_dest.as_str())
.body(serde_json::to_string(&auth_anon).unwrap())
.send()
.unwrap();
debug!("{}", response.status());
assert!(response.status() == reqwest::StatusCode::OK);
// Check that we got the next step
let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap();
println!("==> AUTHRESPONSE ==> {:?}", r);
assert!(match &r.state {
AuthState::Success(uat) => {
println!("==> Authed as uat; {:?}", uat);
true
}
_ => false,
});
// Now do a whoami. // Now do a whoami.
let mut response = client.get(whoami_dest.as_str()).send().unwrap(); let post_res = rsclient.whoami().unwrap();
println!("WHOAMI -> {}", response.text().unwrap().as_str()); assert!(post_res.is_some());
println!("WHOAMI STATUS -> {}", response.status()); // TODO: Now unwrap and ensure anony
assert!(response.status() == reqwest::StatusCode::OK); println!("{:?}", post_res);
// Check the json now ... response.json()
}); });
} }
#[test] #[test]
fn test_server_whoami_admin_simple_password() { fn test_server_whoami_admin_simple_password() {
run_test(|client: reqwest::Client, addr: &str| { run_test(|rsclient: RsidmClient| {
// First show we are un-authenticated. // First show we are un-authenticated.
let whoami_dest = format!("{}/v1/whoami", addr); let pre_res = rsclient.whoami();
let auth_dest = format!("{}/v1/auth", addr); // This means it was okay whoami, but no uat attached.
// Now login as admin assert!(pre_res.unwrap().is_none());
// Setup the auth initialisation let res = rsclient.auth_simple_password("admin", ADMIN_TEST_PASSWORD);
let auth_init = AuthRequest { assert!(res.is_ok());
step: AuthStep::Init("admin".to_string(), None),
};
let mut response = client
.post(auth_dest.as_str())
.body(serde_json::to_string(&auth_init).unwrap())
.send()
.unwrap();
assert!(response.status() == reqwest::StatusCode::OK);
// Check that we got the next step
let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap();
println!("==> AUTHRESPONSE ==> {:?}", r);
assert!(match &r.state {
AuthState::Continue(_all_list) => {
// Check anonymous is present? It will fail on next step if not ...
true
}
_ => false,
});
// Send the credentials required now
let auth_admin = AuthRequest {
step: AuthStep::Creds(vec![AuthCredential::Password(
ADMIN_TEST_PASSWORD.to_string(),
)]),
};
let mut response = client
.post(auth_dest.as_str())
.body(serde_json::to_string(&auth_admin).unwrap())
.send()
.unwrap();
debug!("{}", response.status());
assert!(response.status() == reqwest::StatusCode::OK);
// Check that we got the next step
let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap();
println!("==> AUTHRESPONSE ==> {:?}", r);
assert!(match &r.state {
AuthState::Success(uat) => {
println!("==> Authed as uat; {:?}", uat);
true
}
_ => false,
});
// Now do a whoami. // Now do a whoami.
let mut response = client.get(whoami_dest.as_str()).send().unwrap(); let post_res = rsclient.whoami().unwrap();
println!("WHOAMI -> {}", response.text().unwrap().as_str()); assert!(post_res.is_some());
println!("WHOAMI STATUS -> {}", response.status()); // TODO: Now unwrap and ensure anony
assert!(response.status() == reqwest::StatusCode::OK); debug!("{:?}", post_res);
// Check the json now ... response.json()
}); });
} }

View file

@ -4,9 +4,6 @@ version = "0.1.0"
authors = ["William Brown <william@blackhats.net.au>"] authors = ["William Brown <william@blackhats.net.au>"]
edition = "2018" edition = "2018"
[features]
rsidm_internal = ["actix"]
[dependencies] [dependencies]
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"

View file

@ -1,9 +1,7 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::fmt;
use uuid::Uuid; use uuid::Uuid;
#[cfg(feature = "rsidm_internal")]
use actix::prelude::*;
// These proto implementations are here because they have public definitions // These proto implementations are here because they have public definitions
/* ===== errors ===== */ /* ===== errors ===== */
@ -120,6 +118,16 @@ pub struct UserAuthToken {
// Should we allow supplemental ava's to be added on request? // Should we allow supplemental ava's to be added on request?
} }
impl fmt::Display for UserAuthToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "name: {}", self.name)?;
writeln!(f, "display: {}", self.displayname)?;
writeln!(f, "uuid: {}", self.uuid)?;
writeln!(f, "groups: {:?}", self.groups)?;
writeln!(f, "claims: {:?}", self.claims)
}
}
// UAT will need a downcast to Entry, which adds in the claims to the entry // UAT will need a downcast to Entry, which adds in the claims to the entry
// for the purpose of filtering. // for the purpose of filtering.
@ -178,22 +186,13 @@ impl OperationResponse {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct SearchRequest { pub struct SearchRequest {
pub filter: Filter, pub filter: Filter,
pub user_uuid: String,
} }
impl SearchRequest { impl SearchRequest {
pub fn new(filter: Filter, user_uuid: &str) -> Self { pub fn new(filter: Filter) -> Self {
SearchRequest { SearchRequest { filter: filter }
filter: filter,
user_uuid: user_uuid.to_string(),
} }
} }
}
#[cfg(feature = "rsidm_internal")]
impl Message for SearchRequest {
type Result = Result<SearchResponse, OperationError>;
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct SearchResponse { pub struct SearchResponse {
@ -209,66 +208,41 @@ impl SearchResponse {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct CreateRequest { pub struct CreateRequest {
pub entries: Vec<Entry>, pub entries: Vec<Entry>,
pub user_uuid: String,
} }
impl CreateRequest { impl CreateRequest {
pub fn new(entries: Vec<Entry>, user_uuid: &str) -> Self { pub fn new(entries: Vec<Entry>) -> Self {
CreateRequest { CreateRequest { entries: entries }
entries: entries,
user_uuid: user_uuid.to_string(),
} }
} }
}
#[cfg(feature = "rsidm_internal")]
impl Message for CreateRequest {
type Result = Result<OperationResponse, OperationError>;
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct DeleteRequest { pub struct DeleteRequest {
pub filter: Filter, pub filter: Filter,
pub user_uuid: String,
} }
impl DeleteRequest { impl DeleteRequest {
pub fn new(filter: Filter, user_uuid: &str) -> Self { pub fn new(filter: Filter) -> Self {
DeleteRequest { DeleteRequest { filter: filter }
filter: filter,
user_uuid: user_uuid.to_string(),
} }
} }
}
#[cfg(feature = "rsidm_internal")]
impl Message for DeleteRequest {
type Result = Result<OperationResponse, OperationError>;
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct ModifyRequest { pub struct ModifyRequest {
// Probably needs a modlist? // Probably needs a modlist?
pub filter: Filter, pub filter: Filter,
pub modlist: ModifyList, pub modlist: ModifyList,
pub user_uuid: String,
} }
impl ModifyRequest { impl ModifyRequest {
pub fn new(filter: Filter, modlist: ModifyList, user_uuid: &str) -> Self { pub fn new(filter: Filter, modlist: ModifyList) -> Self {
ModifyRequest { ModifyRequest {
filter: filter, filter: filter,
modlist: modlist, modlist: modlist,
user_uuid: user_uuid.to_string(),
} }
} }
} }
#[cfg(feature = "rsidm_internal")]
impl Message for ModifyRequest {
type Result = Result<OperationResponse, OperationError>;
}
// Login is a multi-step process potentially. First the client says who they // Login is a multi-step process potentially. First the client says who they
// want to request // want to request
// //
@ -340,15 +314,11 @@ pub struct AuthResponse {
pub struct SearchRecycledRequest { pub struct SearchRecycledRequest {
pub filter: Filter, pub filter: Filter,
pub user_uuid: String,
} }
impl SearchRecycledRequest { impl SearchRecycledRequest {
pub fn new(filter: Filter, user_uuid: &str) -> Self { pub fn new(filter: Filter) -> Self {
SearchRecycledRequest { SearchRecycledRequest { filter: filter }
filter: filter,
user_uuid: user_uuid.to_string(),
}
} }
} }
@ -356,15 +326,11 @@ impl SearchRecycledRequest {
pub struct ReviveRecycledRequest { pub struct ReviveRecycledRequest {
pub filter: Filter, pub filter: Filter,
pub user_uuid: String,
} }
impl ReviveRecycledRequest { impl ReviveRecycledRequest {
pub fn new(filter: Filter, user_uuid: &str) -> Self { pub fn new(filter: Filter) -> Self {
ReviveRecycledRequest { ReviveRecycledRequest { filter: filter }
filter: filter,
user_uuid: user_uuid.to_string(),
}
} }
} }
@ -382,11 +348,15 @@ impl WhoamiRequest {
pub struct WhoamiResponse { pub struct WhoamiResponse {
// Should we just embed the entry? Or destructure it? // Should we just embed the entry? Or destructure it?
pub youare: Entry, pub youare: Entry,
pub uat: UserAuthToken,
} }
impl WhoamiResponse { impl WhoamiResponse {
pub fn new(e: Entry) -> Self { pub fn new(e: Entry, uat: UserAuthToken) -> Self {
WhoamiResponse { youare: e } WhoamiResponse {
youare: e,
uat: uat,
}
} }
} }

View file

@ -10,4 +10,6 @@ path = "src/main.rs"
[dependencies] [dependencies]
rsidm_client = { path = "../rsidm_client" } rsidm_client = { path = "../rsidm_client" }
rpassword = "0.4"
structopt = { version = "0.2", default-features = false }

View file

@ -1,3 +1,54 @@
fn main() { extern crate structopt;
println!("Hello kanidm"); use rsidm_client::RsidmClient;
use structopt::StructOpt;
#[derive(Debug, StructOpt)]
struct CommonOpt {
#[structopt(short = "H", long = "url")]
addr: String,
#[structopt(short = "D", long = "name")]
username: String,
}
impl CommonOpt {
fn to_client(&self) -> RsidmClient {
RsidmClient::new(self.addr.as_str())
}
}
#[derive(Debug, StructOpt)]
enum ClientOpt {
#[structopt(name = "whoami")]
Whoami(CommonOpt),
}
fn main() {
let opt = ClientOpt::from_args();
match opt {
ClientOpt::Whoami(copt) => {
let client = copt.to_client();
let r = if copt.username == "anonymous" {
client.auth_anonymous()
} else {
let password = rpassword::prompt_password_stderr("Enter password: ").unwrap();
client.auth_simple_password(copt.username.as_str(), password.as_str())
};
if r.is_err() {
println!("Error during authentication phase: {:?}", r);
return;
}
match client.whoami() {
Ok(o_ent) => match o_ent {
Some((_ent, uat)) => {
println!("{}", uat);
}
None => println!("Unauthenticated"),
},
Err(e) => println!("Error: {:?}", e),
}
}
}
} }

View file

@ -1,28 +0,0 @@
extern crate reqwest;
extern crate rsidm;
use rsidm::proto::v1::{WhoamiRequest, WhoamiResponse};
fn main() {
println!("Hello whoami");
// Given the current ~/.rsidm/cookie (or none)
// we should check who we are plus show the auth token that the server
// would generate for us.
let whoami_req = WhoamiRequest {};
// FIXME TODO: Make this url configurable!!!
let client = reqwest::Client::new();
let mut response = client
.get("http://127.0.0.1:8080/v1/whoami")
.send()
.unwrap();
println!("{:?}", response);
// Parse it if desire.
// let r: Response = serde_json::from_str(response.text().unwrap().as_str()).unwrap();
// println!("{:?}", r);
}

View file

@ -17,7 +17,7 @@ path = "src/server/main.rs"
[dependencies] [dependencies]
rsidm_proto = { path = "../rsidm_proto", features = ["rsidm_internal"] } rsidm_proto = { path = "../rsidm_proto" }
actix = "0.7" actix = "0.7"
actix-web = "0.7" actix-web = "0.7"

View file

@ -858,7 +858,7 @@ pub trait AccessControlsTransaction {
}) })
.collect(); .collect();
audit_log!(audit, "Related acs -> {:?}", related_acp); audit_log!(audit, "Related acc -> {:?}", related_acp);
// For each entry // For each entry
let r = entries.iter().fold(true, |acc, e| { let r = entries.iter().fold(true, |acc, e| {

View file

@ -61,6 +61,66 @@ impl Message for AuthMessage {
type Result = Result<AuthResponse, OperationError>; type Result = Result<AuthResponse, OperationError>;
} }
pub struct CreateMessage {
pub uat: Option<UserAuthToken>,
pub req: CreateRequest,
}
impl CreateMessage {
pub fn new(uat: Option<UserAuthToken>, req: CreateRequest) -> Self {
CreateMessage { uat: uat, req: req }
}
}
impl Message for CreateMessage {
type Result = Result<OperationResponse, OperationError>;
}
pub struct DeleteMessage {
pub uat: Option<UserAuthToken>,
pub req: DeleteRequest,
}
impl DeleteMessage {
pub fn new(uat: Option<UserAuthToken>, req: DeleteRequest) -> Self {
DeleteMessage { uat: uat, req: req }
}
}
impl Message for DeleteMessage {
type Result = Result<OperationResponse, OperationError>;
}
pub struct ModifyMessage {
pub uat: Option<UserAuthToken>,
pub req: ModifyRequest,
}
impl ModifyMessage {
pub fn new(uat: Option<UserAuthToken>, req: ModifyRequest) -> Self {
ModifyMessage { uat: uat, req: req }
}
}
impl Message for ModifyMessage {
type Result = Result<OperationResponse, OperationError>;
}
pub struct SearchMessage {
pub uat: Option<UserAuthToken>,
pub req: SearchRequest,
}
impl SearchMessage {
pub fn new(uat: Option<UserAuthToken>, req: SearchRequest) -> Self {
SearchMessage { uat: uat, req: req }
}
}
impl Message for SearchMessage {
type Result = Result<SearchResponse, OperationError>;
}
pub struct QueryServerV1 { pub struct QueryServerV1 {
log: actix::Addr<EventLog>, log: actix::Addr<EventLog>,
qs: QueryServer, qs: QueryServer,
@ -98,22 +158,22 @@ impl QueryServerV1 {
} }
} }
// The server only recieves "Event" structures, which // The server only recieves "Message" structures, which
// are whole self contained DB operations with all parsing // are whole self contained DB operations with all parsing
// required complete. We still need to do certain validation steps, but // required complete. We still need to do certain validation steps, but
// at this point our just is just to route to do_<action> // at this point our just is just to route to do_<action>
impl Handler<SearchRequest> for QueryServerV1 { impl Handler<SearchMessage> for QueryServerV1 {
type Result = Result<SearchResponse, OperationError>; type Result = Result<SearchResponse, OperationError>;
fn handle(&mut self, msg: SearchRequest, _: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: SearchMessage, _: &mut Self::Context) -> Self::Result {
let mut audit = AuditScope::new("search"); let mut audit = AuditScope::new("search");
let res = audit_segment!(&mut audit, || { let res = audit_segment!(&mut audit, || {
// Begin a read // Begin a read
let qs_read = self.qs.read(); let qs_read = self.qs.read();
// Make an event from the request // Make an event from the request
let srch = match SearchEvent::from_request(&mut audit, msg, &qs_read) { let srch = match SearchEvent::from_message(&mut audit, msg, &qs_read) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
audit_log!(audit, "Failed to begin search: {:?}", e); audit_log!(audit, "Failed to begin search: {:?}", e);
@ -138,15 +198,15 @@ impl Handler<SearchRequest> for QueryServerV1 {
} }
} }
impl Handler<CreateRequest> for QueryServerV1 { impl Handler<CreateMessage> for QueryServerV1 {
type Result = Result<OperationResponse, OperationError>; type Result = Result<OperationResponse, OperationError>;
fn handle(&mut self, msg: CreateRequest, _: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: CreateMessage, _: &mut Self::Context) -> Self::Result {
let mut audit = AuditScope::new("create"); let mut audit = AuditScope::new("create");
let res = audit_segment!(&mut audit, || { let res = audit_segment!(&mut audit, || {
let mut qs_write = self.qs.write(); let mut qs_write = self.qs.write();
let crt = match CreateEvent::from_request(&mut audit, msg, &qs_write) { let crt = match CreateEvent::from_message(&mut audit, msg, &qs_write) {
Ok(c) => c, Ok(c) => c,
Err(e) => { Err(e) => {
audit_log!(audit, "Failed to begin create: {:?}", e); audit_log!(audit, "Failed to begin create: {:?}", e);
@ -166,14 +226,14 @@ impl Handler<CreateRequest> for QueryServerV1 {
} }
} }
impl Handler<ModifyRequest> for QueryServerV1 { impl Handler<ModifyMessage> for QueryServerV1 {
type Result = Result<OperationResponse, OperationError>; type Result = Result<OperationResponse, OperationError>;
fn handle(&mut self, msg: ModifyRequest, _: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: ModifyMessage, _: &mut Self::Context) -> Self::Result {
let mut audit = AuditScope::new("modify"); let mut audit = AuditScope::new("modify");
let res = audit_segment!(&mut audit, || { let res = audit_segment!(&mut audit, || {
let mut qs_write = self.qs.write(); let mut qs_write = self.qs.write();
let mdf = match ModifyEvent::from_request(&mut audit, msg, &qs_write) { let mdf = match ModifyEvent::from_message(&mut audit, msg, &qs_write) {
Ok(m) => m, Ok(m) => m,
Err(e) => { Err(e) => {
audit_log!(audit, "Failed to begin modify: {:?}", e); audit_log!(audit, "Failed to begin modify: {:?}", e);
@ -192,15 +252,15 @@ impl Handler<ModifyRequest> for QueryServerV1 {
} }
} }
impl Handler<DeleteRequest> for QueryServerV1 { impl Handler<DeleteMessage> for QueryServerV1 {
type Result = Result<OperationResponse, OperationError>; type Result = Result<OperationResponse, OperationError>;
fn handle(&mut self, msg: DeleteRequest, _: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: DeleteMessage, _: &mut Self::Context) -> Self::Result {
let mut audit = AuditScope::new("delete"); let mut audit = AuditScope::new("delete");
let res = audit_segment!(&mut audit, || { let res = audit_segment!(&mut audit, || {
let mut qs_write = self.qs.write(); let mut qs_write = self.qs.write();
let del = match DeleteEvent::from_request(&mut audit, msg, &qs_write) { let del = match DeleteEvent::from_message(&mut audit, msg, &qs_write) {
Ok(d) => d, Ok(d) => d,
Err(e) => { Err(e) => {
audit_log!(audit, "Failed to begin delete: {:?}", e); audit_log!(audit, "Failed to begin delete: {:?}", e);
@ -285,6 +345,8 @@ impl Handler<WhoamiMessage> for QueryServerV1 {
// trigger the failure, but if we can manage to work out async // trigger the failure, but if we can manage to work out async
// then move this to core.rs, and don't allow Option<UAT> to get // then move this to core.rs, and don't allow Option<UAT> to get
// this far. // this far.
let uat = msg.uat.clone().ok_or(OperationError::NotAuthenticated)?;
let srch = match SearchEvent::from_whoami_request(&mut audit, msg.uat, &qs_read) { let srch = match SearchEvent::from_whoami_request(&mut audit, msg.uat, &qs_read) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
@ -303,7 +365,7 @@ impl Handler<WhoamiMessage> for QueryServerV1 {
1 => { 1 => {
let e = entries.pop().expect("Entry length mismatch!!!"); let e = entries.pop().expect("Entry length mismatch!!!");
// Now convert to a response, and return // Now convert to a response, and return
let wr = WhoamiResult::new(e); let wr = WhoamiResult::new(e, uat);
Ok(wr.response()) Ok(wr.response())
} }
// Somehow we matched multiple, which should be impossible. // Somehow we matched multiple, which should be impossible.

View file

@ -32,7 +32,8 @@ impl Configuration {
// log type // log type
// log path // log path
// TODO #63: default true in prd // TODO #63: default true in prd
secure_cookies: if cfg!(test) { false } else { true }, // secure_cookies: if cfg!(test) { false } else { true },
secure_cookies: false,
cookie_key: [0; 32], cookie_key: [0; 32],
server_id: [0; 4], server_id: [0; 4],
integration_test_config: None, integration_test_config: None,

View file

@ -24,7 +24,7 @@ pub static JSON_ADMIN_V1: &'static str = r#"{
}, },
"state": null, "state": null,
"attrs": { "attrs": {
"class": ["account", "object"], "class": ["account", "memberof", "object"],
"name": ["admin"], "name": ["admin"],
"uuid": ["00000000-0000-0000-0000-000000000000"], "uuid": ["00000000-0000-0000-0000-000000000000"],
"description": ["Builtin Admin account."], "description": ["Builtin Admin account."],
@ -80,7 +80,7 @@ pub static JSON_IDM_ADMINS_ACP_SEARCH_V1: &'static str = r#"{
"acp_targetscope": [ "acp_targetscope": [
"{\"Pres\":\"class\"}" "{\"Pres\":\"class\"}"
], ],
"acp_search_attr": ["name", "class", "uuid"] "acp_search_attr": ["name", "class", "uuid", "description", "displayname"]
} }
}"#; }"#;
@ -129,6 +129,40 @@ pub static JSON_IDM_SELF_ACP_READ_V1: &'static str = r#"{
} }
}"#; }"#;
pub static _UUID_IDM_ADMINS_ACP_MANAGE_V1: &'static str = "00000000-0000-0000-0000-ffffff000005";
pub static JSON_IDM_ADMINS_ACP_MANAGE_V1: &'static str = r#"{
"valid": {
"uuid": "00000000-0000-0000-0000-ffffff000005"
},
"state": null,
"attrs": {
"class": [
"object",
"access_control_profile",
"access_control_modify",
"access_control_create",
"access_control_delete",
"access_control_search"
],
"name": ["idm_admins_acp_manage"],
"uuid": ["00000000-0000-0000-0000-ffffff000005"],
"description": ["Builtin IDM Administrators Access Controls to manage the install."],
"acp_enable": ["true"],
"acp_receiver": [
"{\"Eq\":[\"memberof\",\"00000000-0000-0000-0000-000000000001\"]}"
],
"acp_targetscope": [
"{\"Pres\":\"class\"}"
],
"acp_search_attr": ["name", "class", "uuid", "classname", "attributename"],
"acp_modify_class": ["person"],
"acp_modify_removedattr": ["class", "displayname", "name", "description"],
"acp_modify_presentattr": ["class", "displayname", "name", "description"],
"acp_create_class": ["object", "person", "account"],
"acp_create_attr": ["name", "class", "description", "displayname"]
}
}"#;
pub static JSON_ANONYMOUS_V1: &'static str = r#"{ pub static JSON_ANONYMOUS_V1: &'static str = r#"{
"valid": { "valid": {
"uuid": "00000000-0000-0000-0000-ffffffffffff" "uuid": "00000000-0000-0000-0000-ffffffffffff"

View file

@ -13,7 +13,9 @@ use crate::config::Configuration;
// SearchResult // SearchResult
use crate::actors::v1::QueryServerV1; use crate::actors::v1::QueryServerV1;
use crate::actors::v1::{AuthMessage, WhoamiMessage}; use crate::actors::v1::{
AuthMessage, CreateMessage, DeleteMessage, ModifyMessage, SearchMessage, WhoamiMessage,
};
use crate::async_log; use crate::async_log;
use crate::audit::AuditScope; use crate::audit::AuditScope;
use crate::be::{Backend, BackendTransaction}; use crate::be::{Backend, BackendTransaction};
@ -46,12 +48,15 @@ fn get_current_user(req: &HttpRequest<AppState>) -> Option<UserAuthToken> {
} }
macro_rules! json_event_post { macro_rules! json_event_post {
($req:expr, $state:expr, $event_type:ty, $message_type:ty) => {{ ($req:expr, $state:expr, $message_type:ty, $request_type:ty) => {{
// This is copied every request. Is there a better way? // This is copied every request. Is there a better way?
// The issue is the fold move takes ownership of state if // The issue is the fold move takes ownership of state if
// we don't copy this here // we don't copy this here
let max_size = $state.max_size; let max_size = $state.max_size;
// Get auth if any?
let uat = get_current_user(&$req);
// HttpRequest::payload() is stream of Bytes objects // HttpRequest::payload() is stream of Bytes objects
$req.payload() $req.payload()
// `Future::from_err` acts like `?` in that it coerces the error type from // `Future::from_err` acts like `?` in that it coerces the error type from
@ -73,20 +78,17 @@ macro_rules! json_event_post {
.and_then( .and_then(
move |body| -> Box<dyn Future<Item = HttpResponse, Error = Error>> { move |body| -> Box<dyn Future<Item = HttpResponse, Error = Error>> {
// body is loaded, now we can deserialize serde-json // body is loaded, now we can deserialize serde-json
// let r_obj = serde_json::from_slice::<SearchRequest>(&body); let r_obj = serde_json::from_slice::<$request_type>(&body);
let r_obj = serde_json::from_slice::<$message_type>(&body);
// Send to the db for handling // Send to the db for handling
match r_obj { match r_obj {
Ok(obj) => { Ok(obj) => {
// combine request + uat -> message.
let m_obj = <($message_type)>::new(uat, obj);
let res = $state let res = $state
.qe .qe
.send( .send(m_obj)
// Could make this a .into_inner() and move? // What is from_err?
// event::SearchEvent::new(obj.filter),
// <($event_type)>::from_request(obj),
obj,
)
.from_err() .from_err()
.and_then(|res| match res { .and_then(|res| match res {
Ok(event_result) => Ok(HttpResponse::Ok().json(event_result)), Ok(event_result) => Ok(HttpResponse::Ok().json(event_result)),
@ -106,7 +108,7 @@ macro_rules! json_event_post {
} }
macro_rules! json_event_get { macro_rules! json_event_get {
($req:expr, $state:expr, $event_type:ty, $message_type:ty) => {{ ($req:expr, $state:expr, $message_type:ty) => {{
// Get current auth data - remember, the QS checks if the // Get current auth data - remember, the QS checks if the
// none/some is okay, because it's too hard to make it work here // none/some is okay, because it's too hard to make it work here
// with all the async parts. // with all the async parts.
@ -132,32 +134,31 @@ macro_rules! json_event_get {
fn create( fn create(
(req, state): (HttpRequest<AppState>, State<AppState>), (req, state): (HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> { ) -> impl Future<Item = HttpResponse, Error = Error> {
json_event_post!(req, state, CreateEvent, CreateRequest) json_event_post!(req, state, CreateMessage, CreateRequest)
} }
fn modify( fn modify(
(req, state): (HttpRequest<AppState>, State<AppState>), (req, state): (HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> { ) -> impl Future<Item = HttpResponse, Error = Error> {
json_event_post!(req, state, ModifyEvent, ModifyRequest) json_event_post!(req, state, ModifyMessage, ModifyRequest)
} }
fn delete( fn delete(
(req, state): (HttpRequest<AppState>, State<AppState>), (req, state): (HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> { ) -> impl Future<Item = HttpResponse, Error = Error> {
json_event_post!(req, state, DeleteEvent, DeleteRequest) json_event_post!(req, state, DeleteMessage, DeleteRequest)
} }
fn search( fn search(
(req, state): (HttpRequest<AppState>, State<AppState>), (req, state): (HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> { ) -> impl Future<Item = HttpResponse, Error = Error> {
json_event_post!(req, state, SearchEvent, SearchRequest) json_event_post!(req, state, SearchMessage, SearchRequest)
} }
fn whoami( fn whoami(
(req, state): (HttpRequest<AppState>, State<AppState>), (req, state): (HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> { ) -> impl Future<Item = HttpResponse, Error = Error> {
// Actually this may not work as it assumes post not get. json_event_get!(req, state, WhoamiMessage)
json_event_get!(req, state, WhoamiEvent, WhoamiMessage)
} }
// We probably need an extract auth or similar to handle the different // We probably need an extract auth or similar to handle the different

View file

@ -3,8 +3,8 @@ use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryReduced,
use crate::filter::{Filter, FilterValid}; use crate::filter::{Filter, FilterValid};
use rsidm_proto::v1::Entry as ProtoEntry; use rsidm_proto::v1::Entry as ProtoEntry;
use rsidm_proto::v1::{ use rsidm_proto::v1::{
AuthCredential, AuthResponse, AuthState, AuthStep, CreateRequest, DeleteRequest, ModifyRequest, AuthCredential, AuthResponse, AuthState, AuthStep, SearchResponse, UserAuthToken,
ReviveRecycledRequest, SearchRequest, SearchResponse, UserAuthToken, WhoamiResponse, WhoamiResponse,
}; };
// use error::OperationError; // use error::OperationError;
use crate::modify::{ModifyList, ModifyValid}; use crate::modify::{ModifyList, ModifyValid};
@ -13,7 +13,7 @@ use crate::server::{
}; };
use rsidm_proto::v1::OperationError; use rsidm_proto::v1::OperationError;
use crate::actors::v1::AuthMessage; use crate::actors::v1::{AuthMessage, CreateMessage, DeleteMessage, ModifyMessage, SearchMessage};
// Bring in schematransaction trait for validate // Bring in schematransaction trait for validate
// use crate::schema::SchemaTransaction; // use crate::schema::SchemaTransaction;
@ -22,8 +22,6 @@ use crate::actors::v1::AuthMessage;
use crate::filter::FilterInvalid; use crate::filter::FilterInvalid;
#[cfg(test)] #[cfg(test)]
use crate::modify::ModifyInvalid; use crate::modify::ModifyInvalid;
#[cfg(test)]
use rsidm_proto::v1::SearchRecycledRequest;
use actix::prelude::*; use actix::prelude::*;
use uuid::Uuid; use uuid::Uuid;
@ -120,6 +118,27 @@ impl Event {
}) })
} }
pub fn from_rw_uat(
audit: &mut AuditScope,
qs: &QueryServerWriteTransaction,
uat: Option<UserAuthToken>,
) -> Result<Self, OperationError> {
audit_log!(audit, "from_rw_uat -> {:?}", uat);
let uat = uat.ok_or(OperationError::NotAuthenticated)?;
let u = try_audit!(
audit,
Uuid::parse_str(uat.uuid.as_str()).map_err(|_| OperationError::InvalidUuid)
);
let e = try_audit!(audit, qs.internal_search_uuid(audit, &u));
// TODO #64: Now apply claims from the uat into the Entry
// to allow filtering.
Ok(Event {
origin: EventOrigin::User(e),
})
}
pub fn from_rw_request( pub fn from_rw_request(
audit: &mut AuditScope, audit: &mut AuditScope,
qs: &QueryServerWriteTransaction, qs: &QueryServerWriteTransaction,
@ -186,14 +205,14 @@ pub struct SearchEvent {
} }
impl SearchEvent { impl SearchEvent {
pub fn from_request( pub fn from_message(
audit: &mut AuditScope, audit: &mut AuditScope,
request: SearchRequest, msg: SearchMessage,
qs: &QueryServerReadTransaction, qs: &QueryServerReadTransaction,
) -> Result<Self, OperationError> { ) -> Result<Self, OperationError> {
match Filter::from_ro(audit, &request.filter, qs) { match Filter::from_ro(audit, &msg.req.filter, qs) {
Ok(f) => Ok(SearchEvent { Ok(f) => Ok(SearchEvent {
event: Event::from_ro_request(audit, qs, request.user_uuid.as_str())?, event: Event::from_ro_uat(audit, qs, msg.uat)?,
// We do need to do this twice to account for the ignore_hidden // We do need to do this twice to account for the ignore_hidden
// changes. // changes.
filter: f filter: f
@ -259,6 +278,7 @@ impl SearchEvent {
} }
} }
/*
#[cfg(test)] #[cfg(test)]
#[allow(dead_code)] #[allow(dead_code)]
pub fn from_rec_request( pub fn from_rec_request(
@ -268,7 +288,7 @@ impl SearchEvent {
) -> Result<Self, OperationError> { ) -> Result<Self, OperationError> {
match Filter::from_ro(audit, &request.filter, qs) { match Filter::from_ro(audit, &request.filter, qs) {
Ok(f) => Ok(SearchEvent { Ok(f) => Ok(SearchEvent {
event: Event::from_ro_request(audit, qs, request.user_uuid.as_str())?, event: Event::from_ro_uat(audit, qs, msg.uat)?,
filter: f filter: f
.clone() .clone()
.to_recycled() .to_recycled()
@ -281,6 +301,7 @@ impl SearchEvent {
Err(e) => Err(e), Err(e) => Err(e),
} }
} }
*/
#[cfg(test)] #[cfg(test)]
/* Impersonate a request for recycled objects */ /* Impersonate a request for recycled objects */
@ -340,12 +361,13 @@ pub struct CreateEvent {
} }
impl CreateEvent { impl CreateEvent {
pub fn from_request( pub fn from_message(
audit: &mut AuditScope, audit: &mut AuditScope,
request: CreateRequest, msg: CreateMessage,
qs: &QueryServerWriteTransaction, qs: &QueryServerWriteTransaction,
) -> Result<Self, OperationError> { ) -> Result<Self, OperationError> {
let rentries: Result<Vec<_>, _> = request let rentries: Result<Vec<_>, _> = msg
.req
.entries .entries
.iter() .iter()
.map(|e| Entry::from_proto_entry(audit, e, qs)) .map(|e| Entry::from_proto_entry(audit, e, qs))
@ -355,7 +377,7 @@ impl CreateEvent {
// From ProtoEntry -> Entry // From ProtoEntry -> Entry
// What is the correct consuming iterator here? Can we // What is the correct consuming iterator here? Can we
// even do that? // even do that?
event: Event::from_rw_request(audit, qs, request.user_uuid.as_str())?, event: Event::from_rw_uat(audit, qs, msg.uat)?,
entries: entries, entries: entries,
}), }),
Err(e) => Err(e), Err(e) => Err(e),
@ -421,14 +443,14 @@ pub struct DeleteEvent {
} }
impl DeleteEvent { impl DeleteEvent {
pub fn from_request( pub fn from_message(
audit: &mut AuditScope, audit: &mut AuditScope,
request: DeleteRequest, msg: DeleteMessage,
qs: &QueryServerWriteTransaction, qs: &QueryServerWriteTransaction,
) -> Result<Self, OperationError> { ) -> Result<Self, OperationError> {
match Filter::from_rw(audit, &request.filter, qs) { match Filter::from_rw(audit, &msg.req.filter, qs) {
Ok(f) => Ok(DeleteEvent { Ok(f) => Ok(DeleteEvent {
event: Event::from_rw_request(audit, qs, request.user_uuid.as_str())?, event: Event::from_rw_uat(audit, qs, msg.uat)?,
filter: f filter: f
.clone() .clone()
.to_ignore_hidden() .to_ignore_hidden()
@ -442,6 +464,18 @@ impl DeleteEvent {
} }
} }
#[cfg(test)]
pub unsafe fn new_impersonate_entry(
e: Entry<EntryValid, EntryCommitted>,
filter: Filter<FilterInvalid>,
) -> Self {
DeleteEvent {
event: Event::from_impersonate_entry(e),
filter: filter.clone().to_valid(),
filter_orig: filter.to_valid(),
}
}
#[cfg(test)] #[cfg(test)]
pub unsafe fn new_impersonate_entry_ser(e: &str, filter: Filter<FilterInvalid>) -> Self { pub unsafe fn new_impersonate_entry_ser(e: &str, filter: Filter<FilterInvalid>) -> Self {
DeleteEvent { DeleteEvent {
@ -480,15 +514,15 @@ pub struct ModifyEvent {
} }
impl ModifyEvent { impl ModifyEvent {
pub fn from_request( pub fn from_message(
audit: &mut AuditScope, audit: &mut AuditScope,
request: ModifyRequest, msg: ModifyMessage,
qs: &QueryServerWriteTransaction, qs: &QueryServerWriteTransaction,
) -> Result<Self, OperationError> { ) -> Result<Self, OperationError> {
match Filter::from_rw(audit, &request.filter, qs) { match Filter::from_rw(audit, &msg.req.filter, qs) {
Ok(f) => match ModifyList::from(audit, &request.modlist, qs) { Ok(f) => match ModifyList::from(audit, &msg.req.modlist, qs) {
Ok(m) => Ok(ModifyEvent { Ok(m) => Ok(ModifyEvent {
event: Event::from_rw_request(audit, qs, request.user_uuid.as_str())?, event: Event::from_rw_uat(audit, qs, msg.uat)?,
filter: f filter: f
.clone() .clone()
.to_ignore_hidden() .to_ignore_hidden()
@ -544,6 +578,20 @@ impl ModifyEvent {
} }
} }
#[cfg(test)]
pub unsafe fn new_impersonate_entry(
e: Entry<EntryValid, EntryCommitted>,
filter: Filter<FilterInvalid>,
modlist: ModifyList<ModifyInvalid>,
) -> Self {
ModifyEvent {
event: Event::from_impersonate_entry(e),
filter: filter.clone().to_valid(),
filter_orig: filter.to_valid(),
modlist: modlist.to_valid(),
}
}
pub fn new_impersonate( pub fn new_impersonate(
event: &Event, event: &Event,
filter: Filter<FilterValid>, filter: Filter<FilterValid>,
@ -703,18 +751,21 @@ impl AuthResult {
pub struct WhoamiResult { pub struct WhoamiResult {
youare: ProtoEntry, youare: ProtoEntry,
uat: UserAuthToken,
} }
impl WhoamiResult { impl WhoamiResult {
pub fn new(e: Entry<EntryReduced, EntryCommitted>) -> Self { pub fn new(e: Entry<EntryReduced, EntryCommitted>, uat: UserAuthToken) -> Self {
WhoamiResult { WhoamiResult {
youare: e.into_pe(), youare: e.into_pe(),
uat: uat,
} }
} }
pub fn response(self) -> WhoamiResponse { pub fn response(self) -> WhoamiResponse {
WhoamiResponse { WhoamiResponse {
youare: self.youare, youare: self.youare,
uat: self.uat,
} }
} }
} }
@ -769,14 +820,15 @@ impl Message for ReviveRecycledEvent {
} }
impl ReviveRecycledEvent { impl ReviveRecycledEvent {
pub fn from_request( /*
pub fn from_message(
audit: &mut AuditScope, audit: &mut AuditScope,
request: ReviveRecycledRequest, msg: ReviveRecycledMessage,
qs: &QueryServerWriteTransaction, qs: &QueryServerWriteTransaction,
) -> Result<Self, OperationError> { ) -> Result<Self, OperationError> {
match Filter::from_rw(audit, &request.filter, qs) { match Filter::from_rw(audit, &msg.req.filter, qs) {
Ok(f) => Ok(ReviveRecycledEvent { Ok(f) => Ok(ReviveRecycledEvent {
event: Event::from_rw_request(audit, qs, request.user_uuid.as_str())?, event: Event::from_rw_uat(audit, qs, msg.uat)?,
filter: f filter: f
.to_recycled() .to_recycled()
.validate(qs.get_schema()) .validate(qs.get_schema())
@ -785,4 +837,16 @@ impl ReviveRecycledEvent {
Err(e) => Err(e), Err(e) => Err(e),
} }
} }
*/
#[cfg(test)]
pub unsafe fn new_impersonate_entry(
e: Entry<EntryValid, EntryCommitted>,
filter: Filter<FilterInvalid>,
) -> Self {
ReviveRecycledEvent {
event: Event::from_impersonate_entry(e),
filter: filter.to_valid(),
}
}
} }

View file

@ -1,4 +1,4 @@
// #![deny(warnings)] #![deny(warnings)]
#![warn(unused_extern_crates)] #![warn(unused_extern_crates)]
#[macro_use] #[macro_use]

View file

@ -25,7 +25,11 @@ lazy_static! {
m m
}; };
static ref PVCLASS_SYSTEM: PartialValue = PartialValue::new_class("system"); static ref PVCLASS_SYSTEM: PartialValue = PartialValue::new_class("system");
static ref PVCLASS_TOMBSTONE: PartialValue = PartialValue::new_class("tombstone");
static ref PVCLASS_RECYCLED: PartialValue = PartialValue::new_class("recycled");
static ref VCLASS_SYSTEM: Value = Value::new_class("system"); static ref VCLASS_SYSTEM: Value = Value::new_class("system");
static ref VCLASS_TOMBSTONE: Value = Value::new_class("tombstone");
static ref VCLASS_RECYCLED: Value = Value::new_class("recycled");
} }
impl Plugin for Protected { impl Plugin for Protected {
@ -51,7 +55,10 @@ impl Plugin for Protected {
cand.iter().fold(Ok(()), |acc, cand| match acc { cand.iter().fold(Ok(()), |acc, cand| match acc {
Err(_) => acc, Err(_) => acc,
Ok(_) => { Ok(_) => {
if cand.attribute_value_pres("class", &PVCLASS_SYSTEM) { if cand.attribute_value_pres("class", &PVCLASS_SYSTEM)
|| cand.attribute_value_pres("class", &PVCLASS_TOMBSTONE)
|| cand.attribute_value_pres("class", &PVCLASS_RECYCLED)
{
Err(OperationError::SystemProtectedObject) Err(OperationError::SystemProtectedObject)
} else { } else {
acc acc
@ -74,7 +81,7 @@ impl Plugin for Protected {
); );
return Ok(()); return Ok(());
} }
// Prevent adding class: system // Prevent adding class: system, tombstone, or recycled.
me.modlist.iter().fold(Ok(()), |acc, m| { me.modlist.iter().fold(Ok(()), |acc, m| {
if acc.is_err() { if acc.is_err() {
acc acc
@ -82,7 +89,11 @@ impl Plugin for Protected {
match m { match m {
Modify::Present(a, v) => { Modify::Present(a, v) => {
// TODO: Can we avoid this clone? // TODO: Can we avoid this clone?
if a == "class" && v == &(VCLASS_SYSTEM.clone()) { if a == "class"
&& (v == &(VCLASS_SYSTEM.clone())
|| v == &(VCLASS_TOMBSTONE.clone())
|| v == &(VCLASS_RECYCLED.clone()))
{
Err(OperationError::SystemProtectedObject) Err(OperationError::SystemProtectedObject)
} else { } else {
Ok(()) Ok(())
@ -92,8 +103,22 @@ impl Plugin for Protected {
} }
} }
})?; })?;
// if class: system, check the mods are "allowed"
// HARD block mods on tombstone or recycle.
cand.iter().fold(Ok(()), |acc, cand| match acc {
Err(_) => acc,
Ok(_) => {
if cand.attribute_value_pres("class", &PVCLASS_TOMBSTONE)
|| cand.attribute_value_pres("class", &PVCLASS_RECYCLED)
{
Err(OperationError::SystemProtectedObject)
} else {
acc
}
}
})?;
// if class: system, check the mods are "allowed"
let system_pres = cand.iter().fold(false, |acc, c| { let system_pres = cand.iter().fold(false, |acc, c| {
if acc { if acc {
acc acc
@ -145,7 +170,10 @@ impl Plugin for Protected {
cand.iter().fold(Ok(()), |acc, cand| match acc { cand.iter().fold(Ok(()), |acc, cand| match acc {
Err(_) => acc, Err(_) => acc,
Ok(_) => { Ok(_) => {
if cand.attribute_value_pres("class", &PVCLASS_SYSTEM) { if cand.attribute_value_pres("class", &PVCLASS_SYSTEM)
|| cand.attribute_value_pres("class", &PVCLASS_TOMBSTONE)
|| cand.attribute_value_pres("class", &PVCLASS_RECYCLED)
{
Err(OperationError::SystemProtectedObject) Err(OperationError::SystemProtectedObject)
} else { } else {
acc acc

View file

@ -13,11 +13,11 @@ use crate::access::{
AccessControlsWriteTransaction, AccessControlsWriteTransaction,
}; };
use crate::constants::{ use crate::constants::{
JSON_ADMIN_V1, JSON_ANONYMOUS_V1, JSON_IDM_ADMINS_ACP_REVIVE_V1, JSON_IDM_ADMINS_ACP_SEARCH_V1, JSON_ADMIN_V1, JSON_ANONYMOUS_V1, JSON_IDM_ADMINS_ACP_MANAGE_V1, JSON_IDM_ADMINS_ACP_REVIVE_V1,
JSON_IDM_ADMINS_V1, JSON_IDM_SELF_ACP_READ_V1, JSON_SCHEMA_ATTR_DISPLAYNAME, JSON_IDM_ADMINS_ACP_SEARCH_V1, JSON_IDM_ADMINS_V1, JSON_IDM_SELF_ACP_READ_V1,
JSON_SCHEMA_ATTR_MAIL, JSON_SCHEMA_ATTR_PRIMARY_CREDENTIAL, JSON_SCHEMA_ATTR_SSH_PUBLICKEY, JSON_SCHEMA_ATTR_DISPLAYNAME, JSON_SCHEMA_ATTR_MAIL, JSON_SCHEMA_ATTR_PRIMARY_CREDENTIAL,
JSON_SCHEMA_CLASS_ACCOUNT, JSON_SCHEMA_CLASS_GROUP, JSON_SCHEMA_CLASS_PERSON, JSON_SCHEMA_ATTR_SSH_PUBLICKEY, JSON_SCHEMA_CLASS_ACCOUNT, JSON_SCHEMA_CLASS_GROUP,
JSON_SYSTEM_INFO_V1, UUID_DOES_NOT_EXIST, JSON_SCHEMA_CLASS_PERSON, JSON_SYSTEM_INFO_V1, UUID_DOES_NOT_EXIST,
}; };
use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryReduced, EntryValid}; use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryReduced, EntryValid};
use crate::event::{ use crate::event::{
@ -944,6 +944,11 @@ impl<'a> QueryServerWriteTransaction<'a> {
Err(e) => return Err(e), Err(e) => return Err(e),
}; };
if ts.len() == 0 {
audit_log!(au, "No Tombstones present - purge operation success");
return Ok(());
}
// TODO #68: Has an appropriate amount of time/condition past (ie replication events?) // TODO #68: Has an appropriate amount of time/condition past (ie replication events?)
// Delete them // Delete them
@ -975,6 +980,11 @@ impl<'a> QueryServerWriteTransaction<'a> {
Err(e) => return Err(e), Err(e) => return Err(e),
}; };
if rc.len() == 0 {
audit_log!(au, "No recycled present - purge operation success");
return Ok(());
}
// Modify them to strip all avas except uuid // Modify them to strip all avas except uuid
let tombstone_cand = rc.iter().map(|e| e.to_tombstone()).collect(); let tombstone_cand = rc.iter().map(|e| e.to_tombstone()).collect();
@ -1514,6 +1524,9 @@ impl<'a> QueryServerWriteTransaction<'a> {
.and_then(|_| { .and_then(|_| {
self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_ADMINS_ACP_REVIVE_V1) self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_ADMINS_ACP_REVIVE_V1)
}) })
.and_then(|_| {
self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_ADMINS_ACP_MANAGE_V1)
})
.and_then(|_| { .and_then(|_| {
self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_SELF_ACP_READ_V1) self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_SELF_ACP_READ_V1)
}); });
@ -1689,17 +1702,13 @@ impl<'a> QueryServerWriteTransaction<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::constants::{JSON_ADMIN_V1, STR_UUID_ADMIN, UUID_ADMIN}; use crate::constants::{JSON_ADMIN_V1, UUID_ADMIN};
use crate::credential::Credential; use crate::credential::Credential;
use crate::entry::{Entry, EntryInvalid, EntryNew}; use crate::entry::{Entry, EntryInvalid, EntryNew};
use crate::event::{CreateEvent, DeleteEvent, ModifyEvent, ReviveRecycledEvent, SearchEvent}; use crate::event::{CreateEvent, DeleteEvent, ModifyEvent, ReviveRecycledEvent, SearchEvent};
use crate::modify::{Modify, ModifyList}; use crate::modify::{Modify, ModifyList};
use crate::server::QueryServerTransaction; use crate::server::QueryServerTransaction;
use crate::value::{PartialValue, Value}; use crate::value::{PartialValue, Value};
use rsidm_proto::v1::Filter as ProtoFilter;
use rsidm_proto::v1::Modify as ProtoModify;
use rsidm_proto::v1::ModifyList as ProtoModifyList;
use rsidm_proto::v1::{DeleteRequest, ModifyRequest, ReviveRecycledRequest};
use rsidm_proto::v1::{OperationError, SchemaError}; use rsidm_proto::v1::{OperationError, SchemaError};
use uuid::Uuid; use uuid::Uuid;
@ -2079,31 +2088,23 @@ mod tests {
.internal_search_uuid(audit, &UUID_ADMIN) .internal_search_uuid(audit, &UUID_ADMIN)
.expect("failed"); .expect("failed");
let filt_ts = ProtoFilter::Eq("class".to_string(), "tombstone".to_string());
let filt_i_ts = filter_all!(f_eq("class", PartialValue::new_class("tombstone"))); let filt_i_ts = filter_all!(f_eq("class", PartialValue::new_class("tombstone")));
// Create fake external requests. Probably from admin later // Create fake external requests. Probably from admin later
// Should we do this with impersonate instead of using the external // Should we do this with impersonate instead of using the external
let me_ts = ModifyEvent::from_request( let me_ts = unsafe {
audit, ModifyEvent::new_impersonate_entry(
ModifyRequest::new( admin.clone(),
filt_ts.clone(), filt_i_ts.clone(),
ProtoModifyList::new_list(vec![ProtoModify::Present( ModifyList::new_list(vec![Modify::Present(
"class".to_string(), "class".to_string(),
"tombstone".to_string(), Value::new_class("tombstone"),
)]), )]),
STR_UUID_ADMIN,
),
&server_txn,
) )
.expect("modify event create failed"); };
let de_ts = DeleteEvent::from_request(
audit, let de_ts =
DeleteRequest::new(filt_ts.clone(), STR_UUID_ADMIN), unsafe { DeleteEvent::new_impersonate_entry(admin.clone(), filt_i_ts.clone()) };
&server_txn,
)
.expect("delete event create failed");
let se_ts = unsafe { SearchEvent::new_ext_impersonate_entry(admin, filt_i_ts.clone()) }; let se_ts = unsafe { SearchEvent::new_ext_impersonate_entry(admin, filt_i_ts.clone()) };
// First, create a tombstone // First, create a tombstone
@ -2163,8 +2164,6 @@ mod tests {
.internal_search_uuid(audit, &UUID_ADMIN) .internal_search_uuid(audit, &UUID_ADMIN)
.expect("failed"); .expect("failed");
let filt_rc = ProtoFilter::Eq("class".to_string(), "recycled".to_string());
let filt_i_rc = filter_all!(f_eq("class", PartialValue::new_class("recycled"))); let filt_i_rc = filter_all!(f_eq("class", PartialValue::new_class("recycled")));
let filt_i_ts = filter_all!(f_eq("class", PartialValue::new_class("tombstone"))); let filt_i_ts = filter_all!(f_eq("class", PartialValue::new_class("tombstone")));
@ -2172,40 +2171,32 @@ mod tests {
let filt_i_per = filter_all!(f_eq("class", PartialValue::new_class("person"))); let filt_i_per = filter_all!(f_eq("class", PartialValue::new_class("person")));
// Create fake external requests. Probably from admin later // Create fake external requests. Probably from admin later
let me_rc = ModifyEvent::from_request( let me_rc = unsafe {
audit, ModifyEvent::new_impersonate_entry(
ModifyRequest::new( admin.clone(),
filt_rc.clone(), filt_i_rc.clone(),
ProtoModifyList::new_list(vec![ProtoModify::Present( ModifyList::new_list(vec![Modify::Present(
"class".to_string(), "class".to_string(),
"recycled".to_string(), Value::new_class("recycled"),
)]), )]),
STR_UUID_ADMIN,
),
&server_txn,
) )
.expect("modify event create failed"); };
let de_rc = DeleteEvent::from_request(
audit, let de_rc =
DeleteRequest::new(filt_rc.clone(), STR_UUID_ADMIN), unsafe { DeleteEvent::new_impersonate_entry(admin.clone(), filt_i_rc.clone()) };
&server_txn,
)
.expect("delete event create failed");
let se_rc = let se_rc =
unsafe { SearchEvent::new_ext_impersonate_entry(admin.clone(), filt_i_rc.clone()) }; unsafe { SearchEvent::new_ext_impersonate_entry(admin.clone(), filt_i_rc.clone()) };
let sre_rc = let sre_rc =
unsafe { SearchEvent::new_rec_impersonate_entry(admin, filt_i_rc.clone()) }; unsafe { SearchEvent::new_rec_impersonate_entry(admin.clone(), filt_i_rc.clone()) };
let rre_rc = ReviveRecycledEvent::from_request( let rre_rc = unsafe {
audit, ReviveRecycledEvent::new_impersonate_entry(
ReviveRecycledRequest::new( admin,
ProtoFilter::Eq("name".to_string(), "testperson1".to_string()), filter_all!(f_eq("name", PartialValue::new_iutf8s("testperson1"))),
STR_UUID_ADMIN,
),
&server_txn,
) )
.expect("revive recycled create failed"); };
// Create some recycled objects // Create some recycled objects
let e1: Entry<EntryInvalid, EntryNew> = Entry::unsafe_from_entry_str( let e1: Entry<EntryInvalid, EntryNew> = Entry::unsafe_from_entry_str(