3 authentication (#79)

This adds support for authentication and credential storage to the server. 
It also adds account recovery and options for integration test fixtures, refactors
to make the client library easier to manage, and support clean seperation of the
proto vs lib.
This commit is contained in:
Firstyear 2019-09-04 11:06:37 +10:00 committed by GitHub
parent d0e62ad85a
commit da1af02f2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 1599 additions and 456 deletions

2
.gitignore vendored
View file

@ -1,4 +1,6 @@
.DS_Store
.backup_test.db
/target
**/*.rs.bk
test.db

View file

@ -1,60 +1,9 @@
# cargo-features = ["default-run"]
[package]
name = "rsidm"
version = "0.1.0"
authors = ["William Brown <william@blackhats.net.au>"]
# 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",
]

View file

@ -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.

18
rsidm_client/Cargo.toml Normal file
View file

@ -0,0 +1,18 @@
[package]
name = "rsidm_client"
version = "0.1.0"
authors = ["William Brown <william@blackhats.net.au>"]
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"

29
rsidm_client/src/lib.rs Normal file
View file

@ -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
//
}

View file

@ -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.
/*

17
rsidm_proto/Cargo.toml Normal file
View file

@ -0,0 +1,17 @@
[package]
name = "rsidm_proto"
version = "0.1.0"
authors = ["William Brown <william@blackhats.net.au>"]
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"

7
rsidm_proto/src/lib.rs Normal file
View file

@ -0,0 +1,7 @@
#![deny(warnings)]
#![warn(unused_extern_crates)]
#[macro_use]
extern crate serde_derive;
pub mod v1;

View file

@ -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<Result<(), ConsistencyError>>),
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<SearchResponse, OperationError>;
}
@ -163,6 +220,7 @@ impl CreateRequest {
}
}
#[cfg(feature = "rsidm_internal")]
impl Message for CreateRequest {
type Result = Result<OperationResponse, OperationError>;
}
@ -182,6 +240,7 @@ impl DeleteRequest {
}
}
#[cfg(feature = "rsidm_internal")]
impl Message for DeleteRequest {
type Result = Result<OperationResponse, OperationError>;
}
@ -204,6 +263,7 @@ impl ModifyRequest {
}
}
#[cfg(feature = "rsidm_internal")]
impl Message for ModifyRequest {
type Result = Result<OperationResponse, OperationError>;
}
@ -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());

13
rsidm_tools/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "rsidm_tools"
version = "0.1.0"
authors = ["William Brown <william@blackhats.net.au>"]
edition = "2018"
[[bin]]
name = "kanidm"
path = "src/main.rs"
[dependencies]
rsidm_client = { path = "../rsidm_client" }

55
rsidmd/Cargo.toml Normal file
View file

@ -0,0 +1,55 @@
# cargo-features = ["default-run"]
[package]
name = "rsidm"
version = "0.1.0"
authors = ["William Brown <william@blackhats.net.au>"]
# 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"

View file

@ -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;

View file

@ -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<UserAuthToken>,
}
impl WhoamiMessage {
pub fn new(uat: Option<UserAuthToken>) -> Self {
WhoamiMessage { uat: uat }
}
}
impl Message for WhoamiMessage {
type Result = Result<WhoamiResponse, OperationError>;
}
#[derive(Debug)]
pub struct AuthMessage {
pub sessionid: Option<Uuid>,
pub req: AuthRequest,
}
impl AuthMessage {
pub fn new(req: AuthRequest, sessionid: Option<Uuid>) -> Self {
AuthMessage {
sessionid: sessionid,
req: req,
}
}
}
impl Message for AuthMessage {
type Result = Result<AuthResponse, OperationError>;
}
pub struct QueryServerV1 {
log: actix::Addr<EventLog>,
@ -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<EventLog>,
be: Backend,
query_server: QueryServer,
idms: IdmServer,
threads: usize,
) -> Result<actix::Addr<QueryServerV1>, OperationError> {
let mut audit = AuditScope::new("server_start");
let log_inner = log.clone();
let qs_addr: Result<actix::Addr<QueryServerV1>, _> = 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<QueryServerV1> {
let idms_arc = Arc::new(idms);
SyncArbiter::start(threads, move || {
QueryServerV1::new(log.clone(), query_server.clone(), idms_arc.clone())
})
}
}

View file

@ -0,0 +1,32 @@
use uuid::Uuid;
#[derive(Serialize, Deserialize, Debug)]
pub enum DbPasswordV1 {
PBKDF2(usize, Vec<u8>, Vec<u8>),
}
#[derive(Serialize, Deserialize, Debug)]
pub struct DbCredV1 {
pub password: Option<DbPasswordV1>,
pub claims: Vec<String>,
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),
}

View file

@ -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;

View file

@ -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<Box<IntegrationTestConfig>>,
}
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);

View file

@ -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": [

View file

@ -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<Backend, OperationError> {
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 -> {:?}",
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();

View file

@ -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<u8>, Vec<u8>),
}
#[derive(Clone, Debug)]
pub struct Password {
material: KDF,
}
impl TryFrom<DbPasswordV1> for Password {
type Error = ();
fn try_from(value: DbPasswordV1) -> Result<Self, Self::Error> {
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<u8> = (0..PBKDF2_SALT_LEN).map(|_| rng.gen()).collect();
// This is 512 bits of output
let mut key: Vec<u8> = (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<u8> = (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<Password>,
// webauthn: Option<NonEmptyVec<Webauthn>>
// totp: Option<NonEmptyVec<TOTP>>
pub(crate) claims: Vec<String>,
// 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<DbCredV1> for Credential {
type Error = ();
fn try_from(value: DbCredV1) -> Result<Self, Self::Error> {
// 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"));
}
}

View file

@ -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<EntryValid, EntryCommitted> {
}
}
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<Vec<&Uuid>> {
// If any value is NOT a reference, return none!
match self.attrs.get(attr) {

View file

@ -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),
}
}
}

View file

@ -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;

View file

@ -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<Group>,
// creds (various types)
// groups?
// claims?
// account expiry?
pub primary: Option<Credential>,
// primary: Credential
// app_creds: Vec<Credential>
// 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<String>,
) -> Result<ModifyList<ModifyInvalid>, 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
}
}

View file

@ -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,11 +22,13 @@ enum CredState {
#[derive(Clone, Debug)]
enum CredHandler {
Denied,
// The bool is a flag if the cred has been authed against.
Anonymous,
// AppPassword
// {
// Password
// Webauthn
Password(Password), // Webauthn
// Webauthn + Password
// TOTP
// TOTP + Password
@ -31,9 +37,25 @@ enum CredHandler {
// Verification Link?
}
impl TryFrom<&Credential> for CredHandler {
type Error = ();
// Is there a nicer implementation of this?
fn try_from(c: &Credential) -> Result<Self, Self::Error> {
if c.password.is_some() {
Ok(CredHandler::Password(c.password.clone().unwrap()))
} else {
Err(())
}
}
}
impl CredHandler {
pub fn validate(&mut self, creds: &Vec<AuthCredential>) -> 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<AuthAllowed> {
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<String>,
// 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,
})
);
}
}

View file

@ -1,4 +1,4 @@
use crate::proto::v1::Claim as ProtoClaim;
use rsidm_proto::v1::Claim as ProtoClaim;
#[derive(Debug)]
pub struct Claim {

View file

@ -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<String>,
}
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()),
}
}
}

View file

@ -1,4 +1,4 @@
use crate::proto::v1::Group as ProtoGroup;
use rsidm_proto::v1::Group as ProtoGroup;
#[derive(Debug, Clone)]
pub struct Group {

View file

@ -1,5 +1,6 @@
#[macro_use]
mod macros;
mod event;
pub(crate) mod account;
pub(crate) mod authsession;

View file

@ -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<BTreeMap<Uuid, AuthSession>>,
// 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());
})
}
}

View file

@ -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

37
rsidmd/src/lib/lib.rs Normal file
View file

@ -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;

View file

@ -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: VALID,
// The order of this list matters. Each change must be done in order.
@ -87,6 +87,10 @@ impl ModifyList<ModifyInvalid> {
}
}
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)
}

View file

@ -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,

View file

@ -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;

View file

@ -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;

View file

@ -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,

View file

@ -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]

View file

@ -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<bool, SchemaError> {
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};

View file

@ -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<String, OperationError> {
fn resolve_value(&self, _value: &Value) -> Result<String, OperationError> {
// Ok(value.to_proto_string_clone())
unimplemented!();
// Ok(value.clone())
}
}
@ -1274,15 +1276,24 @@ impl<'a> QueryServerWriteTransaction<'a> {
modlist: ModifyList<ModifyInvalid>,
event: &Event,
) -> Result<(), OperationError> {
let f_valid = filter
let f_valid = try_audit!(
audit,
filter
.validate(self.get_schema())
.map_err(|e| OperationError::SchemaViolation(e))?;
let f_intent_valid = filter_intent
.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 = modlist
.map_err(|e| OperationError::SchemaViolation(e))
);
let m_valid = try_audit!(
audit,
modlist
.validate(self.get_schema())
.map_err(|e| OperationError::SchemaViolation(e))?;
.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<EntryInvalid, EntryNew> = 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"));
})
}
}

View file

@ -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<usize> 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<DataValue>,
}
// 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<Ordering> {
Some(self.pv.cmp(&other.pv))
}
}
impl Ord for Value {
fn cmp(&self, other: &Self) -> Ordering {
self.pv.cmp(&other.pv)
}
}
// Need new_<type> -> Result<_, _>
// Need from_db_value
// Need to_db_value
@ -338,6 +389,7 @@ impl From<bool> 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<SyntaxType> for Value {
fn from(s: SyntaxType) -> Self {
Value {
pv: PartialValue::Syntax(s),
data: None,
}
}
}
@ -362,6 +416,7 @@ impl From<IndexType> 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<Uuid> 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<Self> {
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<Self> {
Some(Value {
pv: PartialValue::new_bools(s)?,
data: None,
})
}
@ -503,6 +573,7 @@ impl Value {
pub fn new_syntaxs(s: &str) -> Option<Self> {
Some(Value {
pv: PartialValue::new_syntaxs(s)?,
data: None,
})
}
@ -516,6 +587,7 @@ impl Value {
pub fn new_indexs(s: &str) -> Option<Self> {
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<Self> {
Some(Value {
pv: PartialValue::new_refer_s(us)?,
data: None,
})
}
@ -554,6 +629,7 @@ impl Value {
pub fn new_json_filter(s: &str) -> Option<Self> {
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,
}
}
}

View file

@ -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);
}
}
}

View file

@ -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),
}

View file

@ -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<Result<(), ConsistencyError>>),
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),
}

View file

@ -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;

View file

@ -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() -> () {}
}
*/

View file

@ -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<UserAuthToken>,
}
impl WhoamiMessage {
pub fn new(uat: Option<UserAuthToken>) -> Self {
WhoamiMessage { uat: uat }
}
}
impl Message for WhoamiMessage {
type Result = Result<WhoamiResponse, OperationError>;
}
#[derive(Debug)]
pub struct AuthMessage {
pub sessionid: Option<Uuid>,
pub req: AuthRequest,
}
impl AuthMessage {
pub fn new(req: AuthRequest, sessionid: Option<Uuid>) -> Self {
AuthMessage {
sessionid: sessionid,
req: req,
}
}
}
impl Message for AuthMessage {
type Result = Result<AuthResponse, OperationError>;
}