20190607 authentication (#55)

Implement #2 anonymous authentication. This also puts into place the majority of the authentication framework, and starts to build the IDM layers ontop of the DB engine.
This commit is contained in:
Firstyear 2019-07-12 15:28:46 +10:00 committed by GitHub
parent 426426a18f
commit 94a6bde269
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 1643 additions and 347 deletions

View file

@ -24,18 +24,20 @@ name = "rsidm_whoami"
path = "src/clients/whoami.rs" path = "src/clients/whoami.rs"
[dependencies] [dependencies]
actix = "0.7" actix = "0.7"
actix-web = "0.7" actix-web = "0.7"
bytes = "0.4" bytes = "0.4"
env_logger = "0.5" log = "0.4"
env_logger = "0.6"
reqwest = "0.9" reqwest = "0.9"
# reqwest = { path = "../reqwest" }
chrono = "0.4" chrono = "0.4"
cookie = "0.11" cookie = "0.11"
regex = "1" regex = "1"
lazy_static = "1.2.0" lazy_static = "1.2.0"
lru = "0.1"
tokio = "0.1" tokio = "0.1"
futures = "0.1" futures = "0.1"

View file

@ -1,9 +1,6 @@
FROM opensuse/tumbleweed:latest FROM opensuse/tumbleweed:latest
MAINTAINER william@blackhats.net.au MAINTAINER william@blackhats.net.au
# /usr/bin/docker run --restart always --name lifx registry.blackhats.net.au/lifx
RUN echo HTTP_PROXY="http://proxy-bne1.net.blackhats.net.au:3128" > /etc/sysconfig/proxy
COPY . /home/rsidm/ COPY . /home/rsidm/
WORKDIR /home/rsidm/ WORKDIR /home/rsidm/

View file

@ -34,6 +34,8 @@ the session, which may request further credentials. They are then redirected to
site with an appropriate (oauth) token describing the requested rights. site with an appropriate (oauth) token describing the requested rights.
https://developers.google.com/identity/sign-in/web/incremental-auth https://developers.google.com/identity/sign-in/web/incremental-auth
https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
https://tools.ietf.org/html/rfc7519
Login to workstation (connected) Login to workstation (connected)
================================ ================================
@ -168,6 +170,8 @@ that have unique cookie keys to prevent forgery of writable master cookies)
of group uuids + names derferenced so that a client can make all authorisation of group uuids + names derferenced so that a client can make all authorisation
decisions from a single datapoint decisions from a single datapoint
* Groups require the ability to be ephemeral/temporary or permament.
* each token can be unique based on the type of auth (ie 2fa needed to get access * each token can be unique based on the type of auth (ie 2fa needed to get access
to admin groups) to admin groups)
@ -185,14 +189,57 @@ Cookie/Token Auth Detail
Clients begin with no cookie, and no session. Clients begin with no cookie, and no session.
The client sends an AuthRequest to the server in the Init state. Any other request The client sends an AuthRequest to the server in the Init state. Any other request
results in AuthDenied due to lack of cookie. results in AuthDenied due to lack of cookie. This should contain the optional
application id.
struct AuthClientRequest {
name: String
application: Option<String>
}
The server issues a cookie, and allocates a session id to the cookie. The session id is The server issues a cookie, and allocates a session id to the cookie. The session id is
also stored in the server with a timeout. The AuthResponse indicates the current possible also stored in the server with a timeout. The AuthResponse indicates the current possible
auth types that can proceed. auth types that can proceed. This should provided challenges or nonces if required by the auth type.
enum AuthAllowed {
Anonymous,
Password,
Webauthn {
challenge: // see the webauthn implementation for this
},
TOTP,
}
enum AuthState {
Response {
next: AuthAllowedMech
},
AuthDenied,
AuthSuccess,
}
struct AuthServerResponse {
state AuthState
}
The client now sends the cookie and an AuthRequest with type Step, that contains the type The client now sends the cookie and an AuthRequest with type Step, that contains the type
of authentication credential being provided. of authentication credential being provided, and any other details. This COULD contain multiple
credentials, or a single one.
enum AuthCredential {
Anonymous,
Password { String },
Webauthn {
// see the webauthn impl for all the bits this will contain ...
},
TOTP {
String
}
}
struct AuthClientStep {
Vec<AuthDetails>
}
The server verifies the credential, and marks that type of credential as failed or fufilled. The server verifies the credential, and marks that type of credential as failed or fufilled.
On failure of a credential, AuthDenied is immediately sent. On success of a credential On failure of a credential, AuthDenied is immediately sent. On success of a credential
@ -211,6 +258,9 @@ the state machine part way through. THe server enforces the client must always a
initial authRequest, which cause the client to always be denied. initial authRequest, which cause the client to always be denied.
* If the AuthRequest is started but not completed, we time it out within a set number of minutes * If the AuthRequest is started but not completed, we time it out within a set number of minutes
by walking the set of sessions and purging incomplete ones which have passed the time stamp. by walking the set of sessions and purging incomplete ones which have passed the time stamp.
* The session id is in the cookie to eliminate leaking of the session id (secure cookies), and
to prevent tampering of the session id if possible. It's not perfect, but it helps to prevent
casual attkcs. The session id itself is really the thing that protects us from replays.
Auth Questions Auth Questions
-------------- --------------
@ -325,5 +375,91 @@ required tagging or other details.
How do we ensure integrity of the token? Do we have to? Is the clients job to trust the token given How do we ensure integrity of the token? Do we have to? Is the clients job to trust the token given
the TLS tunnel? the TLS tunnel?
More Brain Dumping
==================
- need a way to just pw check even if mfa is on (for sudo). Perhaps have a seperate sudo password attr?
- ntpassword attr is seperate
- a way to check application pw which attaches certain rights (is this just a generalisation of sudo?)
- the provided token (bearer etc?) contains the "memberof" for the session.
- How to determine what memberof an api provides? Could be policy object that says "api pw of name X
is allowed Y, Z group". Could be that the user is presented with a list or subset of the related?
Could be both?
- Means we need a "name" and "type" for the api password, also need to be able to search
on both of those details potentially.
- The oauth system is just a case of follow that and provide the scope/groups as required.
- That would make userPassword and webauthn only for webui and api direct access.
- All other pw validations would use application pw case.
- SSH would just read ssh key - should this have a similar group filter/allow
mechanism like aplication pw?
- Groups take a "type"
- credentials also have a "type"
- The credential if used can provide groups of "type" to that session during auth token
generation
- An auth request says it as an auth of type X, to associate what creds it might check.
- Means a change to auth to take an entry as part of auth, or at least, it's group list for the
session.
- policy to define if pw types like sudo or radius are linked.
- Some applications may need to read a credential type.
- attribute/value tagging required?
apptype: unix
apptype: groupware
group: admins
type: unix <<-- indicates it's a requested group
group: emailusers
type: groupware <<-- indicates it's a requested group
user: admin
memberof: admins <<-- Should this be in mo if they are reqgroups? I think yes, because it's only for that "session"
based on the cred do they get the "group list" in cred.
memberof: emailusers
cred: {
'type': unix,
'hash': ...
'grants': 'admins'
}
cred: {
'type': groupware
'hash': ...,
'grants': 'emailusers',
}
cred: {
'type': blah
'hash': ...,
'grants': 'bar', // Can't work because not a memberof bar. Should this only grant valid MO's?
}
ntpassword: ... <<-- needs limited read, and doesn't allocate groups.
sshPublicKey: ... <<-- different due to needing anon read.
Some Dirty Rust Brain Dumps
===========================
- Credentials need per-cred locking
- This means they have to be in memory and uniquely ided.
- How can we display to a user that a credential back-off is inplace?
- UAT need to know what Credential was used and it's state.
- The Credential associates the claims

View file

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

View file

