diff --git a/.gitignore b/.gitignore index bb9c61f0f..1b0d95468 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ +.DS_Store +.backup_test.db /target **/*.rs.bk test.db diff --git a/Cargo.toml b/Cargo.toml index 11d146755..ef7ee247e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,60 +1,9 @@ -# cargo-features = ["default-run"] - -[package] -name = "rsidm" -version = "0.1.0" -authors = ["William Brown "] -# default-run = "rsidm_core" -edition = "2018" - - -# We need three major binaries. The server itself, the unix client, and a cli -# mgmt tool. - -[lib] -name = "rsidm" -path = "src/lib/lib.rs" - -[[bin]] -name = "rsidmd" -path = "src/server/main.rs" - -[[bin]] -name = "kanidm" -path = "src/clients/main.rs" - - -[dependencies] -actix = "0.7" -actix-web = "0.7" -bytes = "0.4" -log = "0.4" -env_logger = "0.6" -reqwest = "0.9" -# reqwest = { path = "../reqwest" } -rand = "0.6" - -chrono = "0.4" -cookie = "0.11" -regex = "1" -lazy_static = "1.2.0" -lru = "0.1" - -tokio = "0.1" -futures = "0.1" -uuid = { version = "0.7", features = ["serde", "v4"] } -serde = "1.0" -serde_cbor = "0.10" -serde_json = "1.0" -serde_derive = "1.0" - -rusqlite = { version = "0.15", features = ["backup"] } -r2d2 = "0.8" -r2d2_sqlite = "0.7" - -structopt = { version = "0.2", default-features = false } -time = "0.1" - -concread = "0.1" +[workspace] +members = [ + "rsidm_proto", + "rsidmd", + "rsidm_client", + "rsidm_tools", +] diff --git a/artwork/README.md b/artwork/README.md index 13deb816f..4bf9987fb 100644 --- a/artwork/README.md +++ b/artwork/README.md @@ -2,7 +2,7 @@ About these artworks -------------------- -These artworks were commissioned and produced by Jessie Irwin (tw: @wizardfortress). They are very +These artworks were commissioned and produced by Jesse Irwin (tw: @wizardfortress). They are very much appreciated! Both artworks are licensed as CC-BY-NC-ND. diff --git a/rsidm_client/Cargo.toml b/rsidm_client/Cargo.toml new file mode 100644 index 000000000..a1c0355b6 --- /dev/null +++ b/rsidm_client/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "rsidm_client" +version = "0.1.0" +authors = ["William Brown "] +edition = "2018" + +[dependencies] +log = "0.4" +env_logger = "0.6" +reqwest = "0.9" +rsidm_proto = { path = "../rsidm_proto" } + +[dev-dependencies] +tokio = "0.1" +actix = "0.7" +rsidm = { path = "../rsidmd" } +futures = "0.1" +serde_json = "1.0" diff --git a/rsidm_client/src/lib.rs b/rsidm_client/src/lib.rs new file mode 100644 index 000000000..83c26ae9a --- /dev/null +++ b/rsidm_client/src/lib.rs @@ -0,0 +1,29 @@ +// #![deny(warnings)] +#![warn(unused_extern_crates)] + +use reqwest; + +pub struct RsidmClient { + client: reqwest::Client, + addr: String, +} + +impl RsidmClient { + pub fn new(addr: &str) -> Self { + let client = reqwest::Client::builder() + .cookie_store(true) + .build() + .expect("Unexpected reqwest builder failure!"); + RsidmClient { + client: client, + addr: addr.to_string(), + } + } + + // auth + // whoami + // search + // create + // modify + // +} diff --git a/tests/proto_v1_test.rs b/rsidm_client/tests/proto_v1_test.rs similarity index 68% rename from tests/proto_v1_test.rs rename to rsidm_client/tests/proto_v1_test.rs index 151bb4de0..ddb8d8f58 100644 --- a/tests/proto_v1_test.rs +++ b/rsidm_client/tests/proto_v1_test.rs @@ -7,10 +7,13 @@ extern crate actix; use actix::prelude::*; extern crate rsidm; -use rsidm::config::Configuration; +extern crate rsidm_proto; +extern crate serde_json; + +use rsidm::config::{Configuration, IntegrationTestConfig}; use rsidm::constants::UUID_ADMIN; use rsidm::core::create_server_core; -use rsidm::proto::v1::{ +use rsidm_proto::v1::{ AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, Entry, OperationResponse, }; @@ -29,6 +32,7 @@ extern crate env_logger; extern crate tokio; static PORT_ALLOC: AtomicUsize = AtomicUsize::new(8080); +static ADMIN_TEST_PASSWORD: &'static str = "integration test admin password"; // Test external behaviorus of the service. @@ -36,8 +40,15 @@ fn run_test(test_fn: fn(reqwest::Client, &str) -> ()) { let _ = env_logger::builder().is_test(true).try_init(); let (tx, rx) = mpsc::channel(); let port = PORT_ALLOC.fetch_add(1, Ordering::SeqCst); + + let int_config = Box::new(IntegrationTestConfig { + admin_password: ADMIN_TEST_PASSWORD.to_string(), + }); + let mut config = Configuration::new(); config.address = format!("127.0.0.1:{}", port); + config.secure_cookies = false; + config.integration_test_config = Some(int_config); // Setup the config ... thread::spawn(move || { @@ -185,6 +196,73 @@ fn test_server_whoami_anonymous() { }); } +#[test] +fn test_server_whoami_admin_simple_password() { + run_test(|client: reqwest::Client, addr: &str| { + // First show we are un-authenticated. + let whoami_dest = format!("{}/v1/whoami", addr); + let auth_dest = format!("{}/v1/auth", addr); + // Now login as admin + + // Setup the auth initialisation + let auth_init = AuthRequest { + 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. + let mut response = client.get(whoami_dest.as_str()).send().unwrap(); + println!("WHOAMI -> {}", response.text().unwrap().as_str()); + println!("WHOAMI STATUS -> {}", response.status()); + assert!(response.status() == reqwest::StatusCode::OK); + + // Check the json now ... response.json() + }); +} + // Test hitting all auth-required endpoints and assert they give unauthorized. /* diff --git a/rsidm_proto/Cargo.toml b/rsidm_proto/Cargo.toml new file mode 100644 index 000000000..c0a77a3a6 --- /dev/null +++ b/rsidm_proto/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "rsidm_proto" +version = "0.1.0" +authors = ["William Brown "] +edition = "2018" + +[features] +rsidm_internal = ["actix"] + +[dependencies] +serde = "1.0" +serde_derive = "1.0" +uuid = { version = "0.7", features = ["serde", "v4"] } +actix = { version = "0.7", optional = true } + +[dev-dependencies] +serde_json = "1.0" diff --git a/rsidm_proto/src/lib.rs b/rsidm_proto/src/lib.rs new file mode 100644 index 000000000..91d51af1f --- /dev/null +++ b/rsidm_proto/src/lib.rs @@ -0,0 +1,7 @@ +#![deny(warnings)] +#![warn(unused_extern_crates)] + +#[macro_use] +extern crate serde_derive; + +pub mod v1; diff --git a/src/lib/proto/v1/mod.rs b/rsidm_proto/src/v1.rs similarity index 83% rename from src/lib/proto/v1/mod.rs rename to rsidm_proto/src/v1.rs index 1b3255b0e..18d9bdaf5 100644 --- a/src/lib/proto/v1/mod.rs +++ b/rsidm_proto/src/v1.rs @@ -1,16 +1,72 @@ -// use super::entry::Entry; -// use super::filter::Filter; -use crate::error::OperationError; -use actix::prelude::*; use std::collections::BTreeMap; use uuid::Uuid; -pub(crate) mod actors; -pub mod client; -pub(crate) mod messages; +#[cfg(feature = "rsidm_internal")] +use actix::prelude::*; // These proto implementations are here because they have public definitions +/* ===== errors ===== */ + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub enum SchemaError { + NotImplemented, + InvalidClass, + MissingMustAttribute(String), + InvalidAttribute, + InvalidAttributeSyntax, + EmptyFilter, + Corrupted, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub enum OperationError { + EmptyRequest, + Backend, + NoMatchingEntries, + CorruptedEntry(u64), + ConsistencyError(Vec>), + SchemaViolation(SchemaError), + Plugin, + FilterGeneration, + FilterUUIDResolution, + InvalidAttributeName(String), + InvalidAttribute(&'static str), + InvalidDBState, + InvalidEntryID, + InvalidRequestState, + InvalidState, + InvalidEntryState, + InvalidUuid, + InvalidACPState(&'static str), + InvalidSchemaState(&'static str), + InvalidAccountState(&'static str), + BackendEngine, + SQLiteError, //(RusqliteError) + FsError, + SerdeJsonError, + SerdeCborError, + AccessDenied, + NotAuthenticated, + InvalidAuthState(&'static str), + InvalidSessionState, + SystemProtectedObject, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub enum ConsistencyError { + Unknown, + // Class, Attribute + SchemaClassMissingAttribute(String, String), + QueryServerSearchFailure, + EntryUuidCorrupt(u64), + UuidIndexCorrupt(String), + UuidNotUnique(String), + RefintNotUpheld(u64), + MemberOfInvalid(u64), + InvalidAttributeType(&'static str), +} + /* ===== higher level types ===== */ // These are all types that are conceptually layers ontop of entry and // friends. They allow us to process more complex requests and provide @@ -133,6 +189,7 @@ impl SearchRequest { } } +#[cfg(feature = "rsidm_internal")] impl Message for SearchRequest { type Result = Result; } @@ -163,6 +220,7 @@ impl CreateRequest { } } +#[cfg(feature = "rsidm_internal")] impl Message for CreateRequest { type Result = Result; } @@ -182,6 +240,7 @@ impl DeleteRequest { } } +#[cfg(feature = "rsidm_internal")] impl Message for DeleteRequest { type Result = Result; } @@ -204,6 +263,7 @@ impl ModifyRequest { } } +#[cfg(feature = "rsidm_internal")] impl Message for ModifyRequest { type Result = Result; } @@ -253,7 +313,6 @@ pub struct AuthRequest { pub enum AuthAllowed { Anonymous, Password, - // TOTP, // Webauthn(String), } @@ -332,7 +391,7 @@ impl WhoamiResponse { #[cfg(test)] mod tests { - use crate::proto::v1::Filter as ProtoFilter; + use crate::v1::Filter as ProtoFilter; #[test] fn test_protofilter_simple() { let pf: ProtoFilter = ProtoFilter::Pres("class".to_string()); diff --git a/rsidm_tools/Cargo.toml b/rsidm_tools/Cargo.toml new file mode 100644 index 000000000..537f7e024 --- /dev/null +++ b/rsidm_tools/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "rsidm_tools" +version = "0.1.0" +authors = ["William Brown "] +edition = "2018" + +[[bin]] +name = "kanidm" +path = "src/main.rs" + +[dependencies] +rsidm_client = { path = "../rsidm_client" } + diff --git a/src/clients/main.rs b/rsidm_tools/src/main.rs similarity index 100% rename from src/clients/main.rs rename to rsidm_tools/src/main.rs diff --git a/src/clients/whoami.rs b/rsidm_tools/src/whoami.rs similarity index 100% rename from src/clients/whoami.rs rename to rsidm_tools/src/whoami.rs diff --git a/rsidmd/Cargo.toml b/rsidmd/Cargo.toml new file mode 100644 index 000000000..6279cffa0 --- /dev/null +++ b/rsidmd/Cargo.toml @@ -0,0 +1,55 @@ +# cargo-features = ["default-run"] + +[package] +name = "rsidm" +version = "0.1.0" +authors = ["William Brown "] +# default-run = "rsidm_core" +edition = "2018" + +[lib] +name = "rsidm" +path = "src/lib/lib.rs" + +[[bin]] +name = "rsidmd" +path = "src/server/main.rs" + + +[dependencies] +rsidm_proto = { path = "../rsidm_proto", features = ["rsidm_internal"] } + +actix = "0.7" +actix-web = "0.7" +bytes = "0.4" +log = "0.4" +env_logger = "0.6" +rand = "0.6" + +chrono = "0.4" +cookie = "0.11" +regex = "1" +lazy_static = "1.2.0" +lru = "0.1" + +tokio = "0.1" +futures = "0.1" +uuid = { version = "0.7", features = ["serde", "v4"] } +serde = "1.0" +serde_cbor = "0.10" +serde_json = "1.0" +serde_derive = "1.0" + +rusqlite = { version = "0.15", features = ["backup"] } +r2d2 = "0.8" +r2d2_sqlite = "0.7" + +structopt = { version = "0.2", default-features = false } +time = "0.1" + +concread = "0.1" + +openssl = "0.10" + +rpassword = "0.4" + diff --git a/src/lib/access.rs b/rsidmd/src/lib/access.rs similarity index 99% rename from src/lib/access.rs rename to rsidmd/src/lib/access.rs index a3655a31f..c6167116b 100644 --- a/src/lib/access.rs +++ b/rsidmd/src/lib/access.rs @@ -16,15 +16,15 @@ // use concread::cowcell::{CowCell, CowCellReadTxn, CowCellWriteTxn}; +use rsidm_proto::v1::Filter as ProtoFilter; +use rsidm_proto::v1::OperationError; use std::collections::{BTreeMap, BTreeSet}; use uuid::Uuid; use crate::audit::AuditScope; use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryReduced, EntryValid}; -use crate::error::OperationError; use crate::filter::{Filter, FilterValid}; use crate::modify::Modify; -use crate::proto::v1::Filter as ProtoFilter; use crate::server::{QueryServerTransaction, QueryServerWriteTransaction}; use crate::value::PartialValue; diff --git a/src/lib/proto/mod.rs b/rsidmd/src/lib/actors/mod.rs similarity index 100% rename from src/lib/proto/mod.rs rename to rsidmd/src/lib/actors/mod.rs diff --git a/src/lib/proto/v1/actors.rs b/rsidmd/src/lib/actors/v1.rs similarity index 81% rename from src/lib/proto/v1/actors.rs rename to rsidmd/src/lib/actors/v1.rs index b0d9db753..d7a63a3f9 100644 --- a/src/lib/proto/v1/actors.rs +++ b/rsidmd/src/lib/actors/v1.rs @@ -1,26 +1,64 @@ -use actix::prelude::*; use std::sync::Arc; use crate::audit::AuditScope; -use crate::be::Backend; use crate::async_log::EventLog; -use crate::error::OperationError; use crate::event::{ AuthEvent, CreateEvent, DeleteEvent, ModifyEvent, PurgeRecycledEvent, PurgeTombstoneEvent, SearchEvent, SearchResult, WhoamiResult, }; -use crate::schema::Schema; +use rsidm_proto::v1::OperationError; use crate::idm::server::IdmServer; use crate::server::{QueryServer, QueryServerTransaction}; -use crate::proto::v1::{ - AuthResponse, CreateRequest, DeleteRequest, ModifyRequest, OperationResponse, SearchRequest, - SearchResponse, WhoamiResponse, +use rsidm_proto::v1::{ + AuthRequest, AuthResponse, CreateRequest, DeleteRequest, ModifyRequest, OperationResponse, + SearchRequest, SearchResponse, UserAuthToken, WhoamiResponse, }; -use crate::proto::v1::messages::{AuthMessage, WhoamiMessage}; +use actix::prelude::*; +use uuid::Uuid; + +// These are used when the request (IE Get) has no intrising request +// type. Additionally, they are used in some requests where we need +// to supplement extra server state (IE userauthtokens) to a request. +// +// Generally we don't need to have the responses here because they are +// part of the protocol. + +pub struct WhoamiMessage { + pub uat: Option, +} + +impl WhoamiMessage { + pub fn new(uat: Option) -> Self { + WhoamiMessage { uat: uat } + } +} + +impl Message for WhoamiMessage { + type Result = Result; +} + +#[derive(Debug)] +pub struct AuthMessage { + pub sessionid: Option, + pub req: AuthRequest, +} + +impl AuthMessage { + pub fn new(req: AuthRequest, sessionid: Option) -> Self { + AuthMessage { + sessionid: sessionid, + req: req, + } + } +} + +impl Message for AuthMessage { + type Result = Result; +} pub struct QueryServerV1 { log: actix::Addr, @@ -46,54 +84,16 @@ impl QueryServerV1 { } } - // TODO #54: We could move most of the be/schema/qs setup and startup - // outside of this call, then pass in "what we need" in a cloneable - // form, this way we could have seperate Idm vs Qs threads, and dedicated - // threads for write vs read pub fn start( log: actix::Addr, - be: Backend, + query_server: QueryServer, + idms: IdmServer, threads: usize, - ) -> Result, OperationError> { - let mut audit = AuditScope::new("server_start"); - let log_inner = log.clone(); - - let qs_addr: Result, _> = audit_segment!(audit, || { - // Create "just enough" schema for us to be able to load from - // disk ... Schema loading is one time where we validate the - // entries as we read them, so we need this here. - let schema = match Schema::new(&mut audit) { - Ok(s) => s, - Err(e) => return Err(e), - }; - - // Create a query_server implementation - let query_server = QueryServer::new(be, schema); - - let mut audit_qsc = AuditScope::new("query_server_init"); - // TODO #62: Should the IDM parts be broken out to the IdmServer? - // What's important about this initial setup here is that it also triggers - // the schema and acp reload, so they are now configured correctly! - // Initialise the schema core. - // - // Now search for the schema itself, and validate that the system - // in memory matches the BE on disk, and that it's syntactically correct. - // Write it out if changes are needed. - query_server.initialise_helper(&mut audit_qsc)?; - - // We generate a SINGLE idms only! - - let idms = Arc::new(IdmServer::new(query_server.clone())); - - audit.append_scope(audit_qsc); - - let x = SyncArbiter::start(threads, move || { - QueryServerV1::new(log_inner.clone(), query_server.clone(), idms.clone()) - }); - Ok(x) - }); - log.do_send(audit); - qs_addr + ) -> actix::Addr { + let idms_arc = Arc::new(idms); + SyncArbiter::start(threads, move || { + QueryServerV1::new(log.clone(), query_server.clone(), idms_arc.clone()) + }) } } diff --git a/src/lib/async_log.rs b/rsidmd/src/lib/async_log.rs similarity index 100% rename from src/lib/async_log.rs rename to rsidmd/src/lib/async_log.rs diff --git a/src/lib/audit.rs b/rsidmd/src/lib/audit.rs similarity index 100% rename from src/lib/audit.rs rename to rsidmd/src/lib/audit.rs diff --git a/src/lib/be/dbentry.rs b/rsidmd/src/lib/be/dbentry.rs similarity index 100% rename from src/lib/be/dbentry.rs rename to rsidmd/src/lib/be/dbentry.rs diff --git a/rsidmd/src/lib/be/dbvalue.rs b/rsidmd/src/lib/be/dbvalue.rs new file mode 100644 index 000000000..5d511ca36 --- /dev/null +++ b/rsidmd/src/lib/be/dbvalue.rs @@ -0,0 +1,32 @@ +use uuid::Uuid; + +#[derive(Serialize, Deserialize, Debug)] +pub enum DbPasswordV1 { + PBKDF2(usize, Vec, Vec), +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct DbCredV1 { + pub password: Option, + pub claims: Vec, + pub uuid: Uuid, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct DbValueCredV1 { + pub t: String, + pub d: DbCredV1, +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum DbValueV1 { + U8(String), + I8(String), + UU(Uuid), + BO(bool), + SY(usize), + IN(usize), + RF(Uuid), + JF(String), + CR(DbValueCredV1), +} diff --git a/src/lib/be/idl.rs b/rsidmd/src/lib/be/idl.rs similarity index 100% rename from src/lib/be/idl.rs rename to rsidmd/src/lib/be/idl.rs diff --git a/src/lib/be/mem_be/mod.rs b/rsidmd/src/lib/be/mem_be/mod.rs similarity index 100% rename from src/lib/be/mem_be/mod.rs rename to rsidmd/src/lib/be/mem_be/mod.rs diff --git a/src/lib/be/mod.rs b/rsidmd/src/lib/be/mod.rs similarity index 99% rename from src/lib/be/mod.rs rename to rsidmd/src/lib/be/mod.rs index faaecf4b1..91a5d3eb0 100644 --- a/src/lib/be/mod.rs +++ b/rsidmd/src/lib/be/mod.rs @@ -12,8 +12,8 @@ use std::fs; use crate::audit::AuditScope; use crate::be::dbentry::DbEntry; use crate::entry::{Entry, EntryCommitted, EntryNew, EntryValid}; -use crate::error::{ConsistencyError, OperationError}; use crate::filter::{Filter, FilterValidResolved}; +use rsidm_proto::v1::{ConsistencyError, OperationError}; pub mod dbentry; pub mod dbvalue; diff --git a/src/lib/be/sqlite_be/mod.rs b/rsidmd/src/lib/be/sqlite_be/mod.rs similarity index 100% rename from src/lib/be/sqlite_be/mod.rs rename to rsidmd/src/lib/be/sqlite_be/mod.rs diff --git a/src/lib/config.rs b/rsidmd/src/lib/config.rs similarity index 79% rename from src/lib/config.rs rename to rsidmd/src/lib/config.rs index a4bbdf312..bcb416b77 100644 --- a/src/lib/config.rs +++ b/rsidmd/src/lib/config.rs @@ -1,6 +1,11 @@ use rand::prelude::*; use std::path::PathBuf; +#[derive(Serialize, Deserialize, Debug)] +pub struct IntegrationTestConfig { + pub admin_password: String, +} + #[derive(Serialize, Deserialize, Debug)] pub struct Configuration { pub address: String, @@ -11,6 +16,7 @@ pub struct Configuration { pub maximum_request: usize, pub secure_cookies: bool, pub cookie_key: [u8; 32], + pub integration_test_config: Option>, } impl Configuration { @@ -24,8 +30,9 @@ impl Configuration { // log type // log path // TODO #63: default true in prd - secure_cookies: false, + secure_cookies: if cfg!(test) { false } else { true }, cookie_key: [0; 32], + integration_test_config: None, }; let mut rng = StdRng::from_entropy(); rng.fill(&mut c.cookie_key); diff --git a/src/lib/constants.rs b/rsidmd/src/lib/constants.rs similarity index 97% rename from src/lib/constants.rs rename to rsidmd/src/lib/constants.rs index 793525871..dd4e692a3 100644 --- a/src/lib/constants.rs +++ b/rsidmd/src/lib/constants.rs @@ -293,8 +293,9 @@ pub static JSON_SCHEMA_ATTR_SSH_PUBLICKEY: &'static str = r#" } } "#; -pub static UUID_SCHEMA_ATTR_PASSWORD: &'static str = "00000000-0000-0000-0000-ffff00000043"; -pub static JSON_SCHEMA_ATTR_PASSWORD: &'static str = r#" +pub static UUID_SCHEMA_ATTR_PRIMARY_CREDENTIAL: &'static str = + "00000000-0000-0000-0000-ffff00000043"; +pub static JSON_SCHEMA_ATTR_PRIMARY_CREDENTIAL: &'static str = r#" { "valid": { "uuid": "00000000-0000-0000-0000-ffff00000043" @@ -307,17 +308,17 @@ pub static JSON_SCHEMA_ATTR_PASSWORD: &'static str = r#" "attributetype" ], "description": [ - "password hash material of the object for authentication" + "Primary credential material of the account for authentication interactively." ], "index": [], "multivalue": [ - "true" + "false" ], "name": [ - "password" + "primary_credential" ], "syntax": [ - "UTF8STRING" + "CREDENTIAL" ], "uuid": [ "00000000-0000-0000-0000-ffff00000043" @@ -411,7 +412,7 @@ pub static JSON_SCHEMA_CLASS_ACCOUNT: &'static str = r#" "account" ], "systemmay": [ - "password", + "primary_credential", "ssh_publickey" ], "systemmust": [ diff --git a/src/lib/core.rs b/rsidmd/src/lib/core.rs similarity index 79% rename from src/lib/core.rs rename to rsidmd/src/lib/core.rs index 2fffec58c..08156b735 100644 --- a/src/lib/core.rs +++ b/rsidmd/src/lib/core.rs @@ -12,19 +12,20 @@ use time::Duration; use crate::config::Configuration; // SearchResult +use crate::actors::v1::QueryServerV1; +use crate::actors::v1::{AuthMessage, WhoamiMessage}; use crate::async_log; use crate::audit::AuditScope; use crate::be::{Backend, BackendTransaction}; -use crate::error::OperationError; +use crate::idm::server::IdmServer; use crate::interval::IntervalActor; -use crate::proto::v1::actors::QueryServerV1; -use crate::proto::v1::messages::{AuthMessage, WhoamiMessage}; -use crate::proto::v1::{ +use crate::schema::Schema; +use crate::server::QueryServer; +use rsidm_proto::v1::OperationError; +use rsidm_proto::v1::{ AuthRequest, AuthState, CreateRequest, DeleteRequest, ModifyRequest, SearchRequest, UserAuthToken, }; -use crate::schema::Schema; -use crate::server::QueryServer; use uuid::Uuid; @@ -266,6 +267,45 @@ fn setup_backend(config: &Configuration) -> Result { be } +// TODO #54: We could move most of the be/schema/qs setup and startup +// outside of this call, then pass in "what we need" in a cloneable +// form, this way we could have seperate Idm vs Qs threads, and dedicated +// threads for write vs read +fn setup_qs_idms( + audit: &mut AuditScope, + be: Backend, +) -> Result<(QueryServer, IdmServer), OperationError> { + // Create "just enough" schema for us to be able to load from + // disk ... Schema loading is one time where we validate the + // entries as we read them, so we need this here. + let schema = match Schema::new(audit) { + Ok(s) => s, + Err(e) => { + error!("Failed to setup in memory schema: {:?}", e); + return Err(e); + } + }; + + // Create a query_server implementation + let query_server = QueryServer::new(be, schema); + + // TODO #62: Should the IDM parts be broken out to the IdmServer? + // What's important about this initial setup here is that it also triggers + // the schema and acp reload, so they are now configured correctly! + // Initialise the schema core. + // + // Now search for the schema itself, and validate that the system + // in memory matches the BE on disk, and that it's syntactically correct. + // Write it out if changes are needed. + query_server.initialise_helper(audit)?; + + // We generate a SINGLE idms only! + + let idms = IdmServer::new(query_server.clone()); + + Ok((query_server, idms)) +} + pub fn backup_server_core(config: Configuration, dst_path: &str) { let be = match setup_backend(&config) { Ok(be) => be, @@ -351,13 +391,59 @@ pub fn verify_server_core(config: Configuration) { // Now add IDM server verifications? } +pub fn recover_account_core(config: Configuration, name: String, password: String) { + let mut audit = AuditScope::new("recover_account"); + + // Start the backend. + let be = match setup_backend(&config) { + Ok(be) => be, + Err(e) => { + error!("Failed to setup BE: {:?}", e); + return; + } + }; + // setup the qs - *with* init of the migrations and schema. + let (_qs, idms) = match setup_qs_idms(&mut audit, be) { + Ok(t) => t, + Err(e) => { + debug!("{}", audit); + error!("Unable to setup query server or idm server -> {:?}", e); + return; + } + }; + + // Run the password change. + let mut idms_prox_write = idms.proxy_write(); + match idms_prox_write.recover_account(&mut audit, name, password) { + Ok(_) => { + idms_prox_write + .commit(&mut audit) + .expect("A critical error during commit occured."); + debug!("{}", audit); + info!("Password reset!"); + } + Err(e) => { + error!("Error during password reset -> {:?}", e); + debug!("{}", audit); + // abort the txn + std::mem::drop(idms_prox_write); + std::process::exit(1); + } + }; +} + pub fn create_server_core(config: Configuration) { // Until this point, we probably want to write to the log macro fns. + if config.integration_test_config.is_some() { + warn!("RUNNING IN INTEGRATION TEST MODE."); + warn!("IF YOU SEE THIS IN PRODUCTION YOU MUST CONTACT SUPPORT IMMEDIATELY."); + } + + info!("Starting rsidm with configuration: {:?}", config); // The log server is started on it's own thread, and is contacted // asynchronously. let log_addr = async_log::start(); - log_event!(log_addr, "Starting rsidm with configuration: {:?}", config); // Similar, create a stats thread which aggregates statistics from the // server as they come in. @@ -366,22 +452,56 @@ pub fn create_server_core(config: Configuration) { let be = match setup_backend(&config) { Ok(be) => be, Err(e) => { - error!("Failed to setup BE: {:?}", e); + error!("Failed to setup BE -> {:?}", e); return; } }; - // Start the query server with the given be path: future config - let server_addr = match QueryServerV1::start(log_addr.clone(), be, config.threads) { - Ok(addr) => addr, + let mut audit = AuditScope::new("setup_qs_idms"); + // Start the IDM server. + let (qs, idms) = match setup_qs_idms(&mut audit, be) { + Ok(t) => t, Err(e) => { - println!( - "An unknown failure in startup has occured - exiting -> {:?}", - e - ); + debug!("{}", audit); + error!("Unable to setup query server or idm server -> {:?}", e); return; } }; + // Any pre-start tasks here. + match &config.integration_test_config { + Some(itc) => { + let mut idms_prox_write = idms.proxy_write(); + match idms_prox_write.recover_account( + &mut audit, + "admin".to_string(), + itc.admin_password.clone(), + ) { + Ok(_) => {} + Err(e) => { + debug!("{}", audit); + error!( + "Unable to configure INTERGATION TEST admin account -> {:?}", + e + ); + return; + } + }; + match idms_prox_write.commit(&mut audit) { + Ok(_) => {} + Err(e) => { + debug!("{}", audit); + error!("Unable to commit INTERGATION TEST setup -> {:?}", e); + return; + } + } + } + None => {} + } + log_addr.do_send(audit); + + // Pass it to the actor for threading. + // Start the query server with the given be path: future config + let server_addr = QueryServerV1::start(log_addr.clone(), qs, idms, config.threads); // Setup timed events let _int_addr = IntervalActor::new(server_addr.clone()).start(); diff --git a/rsidmd/src/lib/credential.rs b/rsidmd/src/lib/credential.rs new file mode 100644 index 000000000..711ff5b64 --- /dev/null +++ b/rsidmd/src/lib/credential.rs @@ -0,0 +1,222 @@ +use crate::be::dbvalue::{DbCredV1, DbPasswordV1}; +use openssl::hash::MessageDigest; +use openssl::pkcs5::pbkdf2_hmac; +use rand::prelude::*; +use std::convert::TryFrom; +use uuid::Uuid; + +// These are in order of "relative" strength. +/* +#[derive(Clone, Debug)] +pub enum Policy { + PasswordOnly, + WebauthnOnly, + GeneratedPassword, + PasswordAndWebauthn, +} +*/ + +// TODO: Determine this at startup based on a time factor +const PBKDF2_COST: usize = 10000; +// NIST 800-63.b salt should be 112 bits -> 14 8u8. +// I choose tinfoil hat though ... +const PBKDF2_SALT_LEN: usize = 24; +// 64 * u8 -> 512 bits of out. +const PBKDF2_KEY_LEN: usize = 64; + +// Why PBKDF2? Rust's bcrypt has a number of hardcodings like max pw len of 72 +// I don't really feel like adding in so many restrictions, so I'll use +// pbkdf2 in openssl because it doesn't have the same limits. +#[derive(Clone, Debug)] +enum KDF { + // cost, salt, hash + PBKDF2(usize, Vec, Vec), +} + +#[derive(Clone, Debug)] +pub struct Password { + material: KDF, +} + +impl TryFrom for Password { + type Error = (); + + fn try_from(value: DbPasswordV1) -> Result { + match value { + DbPasswordV1::PBKDF2(c, s, h) => Ok(Password { + material: KDF::PBKDF2(c, s, h), + }), + } + } +} + +impl Password { + fn new_pbkdf2(cleartext: &str) -> KDF { + let mut rng = rand::thread_rng(); + let salt: Vec = (0..PBKDF2_SALT_LEN).map(|_| rng.gen()).collect(); + // This is 512 bits of output + let mut key: Vec = (0..PBKDF2_KEY_LEN).map(|_| 0).collect(); + + pbkdf2_hmac( + cleartext.as_bytes(), + salt.as_slice(), + PBKDF2_COST, + MessageDigest::sha256(), + key.as_mut_slice(), + ) + .expect("PBKDF2 failure"); + // Turn key to a vec. + KDF::PBKDF2(PBKDF2_COST, salt, key) + } + + pub fn new(cleartext: &str) -> Self { + Password { + material: Self::new_pbkdf2(cleartext), + } + } + + pub fn verify(&self, cleartext: &str) -> bool { + match &self.material { + KDF::PBKDF2(cost, salt, key) => { + let mut chal_key: Vec = (0..PBKDF2_KEY_LEN).map(|_| 0).collect(); + pbkdf2_hmac( + cleartext.as_bytes(), + salt.as_slice(), + *cost, + MessageDigest::sha256(), + chal_key.as_mut_slice(), + ) + .expect("PBKDF2 failure"); + // Actually compare the outputs. + &chal_key == key + } + } + } +} + +#[derive(Clone, Debug)] +/// This is how we store credentials in the server. An account can have many credentials, and +/// a credential can have many factors. Only successful auth to a credential as a whole unit +/// will succeed. For example: +/// A: Credential { password: aaa } +/// B: Credential { password: bbb, otp: ... } +/// In this case, if we selected credential B, and then provided password "aaa" we would deny +/// the auth as the password of B was incorrect. Additionally, while A only needs the "password", +/// B requires both the password and otp to be valid. +/// +/// In this way, each Credential provides it's own password requirements and policy, and requires +/// some metadata to support this such as it's source and strength etc. Some of these details are +/// to be resolved ... +pub struct Credential { + // Source (machine, user, ....). Strength? + // policy: Policy, + pub(crate) password: Option, + // webauthn: Option> + // totp: Option> + pub(crate) claims: Vec, + // Uuid of Credential, used by auth session to lock this specific credential + // if required. + pub(crate) uuid: Uuid, + // TODO: Add auth policy IE validUntil, lock state ... + // locked: bool +} + +impl TryFrom for Credential { + type Error = (); + + fn try_from(value: DbCredV1) -> Result { + // Work out what the policy is? + let DbCredV1 { + password, + claims, + uuid, + } = value; + + let v_password = match password { + Some(dbp) => Some(Password::try_from(dbp)?), + None => None, + }; + + Ok(Credential { + password: v_password, + claims: claims, + uuid: uuid, + }) + } +} + +impl Credential { + pub fn new_password_only(cleartext: &str) -> Self { + Credential { + password: Some(Password::new(cleartext)), + claims: Vec::new(), + uuid: Uuid::new_v4(), + } + } + + pub fn set_password(&self, cleartext: &str) -> Self { + Credential { + password: Some(Password::new(cleartext)), + claims: self.claims.clone(), + uuid: self.uuid.clone(), + } + } + + #[cfg(test)] + pub fn verify_password(&self, cleartext: &str) -> bool { + match &self.password { + Some(pw) => pw.verify(cleartext), + None => panic!(), + } + } + + pub fn to_db_valuev1(&self) -> DbCredV1 { + DbCredV1 { + password: match &self.password { + Some(pw) => match &pw.material { + KDF::PBKDF2(cost, salt, hash) => { + Some(DbPasswordV1::PBKDF2(*cost, salt.clone(), hash.clone())) + } + }, + None => None, + }, + claims: self.claims.clone(), + uuid: self.uuid.clone(), + } + } + + /* + pub fn add_claim(&mut self) { + } + + pub fn remove_claim(&mut self) { + } + */ + + /* + pub fn modify_password(&mut self) { + // Change the password + } + + pub fn add_webauthn_token() { + } + + pub fn remove_webauthn_token() { + } + */ +} + +#[cfg(test)] +mod tests { + use crate::credential::*; + + #[test] + fn test_credential_simple() { + let c = Credential::new_password_only("password"); + assert!(c.verify_password("password")); + assert!(!c.verify_password("password1")); + assert!(!c.verify_password("Password1")); + assert!(!c.verify_password("It Works!")); + assert!(!c.verify_password("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); + } +} diff --git a/src/lib/entry.rs b/rsidmd/src/lib/entry.rs similarity index 99% rename from src/lib/entry.rs rename to rsidmd/src/lib/entry.rs index ed24a9517..fdd7c98ca 100644 --- a/src/lib/entry.rs +++ b/rsidmd/src/lib/entry.rs @@ -1,14 +1,15 @@ // use serde_json::{Error, Value}; use crate::audit::AuditScope; -use crate::error::{OperationError, SchemaError}; +use crate::credential::Credential; use crate::filter::{Filter, FilterInvalid, FilterResolved, FilterValidResolved}; use crate::modify::{Modify, ModifyInvalid, ModifyList, ModifyValid}; -use crate::proto::v1::Entry as ProtoEntry; -use crate::proto::v1::Filter as ProtoFilter; use crate::schema::{SchemaAttribute, SchemaClass, SchemaTransaction}; use crate::server::{QueryServerTransaction, QueryServerWriteTransaction}; use crate::value::{IndexType, SyntaxType}; use crate::value::{PartialValue, Value}; +use rsidm_proto::v1::Entry as ProtoEntry; +use rsidm_proto::v1::Filter as ProtoFilter; +use rsidm_proto::v1::{OperationError, SchemaError}; use crate::be::dbentry::{DbEntry, DbEntryV1, DbEntryVers}; @@ -831,6 +832,13 @@ impl Entry { } } + pub fn get_ava_single_credential(&self, attr: &str) -> Option<&Credential> { + match self.get_ava_single(attr) { + Some(a) => a.to_credential(), + None => None, + } + } + pub fn get_ava_reference_uuid(&self, attr: &str) -> Option> { // If any value is NOT a reference, return none! match self.attrs.get(attr) { diff --git a/src/lib/event.rs b/rsidmd/src/lib/event.rs similarity index 95% rename from src/lib/event.rs rename to rsidmd/src/lib/event.rs index f692a2ad6..212102edc 100644 --- a/src/lib/event.rs +++ b/rsidmd/src/lib/event.rs @@ -1,19 +1,19 @@ use crate::audit::AuditScope; use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryReduced, EntryValid}; use crate::filter::{Filter, FilterValid}; -use crate::proto::v1::Entry as ProtoEntry; -use crate::proto::v1::{ +use rsidm_proto::v1::Entry as ProtoEntry; +use rsidm_proto::v1::{ AuthCredential, AuthResponse, AuthState, AuthStep, CreateRequest, DeleteRequest, ModifyRequest, ReviveRecycledRequest, SearchRequest, SearchResponse, UserAuthToken, WhoamiResponse, }; // use error::OperationError; -use crate::error::OperationError; use crate::modify::{ModifyList, ModifyValid}; use crate::server::{ QueryServerReadTransaction, QueryServerTransaction, QueryServerWriteTransaction, }; +use rsidm_proto::v1::OperationError; -use crate::proto::v1::messages::AuthMessage; +use crate::actors::v1::AuthMessage; // Bring in schematransaction trait for validate // use crate::schema::SchemaTransaction; @@ -23,7 +23,7 @@ use crate::filter::FilterInvalid; #[cfg(test)] use crate::modify::ModifyInvalid; #[cfg(test)] -use crate::proto::v1::SearchRecycledRequest; +use rsidm_proto::v1::SearchRecycledRequest; use actix::prelude::*; use uuid::Uuid; @@ -613,12 +613,28 @@ impl AuthEventStep { } #[cfg(test)] - pub fn anonymous_cred_step(sid: Uuid) -> Self { + pub fn named_init(name: &str) -> Self { + AuthEventStep::Init(AuthEventStepInit { + name: name.to_string(), + appid: None, + }) + } + + #[cfg(test)] + pub fn cred_step_anonymous(sid: Uuid) -> Self { AuthEventStep::Creds(AuthEventStepCreds { sessionid: sid, creds: vec![AuthCredential::Anonymous], }) } + + #[cfg(test)] + pub fn cred_step_password(sid: Uuid, pw: &str) -> Self { + AuthEventStep::Creds(AuthEventStepCreds { + sessionid: sid, + creds: vec![AuthCredential::Password(pw.to_string())], + }) + } } #[derive(Debug)] @@ -645,10 +661,26 @@ impl AuthEvent { } #[cfg(test)] - pub fn anonymous_cred_step(sid: Uuid) -> Self { + pub fn named_init(name: &str) -> Self { AuthEvent { event: None, - step: AuthEventStep::anonymous_cred_step(sid), + step: AuthEventStep::named_init(name), + } + } + + #[cfg(test)] + pub fn cred_step_anonymous(sid: Uuid) -> Self { + AuthEvent { + event: None, + step: AuthEventStep::cred_step_anonymous(sid), + } + } + + #[cfg(test)] + pub fn cred_step_password(sid: Uuid, pw: &str) -> Self { + AuthEvent { + event: None, + step: AuthEventStep::cred_step_password(sid, pw), } } } diff --git a/src/lib/filter.rs b/rsidmd/src/lib/filter.rs similarity index 99% rename from src/lib/filter.rs rename to rsidmd/src/lib/filter.rs index fdeedead3..b523586a7 100644 --- a/src/lib/filter.rs +++ b/rsidmd/src/lib/filter.rs @@ -3,14 +3,14 @@ // entry to assert it matches. use crate::audit::AuditScope; -use crate::error::{OperationError, SchemaError}; use crate::event::{Event, EventOrigin}; -use crate::proto::v1::Filter as ProtoFilter; use crate::schema::SchemaTransaction; use crate::server::{ QueryServerReadTransaction, QueryServerTransaction, QueryServerWriteTransaction, }; use crate::value::PartialValue; +use rsidm_proto::v1::Filter as ProtoFilter; +use rsidm_proto::v1::{OperationError, SchemaError}; use std::cmp::{Ordering, PartialOrd}; use std::collections::BTreeSet; diff --git a/src/lib/idm/account.rs b/rsidmd/src/lib/idm/account.rs similarity index 63% rename from src/lib/idm/account.rs rename to rsidmd/src/lib/idm/account.rs index d3667bbec..d0cd57909 100644 --- a/src/lib/idm/account.rs +++ b/rsidmd/src/lib/idm/account.rs @@ -1,11 +1,13 @@ use crate::entry::{Entry, EntryCommitted, EntryValid}; -use crate::error::OperationError; +use rsidm_proto::v1::OperationError; -use crate::proto::v1::UserAuthToken; +use rsidm_proto::v1::UserAuthToken; +use crate::credential::Credential; use crate::idm::claim::Claim; use crate::idm::group::Group; -use crate::value::PartialValue; +use crate::modify::{ModifyInvalid, ModifyList}; +use crate::value::{PartialValue, Value}; use uuid::Uuid; @@ -25,10 +27,10 @@ pub(crate) struct Account { pub displayname: String, pub uuid: Uuid, pub groups: Vec, - // creds (various types) - // groups? - // claims? - // account expiry? + pub primary: Option, + // primary: Credential + // app_creds: Vec + // account expiry? (as opposed to cred expiry) } impl Account { @@ -55,6 +57,10 @@ impl Account { OperationError::InvalidAccountState("Missing attribute: displayname"), )?; + let primary = value + .get_ava_single_credential("primary_credential") + .map(|v| v.clone()); + // TODO #71: Resolve groups!!!! let groups = Vec::new(); @@ -65,6 +71,7 @@ impl Account { name: name, displayname: displayname, groups: groups, + primary: primary, }) } @@ -85,6 +92,36 @@ impl Account { claims: claims.iter().map(|c| c.into_proto()).collect(), }) } + + pub(crate) fn gen_password_mod( + &self, + cleartext: &str, + appid: &Option, + ) -> Result, OperationError> { + // What should this look like? Probablf an appid + stuff -> modify? + // then the caller has to apply the modify under the requests event + // for proper auth checks. + match appid { + Some(_) => Err(OperationError::InvalidState), + None => { + // TODO: Enforce PW policy. Can we allow this change? + match &self.primary { + // Change the cred + Some(primary) => { + let ncred = primary.set_password(cleartext); + let vcred = Value::new_credential("primary", ncred); + Ok(ModifyList::new_purge_and_set("primary_credential", vcred)) + } + // Make a new credential instead + None => { + let ncred = Credential::new_password_only(cleartext); + let vcred = Value::new_credential("primary", ncred); + Ok(ModifyList::new_purge_and_set("primary_credential", vcred)) + } + } + } // no appid + } + } } // Need to also add a "to UserAuthToken" ... @@ -113,4 +150,10 @@ mod tests { // For now, nothing, but later, we'll test different types of cred // passing. } + + #[test] + fn test_idm_account_set_credential() { + // Using a real entry, set a credential back to it's entry. + // In the end, this boils down to a modify operation on the Value + } } diff --git a/src/lib/idm/authsession.rs b/rsidmd/src/lib/idm/authsession.rs similarity index 59% rename from src/lib/idm/authsession.rs rename to rsidmd/src/lib/idm/authsession.rs index ca3a40c02..289d9d823 100644 --- a/src/lib/idm/authsession.rs +++ b/rsidmd/src/lib/idm/authsession.rs @@ -1,9 +1,13 @@ use crate::audit::AuditScope; use crate::constants::UUID_ANONYMOUS; -use crate::error::OperationError; use crate::idm::account::Account; use crate::idm::claim::Claim; -use crate::proto::v1::{AuthAllowed, AuthCredential, AuthState}; +use rsidm_proto::v1::OperationError; +use rsidm_proto::v1::{AuthAllowed, AuthCredential, AuthState}; + +use crate::credential::{Credential, Password}; + +use std::convert::TryFrom; // Each CredHandler takes one or more credentials and determines if the // handlers requirements can be 100% fufilled. This is where MFA or other @@ -18,22 +22,40 @@ enum CredState { #[derive(Clone, Debug)] enum CredHandler { + Denied, + // The bool is a flag if the cred has been authed against. Anonymous, // AppPassword // { // Password - // Webauthn - // Webauthn + Password - // TOTP - // TOTP + Password - // } <<-- could all these be "AccountPrimary" and pass to Account? - // Selection at this level could be premature ... - // Verification Link? + Password(Password), // Webauthn + // Webauthn + Password + // TOTP + // TOTP + Password + // } <<-- could all these be "AccountPrimary" and pass to Account? + // Selection at this level could be premature ... + // Verification Link? +} + +impl TryFrom<&Credential> for CredHandler { + type Error = (); + // Is there a nicer implementation of this? + fn try_from(c: &Credential) -> Result { + if c.password.is_some() { + Ok(CredHandler::Password(c.password.clone().unwrap())) + } else { + Err(()) + } + } } impl CredHandler { pub fn validate(&mut self, creds: &Vec) -> CredState { match self { + CredHandler::Denied => { + // Sad trombone. + CredState::Denied("authentication denied") + } CredHandler::Anonymous => { creds.iter().fold( CredState::Continue(vec![AuthAllowed::Anonymous]), @@ -62,12 +84,48 @@ impl CredHandler { }, ) } // end credhandler::anonymous + CredHandler::Password(pw) => { + creds.iter().fold( + // If no creds, remind that we want pw ... + CredState::Continue(vec![AuthAllowed::Password]), + |acc, cred| { + match acc { + // If failed, continue to fail. + CredState::Denied(_) => acc, + _ => { + match cred { + AuthCredential::Password(cleartext) => { + if pw.verify(cleartext.as_str()) { + CredState::Success(Vec::new()) + } else { + CredState::Denied("incorrect password") + } + } + // All other cases fail. + _ => CredState::Denied("pw authentication denied"), + } + } + } // end match acc + }, + ) + } // end credhandler::password } } pub fn valid_auth_mechs(&self) -> Vec { match &self { + CredHandler::Denied => Vec::new(), CredHandler::Anonymous => vec![AuthAllowed::Anonymous], + CredHandler::Password(_) => vec![AuthAllowed::Password], + // webauth + // mfa + } + } + + pub(crate) fn is_denied(&self) -> bool { + match &self { + CredHandler::Denied => true, + _ => false, } } } @@ -85,6 +143,8 @@ pub(crate) struct AuthSession { handler: CredHandler, // Store any related appid we are processing for. appid: Option, + // Store claims related to the handler + // need to store state somehow? finished: bool, } @@ -94,9 +154,7 @@ impl AuthSession { // for this session. This is currently based on presentation of an application // id. let handler = match appid { - Some(_) => { - unimplemented!(); - } + Some(_) => CredHandler::Denied, None => { // We want the primary handler - this is where we make a decision // based on the anonymous ... in theory this could be cleaner @@ -104,7 +162,15 @@ impl AuthSession { if account.uuid == UUID_ANONYMOUS.clone() { CredHandler::Anonymous } else { - unimplemented!(); + // Now we see if they have one ... + match &account.primary { + Some(cred) => { + // TODO: Log this corruption better ... :( + // Probably means new authsession has to be failable + CredHandler::try_from(cred).unwrap_or_else(|_| CredHandler::Denied) + } + None => CredHandler::Denied, + } } } }; @@ -115,11 +181,14 @@ impl AuthSession { // we store in the account somehow? // TODO #59: Implement handler locking! + // if credhandler == deny, finish = true. + let finished: bool = handler.is_denied(); + AuthSession { account: account, handler: handler, appid: appid, - finished: false, + finished: finished, } } @@ -179,12 +248,13 @@ impl AuthSession { #[cfg(test)] mod tests { - use crate::constants::JSON_ANONYMOUS_V1; + use crate::constants::{JSON_ADMIN_V1, JSON_ANONYMOUS_V1}; + use crate::credential::Credential; use crate::idm::authsession::AuthSession; - use crate::proto::v1::AuthAllowed; + use rsidm_proto::v1::AuthAllowed; #[test] - fn test_idm_account_anonymous_auth_mech() { + fn test_idm_authsession_anonymous_auth_mech() { let anon_account = entry_str_to_account!(JSON_ANONYMOUS_V1); let session = AuthSession::new(anon_account, None); @@ -198,4 +268,36 @@ mod tests { }) ); } + + #[test] + fn test_idm_authsession_missing_appid() { + let anon_account = entry_str_to_account!(JSON_ANONYMOUS_V1); + + let session = AuthSession::new(anon_account, Some("NonExistantAppID".to_string())); + + let auth_mechs = session.valid_auth_mechs(); + + // Will always move to denied. + assert!(auth_mechs == Vec::new()); + } + + #[test] + fn test_idm_authsession_simple_password_mech() { + // create the ent + let mut account = entry_str_to_account!(JSON_ADMIN_V1); + // manually load in a cred + let cred = Credential::new_password_only("test_password"); + account.primary = Some(cred); + + // now check + let session = AuthSession::new(account, None); + let auth_mechs = session.valid_auth_mechs(); + + assert!( + true == auth_mechs.iter().fold(false, |acc, x| match x { + AuthAllowed::Password => true, + _ => acc, + }) + ); + } } diff --git a/src/lib/idm/claim.rs b/rsidmd/src/lib/idm/claim.rs similarity index 87% rename from src/lib/idm/claim.rs rename to rsidmd/src/lib/idm/claim.rs index f456b684d..79492ecf5 100644 --- a/src/lib/idm/claim.rs +++ b/rsidmd/src/lib/idm/claim.rs @@ -1,4 +1,4 @@ -use crate::proto::v1::Claim as ProtoClaim; +use rsidm_proto::v1::Claim as ProtoClaim; #[derive(Debug)] pub struct Claim { diff --git a/rsidmd/src/lib/idm/event.rs b/rsidmd/src/lib/idm/event.rs new file mode 100644 index 000000000..3f5d34ff9 --- /dev/null +++ b/rsidmd/src/lib/idm/event.rs @@ -0,0 +1,21 @@ +use crate::event::Event; +use uuid::Uuid; + +#[derive(Debug)] +pub struct PasswordChangeEvent { + pub event: Event, + pub target: Uuid, + pub cleartext: String, + pub appid: Option, +} + +impl PasswordChangeEvent { + pub fn new_internal(target: &Uuid, cleartext: &str, appid: Option<&str>) -> Self { + PasswordChangeEvent { + event: Event::from_internal(), + target: target.clone(), + cleartext: cleartext.to_string(), + appid: appid.map(|v| v.to_string()), + } + } +} diff --git a/src/lib/idm/group.rs b/rsidmd/src/lib/idm/group.rs similarity index 83% rename from src/lib/idm/group.rs rename to rsidmd/src/lib/idm/group.rs index 4820805e7..8eb2b9db9 100644 --- a/src/lib/idm/group.rs +++ b/rsidmd/src/lib/idm/group.rs @@ -1,4 +1,4 @@ -use crate::proto::v1::Group as ProtoGroup; +use rsidm_proto::v1::Group as ProtoGroup; #[derive(Debug, Clone)] pub struct Group { diff --git a/src/lib/idm/identity.rs b/rsidmd/src/lib/idm/identity.rs similarity index 100% rename from src/lib/idm/identity.rs rename to rsidmd/src/lib/idm/identity.rs diff --git a/src/lib/idm/macros.rs b/rsidmd/src/lib/idm/macros.rs similarity index 100% rename from src/lib/idm/macros.rs rename to rsidmd/src/lib/idm/macros.rs diff --git a/src/lib/idm/mod.rs b/rsidmd/src/lib/idm/mod.rs similarity index 93% rename from src/lib/idm/mod.rs rename to rsidmd/src/lib/idm/mod.rs index a018c29d5..c8d726292 100644 --- a/src/lib/idm/mod.rs +++ b/rsidmd/src/lib/idm/mod.rs @@ -1,5 +1,6 @@ #[macro_use] mod macros; +mod event; pub(crate) mod account; pub(crate) mod authsession; diff --git a/src/lib/idm/server.rs b/rsidmd/src/lib/idm/server.rs similarity index 52% rename from src/lib/idm/server.rs rename to rsidmd/src/lib/idm/server.rs index 62d4f33e1..8e37a3453 100644 --- a/src/lib/idm/server.rs +++ b/rsidmd/src/lib/idm/server.rs @@ -1,16 +1,17 @@ use crate::audit::AuditScope; -use crate::error::OperationError; use crate::event::{AuthEvent, AuthEventStep, AuthResult}; use crate::idm::account::Account; use crate::idm::authsession::AuthSession; -use crate::proto::v1::AuthState; -use crate::server::{QueryServer, QueryServerTransaction}; +use crate::idm::event::PasswordChangeEvent; +use crate::server::{QueryServer, QueryServerTransaction, QueryServerWriteTransaction}; use crate::value::PartialValue; +use rsidm_proto::v1::AuthState; +use rsidm_proto::v1::OperationError; + use concread::cowcell::{CowCell, CowCellWriteTxn}; use std::collections::BTreeMap; use uuid::Uuid; -// use lru::LruCache; pub struct IdmServer { // There is a good reason to keep this single thread - it @@ -19,6 +20,7 @@ pub struct IdmServer { // in memory caches related to locking. // // TODO #60: This needs a mark-and-sweep gc to be added. + // use split_off() sessions: CowCell>, // Need a reference to the query server. qs: QueryServer, @@ -40,6 +42,12 @@ pub struct IdmServerReadTransaction<'a> { } */ +pub struct IdmServerProxyWriteTransaction<'a> { + // This does NOT take any read to the memory content, allowing safe + // qs operations to occur through this interface. + qs_write: QueryServerWriteTransaction<'a>, +} + impl IdmServer { // TODO #59: Make number of authsessions configurable!!! pub fn new(qs: QueryServer) -> IdmServer { @@ -61,6 +69,12 @@ impl IdmServer { IdmServerReadTransaction { qs: &self.qs } } */ + + pub fn proxy_write(&self) -> IdmServerProxyWriteTransaction { + IdmServerProxyWriteTransaction { + qs_write: self.qs.write(), + } + } } impl<'a> IdmServerWriteTransaction<'a> { @@ -76,6 +90,7 @@ impl<'a> IdmServerWriteTransaction<'a> { match &ae.step { AuthEventStep::Init(init) => { // Allocate a session id. + // TODO: #60 - make this new_v1 and use the tstamp. let sessionid = Uuid::new_v4(); // Begin the auth procedure! @@ -180,12 +195,84 @@ impl<'a> IdmServerReadTransaction<'a> { } */ +impl<'a> IdmServerProxyWriteTransaction<'a> { + pub fn set_account_password( + &mut self, + au: &mut AuditScope, + pce: &PasswordChangeEvent, + ) -> Result<(), OperationError> { + // TODO: Is it a security issue to reveal pw policy checks BEFORE permission is + // determined over the credential modification? + // + // I don't think so - because we should only be showing how STRONG the pw is ... + + // Get the account + let account_entry = try_audit!(au, self.qs_write.internal_search_uuid(au, &pce.target)); + let account = try_audit!(au, Account::try_from_entry(account_entry)); + // Ask if tis all good - this step checks pwpolicy and such + // it returns a modify + let modlist = try_audit!( + au, + account.gen_password_mod(pce.cleartext.as_str(), &pce.appid) + ); + audit_log!(au, "processing change {:?}", modlist); + // given the new credential generate a modify + // We use impersonate here to get the event from ae + try_audit!( + au, + self.qs_write.impersonate_modify( + au, + // Filter as executed + filter!(f_eq("uuid", PartialValue::new_uuidr(&pce.target))), + // Filter as intended (acp) + filter_all!(f_eq("uuid", PartialValue::new_uuidr(&pce.target))), + modlist, + &pce.event, + ) + ); + + Ok(()) + } + + pub fn recover_account( + &mut self, + au: &mut AuditScope, + name: String, + cleartext: String, + ) -> Result<(), OperationError> { + // name to uuid + let target = try_audit!(au, self.qs_write.name_to_uuid(au, name.as_str())); + // internal pce. + let pce = PasswordChangeEvent::new_internal(&target, cleartext.as_str(), None); + // now set_account_password. + self.set_account_password(au, &pce) + } + + pub fn commit(self, au: &mut AuditScope) -> Result<(), OperationError> { + self.qs_write.commit(au) + } +} + // Need tests of the sessions and the auth ... #[cfg(test)] mod tests { - use crate::event::{AuthEvent, AuthResult}; - use crate::proto::v1::{AuthAllowed, AuthState}; + use crate::constants::UUID_ADMIN; + use crate::credential::Credential; + use crate::event::{AuthEvent, AuthResult, ModifyEvent}; + use crate::idm::event::PasswordChangeEvent; + use crate::modify::{Modify, ModifyList}; + use crate::value::{PartialValue, Value}; + use rsidm_proto::v1::OperationError; + use rsidm_proto::v1::{AuthAllowed, AuthState}; + + use crate::audit::AuditScope; + use crate::idm::server::IdmServer; + use crate::server::QueryServer; + use uuid::Uuid; + + static TEST_PASSWORD: &'static str = "ntaoeuntnaoeuhraohuercahu😍"; + static TEST_PASSWORD_INC: &'static str = "ntaoentu nkrcgaeunhibwmwmqj;k wqjbkx "; #[test] fn test_idm_anonymous_auth() { @@ -237,7 +324,7 @@ mod tests { { let mut idms_write = idms.write(); // Now send the anonymous request, given the session id. - let anon_step = AuthEvent::anonymous_cred_step(sid); + let anon_step = AuthEvent::cred_step_anonymous(sid); // Expect success let r2 = idms_write.auth(au, &anon_step); @@ -274,4 +361,166 @@ mod tests { } // Test sending anonymous but with no session init. + #[test] + fn test_idm_anonymous_auth_invalid_states() { + run_idm_test!(|_qs: &QueryServer, idms: &IdmServer, au: &mut AuditScope| { + { + let mut idms_write = idms.write(); + let sid = Uuid::new_v4(); + let anon_step = AuthEvent::cred_step_anonymous(sid); + + // Expect failure + let r2 = idms_write.auth(au, &anon_step); + println!("r2 ==> {:?}", r2); + + match r2 { + Ok(_) => { + error!("Auth state machine not correctly enforced!"); + panic!(); + } + Err(e) => match e { + OperationError::InvalidSessionState => {} + _ => panic!(), + }, + }; + } + }) + } + + fn init_admin_w_password( + au: &mut AuditScope, + qs: &QueryServer, + pw: &str, + ) -> Result<(), OperationError> { + let cred = Credential::new_password_only(pw); + let v_cred = Value::new_credential("primary", cred); + let mut qs_write = qs.write(); + + // now modify and provide a primary credential. + let me_inv_m = unsafe { + ModifyEvent::new_internal_invalid( + filter!(f_eq("name", PartialValue::new_iutf8s("admin"))), + ModifyList::new_list(vec![Modify::Present( + "primary_credential".to_string(), + v_cred, + )]), + ) + }; + // go! + assert!(qs_write.modify(au, &me_inv_m).is_ok()); + + qs_write.commit(au) + } + + fn init_admin_authsession_sid(idms: &IdmServer, au: &mut AuditScope) -> Uuid { + let mut idms_write = idms.write(); + let admin_init = AuthEvent::named_init("admin"); + + let r1 = idms_write.auth(au, &admin_init); + let ar = r1.unwrap(); + let AuthResult { sessionid, state } = ar; + + match state { + AuthState::Continue(_) => {} + _ => { + error!("Sessions was not initialised"); + panic!(); + } + }; + + idms_write.commit().expect("Must not fail"); + + sessionid + } + + #[test] + fn test_idm_simple_password_auth() { + run_idm_test!(|qs: &QueryServer, idms: &IdmServer, au: &mut AuditScope| { + init_admin_w_password(au, qs, TEST_PASSWORD).expect("Failed to setup admin account"); + let sid = init_admin_authsession_sid(idms, au); + + let mut idms_write = idms.write(); + let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD); + + // Expect success + let r2 = idms_write.auth(au, &anon_step); + println!("r2 ==> {:?}", r2); + + match r2 { + Ok(ar) => { + let AuthResult { + sessionid: _, + state, + } = ar; + match state { + AuthState::Success(_uat) => { + // Check the uat. + } + _ => { + error!("A critical error has occured! We have a non-succcess result!"); + panic!(); + } + } + } + Err(e) => { + error!("A critical error has occured! {:?}", e); + // Should not occur! + panic!(); + } + }; + + idms_write.commit().expect("Must not fail"); + }) + } + + #[test] + fn test_idm_simple_password_invalid() { + run_idm_test!(|qs: &QueryServer, idms: &IdmServer, au: &mut AuditScope| { + init_admin_w_password(au, qs, TEST_PASSWORD).expect("Failed to setup admin account"); + let sid = init_admin_authsession_sid(idms, au); + let mut idms_write = idms.write(); + let anon_step = AuthEvent::cred_step_password(sid, TEST_PASSWORD_INC); + + // Expect success + let r2 = idms_write.auth(au, &anon_step); + println!("r2 ==> {:?}", r2); + + match r2 { + Ok(ar) => { + let AuthResult { + sessionid: _, + state, + } = ar; + match state { + AuthState::Denied(_reason) => { + // Check the uat. + } + _ => { + error!("A critical error has occured! We have a non-denied result!"); + panic!(); + } + } + } + Err(e) => { + error!("A critical error has occured! {:?}", e); + // Should not occur! + panic!(); + } + }; + + idms_write.commit().expect("Must not fail"); + }) + } + + #[test] + fn test_idm_simple_password_reset() { + run_idm_test!(|_qs: &QueryServer, idms: &IdmServer, au: &mut AuditScope| { + let pce = PasswordChangeEvent::new_internal(&UUID_ADMIN, TEST_PASSWORD, None); + + let mut idms_prox_write = idms.proxy_write(); + assert!(idms_prox_write.set_account_password(au, &pce).is_ok()); + assert!(idms_prox_write.set_account_password(au, &pce).is_ok()); + assert!(idms_prox_write.commit(au).is_ok()); + }) + } } diff --git a/src/lib/interval.rs b/rsidmd/src/lib/interval.rs similarity index 96% rename from src/lib/interval.rs rename to rsidmd/src/lib/interval.rs index 682a464f9..fa3c23fd2 100644 --- a/src/lib/interval.rs +++ b/rsidmd/src/lib/interval.rs @@ -1,9 +1,9 @@ use actix::prelude::*; use std::time::Duration; +use crate::actors::v1::QueryServerV1; use crate::constants::PURGE_TIMEOUT; use crate::event::{PurgeRecycledEvent, PurgeTombstoneEvent}; -use crate::proto::v1::actors::QueryServerV1; pub struct IntervalActor { // Store any addresses we require diff --git a/rsidmd/src/lib/lib.rs b/rsidmd/src/lib/lib.rs new file mode 100644 index 000000000..bff102609 --- /dev/null +++ b/rsidmd/src/lib/lib.rs @@ -0,0 +1,37 @@ +#![deny(warnings)] +#![warn(unused_extern_crates)] + +#[macro_use] +extern crate log; +#[macro_use] +extern crate serde_derive; + +#[macro_use] +extern crate lazy_static; + +// This has to be before be so the import order works +#[macro_use] +mod macros; +#[macro_use] +mod async_log; +#[macro_use] +mod audit; +mod be; +pub mod constants; +mod credential; +mod entry; +mod event; +mod filter; +mod interval; +mod modify; +mod value; +#[macro_use] +mod plugins; +mod access; +mod actors; +mod idm; +mod schema; +mod server; + +pub mod config; +pub mod core; diff --git a/src/lib/macros.rs b/rsidmd/src/lib/macros.rs similarity index 100% rename from src/lib/macros.rs rename to rsidmd/src/lib/macros.rs diff --git a/src/lib/modify.rs b/rsidmd/src/lib/modify.rs similarity index 94% rename from src/lib/modify.rs rename to rsidmd/src/lib/modify.rs index cc0283af7..af5c5c1c8 100644 --- a/src/lib/modify.rs +++ b/rsidmd/src/lib/modify.rs @@ -1,11 +1,11 @@ use crate::audit::AuditScope; -use crate::proto::v1::Modify as ProtoModify; -use crate::proto::v1::ModifyList as ProtoModifyList; +use rsidm_proto::v1::Modify as ProtoModify; +use rsidm_proto::v1::ModifyList as ProtoModifyList; -use crate::error::{OperationError, SchemaError}; use crate::schema::SchemaTransaction; use crate::server::{QueryServerTransaction, QueryServerWriteTransaction}; use crate::value::{PartialValue, Value}; +use rsidm_proto::v1::{OperationError, SchemaError}; // Should this be std? use std::slice; @@ -15,7 +15,7 @@ pub struct ModifyValid; #[derive(Serialize, Deserialize, Debug)] pub struct ModifyInvalid; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Debug)] pub enum Modify { // This value *should* exist. Present(String, Value), @@ -56,7 +56,7 @@ impl Modify { } } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Debug)] pub struct ModifyList { valid: VALID, // The order of this list matters. Each change must be done in order. @@ -87,6 +87,10 @@ impl ModifyList { } } + pub fn new_purge_and_set(attr: &str, v: Value) -> Self { + Self::new_list(vec![m_purge(attr), Modify::Present(attr.to_string(), v)]) + } + pub fn push_mod(&mut self, modify: Modify) { self.mods.push(modify) } diff --git a/src/lib/plugins/base.rs b/rsidmd/src/lib/plugins/base.rs similarity index 99% rename from src/lib/plugins/base.rs rename to rsidmd/src/lib/plugins/base.rs index 8ba1d8637..e104d1419 100644 --- a/src/lib/plugins/base.rs +++ b/rsidmd/src/lib/plugins/base.rs @@ -7,13 +7,13 @@ use crate::audit::AuditScope; // use crate::constants::{STR_UUID_ADMIN, STR_UUID_ANONYMOUS, STR_UUID_DOES_NOT_EXIST}; use crate::constants::{UUID_ADMIN, UUID_ANONYMOUS, UUID_DOES_NOT_EXIST}; use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew}; -use crate::error::{ConsistencyError, OperationError}; use crate::event::{CreateEvent, ModifyEvent}; use crate::modify::Modify; use crate::server::{ QueryServerReadTransaction, QueryServerTransaction, QueryServerWriteTransaction, }; use crate::value::{PartialValue, Value}; +use rsidm_proto::v1::{ConsistencyError, OperationError}; lazy_static! { static ref CLASS_OBJECT: Value = Value::new_class("object"); @@ -273,11 +273,11 @@ mod tests { // use crate::plugins::Plugin; use crate::constants::JSON_ADMIN_V1; use crate::entry::{Entry, EntryInvalid, EntryNew}; - use crate::error::OperationError; use crate::modify::{Modify, ModifyList}; use crate::server::QueryServerTransaction; use crate::server::QueryServerWriteTransaction; use crate::value::{PartialValue, Value}; + use rsidm_proto::v1::OperationError; static JSON_ADMIN_ALLOW_ALL: &'static str = r#"{ "valid": null, diff --git a/src/lib/plugins/failure.rs b/rsidmd/src/lib/plugins/failure.rs similarity index 100% rename from src/lib/plugins/failure.rs rename to rsidmd/src/lib/plugins/failure.rs diff --git a/src/lib/plugins/macros.rs b/rsidmd/src/lib/plugins/macros.rs similarity index 100% rename from src/lib/plugins/macros.rs rename to rsidmd/src/lib/plugins/macros.rs diff --git a/src/lib/plugins/memberof.rs b/rsidmd/src/lib/plugins/memberof.rs similarity index 99% rename from src/lib/plugins/memberof.rs rename to rsidmd/src/lib/plugins/memberof.rs index 6e66a2ef2..c1c26e5c5 100644 --- a/src/lib/plugins/memberof.rs +++ b/rsidmd/src/lib/plugins/memberof.rs @@ -12,13 +12,13 @@ use crate::audit::AuditScope; use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryValid}; -use crate::error::{ConsistencyError, OperationError}; use crate::event::{CreateEvent, DeleteEvent, ModifyEvent}; use crate::modify::{Modify, ModifyList}; use crate::plugins::Plugin; use crate::server::QueryServerTransaction; use crate::server::{QueryServerReadTransaction, QueryServerWriteTransaction}; use crate::value::{PartialValue, Value}; +use rsidm_proto::v1::{ConsistencyError, OperationError}; use std::collections::BTreeSet; use uuid::Uuid; diff --git a/src/lib/plugins/mod.rs b/rsidmd/src/lib/plugins/mod.rs similarity index 99% rename from src/lib/plugins/mod.rs rename to rsidmd/src/lib/plugins/mod.rs index 7949cc523..543d03849 100644 --- a/src/lib/plugins/mod.rs +++ b/rsidmd/src/lib/plugins/mod.rs @@ -1,8 +1,8 @@ use crate::audit::AuditScope; use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryValid}; -use crate::error::{ConsistencyError, OperationError}; use crate::event::{CreateEvent, DeleteEvent, ModifyEvent}; use crate::server::{QueryServerReadTransaction, QueryServerWriteTransaction}; +use rsidm_proto::v1::{ConsistencyError, OperationError}; #[macro_use] mod macros; diff --git a/src/lib/plugins/protected.rs b/rsidmd/src/lib/plugins/protected.rs similarity index 99% rename from src/lib/plugins/protected.rs rename to rsidmd/src/lib/plugins/protected.rs index 5b73b9b9d..259f0232a 100644 --- a/src/lib/plugins/protected.rs +++ b/rsidmd/src/lib/plugins/protected.rs @@ -4,11 +4,11 @@ use crate::plugins::Plugin; use crate::audit::AuditScope; use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryValid}; -use crate::error::OperationError; use crate::event::{CreateEvent, DeleteEvent, ModifyEvent}; use crate::modify::Modify; use crate::server::QueryServerWriteTransaction; use crate::value::{PartialValue, Value}; +use rsidm_proto::v1::OperationError; use std::collections::HashSet; pub struct Protected {} @@ -159,8 +159,8 @@ impl Plugin for Protected { mod tests { use crate::constants::JSON_ADMIN_V1; use crate::entry::{Entry, EntryInvalid, EntryNew}; - use crate::error::OperationError; use crate::value::{PartialValue, Value}; + use rsidm_proto::v1::OperationError; static JSON_ADMIN_ALLOW_ALL: &'static str = r#"{ "valid": null, diff --git a/src/lib/plugins/recycle.rs b/rsidmd/src/lib/plugins/recycle.rs similarity index 100% rename from src/lib/plugins/recycle.rs rename to rsidmd/src/lib/plugins/recycle.rs diff --git a/src/lib/plugins/refint.rs b/rsidmd/src/lib/plugins/refint.rs similarity index 99% rename from src/lib/plugins/refint.rs rename to rsidmd/src/lib/plugins/refint.rs index 0e1060113..036b69d11 100644 --- a/src/lib/plugins/refint.rs +++ b/rsidmd/src/lib/plugins/refint.rs @@ -13,7 +13,6 @@ use std::collections::BTreeSet; use crate::audit::AuditScope; use crate::entry::{Entry, EntryCommitted, EntryNew, EntryValid}; -use crate::error::{ConsistencyError, OperationError}; use crate::event::{CreateEvent, DeleteEvent, ModifyEvent}; use crate::modify::{Modify, ModifyInvalid, ModifyList}; use crate::plugins::Plugin; @@ -21,6 +20,7 @@ use crate::schema::SchemaTransaction; use crate::server::QueryServerTransaction; use crate::server::{QueryServerReadTransaction, QueryServerWriteTransaction}; use crate::value::{PartialValue, Value}; +use rsidm_proto::v1::{ConsistencyError, OperationError}; use uuid::Uuid; // NOTE: This *must* be after base.rs!!! @@ -244,10 +244,10 @@ mod tests { // #[macro_use] // use crate::plugins::Plugin; use crate::entry::{Entry, EntryInvalid, EntryNew}; - use crate::error::OperationError; use crate::modify::{Modify, ModifyList}; use crate::server::{QueryServerTransaction, QueryServerWriteTransaction}; use crate::value::{PartialValue, Value}; + use rsidm_proto::v1::OperationError; // The create references a uuid that doesn't exist - reject #[test] diff --git a/src/lib/schema.rs b/rsidmd/src/lib/schema.rs similarity index 99% rename from src/lib/schema.rs rename to rsidmd/src/lib/schema.rs index d199f3e83..c65a86805 100644 --- a/src/lib/schema.rs +++ b/rsidmd/src/lib/schema.rs @@ -1,8 +1,8 @@ use crate::audit::AuditScope; use crate::constants::*; use crate::entry::{Entry, EntryCommitted, EntryNew, EntryValid}; -use crate::error::{ConsistencyError, OperationError, SchemaError}; use crate::value::{IndexType, PartialValue, SyntaxType, Value}; +use rsidm_proto::v1::{ConsistencyError, OperationError, SchemaError}; use std::borrow::Borrow; use std::collections::BTreeSet; @@ -171,6 +171,14 @@ impl SchemaAttribute { } } + fn validate_credential(&self, v: &Value) -> Result<(), SchemaError> { + if v.is_credential() { + Ok(()) + } else { + Err(SchemaError::InvalidAttributeSyntax) + } + } + fn validate_utf8string_insensitive(&self, v: &Value) -> Result<(), SchemaError> { if v.is_insensitive_utf8() { Ok(()) @@ -200,6 +208,7 @@ impl SchemaAttribute { SyntaxType::UTF8STRING_INSENSITIVE => v.is_iutf8(), SyntaxType::UTF8STRING => v.is_utf8(), SyntaxType::JSON_FILTER => v.is_json_filter(), + SyntaxType::CREDENTIAL => v.is_credential(), }; if r { Ok(()) @@ -287,6 +296,13 @@ impl SchemaAttribute { acc } }), + SyntaxType::CREDENTIAL => ava.iter().fold(Ok(()), |acc, v| { + if acc.is_ok() { + self.validate_credential(v) + } else { + acc + } + }), } } } @@ -1062,13 +1078,6 @@ impl SchemaInner { res } - // Normalise *does not* validate. - // Normalise just fixes some possible common issues, but it - // can't fix *everything* possibly wrong ... - pub fn normalise_filter(&mut self) { - unimplemented!() - } - fn is_multivalue(&self, attr_name: &str) -> Result { match self.attributes.get(attr_name) { Some(a_schema) => Ok(a_schema.multivalue), @@ -1189,7 +1198,7 @@ mod tests { use crate::audit::AuditScope; // use crate::constants::*; use crate::entry::{Entry, EntryInvalid, EntryNew, EntryValid}; - use crate::error::{ConsistencyError, SchemaError}; + use rsidm_proto::v1::{ConsistencyError, SchemaError}; // use crate::filter::{Filter, FilterValid}; use crate::schema::SchemaTransaction; use crate::schema::{IndexType, Schema, SchemaAttribute, SchemaClass, SyntaxType}; diff --git a/src/lib/server.rs b/rsidmd/src/lib/server.rs similarity index 96% rename from src/lib/server.rs rename to rsidmd/src/lib/server.rs index 7a22249f4..3190ddd21 100644 --- a/src/lib/server.rs +++ b/rsidmd/src/lib/server.rs @@ -15,12 +15,11 @@ use crate::access::{ use crate::constants::{ JSON_ADMIN_V1, JSON_ANONYMOUS_V1, JSON_IDM_ADMINS_ACP_REVIVE_V1, JSON_IDM_ADMINS_ACP_SEARCH_V1, JSON_IDM_ADMINS_V1, JSON_IDM_SELF_ACP_READ_V1, JSON_SCHEMA_ATTR_DISPLAYNAME, - JSON_SCHEMA_ATTR_MAIL, JSON_SCHEMA_ATTR_PASSWORD, JSON_SCHEMA_ATTR_SSH_PUBLICKEY, + JSON_SCHEMA_ATTR_MAIL, JSON_SCHEMA_ATTR_PRIMARY_CREDENTIAL, JSON_SCHEMA_ATTR_SSH_PUBLICKEY, JSON_SCHEMA_CLASS_ACCOUNT, JSON_SCHEMA_CLASS_GROUP, JSON_SCHEMA_CLASS_PERSON, JSON_SYSTEM_INFO_V1, UUID_DOES_NOT_EXIST, }; use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryReduced, EntryValid}; -use crate::error::{ConsistencyError, OperationError, SchemaError}; use crate::event::{ CreateEvent, DeleteEvent, Event, EventOrigin, ExistsEvent, ModifyEvent, ReviveRecycledEvent, SearchEvent, @@ -33,6 +32,7 @@ use crate::schema::{ SchemaWriteTransaction, }; use crate::value::{PartialValue, SyntaxType, Value}; +use rsidm_proto::v1::{ConsistencyError, OperationError, SchemaError}; lazy_static! { static ref PVCLASS_ATTRIBUTETYPE: PartialValue = PartialValue::new_class("attributetype"); @@ -393,6 +393,7 @@ pub trait QueryServerTransaction { } SyntaxType::JSON_FILTER => Value::new_json_filter(value) .ok_or(OperationError::InvalidAttribute("Invalid Filter syntax")), + SyntaxType::CREDENTIAL => Err(OperationError::InvalidAttribute("Credentials can not be supplied through modification - please use the IDM api")), } } None => { @@ -456,6 +457,7 @@ pub trait QueryServerTransaction { } SyntaxType::JSON_FILTER => PartialValue::new_json_filter(value) .ok_or(OperationError::InvalidAttribute("Invalid Filter syntax")), + SyntaxType::CREDENTIAL => Ok(PartialValue::new_credential_tag(value.as_str())), } } None => { @@ -467,9 +469,9 @@ pub trait QueryServerTransaction { } // In the opposite direction, we can resolve values for presentation - fn resolve_value(&self, _attr: &String, _value: &Value) -> Result { + fn resolve_value(&self, _value: &Value) -> Result { + // Ok(value.to_proto_string_clone()) unimplemented!(); - // Ok(value.clone()) } } @@ -1274,15 +1276,24 @@ impl<'a> QueryServerWriteTransaction<'a> { modlist: ModifyList, event: &Event, ) -> Result<(), OperationError> { - let f_valid = filter - .validate(self.get_schema()) - .map_err(|e| OperationError::SchemaViolation(e))?; - let f_intent_valid = filter_intent - .validate(self.get_schema()) - .map_err(|e| OperationError::SchemaViolation(e))?; - let m_valid = modlist - .validate(self.get_schema()) - .map_err(|e| OperationError::SchemaViolation(e))?; + let f_valid = try_audit!( + audit, + filter + .validate(self.get_schema()) + .map_err(|e| OperationError::SchemaViolation(e)) + ); + let f_intent_valid = try_audit!( + audit, + filter_intent + .validate(self.get_schema()) + .map_err(|e| OperationError::SchemaViolation(e)) + ); + let m_valid = try_audit!( + audit, + modlist + .validate(self.get_schema()) + .map_err(|e| OperationError::SchemaViolation(e)) + ); self.impersonate_modify_valid(audit, f_valid, f_intent_valid, m_valid, event) } @@ -1352,10 +1363,7 @@ impl<'a> QueryServerWriteTransaction<'a> { audit_log!(audit, "Generated modlist -> {:?}", modlist); self.internal_modify(audit, filt, modlist) } - Err(_e) => { - unimplemented!() - // No action required. - } + Err(e) => Err(OperationError::SchemaViolation(e)), } } else { Err(OperationError::InvalidDBState) @@ -1453,7 +1461,7 @@ impl<'a> QueryServerWriteTransaction<'a> { JSON_SCHEMA_ATTR_DISPLAYNAME, JSON_SCHEMA_ATTR_MAIL, JSON_SCHEMA_ATTR_SSH_PUBLICKEY, - JSON_SCHEMA_ATTR_PASSWORD, + JSON_SCHEMA_ATTR_PRIMARY_CREDENTIAL, JSON_SCHEMA_CLASS_PERSON, JSON_SCHEMA_CLASS_GROUP, JSON_SCHEMA_CLASS_ACCOUNT, @@ -1683,16 +1691,17 @@ impl<'a> QueryServerWriteTransaction<'a> { #[cfg(test)] mod tests { use crate::constants::{JSON_ADMIN_V1, STR_UUID_ADMIN, UUID_ADMIN}; + use crate::credential::Credential; use crate::entry::{Entry, EntryInvalid, EntryNew}; - use crate::error::{OperationError, SchemaError}; use crate::event::{CreateEvent, DeleteEvent, ModifyEvent, ReviveRecycledEvent, SearchEvent}; use crate::modify::{Modify, ModifyList}; - use crate::proto::v1::Filter as ProtoFilter; - use crate::proto::v1::Modify as ProtoModify; - use crate::proto::v1::ModifyList as ProtoModifyList; - use crate::proto::v1::{DeleteRequest, ModifyRequest, ReviveRecycledRequest}; use crate::server::QueryServerTransaction; 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 uuid::Uuid; #[test] @@ -2636,4 +2645,61 @@ mod tests { // Commit. }) } + + #[test] + fn test_qs_modify_password_only() { + run_test!(|server: &QueryServer, audit: &mut AuditScope| { + let e1: Entry = Entry::unsafe_from_entry_str( + r#"{ + "valid": null, + "state": null, + "attrs": { + "class": ["object", "person", "account"], + "name": ["testperson1"], + "uuid": ["cc8e95b4-c24f-4d68-ba54-8bed76f63930"], + "description": ["testperson"], + "displayname": ["testperson1"] + } + }"#, + ); + let mut server_txn = server.write(); + // Add the entry. Today we have no syntax to take simple str to a credential + // but honestly, that's probably okay :) + let ce = CreateEvent::new_internal(vec![e1]); + let cr = server_txn.create(audit, &ce); + assert!(cr.is_ok()); + + // Build the credential. + let cred = Credential::new_password_only("test_password"); + let v_cred = Value::new_credential("primary", cred); + assert!(v_cred.validate()); + + // now modify and provide a primary credential. + let me_inv_m = unsafe { + ModifyEvent::new_internal_invalid( + filter!(f_eq("name", PartialValue::new_iutf8s("testperson1"))), + ModifyList::new_list(vec![Modify::Present( + "primary_credential".to_string(), + v_cred, + )]), + ) + }; + // go! + assert!(server_txn.modify(audit, &me_inv_m).is_ok()); + + // assert it exists and the password checks out + let test_ent = server_txn + .internal_search_uuid( + audit, + &Uuid::parse_str("cc8e95b4-c24f-4d68-ba54-8bed76f63930").unwrap(), + ) + .expect("failed"); + // get the primary ava + let cred_ref = test_ent + .get_ava_single_credential("primary_credential") + .expect("Failed"); + // do a pw check. + assert!(cred_ref.verify_password("test_password")); + }) + } } diff --git a/src/lib/value.rs b/rsidmd/src/lib/value.rs similarity index 83% rename from src/lib/value.rs rename to rsidmd/src/lib/value.rs index 10799098b..2dc48e69c 100644 --- a/src/lib/value.rs +++ b/rsidmd/src/lib/value.rs @@ -1,11 +1,14 @@ -use crate::be::dbvalue::DbValueV1; -use crate::proto::v1::Filter as ProtoFilter; +use crate::be::dbvalue::{DbValueCredV1, DbValueV1}; +use crate::credential::Credential; +use rsidm_proto::v1::Filter as ProtoFilter; use std::borrow::Borrow; use std::convert::TryFrom; use std::str::FromStr; use uuid::Uuid; +use std::cmp::Ordering; + #[allow(non_camel_case_types)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] pub enum IndexType { @@ -72,6 +75,7 @@ pub enum SyntaxType { INDEX_ID, REFERENCE_UUID, JSON_FILTER, + CREDENTIAL, } impl TryFrom<&str> for SyntaxType { @@ -88,6 +92,7 @@ impl TryFrom<&str> for SyntaxType { "INDEX_ID" => Ok(SyntaxType::INDEX_ID), "REFERENCE_UUID" => Ok(SyntaxType::REFERENCE_UUID), "JSON_FILTER" => Ok(SyntaxType::JSON_FILTER), + "CREDENTIAL" => Ok(SyntaxType::CREDENTIAL), _ => Err(()), } } @@ -106,6 +111,7 @@ impl TryFrom for SyntaxType { 5 => Ok(SyntaxType::INDEX_ID), 6 => Ok(SyntaxType::REFERENCE_UUID), 7 => Ok(SyntaxType::JSON_FILTER), + 8 => Ok(SyntaxType::CREDENTIAL), _ => Err(()), } } @@ -122,6 +128,7 @@ impl SyntaxType { SyntaxType::INDEX_ID => "INDEX_ID", SyntaxType::REFERENCE_UUID => "REFERENCE_UUID", SyntaxType::JSON_FILTER => "JSON_FILTER", + SyntaxType::CREDENTIAL => "CREDENTIAL", }) } @@ -135,10 +142,18 @@ impl SyntaxType { SyntaxType::INDEX_ID => 5, SyntaxType::REFERENCE_UUID => 6, SyntaxType::JSON_FILTER => 7, + SyntaxType::CREDENTIAL => 8, } } } +#[derive(Debug, Clone)] +pub enum DataValue { + Cred(Credential), + // SshKey(String), + // RadiusCred(String), +} + #[derive(Debug, Clone, Eq, Ord, PartialOrd, PartialEq, Deserialize, Serialize)] pub enum PartialValue { Utf8(String), @@ -151,6 +166,10 @@ pub enum PartialValue { // Does this make sense? // TODO: We'll probably add tagging to this type for the partial matching JsonFilt(ProtoFilter), + // Tag, matches to a DataValue. + Cred(String), + // SshKey(String), + // RadiusCred(String), } impl PartialValue { @@ -299,6 +318,17 @@ impl PartialValue { } } + pub fn new_credential_tag(s: &str) -> Self { + PartialValue::Cred(s.to_lowercase()) + } + + pub fn is_credential(&self) -> bool { + match self { + PartialValue::Cred(_) => true, + _ => false, + } + } + pub fn to_str(&self) -> Option<&str> { match self { PartialValue::Utf8(s) => Some(s.as_str()), @@ -320,15 +350,36 @@ impl PartialValue { } } -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] +#[derive(Clone, Debug)] pub struct Value { pv: PartialValue, // Later we'll add extra data fields for different v types. They'll have to switch on // pv somehow, so probably need optional or union? + data: Option, } // TODO: Impl display +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + self.pv.eq(&other.pv) + } +} + +impl Eq for Value {} + +impl PartialOrd for Value { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.pv.cmp(&other.pv)) + } +} + +impl Ord for Value { + fn cmp(&self, other: &Self) -> Ordering { + self.pv.cmp(&other.pv) + } +} + // Need new_ -> Result<_, _> // Need from_db_value // Need to_db_value @@ -338,6 +389,7 @@ impl From for Value { fn from(b: bool) -> Self { Value { pv: PartialValue::Bool(b), + data: None, } } } @@ -346,6 +398,7 @@ impl From<&bool> for Value { fn from(b: &bool) -> Self { Value { pv: PartialValue::Bool(*b), + data: None, } } } @@ -354,6 +407,7 @@ impl From for Value { fn from(s: SyntaxType) -> Self { Value { pv: PartialValue::Syntax(s), + data: None, } } } @@ -362,6 +416,7 @@ impl From for Value { fn from(i: IndexType) -> Self { Value { pv: PartialValue::Index(i), + data: None, } } } @@ -376,9 +431,11 @@ impl From<&str> for Value { match Uuid::parse_str(s) { Ok(u) => Value { pv: PartialValue::Uuid(u), + data: None, }, Err(_) => Value { pv: PartialValue::Utf8(s.to_string()), + data: None, }, } } @@ -389,6 +446,7 @@ impl From<&Uuid> for Value { fn from(u: &Uuid) -> Self { Value { pv: PartialValue::Uuid(u.clone()), + data: None, } } } @@ -398,6 +456,7 @@ impl From for Value { fn from(u: Uuid) -> Self { Value { pv: PartialValue::Uuid(u), + data: None, } } } @@ -407,12 +466,14 @@ impl Value { pub fn new_utf8(s: String) -> Self { Value { pv: PartialValue::new_utf8(s), + data: None, } } pub fn new_utf8s(s: &str) -> Self { Value { pv: PartialValue::new_utf8s(s), + data: None, } } @@ -426,12 +487,14 @@ impl Value { pub fn new_iutf8(s: String) -> Self { Value { pv: PartialValue::new_iutf8s(s.as_str()), + data: None, } } pub fn new_iutf8s(s: &str) -> Self { Value { pv: PartialValue::new_iutf8s(s), + data: None, } } @@ -445,18 +508,21 @@ impl Value { pub fn new_uuid(u: Uuid) -> Self { Value { pv: PartialValue::new_uuid(u), + data: None, } } pub fn new_uuids(s: &str) -> Option { Some(Value { pv: PartialValue::new_uuids(s)?, + data: None, }) } pub fn new_uuidr(u: &Uuid) -> Self { Value { pv: PartialValue::new_uuidr(u), + data: None, } } @@ -471,24 +537,28 @@ impl Value { pub fn new_class(s: &str) -> Self { Value { pv: PartialValue::new_iutf8s(s), + data: None, } } pub fn new_attr(s: &str) -> Self { Value { pv: PartialValue::new_iutf8s(s), + data: None, } } pub fn new_bool(b: bool) -> Self { Value { pv: PartialValue::new_bool(b), + data: None, } } pub fn new_bools(s: &str) -> Option { Some(Value { pv: PartialValue::new_bools(s)?, + data: None, }) } @@ -503,6 +573,7 @@ impl Value { pub fn new_syntaxs(s: &str) -> Option { Some(Value { pv: PartialValue::new_syntaxs(s)?, + data: None, }) } @@ -516,6 +587,7 @@ impl Value { pub fn new_indexs(s: &str) -> Option { Some(Value { pv: PartialValue::new_indexs(s)?, + data: None, }) } @@ -529,18 +601,21 @@ impl Value { pub fn new_refer(u: Uuid) -> Self { Value { pv: PartialValue::new_refer(u), + data: None, } } pub fn new_refer_r(u: &Uuid) -> Self { Value { pv: PartialValue::new_refer_r(u), + data: None, } } pub fn new_refer_s(us: &str) -> Option { Some(Value { pv: PartialValue::new_refer_s(us)?, + data: None, }) } @@ -554,6 +629,7 @@ impl Value { pub fn new_json_filter(s: &str) -> Option { Some(Value { pv: PartialValue::new_json_filter(s)?, + data: None, }) } @@ -571,6 +647,32 @@ impl Value { } } + pub fn new_credential(tag: &str, cred: Credential) -> Self { + Value { + pv: PartialValue::new_credential_tag(tag), + data: Some(DataValue::Cred(cred)), + } + } + + pub fn is_credential(&self) -> bool { + match &self.pv { + PartialValue::Cred(_) => true, + _ => false, + } + } + + pub fn to_credential(&self) -> Option<&Credential> { + match &self.pv { + PartialValue::Cred(_) => match &self.data { + Some(dv) => match dv { + DataValue::Cred(c) => Some(&c), + }, + None => None, + }, + _ => None, + } + } + pub fn contains(&self, s: &PartialValue) -> bool { self.pv.contains(s) } @@ -584,35 +686,50 @@ impl Value { match v { DbValueV1::U8(s) => Ok(Value { pv: PartialValue::Utf8(s), + data: None, }), DbValueV1::I8(s) => { Ok(Value { // TODO: Should we be lowercasing here? The dbv should be normalised // already, but is there a risk of corruption/tampering if we don't touch this? pv: PartialValue::Iutf8(s.to_lowercase()), + data: None, }) } DbValueV1::UU(u) => Ok(Value { pv: PartialValue::Uuid(u), + data: None, }), DbValueV1::BO(b) => Ok(Value { pv: PartialValue::Bool(b), + data: None, }), DbValueV1::SY(us) => Ok(Value { pv: PartialValue::Syntax(SyntaxType::try_from(us)?), + data: None, }), DbValueV1::IN(us) => Ok(Value { pv: PartialValue::Index(IndexType::try_from(us)?), + data: None, }), DbValueV1::RF(u) => Ok(Value { pv: PartialValue::Refer(u), + data: None, }), DbValueV1::JF(s) => Ok(Value { pv: match PartialValue::new_json_filter(s.as_str()) { Some(pv) => pv, None => return Err(()), }, + data: None, }), + DbValueV1::CR(dvc) => { + // Deserialise the db cred here. + Ok(Value { + pv: PartialValue::Cred(dvc.t.to_lowercase()), + data: Some(DataValue::Cred(Credential::try_from(dvc.d)?)), + }) + } } } @@ -630,6 +747,24 @@ impl Value { serde_json::to_string(s) .expect("A json filter value was corrupted during run-time"), ), + PartialValue::Cred(tag) => { + // Get the credential out and make sure it matches the type we expect. + let c = match &self.data { + Some(v) => { + match &v { + DataValue::Cred(c) => c, + // _ => panic!(), + } + } + None => panic!(), + }; + + // Save the tag AND the dataValue here! + DbValueV1::CR(DbValueCredV1 { + t: tag.clone(), + d: c.to_db_valuev1(), + }) + } } } @@ -647,6 +782,11 @@ impl Value { PartialValue::JsonFilt(s) => { serde_json::to_string(s).expect("A json filter value was corrupted during run-time") } + PartialValue::Cred(tag) => { + // You can't actually read the credential values because we only display the + // tag to the proto side. The credentials private data is stored seperately. + tag.to_string() + } } } @@ -717,7 +857,18 @@ impl Value { // Validate that extra-data constraints on the type exist and are // valid. IE json filter is really a filter, or cred types have supplemental // data. - true + match &self.pv { + PartialValue::Cred(_) => match &self.data { + Some(v) => { + match &v { + DataValue::Cred(_) => true, + // _ => false, + } + } + None => false, + }, + _ => true, + } } } diff --git a/src/server/main.rs b/rsidmd/src/server/main.rs similarity index 78% rename from src/server/main.rs rename to rsidmd/src/server/main.rs index b30210d0f..7a34f0302 100644 --- a/src/server/main.rs +++ b/rsidmd/src/server/main.rs @@ -2,6 +2,7 @@ extern crate actix; extern crate env_logger; +extern crate rpassword; extern crate rsidm; extern crate structopt; @@ -10,7 +11,8 @@ extern crate log; use rsidm::config::Configuration; use rsidm::core::{ - backup_server_core, create_server_core, restore_server_core, verify_server_core, + backup_server_core, create_server_core, recover_account_core, restore_server_core, + verify_server_core, }; use std::path::PathBuf; @@ -40,6 +42,14 @@ struct RestoreOpt { serveropts: ServerOpt, } +#[derive(Debug, StructOpt)] +struct RecoverAccountOpt { + #[structopt(short)] + name: String, + #[structopt(flatten)] + serveropts: ServerOpt, +} + #[derive(Debug, StructOpt)] enum Opt { #[structopt(name = "server")] @@ -50,6 +60,8 @@ enum Opt { Restore(RestoreOpt), #[structopt(name = "verify")] Verify(ServerOpt), + #[structopt(name = "recover_account")] + RecoverAccount(RecoverAccountOpt), } fn main() { @@ -62,7 +74,7 @@ fn main() { // Configure the server logger. This could be adjusted based on what config // says. - ::std::env::set_var("RUST_LOG", "actix_web=info,rsidm=info"); + // ::std::env::set_var("RUST_LOG", "actix_web=info,rsidm=info"); env_logger::init(); match opt { @@ -109,5 +121,13 @@ fn main() { config.update_db_path(&vopt.db_path); verify_server_core(config); } + Opt::RecoverAccount(raopt) => { + info!("Running account recovery ..."); + + let password = rpassword::prompt_password_stderr("new password: ").unwrap(); + config.update_db_path(&raopt.serveropts.db_path); + + recover_account_core(config, raopt.name, password); + } } } diff --git a/src/lib/be/dbvalue.rs b/src/lib/be/dbvalue.rs deleted file mode 100644 index 6634bc251..000000000 --- a/src/lib/be/dbvalue.rs +++ /dev/null @@ -1,13 +0,0 @@ -use uuid::Uuid; - -#[derive(Serialize, Deserialize, Debug)] -pub enum DbValueV1 { - U8(String), - I8(String), - UU(Uuid), - BO(bool), - SY(usize), - IN(usize), - RF(Uuid), - JF(String), -} diff --git a/src/lib/error.rs b/src/lib/error.rs deleted file mode 100644 index 125821f1c..000000000 --- a/src/lib/error.rs +++ /dev/null @@ -1,60 +0,0 @@ -//use rusqlite::Error as RusqliteError; - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub enum SchemaError { - NotImplemented, - InvalidClass, - MissingMustAttribute(String), - InvalidAttribute, - InvalidAttributeSyntax, - EmptyFilter, - Corrupted, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub enum OperationError { - EmptyRequest, - Backend, - NoMatchingEntries, - CorruptedEntry(u64), - ConsistencyError(Vec>), - SchemaViolation(SchemaError), - Plugin, - FilterGeneration, - FilterUUIDResolution, - InvalidAttributeName(String), - InvalidAttribute(&'static str), - InvalidDBState, - InvalidEntryID, - InvalidRequestState, - InvalidState, - InvalidEntryState, - InvalidUuid, - InvalidACPState(&'static str), - InvalidSchemaState(&'static str), - InvalidAccountState(&'static str), - BackendEngine, - SQLiteError, //(RusqliteError) - FsError, - SerdeJsonError, - SerdeCborError, - AccessDenied, - NotAuthenticated, - InvalidAuthState(&'static str), - InvalidSessionState, - SystemProtectedObject, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub enum ConsistencyError { - Unknown, - // Class, Attribute - SchemaClassMissingAttribute(String, String), - QueryServerSearchFailure, - EntryUuidCorrupt(u64), - UuidIndexCorrupt(String), - UuidNotUnique(String), - RefintNotUpheld(u64), - MemberOfInvalid(u64), - InvalidAttributeType(&'static str), -} diff --git a/src/lib/lib.rs b/src/lib/lib.rs deleted file mode 100644 index 74ac5938e..000000000 --- a/src/lib/lib.rs +++ /dev/null @@ -1,63 +0,0 @@ -#![deny(warnings)] - -#[macro_use] -extern crate log; -extern crate serde; -extern crate serde_cbor; -extern crate serde_json; -#[macro_use] -extern crate serde_derive; -extern crate actix; -extern crate actix_web; -extern crate futures; -extern crate r2d2; -extern crate r2d2_sqlite; -extern crate rand; -extern crate rusqlite; -extern crate time; -extern crate uuid; - -extern crate bytes; -extern crate chrono; -extern crate cookie; -extern crate env_logger; - -extern crate regex; -#[macro_use] -extern crate lazy_static; - -extern crate concread; - -// use actix::prelude::*; -// use actix_web::{ -// http, middleware, App, AsyncResponder, FutureResponse, HttpRequest, HttpResponse, Path, State, -// }; - -// use futures::Future; - -// This has to be before be so the import order works -#[macro_use] -mod macros; -#[macro_use] -mod async_log; -#[macro_use] -mod audit; -mod be; -pub mod constants; -mod entry; -mod event; -mod filter; -mod interval; -mod modify; -mod value; -#[macro_use] -mod plugins; -mod access; -mod idm; -mod schema; -mod server; - -pub mod config; -pub mod core; -pub mod error; -pub mod proto; diff --git a/src/lib/proto/v1/client.rs b/src/lib/proto/v1/client.rs deleted file mode 100644 index e96fa87dd..000000000 --- a/src/lib/proto/v1/client.rs +++ /dev/null @@ -1,28 +0,0 @@ -// Implement a client library that use used to interact with -// a kanidm server and perform operations - it is expected that -// this client can store some internal state, and will generally -// attempt to reflect and map to a simple representation of -// the protocol, which was intended to be easy-to-use and accessible. - -/* -struct ClientV1 {} - -impl ClientV1 { - fn auth_anonymous() -> () {} - - fn auth_password() -> () {} - - fn auth_application_password() -> () {} - - fn whoami() -> () {} - - // The four raw operations. - fn search() -> () {} - - fn modify() -> () {} - - fn create() -> () {} - - fn delete() -> () {} -} -*/ diff --git a/src/lib/proto/v1/messages.rs b/src/lib/proto/v1/messages.rs deleted file mode 100644 index e9d2d129a..000000000 --- a/src/lib/proto/v1/messages.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::error::OperationError; -use actix::prelude::*; -use uuid::Uuid; - -use crate::proto::v1::{AuthRequest, AuthResponse, UserAuthToken, WhoamiResponse}; - -// These are used when the request (IE Get) has no intrising request -// type. Additionally, they are used in some requests where we need -// to supplement extra server state (IE userauthtokens) to a request. -// -// Generally we don't need to have the responses here because they are -// part of the protocol. - -pub struct WhoamiMessage { - pub uat: Option, -} - -impl WhoamiMessage { - pub fn new(uat: Option) -> Self { - WhoamiMessage { uat: uat } - } -} - -impl Message for WhoamiMessage { - type Result = Result; -} - -#[derive(Debug)] -pub struct AuthMessage { - pub sessionid: Option, - pub req: AuthRequest, -} - -impl AuthMessage { - pub fn new(req: AuthRequest, sessionid: Option) -> Self { - AuthMessage { - sessionid: sessionid, - req: req, - } - } -} - -impl Message for AuthMessage { - type Result = Result; -}