@ -23,7 +23,7 @@ use crate::entry::{Entry, EntryCommitted, EntryNew, EntryNormalised, EntryValid}
use crate::error::OperationError; use crate::error::OperationError;
use crate::filter::{Filter, FilterValid}; use crate::filter::{Filter, FilterValid};
use crate::modify::Modify; use crate::modify::Modify;
use crate::proto_v1::Filter as ProtoFilter; use crate::proto::v1::Filter as ProtoFilter;
use crate::server::{QueryServerTransaction, QueryServerWriteTransaction}; use crate::server::{QueryServerTransaction, QueryServerWriteTransaction};
use crate::event::{CreateEvent, DeleteEvent, EventOrigin, ModifyEvent, SearchEvent}; use crate::event::{CreateEvent, DeleteEvent, EventOrigin, ModifyEvent, SearchEvent};
@ -47,14 +47,16 @@ impl AccessControlSearch {
) -> Result<Self, OperationError> { ) -> Result<Self, OperationError> {
if !value.attribute_value_pres("class", "access_control_search") { if !value.attribute_value_pres("class", "access_control_search") {
audit_log!(audit, "class access_control_search not present."); audit_log!(audit, "class access_control_search not present.");
return Err(OperationError::InvalidACPState); return Err(OperationError::InvalidACPState(
"Missing access_control_search",
));
} }
let attrs = try_audit!( let attrs = try_audit!(
audit, audit,
value value
.get_ava("acp_search_attr") .get_ava("acp_search_attr")
.ok_or(OperationError::InvalidACPState) .ok_or(OperationError::InvalidACPState("Missing acp_search_attr"))
.map(|vs: &Vec<String>| vs.clone()) .map(|vs: &Vec<String>| vs.clone())
); );
@ -99,7 +101,9 @@ impl AccessControlDelete {
) -> Result<Self, OperationError> { ) -> Result<Self, OperationError> {
if !value.attribute_value_pres("class", "access_control_delete") { if !value.attribute_value_pres("class", "access_control_delete") {
audit_log!(audit, "class access_control_delete not present."); audit_log!(audit, "class access_control_delete not present.");
return Err(OperationError::InvalidACPState); return Err(OperationError::InvalidACPState(
"Missing access_control_delete",
));
} }
Ok(AccessControlDelete { Ok(AccessControlDelete {
@ -142,7 +146,9 @@ impl AccessControlCreate {
) -> Result<Self, OperationError> { ) -> Result<Self, OperationError> {
if !value.attribute_value_pres("class", "access_control_create") { if !value.attribute_value_pres("class", "access_control_create") {
audit_log!(audit, "class access_control_create not present."); audit_log!(audit, "class access_control_create not present.");
return Err(OperationError::InvalidACPState); return Err(OperationError::InvalidACPState(
"Missing access_control_create",
));
} }
let attrs = value let attrs = value
@ -203,7 +209,9 @@ impl AccessControlModify {
) -> Result<Self, OperationError> { ) -> Result<Self, OperationError> {
if !value.attribute_value_pres("class", "access_control_modify") { if !value.attribute_value_pres("class", "access_control_modify") {
audit_log!(audit, "class access_control_modify not present."); audit_log!(audit, "class access_control_modify not present.");
return Err(OperationError::InvalidACPState); return Err(OperationError::InvalidACPState(
"Missing access_control_modify",
));
} }
let presattrs = value let presattrs = value
@ -273,7 +281,9 @@ impl AccessControlProfile {
// Assert we have class access_control_profile // Assert we have class access_control_profile
if !value.attribute_value_pres("class", "access_control_profile") { if !value.attribute_value_pres("class", "access_control_profile") {
audit_log!(audit, "class access_control_profile not present."); audit_log!(audit, "class access_control_profile not present.");
return Err(OperationError::InvalidACPState); return Err(OperationError::InvalidACPState(
"Missing access_control_profile",
));
} }
// copy name // copy name
@ -281,7 +291,7 @@ impl AccessControlProfile {
audit, audit,
value value
.get_ava_single("name") .get_ava_single("name")
.ok_or(OperationError::InvalidACPState) .ok_or(OperationError::InvalidACPState("Missing name"))
); );
// copy uuid // copy uuid
let uuid = value.get_uuid(); let uuid = value.get_uuid();
@ -290,21 +300,21 @@ impl AccessControlProfile {
audit, audit,
value value
.get_ava_single("acp_receiver") .get_ava_single("acp_receiver")
.ok_or(OperationError::InvalidACPState) .ok_or(OperationError::InvalidACPState("Missing acp_receiver"))
); );
// targetscope, and turn to real filter // targetscope, and turn to real filter
let targetscope_raw = try_audit!( let targetscope_raw = try_audit!(
audit, audit,
value value
.get_ava_single("acp_targetscope") .get_ava_single("acp_targetscope")
.ok_or(OperationError::InvalidACPState) .ok_or(OperationError::InvalidACPState("Missing acp_targetscope"))
); );
audit_log!(audit, "RAW receiver {:?}", receiver_raw); audit_log!(audit, "RAW receiver {:?}", receiver_raw);
let receiver_f: ProtoFilter = try_audit!( let receiver_f: ProtoFilter = try_audit!(
audit, audit,
serde_json::from_str(receiver_raw.as_str()) serde_json::from_str(receiver_raw.as_str())
.map_err(|_| OperationError::InvalidACPState) .map_err(|_| OperationError::InvalidACPState("Invalid acp_receiver"))
); );
let receiver_i = try_audit!(audit, Filter::from_rw(audit, &receiver_f, qs)); let receiver_i = try_audit!(audit, Filter::from_rw(audit, &receiver_f, qs));
let receiver = try_audit!( let receiver = try_audit!(
@ -319,7 +329,7 @@ impl AccessControlProfile {
audit, audit,
serde_json::from_str(targetscope_raw.as_str()).map_err(|e| { serde_json::from_str(targetscope_raw.as_str()).map_err(|e| {
audit_log!(audit, "JSON error {:?}", e); audit_log!(audit, "JSON error {:?}", e);
OperationError::InvalidACPState OperationError::InvalidACPState("Invalid acp_targetscope")
}) })
); );
let targetscope_i = try_audit!(audit, Filter::from_rw(audit, &targetscope_f, qs)); let targetscope_i = try_audit!(audit, Filter::from_rw(audit, &targetscope_f, qs));
@ -382,6 +392,7 @@ pub trait AccessControlsTransaction {
// If this is an internal search, return our working set. // If this is an internal search, return our working set.
let rec_entry: &Entry<EntryValid, EntryCommitted> = match &se.event.origin { let rec_entry: &Entry<EntryValid, EntryCommitted> = match &se.event.origin {
EventOrigin::Internal => { EventOrigin::Internal => {
audit_log!(audit, "Internal operation, bypassing access check");
// No need to check ACS // No need to check ACS
return Ok(entries); return Ok(entries);
} }

View file

@ -12,8 +12,11 @@ macro_rules! audit_log {
($audit:expr, $($arg:tt)*) => ({ ($audit:expr, $($arg:tt)*) => ({
use std::fmt; use std::fmt;
if cfg!(test) || cfg!(debug_assertions) { if cfg!(test) || cfg!(debug_assertions) {
print!("DEBUG AUDIT ({}:{} {})-> ", file!(), line!(), $audit.id()); // debug!("DEBUG AUDIT ({}:{} {})-> ", file!(), line!(), $audit.id());
println!($($arg)*) // debug!($($arg)*)
// debug!("DEBUG AUDIT ({}:{} {})-> ", file!(), line!(), $audit.id());
// debug!("line: {}", line!());
debug!($($arg)*)
} }
$audit.log_event( $audit.log_event(
fmt::format( fmt::format(

View file

@ -168,7 +168,7 @@ impl Drop for BackendReadTransaction {
// TODO: Is this correct for RO txn? // TODO: Is this correct for RO txn?
fn drop(self: &mut Self) { fn drop(self: &mut Self) {
if !self.committed { if !self.committed {
println!("Aborting txn"); debug!("Aborting BE RO txn");
self.conn self.conn
.execute("ROLLBACK TRANSACTION", NO_PARAMS) .execute("ROLLBACK TRANSACTION", NO_PARAMS)
// TODO: Can we do this without expect? I think we can't due // TODO: Can we do this without expect? I think we can't due
@ -183,7 +183,7 @@ impl Drop for BackendReadTransaction {
impl BackendReadTransaction { impl BackendReadTransaction {
pub fn new(conn: r2d2::PooledConnection<SqliteConnectionManager>) -> Self { pub fn new(conn: r2d2::PooledConnection<SqliteConnectionManager>) -> Self {
// Start the transaction // Start the transaction
println!("Starting RO txn ..."); debug!("Starting BE RO txn ...");
// TODO: Way to flag that this will be a read only? // TODO: Way to flag that this will be a read only?
// TODO: Can we do this without expect? I think we need to change the type // TODO: Can we do this without expect? I think we need to change the type
// signature here if we wanted to... // signature here if we wanted to...
@ -209,7 +209,7 @@ impl Drop for BackendWriteTransaction {
// Abort // Abort
fn drop(self: &mut Self) { fn drop(self: &mut Self) {
if !self.committed { if !self.committed {
println!("Aborting txn"); debug!("Aborting BE WR txn");
self.conn self.conn
.execute("ROLLBACK TRANSACTION", NO_PARAMS) .execute("ROLLBACK TRANSACTION", NO_PARAMS)
// TODO: Can we do this without expect? I think we can't due // TODO: Can we do this without expect? I think we can't due
@ -230,7 +230,7 @@ impl BackendTransaction for BackendWriteTransaction {
impl BackendWriteTransaction { impl BackendWriteTransaction {
pub fn new(conn: r2d2::PooledConnection<SqliteConnectionManager>) -> Self { pub fn new(conn: r2d2::PooledConnection<SqliteConnectionManager>) -> Self {
// Start the transaction // Start the transaction
println!("Starting WR txn ..."); debug!("Starting BE WR txn ...");
// TODO: Way to flag that this will be a write? // TODO: Way to flag that this will be a write?
// TODO: Can we do this without expect? I think we need to change the type // TODO: Can we do this without expect? I think we need to change the type
// signature here if we wanted to... // signature here if we wanted to...
@ -579,7 +579,7 @@ impl BackendWriteTransaction {
} }
pub fn commit(mut self) -> Result<(), OperationError> { pub fn commit(mut self) -> Result<(), OperationError> {
println!("Commiting txn"); debug!("Commiting BE txn");
assert!(!self.committed); assert!(!self.committed);
self.committed = true; self.committed = true;
self.conn self.conn

View file

@ -1,6 +1,7 @@
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Configuration { pub struct Configuration {
pub address: String, pub address: String,
pub domain: String,
pub threads: usize, pub threads: usize,
pub db_path: String, pub db_path: String,
pub maximum_request: usize, pub maximum_request: usize,
@ -12,12 +13,14 @@ impl Configuration {
pub fn new() -> Self { pub fn new() -> Self {
Configuration { Configuration {
address: String::from("127.0.0.1:8080"), address: String::from("127.0.0.1:8080"),
domain: String::from("127.0.0.1"),
threads: 8, threads: 8,
db_path: String::from(""), db_path: String::from(""),
maximum_request: 262144, // 256k maximum_request: 262144, // 256k
// log type // log type
// log path // log path
secure_cookies: true, // TODO: default true in prd
secure_cookies: false,
} }
} }
} }

View file

@ -1,3 +1,8 @@
// On test builds, define to 60 seconds
#[cfg(test)]
pub static PURGE_TIMEOUT: u64 = 60;
// For production, 1 hour.
#[cfg(not(test))]
pub static PURGE_TIMEOUT: u64 = 3600; pub static PURGE_TIMEOUT: u64 = 3600;
pub static UUID_ADMIN: &'static str = "00000000-0000-0000-0000-000000000000"; pub static UUID_ADMIN: &'static str = "00000000-0000-0000-0000-000000000000";
@ -32,6 +37,21 @@ pub static JSON_IDM_ADMINS_V1: &'static str = r#"{
} }
}"#; }"#;
pub static _UUID_SYSTEM_INFO: &'static str = "00000000-0000-0000-0000-ffffff000001";
pub static JSON_SYSTEM_INFO_V1: &'static str = r#"{
"valid": {
"uuid": "00000000-0000-0000-0000-ffffff000001"
},
"state": null,
"attrs": {
"class": ["object", "system_info"],
"uuid": ["00000000-0000-0000-0000-ffffff000001"],
"description": ["System info and metadata object."],
"version": ["1"],
"domain": ["example.com"]
}
}"#;
pub static _UUID_IDM_ADMINS_ACP_SEARCH_V1: &'static str = "00000000-0000-0000-0000-ffffff000002"; pub static _UUID_IDM_ADMINS_ACP_SEARCH_V1: &'static str = "00000000-0000-0000-0000-ffffff000002";
pub static JSON_IDM_ADMINS_ACP_SEARCH_V1: &'static str = r#"{ pub static JSON_IDM_ADMINS_ACP_SEARCH_V1: &'static str = r#"{
"valid": { "valid": {
@ -79,7 +99,30 @@ pub static JSON_IDM_ADMINS_ACP_REVIVE_V1: &'static str = r#"{
} }
}"#; }"#;
pub static _UUID_ANONYMOUS: &'static str = "00000000-0000-0000-0000-ffffffffffff"; pub static _UUID_IDM_SELF_ACP_READ_V1: &'static str = "00000000-0000-0000-0000-ffffff000004";
pub static JSON_IDM_SELF_ACP_READ_V1: &'static str = r#"{
"valid": {
"uuid": "00000000-0000-0000-0000-ffffff000004"
},
"state": null,
"attrs": {
"class": ["object", "access_control_profile", "access_control_search"],
"name": ["idm_self_acp_read"],
"uuid": ["00000000-0000-0000-0000-ffffff000004"],
"description": ["Builtin IDM Control for self read - required for whoami."],
"version": ["1"],
"acp_enable": ["true"],
"acp_receiver": [
"\"Self\""
],
"acp_targetscope": [
"\"Self\""
],
"acp_search_attr": ["name", "uuid"]
}
}"#;
pub static UUID_ANONYMOUS: &'static str = "00000000-0000-0000-0000-ffffffffffff";
pub static JSON_ANONYMOUS_V1: &'static str = r#"{ pub static JSON_ANONYMOUS_V1: &'static str = r#"{
"valid": { "valid": {
"uuid": "00000000-0000-0000-0000-ffffffffffff" "uuid": "00000000-0000-0000-0000-ffffffffffff"
@ -95,21 +138,6 @@ pub static JSON_ANONYMOUS_V1: &'static str = r#"{
} }
}"#; }"#;
pub static _UUID_SYSTEM_INFO: &'static str = "00000000-0000-0000-0000-ffffff000001";
pub static JSON_SYSTEM_INFO_V1: &'static str = r#"{
"valid": {
"uuid": "00000000-0000-0000-0000-ffffff000001"
},
"state": null,
"attrs": {
"class": ["object", "system_info"],
"uuid": ["00000000-0000-0000-0000-ffffff000001"],
"description": ["System info and metadata object."],
"version": ["1"],
"domain": ["example.com"]
}
}"#;
// Core // Core
pub static UUID_SCHEMA_ATTR_CLASS: &'static str = "aa0f193f-3010-4783-9c9e-f97edb14d8c2"; pub static UUID_SCHEMA_ATTR_CLASS: &'static str = "aa0f193f-3010-4783-9c9e-f97edb14d8c2";
pub static UUID_SCHEMA_ATTR_UUID: &'static str = "642a893b-fe1a-4fe1-805d-fb78e7f83ee7"; pub static UUID_SCHEMA_ATTR_UUID: &'static str = "642a893b-fe1a-4fe1-805d-fb78e7f83ee7";

View file

@ -11,17 +11,34 @@ use futures::{future, Future, Stream};
use crate::config::Configuration; use crate::config::Configuration;
// SearchResult // SearchResult
use crate::async_log;
use crate::error::OperationError;
use crate::interval::IntervalActor; use crate::interval::IntervalActor;
use crate::log; use crate::proto::v1::actors::QueryServerV1;
use crate::proto_v1::{AuthRequest, CreateRequest, DeleteRequest, ModifyRequest, SearchRequest}; use crate::proto::v1::messages::{AuthMessage, WhoamiMessage};
use crate::proto_v1_actors::QueryServerV1; use crate::proto::v1::{
AuthRequest, AuthResponse, AuthState, CreateRequest, DeleteRequest, ModifyRequest,
SearchRequest, UserAuthToken, WhoamiRequest, WhoamiResponse,
};
use uuid::Uuid;
struct AppState { struct AppState {
qe: actix::Addr<QueryServerV1>, qe: actix::Addr<QueryServerV1>,
max_size: usize, max_size: usize,
} }
macro_rules! json_event_decode { fn get_current_user(req: &HttpRequest<AppState>) -> Option<UserAuthToken> {
match req.session().get::<UserAuthToken>("uat") {
Ok(maybe_uat) => maybe_uat,
Err(_) => {
// return Box::new(future::err(e));
None
}
}
}
macro_rules! json_event_post {
($req:expr, $state:expr, $event_type:ty, $message_type:ty) => {{ ($req:expr, $state:expr, $event_type:ty, $message_type:ty) => {{
// This is copied every request. Is there a better way? // This is copied every request. Is there a better way?
// The issue is the fold move takes ownership of state if // The issue is the fold move takes ownership of state if
@ -52,7 +69,7 @@ macro_rules! json_event_decode {
// let r_obj = serde_json::from_slice::<SearchRequest>(&body); // let r_obj = serde_json::from_slice::<SearchRequest>(&body);
let r_obj = serde_json::from_slice::<$message_type>(&body); let r_obj = serde_json::from_slice::<$message_type>(&body);
// Send to the db for create // Send to the db for handling
match r_obj { match r_obj {
Ok(obj) => { Ok(obj) => {
let res = $state let res = $state
@ -81,33 +98,63 @@ macro_rules! json_event_decode {
}}; }};
} }
macro_rules! json_event_get {
($req:expr, $state:expr, $event_type:ty, $message_type:ty) => {{
// Get current auth data - remember, the QS checks if the
// none/some is okay, because it's too hard to make it work here
// with all the async parts.
let uat = get_current_user(&$req);
// New event, feed current auth data from the token to it.
let obj = <($message_type)>::new(uat);
let res = $state.qe.send(obj).from_err().and_then(|res| match res {
Ok(event_result) => Ok(HttpResponse::Ok().json(event_result)),
Err(e) => match e {
OperationError::NotAuthenticated => Ok(HttpResponse::Unauthorized().json(e)),
_ => Ok(HttpResponse::InternalServerError().json(e)),
},
});
Box::new(res)
}};
}
// Handle the various end points we need to expose // Handle the various end points we need to expose
fn create( fn create(
(req, state): (HttpRequest<AppState>, State<AppState>), (req, state): (HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> { ) -> impl Future<Item = HttpResponse, Error = Error> {
json_event_decode!(req, state, CreateEvent, CreateRequest) json_event_post!(req, state, CreateEvent, CreateRequest)
} }
fn modify( fn modify(
(req, state): (HttpRequest<AppState>, State<AppState>), (req, state): (HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> { ) -> impl Future<Item = HttpResponse, Error = Error> {
json_event_decode!(req, state, ModifyEvent, ModifyRequest) json_event_post!(req, state, ModifyEvent, ModifyRequest)
} }
fn delete( fn delete(
(req, state): (HttpRequest<AppState>, State<AppState>), (req, state): (HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> { ) -> impl Future<Item = HttpResponse, Error = Error> {
json_event_decode!(req, state, DeleteEvent, DeleteRequest) json_event_post!(req, state, DeleteEvent, DeleteRequest)
} }
fn search( fn search(
(req, state): (HttpRequest<AppState>, State<AppState>), (req, state): (HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> { ) -> impl Future<Item = HttpResponse, Error = Error> {
json_event_decode!(req, state, SearchEvent, SearchRequest) json_event_post!(req, state, SearchEvent, SearchRequest)
} }
// delete, modify fn whoami(
(req, state): (HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> {
// Actually this may not work as it assumes post not get.
json_event_get!(req, state, WhoamiEvent, WhoamiMessage)
}
// We probably need an extract auth or similar to handle the different
// types (cookie, bearer), and to generic this over get/post.
fn auth( fn auth(
(req, state): (HttpRequest<AppState>, State<AppState>), (req, state): (HttpRequest<AppState>, State<AppState>),
@ -135,46 +182,66 @@ fn auth(
// First, deal with some state management. // First, deal with some state management.
// Do anything here first that's needed like getting the session details // Do anything here first that's needed like getting the session details
// out of the req cookie. // out of the req cookie.
// let mut counter = 1;
// From the actix source errors here // From the actix source errors here
// seems to be related to the serde_json deserialise of the cookie // seems to be related to the serde_json deserialise of the cookie
// content, and because we control it's get/set it SHOULD be fine // content, and because we control it's get/set it SHOULD be fine
// provided we use secure cookies. But we can't always trust that ... // provided we use secure cookies. But we can't always trust that ...
let maybe_count = match req.session().get::<i32>("counter") { let maybe_sessionid = match req.session().get::<Uuid>("auth-session-id") {
Ok(c) => c, Ok(c) => c,
Err(e) => { Err(e) => {
return Box::new(future::err(e)); return Box::new(future::err(e));
} }
}; };
let c = match maybe_count { let auth_msg = AuthMessage::new(obj, maybe_sessionid);
Some(count) => {
println!("SESSION value: {}", count);
count + 1
}
None => {
println!("INIT value: 1");
1
}
};
match req.session().set("counter", c) {
Ok(_) => {}
Err(e) => {
return Box::new(future::err(e));
}
};
// We probably need to know if we allocate the cookie, that this is a // We probably need to know if we allocate the cookie, that this is a
// new session, and in that case, anything *except* authrequest init is // new session, and in that case, anything *except* authrequest init is
// invalid. // invalid.
let res =
let res = state.qe.send(obj).from_err().and_then(|res| match res { state
Ok(event_result) => Ok(HttpResponse::Ok().json(event_result)), .qe
Err(e) => Ok(HttpResponse::InternalServerError().json(e)), .send(auth_msg)
}); .from_err()
.and_then(move |res| match res {
Ok(ar) => {
match &ar.state {
AuthState::Success(uat) => {
// Remove the auth-session-id
req.session().remove("auth-session-id");
// Set the uat into the cookie
match req.session().set("uat", uat) {
Ok(_) => Ok(HttpResponse::Ok().json(ar)),
Err(_) => {
Ok(HttpResponse::InternalServerError()
.json(()))
}
}
}
AuthState::Denied => {
// Remove the auth-session-id
req.session().remove("auth-session-id");
Ok(HttpResponse::Ok().json(ar))
}
AuthState::Continue(_) => {
// TODO: Where do we get the auth-session-id from?
// Ensure the auth-session-id is set
match req
.session()
.set("auth-session-id", ar.sessionid)
{
Ok(_) => Ok(HttpResponse::Ok().json(ar)),
Err(_) => {
Ok(HttpResponse::InternalServerError()
.json(()))
}
}
}
}
}
Err(e) => Ok(HttpResponse::InternalServerError().json(e)),
});
Box::new(res) Box::new(res)
} }
Err(e) => Box::new(future::err(error::ErrorBadRequest(format!( Err(e) => Box::new(future::err(error::ErrorBadRequest(format!(
@ -186,32 +253,12 @@ fn auth(
) )
} }
fn whoami(req: &HttpRequest<AppState>) -> Result<&'static str> {
println!("{:?}", req);
// RequestSession trait is used for session access
let mut counter = 1;
if let Some(count) = req.session().get::<i32>("counter")? {
println!("SESSION value: {}", count);
counter = count + 1;
req.session().set("counter", counter)?;
} else {
req.session().set("counter", counter)?;
}
Ok("welcome!")
}
pub fn create_server_core(config: Configuration) { pub fn create_server_core(config: Configuration) {
// Configure the middleware logger // Until this point, we probably want to write to the log macro fns.
::std::env::set_var("RUST_LOG", "actix_web=info");
env_logger::init();
// Until this point, we probably want to write to stderr // The log server is started on it's own thread, and is contacted
// Start up the logging system: for now it just maps to stderr // asynchronously.
let log_addr = async_log::start();
// The log server is started on it's own thread
let log_addr = log::start();
log_event!(log_addr, "Starting rsidm with configuration: {:?}", config); log_event!(log_addr, "Starting rsidm with configuration: {:?}", config);
// Similar, create a stats thread which aggregates statistics from the // Similar, create a stats thread which aggregates statistics from the
@ -236,6 +283,7 @@ pub fn create_server_core(config: Configuration) {
// Copy the max size // Copy the max size
let max_size = config.maximum_request; let max_size = config.maximum_request;
let secure_cookies = config.secure_cookies; let secure_cookies = config.secure_cookies;
let domain = config.domain.clone();
// start the web server // start the web server
actix_web::server::new(move || { actix_web::server::new(move || {
@ -246,22 +294,25 @@ pub fn create_server_core(config: Configuration) {
// Connect all our end points here. // Connect all our end points here.
.middleware(middleware::Logger::default()) .middleware(middleware::Logger::default())
.middleware(session::SessionStorage::new( .middleware(session::SessionStorage::new(
// Signed prevents tampering. this 32 byte key MUST // TODO: Signed prevents tampering. this 32 byte key MUST
// be generated (probably stored in DB for cross-host access) // be generated (probably stored in DB for cross-host access)
session::CookieSessionBackend::signed(&[0; 32]) session::CookieSessionBackend::signed(&[0; 32])
.path("/") // Limit to path?
// .path("/")
//.max_age() duration of the token life TODO make this proper! //.max_age() duration of the token life TODO make this proper!
.domain("localhost") // .domain(domain.as_str())
.same_site(cookie::SameSite::Strict) // constrain to the domain // .same_site(cookie::SameSite::Strict) // constrain to the domain
// Disallow from js // Disallow from js and ...?
.http_only(true) .http_only(false)
.name("rsidm-session") .name("rsidm-session")
// This forces https only // This forces https only if true
.secure(secure_cookies), .secure(secure_cookies),
)) ))
// .resource("/", |r| r.f(index)) // .resource("/", |r| r.f(index))
// curl --header ...? // curl --header ...?
.resource("/v1/whoami", |r| r.f(whoami)) .resource("/v1/whoami", |r| {
r.method(http::Method::GET).with_async(whoami)
})
// .resource("/v1/login", ...) // .resource("/v1/login", ...)
// .resource("/v1/logout", ...) // .resource("/v1/logout", ...)
// .resource("/v1/token", ...) generate a token for id servers to use // .resource("/v1/token", ...) generate a token for id servers to use

View file

@ -3,7 +3,8 @@ use crate::audit::AuditScope;
use crate::error::{OperationError, SchemaError}; use crate::error::{OperationError, SchemaError};
use crate::filter::{Filter, FilterInvalid, FilterResolved, FilterValidResolved}; use crate::filter::{Filter, FilterInvalid, FilterResolved, FilterValidResolved};
use crate::modify::{Modify, ModifyInvalid, ModifyList, ModifyValid}; use crate::modify::{Modify, ModifyInvalid, ModifyList, ModifyValid};
use crate::proto_v1::Entry as ProtoEntry; use crate::proto::v1::Entry as ProtoEntry;
use crate::proto::v1::UserAuthToken;
use crate::schema::{SchemaAttribute, SchemaClass, SchemaTransaction}; use crate::schema::{SchemaAttribute, SchemaClass, SchemaTransaction};
use crate::server::{QueryServerTransaction, QueryServerWriteTransaction}; use crate::server::{QueryServerTransaction, QueryServerWriteTransaction};
@ -702,7 +703,10 @@ impl<STATE> Entry<EntryValid, STATE> {
))) )))
} }
pub fn into(&self) -> ProtoEntry { // FIXME: This should probably have an entry state for "reduced"
// and then only that state can provide the into_pe type, so that we
// can guarantee that all entries must have been security checked.
pub fn into_pe(&self) -> ProtoEntry {
// It's very likely that at this stage we'll need to apply // It's very likely that at this stage we'll need to apply
// access controls, dynamic attributes or more. // access controls, dynamic attributes or more.
// As a result, this may not even be the right place // As a result, this may not even be the right place

View file

@ -28,12 +28,16 @@ pub enum OperationError {
InvalidRequestState, InvalidRequestState,
InvalidState, InvalidState,
InvalidEntryState, InvalidEntryState,
InvalidACPState, InvalidACPState(&'static str),
InvalidAccountState(&'static str),
BackendEngine, BackendEngine,
SQLiteError, //(RusqliteError) SQLiteError, //(RusqliteError)
FsError, FsError,
SerdeJsonError, SerdeJsonError,
AccessDenied, AccessDenied,
NotAuthenticated,
InvalidAuthState(&'static str),
InvalidSessionState,
} }
#[derive(Serialize, Deserialize, Debug, PartialEq)] #[derive(Serialize, Deserialize, Debug, PartialEq)]

View file

@ -1,10 +1,11 @@
use crate::audit::AuditScope; use crate::audit::AuditScope;
use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryValid}; use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryValid};
use crate::filter::{Filter, FilterValid}; use crate::filter::{Filter, FilterValid};
use crate::proto_v1::Entry as ProtoEntry; use crate::proto::v1::Entry as ProtoEntry;
use crate::proto_v1::{ use crate::proto::v1::{
AuthRequest, AuthResponse, AuthStatus, CreateRequest, DeleteRequest, ModifyRequest, AuthAllowed, AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest,
OperationResponse, ReviveRecycledRequest, SearchRequest, SearchResponse, DeleteRequest, ModifyRequest, OperationResponse, ReviveRecycledRequest, SearchRequest,
SearchResponse, UserAuthToken, WhoamiRequest, WhoamiResponse,
}; };
// use error::OperationError; // use error::OperationError;
use crate::error::OperationError; use crate::error::OperationError;
@ -12,6 +13,8 @@ use crate::modify::{ModifyList, ModifyValid};
use crate::server::{ use crate::server::{
QueryServerReadTransaction, QueryServerTransaction, QueryServerWriteTransaction, QueryServerReadTransaction, QueryServerTransaction, QueryServerWriteTransaction,
}; };
use crate::proto::v1::messages::AuthMessage;
// Bring in schematransaction trait for validate // Bring in schematransaction trait for validate
// use crate::schema::SchemaTransaction; // use crate::schema::SchemaTransaction;
@ -21,9 +24,10 @@ use crate::filter::FilterInvalid;
#[cfg(test)] #[cfg(test)]
use crate::modify::ModifyInvalid; use crate::modify::ModifyInvalid;
#[cfg(test)] #[cfg(test)]
use crate::proto_v1::SearchRecycledRequest; use crate::proto::v1::SearchRecycledRequest;
use actix::prelude::*; use actix::prelude::*;
use uuid::Uuid;
// Should the event Result have the log items? // Should the event Result have the log items?
// FIXME: Remove seralising here - each type should // FIXME: Remove seralising here - each type should
@ -48,13 +52,13 @@ pub struct SearchResult {
impl SearchResult { impl SearchResult {
pub fn new(entries: Vec<Entry<EntryValid, EntryCommitted>>) -> Self { pub fn new(entries: Vec<Entry<EntryValid, EntryCommitted>>) -> Self {
SearchResult { SearchResult {
// FIXME: Can we consume this iter?
entries: entries entries: entries
.iter() .iter()
.map(|e| { .map(|e| {
// FIXME: The issue here is this probably is applying transforms // All the needed transforms for this result are done
// like access control ... May need to change. // in search_ext. This is just an entry -> protoentry
e.into() // step.
e.into_pe()
}) })
.collect(), .collect(),
} }
@ -106,6 +110,23 @@ impl Event {
}) })
} }
pub fn from_ro_uat(
audit: &mut AuditScope,
qs: &QueryServerReadTransaction,
uat: Option<UserAuthToken>,
) -> Result<Self, OperationError> {
audit_log!(audit, "from_ro_uat -> {:?}", uat);
let uat = uat.ok_or(OperationError::NotAuthenticated)?;
let e = try_audit!(audit, qs.internal_search_uuid(audit, uat.uuid.as_str()));
// FIXME: Now apply claims from the uat into the Entry
// to allow filtering.
Ok(Event {
origin: EventOrigin::User(e),
})
}
pub fn from_rw_request( pub fn from_rw_request(
audit: &mut AuditScope, audit: &mut AuditScope,
qs: &QueryServerWriteTransaction, qs: &QueryServerWriteTransaction,
@ -185,6 +206,22 @@ impl SearchEvent {
} }
} }
pub fn from_whoami_request(
audit: &mut AuditScope,
uat: Option<UserAuthToken>,
qs: &QueryServerReadTransaction,
) -> Result<Self, OperationError> {
Ok(SearchEvent {
event: Event::from_ro_uat(audit, qs, uat)?,
filter: filter!(f_self())
.validate(qs.get_schema())
.map_err(|e| OperationError::SchemaViolation(e))?,
filter_orig: filter_all!(f_self())
.validate(qs.get_schema())
.map_err(|e| OperationError::SchemaViolation(e))?,
})
}
// Just impersonate the account with no filter changes. // Just impersonate the account with no filter changes.
#[cfg(test)] #[cfg(test)]
pub unsafe fn new_impersonate_entry_ser(e: &str, filter: Filter<FilterInvalid>) -> Self { pub unsafe fn new_impersonate_entry_ser(e: &str, filter: Filter<FilterInvalid>) -> Self {
@ -329,7 +366,7 @@ impl CreateEvent {
entries: Vec<Entry<EntryInvalid, EntryNew>>, entries: Vec<Entry<EntryInvalid, EntryNew>>,
) -> Self { ) -> Self {
CreateEvent { CreateEvent {
event: unsafe { Event::from_impersonate_entry_ser(e) }, event: Event::from_impersonate_entry_ser(e),
entries: entries, entries: entries,
} }
} }
@ -519,22 +556,131 @@ impl ModifyEvent {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct AuthEvent { pub struct AuthEventStepInit {
// pub event: Event, pub name: String,
pub appid: Option<String>,
} }
impl AuthEvent { #[derive(Debug)]
pub fn from_request(_request: AuthRequest) -> Self { pub struct AuthEventStepCreds {
AuthEvent {} pub sessionid: Uuid,
pub creds: Vec<AuthCredential>,
}
#[derive(Debug)]
pub enum AuthEventStep {
Init(AuthEventStepInit),
Creds(AuthEventStepCreds),
}
impl AuthEventStep {
fn from_authstep(aus: AuthStep, sid: Option<Uuid>) -> Result<Self, OperationError> {
match aus {
AuthStep::Init(name, appid) => {
if sid.is_some() {
Err(OperationError::InvalidAuthState(
"session id present in init",
))
} else {
Ok(AuthEventStep::Init(AuthEventStepInit {
name: name,
appid: appid,
}))
}
}
AuthStep::Creds(creds) => match sid {
Some(ssid) => Ok(AuthEventStep::Creds(AuthEventStepCreds {
sessionid: ssid,
creds: creds,
})),
None => Err(OperationError::InvalidAuthState(
"session id not present in cred",
)),
},
}
}
#[cfg(test)]
pub fn anonymous_init() -> Self {
AuthEventStep::Init(AuthEventStepInit {
name: "anonymous".to_string(),
appid: None,
})
}
#[cfg(test)]
pub fn anonymous_cred_step(sid: Uuid) -> Self {
AuthEventStep::Creds(AuthEventStepCreds {
sessionid: sid,
creds: vec![AuthCredential::Anonymous],
})
} }
} }
pub struct AuthResult {} #[derive(Debug)]
pub struct AuthEvent {
pub event: Option<Event>,
pub step: AuthEventStep,
// pub sessionid: Option<Uuid>,
}
impl AuthEvent {
pub fn from_message(msg: AuthMessage) -> Result<Self, OperationError> {
Ok(AuthEvent {
// TODO: Change to AuthMessage, and fill in uat?
event: None,
step: AuthEventStep::from_authstep(msg.req.step, msg.sessionid)?,
})
}
#[cfg(test)]
pub fn anonymous_init() -> Self {
AuthEvent {
event: None,
step: AuthEventStep::anonymous_init(),
}
}
#[cfg(test)]
pub fn anonymous_cred_step(sid: Uuid) -> Self {
AuthEvent {
event: None,
step: AuthEventStep::anonymous_cred_step(sid),
}
}
}
// Probably should be a struct with the session id present.
#[derive(Debug)]
pub struct AuthResult {
pub sessionid: Uuid,
// TODO: Make this an event specific authstate type?
pub state: AuthState,
}
impl AuthResult { impl AuthResult {
pub fn response(self) -> AuthResponse { pub fn response(self) -> AuthResponse {
AuthResponse { AuthResponse {
status: AuthStatus::Begin(String::from("hello")), sessionid: self.sessionid,
state: self.state,
}
}
}
pub struct WhoamiResult {
youare: ProtoEntry,
}
impl WhoamiResult {
pub fn new(e: Entry<EntryValid, EntryCommitted>) -> Self {
WhoamiResult {
youare: e.into_pe(),
}
}
pub fn response(self) -> WhoamiResponse {
WhoamiResponse {
youare: self.youare,
} }
} }
} }

View file

@ -5,7 +5,7 @@
use crate::audit::AuditScope; use crate::audit::AuditScope;
use crate::error::{OperationError, SchemaError}; use crate::error::{OperationError, SchemaError};
use crate::event::{Event, EventOrigin}; use crate::event::{Event, EventOrigin};
use crate::proto_v1::Filter as ProtoFilter; use crate::proto::v1::Filter as ProtoFilter;
use crate::schema::SchemaTransaction; use crate::schema::SchemaTransaction;
use crate::server::{ use crate::server::{
QueryServerReadTransaction, QueryServerTransaction, QueryServerWriteTransaction, QueryServerReadTransaction, QueryServerTransaction, QueryServerWriteTransaction,

119
src/lib/idm/account.rs Normal file
View file

@ -0,0 +1,119 @@
use crate::entry::{Entry, EntryCommitted, EntryValid};
use crate::error::OperationError;
use crate::proto::v1::{AuthAllowed, UserAuthToken};
use crate::idm::claim::Claim;
use crate::idm::group::Group;
use std::convert::TryFrom;
use uuid::Uuid;
#[derive(Debug, Clone)]
pub(crate) struct Account {
// Later these could be &str if we cache entry here too ...
// They can't because if we mod the entry, we'll lose the ref.
//
// We do need to decide if we'll cache the entry, or if we just "work out"
// what the ops should be based on the values we cache here ... That's a future
// william problem I think :)
pub name: String,
pub displayname: String,
pub uuid: String,
pub groups: Vec<Group>,
// creds (various types)
// groups?
// claims?
// account expiry?
}
impl TryFrom<Entry<EntryValid, EntryCommitted>> for Account {
type Error = OperationError;
fn try_from(value: Entry<EntryValid, EntryCommitted>) -> Result<Self, Self::Error> {
// Check the classes
if !value.attribute_value_pres("class", "account") {
return Err(OperationError::InvalidAccountState(
"Missing class: account",
));
}
// Now extract our needed attributes
let name = value
.get_ava_single("name")
.ok_or(OperationError::InvalidAccountState(
"Missing attribute: name",
))?
.clone();
let displayname = value
.get_ava_single("displayname")
.ok_or(OperationError::InvalidAccountState(
"Missing attribute: displayname",
))?
.clone();
// TODO: Resolve groups!!!!
let groups = Vec::new();
let uuid = value.get_uuid().clone();
Ok(Account {
uuid: uuid,
name: name,
displayname: displayname,
groups: groups,
})
}
}
impl Account {
// Could this actually take a claims list and application instead?
pub(crate) fn to_userauthtoken(&self, claims: Vec<Claim>) -> Option<UserAuthToken> {
// This could consume self?
// The cred handler provided is what authenticated this user, so we can use it to
// process what the proper claims should be.
// Get the claims from the cred_h
Some(UserAuthToken {
name: self.name.clone(),
displayname: self.name.clone(),
uuid: self.uuid.clone(),
application: None,
groups: self.groups.iter().map(|g| g.into_proto()).collect(),
claims: claims.iter().map(|c| c.into_proto()).collect(),
})
}
}
// Need to also add a "to UserAuthToken" ...
// Need tests for conversion and the cred validations
#[cfg(test)]
mod tests {
use crate::constants::JSON_ANONYMOUS_V1;
use crate::entry::{Entry, EntryNew, EntryValid};
use crate::idm::account::Account;
use crate::proto::v1::AuthAllowed;
use std::convert::TryFrom;
#[test]
fn test_idm_account_from_anonymous() {
let anon_e: Entry<EntryValid, EntryNew> =
serde_json::from_str(JSON_ANONYMOUS_V1).expect("Json deserialise failure!");
let anon_e = unsafe { anon_e.to_valid_committed() };
let anon_account = Account::try_from(anon_e).expect("Must not fail");
println!("{:?}", anon_account);
// I think that's it? we may want to check anonymous mech ...
}
#[test]
fn test_idm_account_from_real() {
// For now, nothing, but later, we'll test different types of cred
// passing.
}
}

188
src/lib/idm/authsession.rs Normal file
View file

@ -0,0 +1,188 @@
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};
// Each CredHandler takes one or more credentials and determines if the
// handlers requirements can be 100% fufilled. This is where MFA or other
// auth policies would exist, but each credHandler has to be a whole
// encapsulated unit of function.
enum CredState {
Success(Vec<Claim>),
Continue(Vec<AuthAllowed>),
// TODO: Should we have a reason in Denied so that we
Denied,
}
#[derive(Clone, Debug)]
enum CredHandler {
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?
}
impl CredHandler {
pub fn validate(&mut self, creds: &Vec<AuthCredential>) -> CredState {
match self {
CredHandler::Anonymous => {
creds.iter().fold(CredState::Denied, |acc, cred| {
// There is no "continuation" from this type.
match cred {
AuthCredential::Anonymous => {
// For anonymous, no claims will ever be issued.
CredState::Success(Vec::new())
}
_ => {
// Should we have a reason in Denied so that we
CredState::Denied
}
}
})
}
}
}
pub fn valid_auth_mechs(&self) -> Vec<AuthAllowed> {
match &self {
CredHandler::Anonymous => vec![AuthAllowed::Anonymous],
}
}
}
#[derive(Clone)]
pub(crate) struct AuthSession {
// Do we store a copy of the entry?
// How do we know what claims to add?
account: Account,
// Store how we plan to handle this sessions authentication: this is generally
// made apparent by the presentation of an application id or not. If none is presented
// we want the primary-interaction credentials.
//
// This handler will then handle the mfa and stepping up through to generate the auth states
handler: CredHandler,
// Store any related appid we are processing for.
appid: Option<String>,
finished: bool,
}
impl AuthSession {
pub fn new(account: Account, appid: Option<String>) -> Self {
// During this setup, determine the credential handler that we'll be using
// for this session. This is currently based on presentation of an application
// id.
let handler = match appid {
Some(_) => {
unimplemented!();
}
None => {
// We want the primary handler - this is where we make a decision
// based on the anonymous ... in theory this could be cleaner
// and interact with the account more?
if account.uuid == UUID_ANONYMOUS {
CredHandler::Anonymous
} else {
unimplemented!();
}
}
};
// Is this handler locked?
// Is the whole account locked?
// What about in memory account locking? Is that something
// we store in the account somehow?
// TODO: Implement handler locking!
AuthSession {
account: account,
handler: handler,
appid: appid,
finished: false,
}
}
// This should return a AuthResult or similar state of checking?
// TODO: This needs some logging ....
pub fn validate_creds(
&mut self,
creds: &Vec<AuthCredential>,
) -> Result<AuthState, OperationError> {
if self.finished {
return Err(OperationError::InvalidAuthState(
"session already finalised!",
));
}
match self.handler.validate(creds) {
CredState::Success(claims) => {
self.finished = true;
let uat = self
.account
.to_userauthtoken(claims)
.ok_or(OperationError::InvalidState)?;
Ok(AuthState::Success(uat))
}
CredState::Continue(allowed) => Ok(AuthState::Continue(allowed)),
CredState::Denied => {
self.finished = true;
Ok(AuthState::Denied)
}
}
// Also send an async message to self to log the auth as provided.
// Alternately, open a write, and commit the needed security metadata here
// now rather than async (probably better for lock-outs etc)
//
// TODO: Async message the account owner about the login?
// If this fails, how can we in memory lock the account?
//
// The lockouts could also be an in-memory concept too?
// If this suceeds audit?
// If success, to authtoken?
}
pub fn valid_auth_mechs(&self) -> Vec<AuthAllowed> {
// TODO: This needs logging ....
if self.finished {
Vec::new()
} else {
self.handler.valid_auth_mechs()
}
}
}
#[cfg(test)]
mod tests {
use crate::constants::JSON_ANONYMOUS_V1;
use crate::entry::{Entry, EntryNew, EntryValid};
use crate::idm::account::Account;
use crate::idm::authsession::AuthSession;
use crate::proto::v1::AuthAllowed;
use std::convert::TryFrom;
#[test]
fn test_idm_account_anonymous_auth_mech() {
let anon_account = entry_str_to_account!(JSON_ANONYMOUS_V1);
let session = AuthSession::new(anon_account, None);
let auth_mechs = session.valid_auth_mechs();
assert!(
true == auth_mechs.iter().fold(false, |acc, x| match x {
AuthAllowed::Anonymous => true,
_ => acc,
})
);
}
}

56
src/lib/idm/macros.rs Normal file
View file

@ -0,0 +1,56 @@
#[cfg(test)]
macro_rules! entry_str_to_account {
($entry_str:expr) => {{
use crate::entry::{Entry, EntryNew, EntryValid};
use crate::idm::account::Account;
let e: Entry<EntryValid, EntryNew> =
serde_json::from_str($entry_str).expect("Json deserialise failure!");
let e = unsafe { e.to_valid_committed() };
Account::try_from(e).expect("Account conversion failure")
}};
}
#[cfg(test)]
macro_rules! run_idm_test {
($test_fn:expr) => {{
use crate::audit::AuditScope;
use crate::be::Backend;
use crate::idm::server::IdmServer;
use crate::schema::Schema;
use crate::server::QueryServer;
use std::sync::Arc;
use env_logger;
::std::env::set_var("RUST_LOG", "actix_web=debug,rsidm=debug");
let _ = env_logger::builder().is_test(true).try_init();
let mut audit = AuditScope::new("run_test");
let be = Backend::new(&mut audit, "").expect("Failed to init be");
let schema_outer = Schema::new(&mut audit).expect("Failed to init schema");
{
let mut schema = schema_outer.write();
schema
.bootstrap_core(&mut audit)
.expect("Failed to bootstrap schema");
schema.commit().expect("Failed to commit schema");
}
let test_server = QueryServer::new(be, schema_outer);
{
let ts_write = test_server.write();
ts_write.initialise(&mut audit).expect("Init failed!");
ts_write.commit(&mut audit).expect("Commit failed!");
}
let test_idm_server = IdmServer::new(test_server.clone());
$test_fn(&test_server, &test_idm_server, &mut audit);
// Any needed teardown?
// Make sure there are no errors.
assert!(test_server.verify(&mut audit).len() == 0);
}};
}

9
src/lib/idm/mod.rs Normal file
View file

@ -0,0 +1,9 @@
#[macro_use]
mod macros;
pub(crate) mod account;
pub(crate) mod authsession;
pub(crate) mod claim;
pub(crate) mod group;
pub(crate) mod server;
// mod identity;

263
src/lib/idm/server.rs Normal file
View file

@ -0,0 +1,263 @@
use crate::audit::AuditScope;
use crate::be::Backend;
use crate::constants::UUID_ANONYMOUS;
use crate::error::OperationError;
use crate::event::{AuthEvent, AuthEventStep, AuthEventStepInit, AuthResult, SearchEvent};
use crate::idm::account::Account;
use crate::idm::authsession::AuthSession;
use crate::proto::v1::{AuthResponse, AuthState, UserAuthToken};
use crate::schema::Schema;
use crate::server::{QueryServer, QueryServerTransaction};
use concread::cowcell::{CowCell, CowCellReadTxn, CowCellWriteTxn};
use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::sync::Arc;
use uuid::Uuid;
// use lru::LruCache;
pub struct IdmServer {
// Need an auth-session table to save in progress authentications
// sessions:
//
// TODO: This should be Lru
// TODO: AuthSession should be per-session mutex to keep locking on the
// cell low to allow more concurrent auths.
sessions: CowCell<BTreeMap<Uuid, AuthSession>>,
// Need a reference to the query server.
qs: QueryServer,
}
pub struct IdmServerWriteTransaction<'a> {
// Contains methods that require writes, but in the context of writing to
// the idm in memory structures (maybe the query server too). This is
// things like authentication
sessions: CowCellWriteTxn<'a, BTreeMap<Uuid, AuthSession>>,
qs: &'a QueryServer,
}
pub struct IdmServerReadTransaction<'a> {
// This contains read-only methods, like getting users, groups
// and other structured content.
qs: &'a QueryServer,
}
impl IdmServer {
// TODO: Make number of authsessions configurable!!!
pub fn new(qs: QueryServer) -> IdmServer {
IdmServer {
sessions: CowCell::new(BTreeMap::new()),
qs: qs,
}
}
pub fn write(&self) -> IdmServerWriteTransaction {
IdmServerWriteTransaction {
sessions: self.sessions.write(),
qs: &self.qs,
}
}
pub fn read(&self) -> IdmServerReadTransaction {
IdmServerReadTransaction { qs: &self.qs }
}
}
impl<'a> IdmServerWriteTransaction<'a> {
// TODO: This should be something else, not the proto token!
pub fn auth(
&mut self,
au: &mut AuditScope,
ae: &AuthEvent,
) -> Result<AuthResult, OperationError> {
audit_log!(au, "Received AuthEvent -> {:?}", ae);
// Match on the auth event, to see what we need to do.
match &ae.step {
AuthEventStep::Init(init) => {
// Allocate a session id.
let sessionid = Uuid::new_v4();
// Begin the auth procedure!
// Start a read
//
// Actually we may not need this - at the time we issue the auth-init
// we could generate the uat, the nonce and cache hashes in memory,
// then this can just be fully without a txn.
//
// We do need a txn so that we can process/search and claims
// or related based on the quality of the provided auth steps
//
// We *DO NOT* need a write though, because I think that lock outs
// and rate limits are *per server* and *in memory* only.
let qs_read = self.qs.read();
// Check anything needed? Get the current auth-session-id from request
// because it associates to the nonce's etc which were all cached.
let filter_entry = filter!(f_or!([
f_eq("name", init.name.as_str()),
// This currently says invalid syntax, which is correct, but also
// annoying because it would be nice to search both ...
// f_eq("uuid", name.as_str()),
]));
// Get the first / single entry we expect here ....
let entry = match qs_read.internal_search(au, filter_entry) {
Ok(mut entries) => {
// Get only one entry out ...
if entries.len() >= 2 {
return Err(OperationError::InvalidDBState);
}
entries.pop().ok_or(OperationError::NoMatchingEntries)?
}
Err(e) => {
// Something went wrong! Abort!
return Err(e);
}
};
audit_log!(au, "Initiating Authentication Session for ... {:?}", entry);
// Now, convert the Entry to an account - this gives us some stronger
// typing and functionality so we can assess what auth types can
// continue, and helps to keep non-needed entry specific data
// out of the LRU.
let account = Account::try_from(entry)?;
let auth_session = AuthSession::new(account, init.appid.clone());
// Get the set of mechanisms that can proceed. This is tied
// to the session so that it can mutate state and have progression
// of what's next, or ordering.
let next_mech = auth_session.valid_auth_mechs();
// TODO: Check that no session id of the same value
// already existed, if so, error!
self.sessions.insert(sessionid, auth_session);
// Debugging: ensure we really inserted ...
assert!(self.sessions.get(&sessionid).is_some());
Ok(AuthResult {
sessionid: sessionid,
// TODO: Change this to a better internal type?
state: AuthState::Continue(next_mech),
})
}
AuthEventStep::Creds(creds) => {
// Do we have a session?
let auth_session = try_audit!(
au,
(*self.sessions)
// Why is the session missing?
.get_mut(&creds.sessionid)
.ok_or(OperationError::InvalidSessionState)
);
// Process the credentials here as required.
// Basically throw them at the auth_session and see what
// falls out.
auth_session.validate_creds(&creds.creds).map(|aus| {
AuthResult {
// Is this right?
sessionid: creds.sessionid,
state: aus,
}
})
}
}
}
pub fn commit(self) -> Result<(), OperationError> {
self.sessions.commit();
Ok(())
}
}
impl<'a> IdmServerReadTransaction<'a> {
pub fn whoami() -> () {}
}
// Need tests of the sessions and the auth ...
#[cfg(test)]
mod tests {
use crate::event::{AuthEvent, AuthResult};
use crate::proto::v1::{AuthAllowed, AuthState};
#[test]
fn test_idm_anonymous_auth() {
run_idm_test!(|_qs: &QueryServer, idms: &IdmServer, au: &mut AuditScope| {
let sid = {
// Start and test anonymous auth.
let mut idms_write = idms.write();
// Send the initial auth event for initialising the session
let anon_init = AuthEvent::anonymous_init();
// Expect success
let r1 = idms_write.auth(au, &anon_init);
/* Some weird lifetime shit happens here ... */
// audit_log!(au, "r1 ==> {:?}", r1);
let sid = match r1 {
Ok(ar) => {
let AuthResult { sessionid, state } = ar;
match state {
AuthState::Continue(mut conts) => {
// Should only be one auth mech
assert!(conts.len() == 1);
// And it should be anonymous
let m = conts.pop().expect("Should not fail");
assert!(m == AuthAllowed::Anonymous);
}
_ => {
panic!();
}
};
// Now pass back the sessionid, we are good to continue.
sessionid
}
Err(e) => {
// Should not occur!
panic!();
}
};
println!("sessionid is ==> {:?}", sid);
idms_write.commit().expect("Must not fail");
sid
};
{
let mut idms_write = idms.write();
// Now send the anonymous request, given the session id.
let anon_step = AuthEvent::anonymous_cred_step(sid);
// 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.
}
_ => {
panic!();
}
}
}
Err(e) => {
// Should not occur!
panic!();
}
};
idms_write.commit().expect("Must not fail");
}
});
}
// Test sending anonymous but with no session init.
}

View file

@ -3,7 +3,7 @@ use std::time::Duration;
use crate::constants::PURGE_TIMEOUT; use crate::constants::PURGE_TIMEOUT;
use crate::event::{PurgeRecycledEvent, PurgeTombstoneEvent}; use crate::event::{PurgeRecycledEvent, PurgeTombstoneEvent};
use crate::proto_v1_actors::QueryServerV1; use crate::proto::v1::actors::QueryServerV1;
pub struct IntervalActor { pub struct IntervalActor {
// Store any addresses we require // Store any addresses we require

View file

@ -1,3 +1,5 @@
#[macro_use]
extern crate log;
extern crate serde; extern crate serde;
extern crate serde_json; extern crate serde_json;
#[macro_use] #[macro_use]
@ -32,7 +34,7 @@ extern crate concread;
#[macro_use] #[macro_use]
mod macros; mod macros;
#[macro_use] #[macro_use]
mod log; mod async_log;
#[macro_use] #[macro_use]
mod audit; mod audit;
mod be; mod be;
@ -40,18 +42,18 @@ mod be;
pub mod constants; pub mod constants;
mod entry; mod entry;
mod event; mod event;
mod identity; // TODO: Does this need pub?
mod filter;
mod interval; mod interval;
mod modify; mod modify;
#[macro_use] #[macro_use]
mod plugins; mod plugins;
mod access; mod access;
mod idm;
mod schema; mod schema;
mod server; mod server;
pub mod config; pub mod config;
pub mod core; pub mod core;
pub mod error; pub mod error;
pub mod filter; pub mod proto;
pub mod proto_v1;
mod proto_v1_actors;

View file

@ -1,81 +0,0 @@
use actix::prelude::*;
use crate::audit::AuditScope;
// Helper for internal logging.
// Should only be used at startup/shutdown
#[macro_export]
macro_rules! log_event {
($log_addr:expr, $($arg:tt)*) => ({
use crate::log::LogEvent;
use std::fmt;
$log_addr.do_send(
LogEvent {
msg: fmt::format(
format_args!($($arg)*)
)
}
)
})
}
// We need to pass in config for this later
// Or we need to pass in the settings for it IE level and dest?
// Is there an efficent way to set a log level filter in the macros
// so that we don't msg unless it's the correct level?
// Do we need config in the log macro?
pub fn start() -> actix::Addr<EventLog> {
SyncArbiter::start(1, move || EventLog {})
}
pub struct EventLog {}
impl Actor for EventLog {
type Context = SyncContext<Self>;
/*
fn started(&mut self, ctx: &mut Self::Context) {
ctx.set_mailbox_capacity(1 << 31);
}
*/
}
// What messages can we be sent. Basically this is all the possible
// inputs we *could* recieve.
// Add a macro for easy msg write
pub struct LogEvent {
pub msg: String,
}
impl Message for LogEvent {
type Result = ();
}
impl Handler<LogEvent> for EventLog {
type Result = ();
fn handle(&mut self, event: LogEvent, _: &mut SyncContext<Self>) -> Self::Result {
println!("LOGEVENT: {}", event.msg);
}
}
impl Handler<AuditScope> for EventLog {
type Result = ();
fn handle(&mut self, event: AuditScope, _: &mut SyncContext<Self>) -> Self::Result {
println!("AUDIT: {}", event);
}
}
/*
impl Handler<Event> for EventLog {
type Result = ();
fn handle(&mut self, event: Event, _: &mut SyncContext<Self>) -> Self::Result {
println!("EVENT: {:?}", event)
}
}
*/

View file

@ -5,7 +5,10 @@ macro_rules! run_test {
use crate::be::Backend; use crate::be::Backend;
use crate::schema::Schema; use crate::schema::Schema;
use crate::server::QueryServer; use crate::server::QueryServer;
use std::sync::Arc;
use env_logger;
::std::env::set_var("RUST_LOG", "actix_web=debug,rsidm=debug");
let _ = env_logger::builder().is_test(true).try_init();
let mut audit = AuditScope::new("run_test"); let mut audit = AuditScope::new("run_test");
@ -18,7 +21,7 @@ macro_rules! run_test {
.expect("Failed to bootstrap schema"); .expect("Failed to bootstrap schema");
schema.commit().expect("Failed to commit schema"); schema.commit().expect("Failed to commit schema");
} }
let test_server = QueryServer::new(be, Arc::new(schema_outer)); let test_server = QueryServer::new(be, schema_outer);
{ {
let ts_write = test_server.write(); let ts_write = test_server.write();
@ -81,7 +84,7 @@ macro_rules! filter {
#[allow(unused_imports)] #[allow(unused_imports)]
use crate::filter::FC; use crate::filter::FC;
#[allow(unused_imports)] #[allow(unused_imports)]
use crate::filter::{f_and, f_andnot, f_eq, f_or, f_pres, f_sub}; use crate::filter::{f_and, f_andnot, f_eq, f_or, f_pres, f_self, f_sub};
Filter::new_ignore_hidden($fc) Filter::new_ignore_hidden($fc)
}}; }};
} }
@ -96,7 +99,7 @@ macro_rules! filter_rec {
#[allow(unused_imports)] #[allow(unused_imports)]
use crate::filter::FC; use crate::filter::FC;
#[allow(unused_imports)] #[allow(unused_imports)]
use crate::filter::{f_and, f_andnot, f_eq, f_or, f_pres, f_sub}; use crate::filter::{f_and, f_andnot, f_eq, f_or, f_pres, f_self, f_sub};
Filter::new_recycled($fc) Filter::new_recycled($fc)
}}; }};
} }
@ -111,7 +114,7 @@ macro_rules! filter_all {
#[allow(unused_imports)] #[allow(unused_imports)]
use crate::filter::FC; use crate::filter::FC;
#[allow(unused_imports)] #[allow(unused_imports)]
use crate::filter::{f_and, f_andnot, f_eq, f_or, f_pres, f_sub}; use crate::filter::{f_and, f_andnot, f_eq, f_or, f_pres, f_self, f_sub};
Filter::new($fc) Filter::new($fc)
}}; }};
} }

View file

@ -1,6 +1,6 @@
use crate::audit::AuditScope; use crate::audit::AuditScope;
use crate::proto_v1::Modify as ProtoModify; use crate::proto::v1::Modify as ProtoModify;
use crate::proto_v1::ModifyList as ProtoModifyList; use crate::proto::v1::ModifyList as ProtoModifyList;
use crate::error::{OperationError, SchemaError}; use crate::error::{OperationError, SchemaError};
use crate::schema::SchemaTransaction; use crate::schema::SchemaTransaction;

View file

@ -13,7 +13,7 @@ macro_rules! setup_test {
schema.bootstrap_core($au).expect("Failed to init schema"); schema.bootstrap_core($au).expect("Failed to init schema");
schema.commit().expect("Failed to commit schema"); schema.commit().expect("Failed to commit schema");
} }
let qs = QueryServer::new(be, Arc::new(schema_outer)); let qs = QueryServer::new(be, schema_outer);
if !$preload_entries.is_empty() { if !$preload_entries.is_empty() {
let qs_write = qs.write(); let qs_write = qs.write();
@ -42,7 +42,6 @@ macro_rules! run_create_test {
use crate::event::CreateEvent; use crate::event::CreateEvent;
use crate::schema::Schema; use crate::schema::Schema;
use crate::server::QueryServer; use crate::server::QueryServer;
use std::sync::Arc;
let mut au = AuditScope::new("run_create_test"); let mut au = AuditScope::new("run_create_test");
audit_segment!(au, || { audit_segment!(au, || {
@ -96,7 +95,6 @@ macro_rules! run_modify_test {
use crate::event::ModifyEvent; use crate::event::ModifyEvent;
use crate::schema::Schema; use crate::schema::Schema;
use crate::server::QueryServer; use crate::server::QueryServer;
use std::sync::Arc;
let mut au = AuditScope::new("run_modify_test"); let mut au = AuditScope::new("run_modify_test");
audit_segment!(au, || { audit_segment!(au, || {
@ -149,7 +147,6 @@ macro_rules! run_delete_test {
use crate::event::DeleteEvent; use crate::event::DeleteEvent;
use crate::schema::Schema; use crate::schema::Schema;
use crate::server::QueryServer; use crate::server::QueryServer;
use std::sync::Arc;
let mut au = AuditScope::new("run_delete_test"); let mut au = AuditScope::new("run_delete_test");
audit_segment!(au, || { audit_segment!(au, || {

1
src/lib/proto/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod v1;

View file

@ -4,24 +4,29 @@ use std::sync::Arc;
use crate::audit::AuditScope; use crate::audit::AuditScope;
use crate::be::Backend; use crate::be::Backend;
use crate::async_log::EventLog;
use crate::error::OperationError; use crate::error::OperationError;
use crate::event::{ use crate::event::{
CreateEvent, DeleteEvent, ModifyEvent, PurgeRecycledEvent, PurgeTombstoneEvent, SearchEvent, AuthEvent, AuthResult, CreateEvent, DeleteEvent, ModifyEvent, PurgeRecycledEvent,
SearchResult, PurgeTombstoneEvent, SearchEvent, SearchResult, WhoamiResult,
}; };
use crate::log::EventLog;
use crate::schema::{Schema, SchemaTransaction}; use crate::schema::{Schema, SchemaTransaction};
use crate::constants::UUID_ANONYMOUS;
use crate::idm::server::IdmServer;
use crate::server::{QueryServer, QueryServerTransaction}; use crate::server::{QueryServer, QueryServerTransaction};
use crate::proto_v1::{ use crate::proto::v1::{
AuthRequest, CreateRequest, DeleteRequest, ModifyRequest, OperationResponse, SearchRequest, AuthRequest, AuthResponse, AuthState, CreateRequest, DeleteRequest, ModifyRequest,
SearchResponse, OperationResponse, SearchRequest, SearchResponse, UserAuthToken, WhoamiRequest, WhoamiResponse,
}; };
use crate::proto::v1::messages::{AuthMessage, WhoamiMessage};
pub struct QueryServerV1 { pub struct QueryServerV1 {
log: actix::Addr<EventLog>, log: actix::Addr<EventLog>,
qs: QueryServer, qs: QueryServer,
idms: Arc<IdmServer>,
} }
impl Actor for QueryServerV1 { impl Actor for QueryServerV1 {
@ -33,14 +38,19 @@ impl Actor for QueryServerV1 {
} }
impl QueryServerV1 { impl QueryServerV1 {
pub fn new(log: actix::Addr<EventLog>, be: Backend, schema: Arc<Schema>) -> Self { pub fn new(log: actix::Addr<EventLog>, qs: QueryServer, idms: Arc<IdmServer>) -> Self {
log_event!(log, "Starting query server v1 worker ..."); log_event!(log, "Starting query server v1 worker ...");
QueryServerV1 { QueryServerV1 {
log: log, log: log,
qs: QueryServer::new(be, schema), qs: qs,
idms: idms,
} }
} }
// TODO: 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( pub fn start(
log: actix::Addr<EventLog>, log: actix::Addr<EventLog>,
path: &str, path: &str,
@ -53,9 +63,8 @@ impl QueryServerV1 {
// Create "just enough" schema for us to be able to load from // Create "just enough" schema for us to be able to load from
// disk ... Schema loading is one time where we validate the // disk ... Schema loading is one time where we validate the
// entries as we read them, so we need this here. // entries as we read them, so we need this here.
// FIXME: Handle results in start correctly
let schema = match Schema::new(&mut audit) { let schema = match Schema::new(&mut audit) {
Ok(s) => Arc::new(s), Ok(s) => s,
Err(e) => return Err(e), Err(e) => return Err(e),
}; };
@ -103,10 +112,14 @@ impl QueryServerV1 {
} }
} }
// Create a temporary query_server implementation // Create a query_server implementation
let query_server = QueryServer::new(be.clone(), schema.clone()); // TODO: FIXME: CRITICAL: Schema must be ARC/Cow properly!!! Right now it's
// not!!!
let query_server = QueryServer::new(be, schema);
let mut audit_qsc = AuditScope::new("query_server_init"); let mut audit_qsc = AuditScope::new("query_server_init");
// This may need to be two parts, one for schema, one for everything else
// that way we can reload schema in between.
let query_server_write = query_server.write(); let query_server_write = query_server.write();
match query_server_write.initialise(&mut audit_qsc).and_then(|_| { match query_server_write.initialise(&mut audit_qsc).and_then(|_| {
audit_segment!(audit_qsc, || query_server_write.commit(&mut audit_qsc)) audit_segment!(audit_qsc, || query_server_write.commit(&mut audit_qsc))
@ -115,11 +128,17 @@ impl QueryServerV1 {
Ok(_) => {} Ok(_) => {}
Err(e) => return Err(e), Err(e) => return Err(e),
}; };
// What's important about this initial setup here is that it also triggers
// the schema and acp reload, so they are now configured correctly!
// We generate a SINGLE idms only!
let idms = Arc::new(IdmServer::new(query_server.clone()));
audit.append_scope(audit_qsc); audit.append_scope(audit_qsc);
let x = SyncArbiter::start(threads, move || { let x = SyncArbiter::start(threads, move || {
QueryServerV1::new(log_inner.clone(), be.clone(), schema.clone()) QueryServerV1::new(log_inner.clone(), query_server.clone(), idms.clone())
}); });
Ok(x) Ok(x)
}); });
@ -249,14 +268,39 @@ impl Handler<DeleteRequest> for QueryServerV1 {
} }
} }
impl Handler<AuthRequest> for QueryServerV1 { // Need an auth session storage. LRU?
type Result = Result<OperationResponse, OperationError>; // requires a lock ...
// needs session id, entry, etc.
fn handle(&mut self, msg: AuthRequest, _: &mut Self::Context) -> Self::Result { impl Handler<AuthMessage> for QueryServerV1 {
type Result = Result<AuthResponse, OperationError>;
fn handle(&mut self, msg: AuthMessage, _: &mut Self::Context) -> Self::Result {
// This is probably the first function that really implements logic
// "on top" of the db server concept. In this case we check if
// the credentials provided is sufficient to say if someone is
// "authenticated" or not.
let mut audit = AuditScope::new("auth"); let mut audit = AuditScope::new("auth");
let res = audit_segment!(&mut audit, || { let res = audit_segment!(&mut audit, || {
audit_log!(audit, "Begin auth event {:?}", msg); audit_log!(audit, "Begin auth event {:?}", msg);
Err(OperationError::InvalidState)
// Destructure it.
// Convert the AuthRequest to an AuthEvent that the idm server
// can use.
let mut idm_write = self.idms.write();
let ae = try_audit!(audit, AuthEvent::from_message(msg));
// Generally things like auth denied are in Ok() msgs
// so true errors should always trigger a rollback.
let r = idm_write
.auth(&mut audit, &ae)
.and_then(|r| idm_write.commit().map(|_| r));
audit_log!(audit, "Sending result -> {:?}", r);
// Build the result.
r.map(|r| r.response())
}); });
// At the end of the event we send it for logging. // At the end of the event we send it for logging.
self.log.do_send(audit); self.log.do_send(audit);
@ -264,6 +308,60 @@ impl Handler<AuthRequest> for QueryServerV1 {
} }
} }
impl Handler<WhoamiMessage> for QueryServerV1 {
type Result = Result<WhoamiResponse, OperationError>;
fn handle(&mut self, msg: WhoamiMessage, _: &mut Self::Context) -> Self::Result {
let mut audit = AuditScope::new("whoami");
let res = audit_segment!(&mut audit, || {
// TODO: Move this to IdmServer!!!
// Begin a read
let qs_read = self.qs.read();
// Make an event from the whoami request. This will process the event and
// generate a selfuuid search.
// FIXME: This current handles the unauthenticated check, and will
// trigger the failure, but if we can manage to work out async
// then move this to core.rs, and don't allow Option<UAT> to get
// this far.
let srch = match SearchEvent::from_whoami_request(&mut audit, msg.uat, &qs_read) {
Ok(s) => s,
Err(e) => {
audit_log!(audit, "Failed to begin whoami: {:?}", e);
return Err(e);
}
};
audit_log!(audit, "Begin event {:?}", srch);
match qs_read.search_ext(&mut audit, &srch) {
Ok(mut entries) => {
// assert there is only one ...
match entries.len() {
0 => Err(OperationError::NoMatchingEntries),
1 => {
let e = entries.pop().expect("Entry length mismatch!!!");
// Now convert to a response, and return
let wr = WhoamiResult::new(e);
Ok(wr.response())
}
// Somehow we matched multiple, which should be impossible.
_ => Err(OperationError::InvalidState),
}
}
// Something else went wrong ...
Err(e) => Err(e),
}
});
// Should we log the final result?
// At the end of the event we send it for logging.
self.log.do_send(audit);
res
}
}
// These below are internal only types.
impl Handler<PurgeTombstoneEvent> for QueryServerV1 { impl Handler<PurgeTombstoneEvent> for QueryServerV1 {
type Result = (); type Result = ();

View file

@ -0,0 +1,26 @@
// 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

@ -0,0 +1,45 @@
use crate::error::OperationError;
use actix::prelude::*;
use uuid::Uuid;
use crate::proto::v1::{AuthRequest, AuthResponse, UserAuthToken, WhoamiRequest, 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>;
}

View file

@ -3,9 +3,71 @@
use crate::error::OperationError; use crate::error::OperationError;
use actix::prelude::*; use actix::prelude::*;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use uuid::Uuid;
pub(crate) mod actors;
pub mod client;
pub(crate) mod messages;
// These proto implementations are here because they have public definitions // These proto implementations are here because they have public definitions
/* ===== 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
// domain specific fields for the purposes of IDM, over the normal
// entry/ava/filter types. These related deeply to schema.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Group {
pub name: String,
pub uuid: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claim {
pub name: String,
pub uuid: String,
// These can be ephemeral, or shortlived in a session.
// some may even need requesting.
// pub expiry: DateTime
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Application {
pub name: String,
pub uuid: String,
}
// The currently authenticated user, and any required metadata for them
// to properly authorise them. This is similar in nature to oauth and the krb
// PAC/PAD structures. Currently we only use this internally, but we should
// consider making it "parseable" by the client so they can have per-session
// group/authorisation data.
//
// This structure and how it works will *very much* change over time from this
// point onward!
//
// It's likely that this must have a relationship to the server's user structure
// and to the Entry so that filters or access controls can be applied.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UserAuthToken {
// When this data should be considered invalid. Interpretation
// may depend on the client application.
// pub expiry: DateTime,
pub name: String,
pub displayname: String,
pub uuid: String,
pub application: Option<Application>,
pub groups: Vec<Group>,
pub claims: Vec<Claim>,
// Should we allow supplemental ava's to be added on request?
}
// UAT will need a downcast to Entry, which adds in the claims to the entry
// for the purpose of filtering.
/* ===== low level proto types ===== */
// FIXME: We probably need a proto entry to transform our // FIXME: We probably need a proto entry to transform our
// server core entry into. We also need to get from proto // server core entry into. We also need to get from proto
// entry to our actual entry. // entry to our actual entry.
@ -160,46 +222,60 @@ impl Message for ModifyRequest {
// //
// On loginSuccess, we send a cookie, and that allows the token to be // On loginSuccess, we send a cookie, and that allows the token to be
// generated. The cookie can be shared between servers. // generated. The cookie can be shared between servers.
#[derive(Debug, Serialize, Deserialize)]
pub enum AuthCredential {
Anonymous,
Password(String),
// TOTP(String),
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum AuthState { pub enum AuthStep {
Init(String, Vec<String>), // name, application id?
Init(String, Option<String>),
/* /*
Step( Step(
Type(params ....) Type(params ....)
), ),
*/ */
Creds(Vec<AuthCredential>),
// Should we have a "finalise" type to attempt to finish based on
// what we have given?
} }
// Request auth for identity X with roles Y? // Request auth for identity X with roles Y?
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct AuthRequest { pub struct AuthRequest {
pub state: AuthState, pub step: AuthStep,
pub user_uuid: String,
}
impl Message for AuthRequest {
type Result = Result<OperationResponse, OperationError>;
} }
// Respond with the list of auth types and nonce, etc. // Respond with the list of auth types and nonce, etc.
// It can also contain a denied, or success. // It can also contain a denied, or success.
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub enum AuthAllowed {
Anonymous,
Password,
// TOTP,
// Webauthn(String),
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum AuthStatus { pub enum AuthState {
Begin(String), // uuid of this session. // Everything is good, your cookie has been issued, and a token is set here
// Continue, // Keep going, here are the things you could still provide ... // for the client to view.
// Go away, you made a mistake somewhere. Success(UserAuthToken),
// Provide reason? // Something was bad, your session is terminated and no cookie.
// Denied(String), Denied,
// Welcome friend. // Continue to auth, allowed mechanisms listed.
// On success provide entry "self", for group assertions? Continue(Vec<AuthAllowed>),
// We also provide the "cookie"/token?
// Success(String, Entry),
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct AuthResponse { pub struct AuthResponse {
pub status: AuthStatus, // TODO: Consider moving to an AuthMessageResponse type, and leave the proto
// without the session id because it's not necesary to know.
pub sessionid: Uuid,
pub state: AuthState,
} }
/* Recycle Requests area */ /* Recycle Requests area */
@ -236,9 +312,31 @@ impl ReviveRecycledRequest {
} }
} }
// This doesn't need seralise because it's only accessed via a "get".
#[derive(Debug)]
pub struct WhoamiRequest {}
impl WhoamiRequest {
pub fn new() -> Self {
WhoamiRequest {}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WhoamiResponse {
// Should we just embed the entry? Or destructure it?
pub youare: Entry,
}
impl WhoamiResponse {
pub fn new(e: Entry) -> Self {
WhoamiResponse { youare: e }
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::proto_v1::Filter as ProtoFilter; use crate::proto::v1::Filter as ProtoFilter;
#[test] #[test]
fn test_protofilter_simple() { fn test_protofilter_simple() {
let pf: ProtoFilter = ProtoFilter::Pres("class".to_string()); let pf: ProtoFilter = ProtoFilter::Pres("class".to_string());

View file

@ -1,7 +1,7 @@
use crate::audit::AuditScope; use crate::audit::AuditScope;
use crate::constants::*; use crate::constants::*;
use crate::error::{ConsistencyError, OperationError, SchemaError}; use crate::error::{ConsistencyError, OperationError, SchemaError};
use crate::proto_v1::Filter as ProtoFilter; use crate::proto::v1::Filter as ProtoFilter;
use regex::Regex; use regex::Regex;
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryFrom; use std::convert::TryFrom;
@ -16,8 +16,6 @@ use concread::cowcell::{CowCell, CowCellReadTxn, CowCellWriteTxn};
// In the future this will parse/read it's schema from the db // In the future this will parse/read it's schema from the db
// but we have to bootstrap with some core types. // but we have to bootstrap with some core types.
// TODO: Schema should be copy-on-write
// TODO: Account should be a login-bind-able object // TODO: Account should be a login-bind-able object
// needs account lock, timeout, policy? // needs account lock, timeout, policy?

View file

@ -13,7 +13,7 @@ use crate::access::{
}; };
use crate::constants::{ use crate::constants::{
JSON_ADMIN_V1, JSON_ANONYMOUS_V1, JSON_IDM_ADMINS_ACP_REVIVE_V1, JSON_IDM_ADMINS_ACP_SEARCH_V1, JSON_ADMIN_V1, JSON_ANONYMOUS_V1, JSON_IDM_ADMINS_ACP_REVIVE_V1, JSON_IDM_ADMINS_ACP_SEARCH_V1,
JSON_IDM_ADMINS_V1, JSON_SYSTEM_INFO_V1, JSON_IDM_ADMINS_V1, JSON_IDM_SELF_ACP_READ_V1, JSON_SYSTEM_INFO_V1,
}; };
use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryNormalised, EntryValid}; use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryNormalised, EntryValid};
use crate::error::{ConsistencyError, OperationError, SchemaError}; use crate::error::{ConsistencyError, OperationError, SchemaError};
@ -554,6 +554,7 @@ impl<'a> QueryServerTransaction for QueryServerWriteTransaction<'a> {
} }
} }
#[derive(Clone)]
pub struct QueryServer { pub struct QueryServer {
// log: actix::Addr<EventLog>, // log: actix::Addr<EventLog>,
be: Backend, be: Backend,
@ -562,11 +563,11 @@ pub struct QueryServer {
} }
impl QueryServer { impl QueryServer {
pub fn new(be: Backend, schema: Arc<Schema>) -> Self { pub fn new(be: Backend, schema: Schema) -> Self {
// log_event!(log, "Starting query worker ..."); // log_event!(log, "Starting query worker ...");
QueryServer { QueryServer {
be: be, be: be,
schema: schema, schema: Arc::new(schema),
accesscontrols: Arc::new(AccessControls::new()), accesscontrols: Arc::new(AccessControls::new()),
} }
} }
@ -1300,16 +1301,11 @@ impl<'a> QueryServerWriteTransaction<'a> {
} }
// Check the admin object exists (migrations). // Check the admin object exists (migrations).
let mut audit_an = AuditScope::new("start_admin");
let res = self.internal_migrate_or_create_str(&mut audit_an, JSON_ADMIN_V1);
audit.append_scope(audit_an);
if res.is_err() {
return res;
}
// Create the default idm_admin group. // Create the default idm_admin group.
let mut audit_an = AuditScope::new("start_idm_admins"); let mut audit_an = AuditScope::new("start_idm_admin_migrations");
let res = self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_ADMINS_V1); let res = self
.internal_migrate_or_create_str(&mut audit_an, JSON_ADMIN_V1)
.and_then(|_| self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_ADMINS_V1));
audit.append_scope(audit_an); audit.append_scope(audit_an);
if res.is_err() { if res.is_err() {
return res; return res;
@ -1318,15 +1314,15 @@ impl<'a> QueryServerWriteTransaction<'a> {
// Create any system default schema entries. // Create any system default schema entries.
// Create any system default access profile entries. // Create any system default access profile entries.
let mut audit_an = AuditScope::new("start_idm_admins_acp"); let mut audit_an = AuditScope::new("start_idm_migrations_internal");
let res = self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_ADMINS_ACP_SEARCH_V1); let res = self
audit.append_scope(audit_an); .internal_migrate_or_create_str(&mut audit_an, JSON_IDM_ADMINS_ACP_SEARCH_V1)
if res.is_err() { .and_then(|_| {
return res; self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_ADMINS_ACP_REVIVE_V1)
} })
.and_then(|_| {
let mut audit_an = AuditScope::new("start_idm_admins_acp"); self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_SELF_ACP_READ_V1)
let res = self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_ADMINS_ACP_REVIVE_V1); });
audit.append_scope(audit_an); audit.append_scope(audit_an);
if res.is_err() { if res.is_err() {
return res; return res;
@ -1459,31 +1455,15 @@ impl<'a> QueryServerWriteTransaction<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
/*
extern crate actix;
use actix::prelude::*;
extern crate futures;
use futures::future;
use futures::future::Future;
extern crate tokio;
use std::sync::Arc;
use crate::be::Backend;
use crate::filter::Filter;
use crate::schema::Schema;
use crate::audit::AuditScope;
*/
use crate::constants::{JSON_ADMIN_V1, UUID_ADMIN}; use crate::constants::{JSON_ADMIN_V1, UUID_ADMIN};
use crate::entry::{Entry, EntryInvalid, EntryNew}; use crate::entry::{Entry, EntryInvalid, EntryNew};
use crate::error::{OperationError, SchemaError}; use crate::error::{OperationError, SchemaError};
use crate::event::{CreateEvent, DeleteEvent, ModifyEvent, ReviveRecycledEvent, SearchEvent}; use crate::event::{CreateEvent, DeleteEvent, ModifyEvent, ReviveRecycledEvent, SearchEvent};
use crate::modify::{Modify, ModifyList}; use crate::modify::{Modify, ModifyList};
use crate::proto_v1::Filter as ProtoFilter; use crate::proto::v1::Filter as ProtoFilter;
use crate::proto_v1::Modify as ProtoModify; use crate::proto::v1::Modify as ProtoModify;
use crate::proto_v1::ModifyList as ProtoModifyList; use crate::proto::v1::ModifyList as ProtoModifyList;
use crate::proto_v1::{DeleteRequest, ModifyRequest, ReviveRecycledRequest}; use crate::proto::v1::{DeleteRequest, ModifyRequest, ReviveRecycledRequest};
use crate::server::QueryServerTransaction; use crate::server::QueryServerTransaction;
#[test] #[test]

View file

@ -1,4 +1,5 @@
extern crate actix; extern crate actix;
extern crate env_logger;
extern crate rsidm; extern crate rsidm;
@ -6,12 +7,14 @@ use rsidm::config::Configuration;
use rsidm::core::create_server_core; use rsidm::core::create_server_core;
fn main() { fn main() {
// read the config (if any?) // Read our config (if any)
// How do we make the config accesible to all threads/workers? clone it?
// Make it an Arc<Config>?
// FIXME: Pass config to the server core
let config = Configuration::new(); let config = Configuration::new();
// Configure the server logger. This could be adjusted based on what config
// says.
::std::env::set_var("RUST_LOG", "actix_web=info,rsidm=info");
env_logger::init();
let sys = actix::System::new("rsidm-server"); let sys = actix::System::new("rsidm-server");
create_server_core(config); create_server_core(config);

View file

@ -5,7 +5,10 @@ extern crate rsidm;
use rsidm::config::Configuration; use rsidm::config::Configuration;
use rsidm::constants::UUID_ADMIN; use rsidm::constants::UUID_ADMIN;
use rsidm::core::create_server_core; use rsidm::core::create_server_core;
use rsidm::proto_v1::{CreateRequest, Entry, OperationResponse}; use rsidm::proto::v1::{
AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, Entry,
OperationResponse, WhoamiRequest,
};
extern crate reqwest; extern crate reqwest;
@ -13,50 +16,60 @@ extern crate futures;
// use futures::future; // use futures::future;
// use futures::future::Future; // use futures::future::Future;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc; use std::sync::mpsc;
use std::thread; use std::thread;
extern crate env_logger;
extern crate tokio; extern crate tokio;
static PORT_ALLOC: AtomicUsize = AtomicUsize::new(8080);
// Test external behaviorus of the service. // Test external behaviorus of the service.
macro_rules! run_test { fn run_test(test_fn: fn(reqwest::Client, &str) -> ()) {
($test_fn:expr) => {{ let _ = env_logger::builder().is_test(true).try_init();
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let port = PORT_ALLOC.fetch_add(1, Ordering::SeqCst);
let mut config = Configuration::new();
config.address = format!("127.0.0.1:{}", port);
// Setup the config ...
thread::spawn(|| { thread::spawn(move || {
// setup // Spawn a thread for the test runner, this should have a unique
// Create a server config in memory for use - use test settings // port....
// Create a log: In memory - for now it's just stdout System::run(move || {
create_server_core(config);
System::run(move || { // This appears to be bind random ...
let config = Configuration::new(); // let srv = srv.bind("127.0.0.1:0").unwrap();
create_server_core(config); let _ = tx.send(System::current());
// This appears to be bind random ...
// let srv = srv.bind("127.0.0.1:0").unwrap();
let _ = tx.send(System::current());
});
}); });
let sys = rx.recv().unwrap(); });
System::set_current(sys.clone()); let sys = rx.recv().unwrap();
System::set_current(sys.clone());
// Do we need any fixtures? // Do we need any fixtures?
// Yes probably, but they'll need to be futures as well ... // Yes probably, but they'll need to be futures as well ...
// later we could accept fixture as it's own future for re-use // later we could accept fixture as it's own future for re-use
$test_fn();
// We DO NOT need teardown, as sqlite is in mem // Setup the client, and the address we selected.
// let the tables hit the floor let client = reqwest::Client::builder()
let _ = sys.stop(); .cookie_store(true)
}}; .build()
.expect("Unexpected reqwest builder failure!");
let addr = format!("http://127.0.0.1:{}", port);
test_fn(client, addr.as_str());
// We DO NOT need teardown, as sqlite is in mem
// let the tables hit the floor
let _ = sys.stop();
} }
#[test] #[test]
fn test_server_proto() { fn test_server_proto() {
run_test!(|| { run_test(|client: reqwest::Client, addr: &str| {
let client = reqwest::Client::new();
let e: Entry = serde_json::from_str( let e: Entry = serde_json::from_str(
r#"{ r#"{
"attrs": { "attrs": {
@ -74,8 +87,10 @@ fn test_server_proto() {
user_uuid: UUID_ADMIN.to_string(), user_uuid: UUID_ADMIN.to_string(),
}; };
let dest = format!("{}/v1/create", addr);
let mut response = client let mut response = client
.post("http://127.0.0.1:8080/v1/create") .post(dest.as_str())
.body(serde_json::to_string(&c).unwrap()) .body(serde_json::to_string(&c).unwrap())
.send() .send()
.unwrap(); .unwrap();
@ -92,6 +107,80 @@ fn test_server_proto() {
}); });
} }
#[test]
fn test_server_whoami_anonymous() {
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);
let response = client.get(whoami_dest.as_str()).send().unwrap();
// https://docs.rs/reqwest/0.9.15/reqwest/struct.Response.html
println!("{:?}", response);
assert!(response.status() == reqwest::StatusCode::UNAUTHORIZED);
// Now login as anonymous
// Setup the auth initialisation
let auth_init = AuthRequest {
step: AuthStep::Init("anonymous".to_string(), None),
};
let mut response = client
.post(auth_dest.as_str())
.body(serde_json::to_string(&auth_init).unwrap())
.send()
.unwrap();
assert!(response.status() == reqwest::StatusCode::OK);
// Check that we got the next step
let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap();
println!("==> AUTHRESPONSE ==> {:?}", r);
assert!(match &r.state {
AuthState::Continue(all_list) => {
// Check anonymous is present? It will fail on next step if not ...
true
}
_ => false,
});
// Send the credentials required now
let auth_anon = AuthRequest {
step: AuthStep::Creds(vec![AuthCredential::Anonymous]),
};
let mut response = client
.post(auth_dest.as_str())
.body(serde_json::to_string(&auth_anon).unwrap())
.send()
.unwrap();
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.
/* /*
#[test] #[test]
fn test_be_create_user() { fn test_be_create_user() {