mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
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:
parent
426426a18f
commit
94a6bde269
|
@ -24,18 +24,20 @@ name = "rsidm_whoami"
|
|||
path = "src/clients/whoami.rs"
|
||||
|
||||
|
||||
|
||||
[dependencies]
|
||||
actix = "0.7"
|
||||
actix-web = "0.7"
|
||||
bytes = "0.4"
|
||||
env_logger = "0.5"
|
||||
log = "0.4"
|
||||
env_logger = "0.6"
|
||||
reqwest = "0.9"
|
||||
# reqwest = { path = "../reqwest" }
|
||||
|
||||
chrono = "0.4"
|
||||
cookie = "0.11"
|
||||
regex = "1"
|
||||
lazy_static = "1.2.0"
|
||||
lru = "0.1"
|
||||
|
||||
tokio = "0.1"
|
||||
futures = "0.1"
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
FROM opensuse/tumbleweed:latest
|
||||
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/
|
||||
|
||||
WORKDIR /home/rsidm/
|
||||
|
|
142
designs/auth.rst
142
designs/auth.rst
|
@ -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.
|
||||
|
||||
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)
|
||||
================================
|
||||
|
@ -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
|
||||
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
|
||||
to admin groups)
|
||||
|
||||
|
@ -185,14 +189,57 @@ Cookie/Token Auth Detail
|
|||
Clients begin with no cookie, and no session.
|
||||
|
||||
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
|
||||
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
|
||||
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.
|
||||
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.
|
||||
* 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.
|
||||
* 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
|
||||
--------------
|
||||
|
@ -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
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
extern crate reqwest;
|
||||
extern crate rsidm;
|
||||
|
||||
// use rsidm::proto_v1;
|
||||
use rsidm::proto::v1::{WhoamiRequest, WhoamiResponse};
|
||||
|
||||
fn main() {
|
||||
println!("Hello whoami");
|
||||
|
@ -8,4 +9,20 @@ fn main() {
|
|||
// Given the current ~/.rsidm/cookie (or none)
|
||||
// we should check who we are plus show the auth token that the server
|
||||
// would generate for us.
|
||||
|
||||
let whoami_req = WhoamiRequest {};
|
||||
|
||||
// FIXME TODO: Make this url configurable!!!
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let mut response = client
|
||||
.get("http://127.0.0.1:8080/v1/whoami")
|
||||
.send()
|
||||
.unwrap();
|
||||
|
||||
println!("{:?}", response);
|
||||
|
||||
// Parse it if desire.
|
||||
// let r: Response = serde_json::from_str(response.text().unwrap().as_str()).unwrap();
|
||||
// println!("{:?}", r);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ use crate::entry::{Entry, EntryCommitted, EntryNew, EntryNormalised, EntryValid}
|
|||
use crate::error::OperationError;
|
||||
use crate::filter::{Filter, FilterValid};
|
||||
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::event::{CreateEvent, DeleteEvent, EventOrigin, ModifyEvent, SearchEvent};
|
||||
|
@ -47,14 +47,16 @@ impl AccessControlSearch {
|
|||
) -> Result<Self, OperationError> {
|
||||
if !value.attribute_value_pres("class", "access_control_search") {
|
||||
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!(
|
||||
audit,
|
||||
value
|
||||
.get_ava("acp_search_attr")
|
||||
.ok_or(OperationError::InvalidACPState)
|
||||
.ok_or(OperationError::InvalidACPState("Missing acp_search_attr"))
|
||||
.map(|vs: &Vec<String>| vs.clone())
|
||||
);
|
||||
|
||||
|
@ -99,7 +101,9 @@ impl AccessControlDelete {
|
|||
) -> Result<Self, OperationError> {
|
||||
if !value.attribute_value_pres("class", "access_control_delete") {
|
||||
audit_log!(audit, "class access_control_delete not present.");
|
||||
return Err(OperationError::InvalidACPState);
|
||||
return Err(OperationError::InvalidACPState(
|
||||
"Missing access_control_delete",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(AccessControlDelete {
|
||||
|
@ -142,7 +146,9 @@ impl AccessControlCreate {
|
|||
) -> Result<Self, OperationError> {
|
||||
if !value.attribute_value_pres("class", "access_control_create") {
|
||||
audit_log!(audit, "class access_control_create not present.");
|
||||
return Err(OperationError::InvalidACPState);
|
||||
return Err(OperationError::InvalidACPState(
|
||||
"Missing access_control_create",
|
||||
));
|
||||
}
|
||||
|
||||
let attrs = value
|
||||
|
@ -203,7 +209,9 @@ impl AccessControlModify {
|
|||
) -> Result<Self, OperationError> {
|
||||
if !value.attribute_value_pres("class", "access_control_modify") {
|
||||
audit_log!(audit, "class access_control_modify not present.");
|
||||
return Err(OperationError::InvalidACPState);
|
||||
return Err(OperationError::InvalidACPState(
|
||||
"Missing access_control_modify",
|
||||
));
|
||||
}
|
||||
|
||||
let presattrs = value
|
||||
|
@ -273,7 +281,9 @@ impl AccessControlProfile {
|
|||
// Assert we have class access_control_profile
|
||||
if !value.attribute_value_pres("class", "access_control_profile") {
|
||||
audit_log!(audit, "class access_control_profile not present.");
|
||||
return Err(OperationError::InvalidACPState);
|
||||
return Err(OperationError::InvalidACPState(
|
||||
"Missing access_control_profile",
|
||||
));
|
||||
}
|
||||
|
||||
// copy name
|
||||
|
@ -281,7 +291,7 @@ impl AccessControlProfile {
|
|||
audit,
|
||||
value
|
||||
.get_ava_single("name")
|
||||
.ok_or(OperationError::InvalidACPState)
|
||||
.ok_or(OperationError::InvalidACPState("Missing name"))
|
||||
);
|
||||
// copy uuid
|
||||
let uuid = value.get_uuid();
|
||||
|
@ -290,21 +300,21 @@ impl AccessControlProfile {
|
|||
audit,
|
||||
value
|
||||
.get_ava_single("acp_receiver")
|
||||
.ok_or(OperationError::InvalidACPState)
|
||||
.ok_or(OperationError::InvalidACPState("Missing acp_receiver"))
|
||||
);
|
||||
// targetscope, and turn to real filter
|
||||
let targetscope_raw = try_audit!(
|
||||
audit,
|
||||
value
|
||||
.get_ava_single("acp_targetscope")
|
||||
.ok_or(OperationError::InvalidACPState)
|
||||
.ok_or(OperationError::InvalidACPState("Missing acp_targetscope"))
|
||||
);
|
||||
|
||||
audit_log!(audit, "RAW receiver {:?}", receiver_raw);
|
||||
let receiver_f: ProtoFilter = try_audit!(
|
||||
audit,
|
||||
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 = try_audit!(
|
||||
|
@ -319,7 +329,7 @@ impl AccessControlProfile {
|
|||
audit,
|
||||
serde_json::from_str(targetscope_raw.as_str()).map_err(|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));
|
||||
|
@ -382,6 +392,7 @@ pub trait AccessControlsTransaction {
|
|||
// If this is an internal search, return our working set.
|
||||
let rec_entry: &Entry<EntryValid, EntryCommitted> = match &se.event.origin {
|
||||
EventOrigin::Internal => {
|
||||
audit_log!(audit, "Internal operation, bypassing access check");
|
||||
// No need to check ACS
|
||||
return Ok(entries);
|
||||
}
|
||||
|
|
|
@ -12,8 +12,11 @@ macro_rules! audit_log {
|
|||
($audit:expr, $($arg:tt)*) => ({
|
||||
use std::fmt;
|
||||
if cfg!(test) || cfg!(debug_assertions) {
|
||||
print!("DEBUG AUDIT ({}:{} {})-> ", file!(), line!(), $audit.id());
|
||||
println!($($arg)*)
|
||||
// debug!("DEBUG AUDIT ({}:{} {})-> ", file!(), line!(), $audit.id());
|
||||
// debug!($($arg)*)
|
||||
// debug!("DEBUG AUDIT ({}:{} {})-> ", file!(), line!(), $audit.id());
|
||||
// debug!("line: {}", line!());
|
||||
debug!($($arg)*)
|
||||
}
|
||||
$audit.log_event(
|
||||
fmt::format(
|
||||
|
|
|
@ -168,7 +168,7 @@ impl Drop for BackendReadTransaction {
|
|||
// TODO: Is this correct for RO txn?
|
||||
fn drop(self: &mut Self) {
|
||||
if !self.committed {
|
||||
println!("Aborting txn");
|
||||
debug!("Aborting BE RO txn");
|
||||
self.conn
|
||||
.execute("ROLLBACK TRANSACTION", NO_PARAMS)
|
||||
// TODO: Can we do this without expect? I think we can't due
|
||||
|
@ -183,7 +183,7 @@ impl Drop for BackendReadTransaction {
|
|||
impl BackendReadTransaction {
|
||||
pub fn new(conn: r2d2::PooledConnection<SqliteConnectionManager>) -> Self {
|
||||
// Start the transaction
|
||||
println!("Starting RO txn ...");
|
||||
debug!("Starting BE RO txn ...");
|
||||
// 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
|
||||
// signature here if we wanted to...
|
||||
|
@ -209,7 +209,7 @@ impl Drop for BackendWriteTransaction {
|
|||
// Abort
|
||||
fn drop(self: &mut Self) {
|
||||
if !self.committed {
|
||||
println!("Aborting txn");
|
||||
debug!("Aborting BE WR txn");
|
||||
self.conn
|
||||
.execute("ROLLBACK TRANSACTION", NO_PARAMS)
|
||||
// TODO: Can we do this without expect? I think we can't due
|
||||
|
@ -230,7 +230,7 @@ impl BackendTransaction for BackendWriteTransaction {
|
|||
impl BackendWriteTransaction {
|
||||
pub fn new(conn: r2d2::PooledConnection<SqliteConnectionManager>) -> Self {
|
||||
// Start the transaction
|
||||
println!("Starting WR txn ...");
|
||||
debug!("Starting BE WR txn ...");
|
||||
// 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
|
||||
// signature here if we wanted to...
|
||||
|
@ -579,7 +579,7 @@ impl BackendWriteTransaction {
|
|||
}
|
||||
|
||||
pub fn commit(mut self) -> Result<(), OperationError> {
|
||||
println!("Commiting txn");
|
||||
debug!("Commiting BE txn");
|
||||
assert!(!self.committed);
|
||||
self.committed = true;
|
||||
self.conn
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Configuration {
|
||||
pub address: String,
|
||||
pub domain: String,
|
||||
pub threads: usize,
|
||||
pub db_path: String,
|
||||
pub maximum_request: usize,
|
||||
|
@ -12,12 +13,14 @@ impl Configuration {
|
|||
pub fn new() -> Self {
|
||||
Configuration {
|
||||
address: String::from("127.0.0.1:8080"),
|
||||
domain: String::from("127.0.0.1"),
|
||||
threads: 8,
|
||||
db_path: String::from(""),
|
||||
maximum_request: 262144, // 256k
|
||||
// log type
|
||||
// log path
|
||||
secure_cookies: true,
|
||||
// TODO: default true in prd
|
||||
secure_cookies: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 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 JSON_IDM_ADMINS_ACP_SEARCH_V1: &'static str = r#"{
|
||||
"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#"{
|
||||
"valid": {
|
||||
"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
|
||||
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";
|
||||
|
|
185
src/lib/core.rs
185
src/lib/core.rs
|
@ -11,17 +11,34 @@ use futures::{future, Future, Stream};
|
|||
use crate::config::Configuration;
|
||||
|
||||
// SearchResult
|
||||
use crate::async_log;
|
||||
use crate::error::OperationError;
|
||||
use crate::interval::IntervalActor;
|
||||
use crate::log;
|
||||
use crate::proto_v1::{AuthRequest, CreateRequest, DeleteRequest, ModifyRequest, SearchRequest};
|
||||
use crate::proto_v1_actors::QueryServerV1;
|
||||
use crate::proto::v1::actors::QueryServerV1;
|
||||
use crate::proto::v1::messages::{AuthMessage, WhoamiMessage};
|
||||
use crate::proto::v1::{
|
||||
AuthRequest, AuthResponse, AuthState, CreateRequest, DeleteRequest, ModifyRequest,
|
||||
SearchRequest, UserAuthToken, WhoamiRequest, WhoamiResponse,
|
||||
};
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
struct AppState {
|
||||
qe: actix::Addr<QueryServerV1>,
|
||||
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) => {{
|
||||
// This is copied every request. Is there a better way?
|
||||
// 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::<$message_type>(&body);
|
||||
|
||||
// Send to the db for create
|
||||
// Send to the db for handling
|
||||
match r_obj {
|
||||
Ok(obj) => {
|
||||
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
|
||||
|
||||
fn create(
|
||||
(req, state): (HttpRequest<AppState>, State<AppState>),
|
||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||
json_event_decode!(req, state, CreateEvent, CreateRequest)
|
||||
json_event_post!(req, state, CreateEvent, CreateRequest)
|
||||
}
|
||||
|
||||
fn modify(
|
||||
(req, state): (HttpRequest<AppState>, State<AppState>),
|
||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||
json_event_decode!(req, state, ModifyEvent, ModifyRequest)
|
||||
json_event_post!(req, state, ModifyEvent, ModifyRequest)
|
||||
}
|
||||
|
||||
fn delete(
|
||||
(req, state): (HttpRequest<AppState>, State<AppState>),
|
||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||
json_event_decode!(req, state, DeleteEvent, DeleteRequest)
|
||||
json_event_post!(req, state, DeleteEvent, DeleteRequest)
|
||||
}
|
||||
|
||||
fn search(
|
||||
(req, state): (HttpRequest<AppState>, State<AppState>),
|
||||
) -> 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(
|
||||
(req, state): (HttpRequest<AppState>, State<AppState>),
|
||||
|
@ -135,46 +182,66 @@ fn auth(
|
|||
// First, deal with some state management.
|
||||
// Do anything here first that's needed like getting the session details
|
||||
// out of the req cookie.
|
||||
// let mut counter = 1;
|
||||
|
||||
// From the actix source errors here
|
||||
// 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
|
||||
// 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,
|
||||
Err(e) => {
|
||||
return Box::new(future::err(e));
|
||||
}
|
||||
};
|
||||
|
||||
let c = match maybe_count {
|
||||
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));
|
||||
}
|
||||
};
|
||||
let auth_msg = AuthMessage::new(obj, maybe_sessionid);
|
||||
|
||||
// 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
|
||||
// invalid.
|
||||
|
||||
let res = state.qe.send(obj).from_err().and_then(|res| match res {
|
||||
Ok(event_result) => Ok(HttpResponse::Ok().json(event_result)),
|
||||
Err(e) => Ok(HttpResponse::InternalServerError().json(e)),
|
||||
});
|
||||
|
||||
let res =
|
||||
state
|
||||
.qe
|
||||
.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)
|
||||
}
|
||||
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) {
|
||||
// Configure the middleware logger
|
||||
::std::env::set_var("RUST_LOG", "actix_web=info");
|
||||
env_logger::init();
|
||||
// Until this point, we probably want to write to the log macro fns.
|
||||
|
||||
// Until this point, we probably want to write to stderr
|
||||
// Start up the logging system: for now it just maps to stderr
|
||||
|
||||
// The log server is started on it's own thread
|
||||
let log_addr = log::start();
|
||||
// The log server is started on it's own thread, and is contacted
|
||||
// asynchronously.
|
||||
let log_addr = async_log::start();
|
||||
log_event!(log_addr, "Starting rsidm with configuration: {:?}", config);
|
||||
|
||||
// Similar, create a stats thread which aggregates statistics from the
|
||||
|
@ -236,6 +283,7 @@ pub fn create_server_core(config: Configuration) {
|
|||
// Copy the max size
|
||||
let max_size = config.maximum_request;
|
||||
let secure_cookies = config.secure_cookies;
|
||||
let domain = config.domain.clone();
|
||||
|
||||
// start the web server
|
||||
actix_web::server::new(move || {
|
||||
|
@ -246,22 +294,25 @@ pub fn create_server_core(config: Configuration) {
|
|||
// Connect all our end points here.
|
||||
.middleware(middleware::Logger::default())
|
||||
.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)
|
||||
session::CookieSessionBackend::signed(&[0; 32])
|
||||
.path("/")
|
||||
// Limit to path?
|
||||
// .path("/")
|
||||
//.max_age() duration of the token life TODO make this proper!
|
||||
.domain("localhost")
|
||||
.same_site(cookie::SameSite::Strict) // constrain to the domain
|
||||
// Disallow from js
|
||||
.http_only(true)
|
||||
// .domain(domain.as_str())
|
||||
// .same_site(cookie::SameSite::Strict) // constrain to the domain
|
||||
// Disallow from js and ...?
|
||||
.http_only(false)
|
||||
.name("rsidm-session")
|
||||
// This forces https only
|
||||
// This forces https only if true
|
||||
.secure(secure_cookies),
|
||||
))
|
||||
// .resource("/", |r| r.f(index))
|
||||
// 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/logout", ...)
|
||||
// .resource("/v1/token", ...) generate a token for id servers to use
|
||||
|
|
|
@ -3,7 +3,8 @@ use crate::audit::AuditScope;
|
|||
use crate::error::{OperationError, SchemaError};
|
||||
use crate::filter::{Filter, FilterInvalid, FilterResolved, FilterValidResolved};
|
||||
use crate::modify::{Modify, ModifyInvalid, ModifyList, ModifyValid};
|
||||
use crate::proto_v1::Entry as ProtoEntry;
|
||||
use crate::proto::v1::Entry as ProtoEntry;
|
||||
use crate::proto::v1::UserAuthToken;
|
||||
use crate::schema::{SchemaAttribute, SchemaClass, SchemaTransaction};
|
||||
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
|
||||
// access controls, dynamic attributes or more.
|
||||
// As a result, this may not even be the right place
|
||||
|
|
|
@ -28,12 +28,16 @@ pub enum OperationError {
|
|||
InvalidRequestState,
|
||||
InvalidState,
|
||||
InvalidEntryState,
|
||||
InvalidACPState,
|
||||
InvalidACPState(&'static str),
|
||||
InvalidAccountState(&'static str),
|
||||
BackendEngine,
|
||||
SQLiteError, //(RusqliteError)
|
||||
FsError,
|
||||
SerdeJsonError,
|
||||
AccessDenied,
|
||||
NotAuthenticated,
|
||||
InvalidAuthState(&'static str),
|
||||
InvalidSessionState,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
|
|
180
src/lib/event.rs
180
src/lib/event.rs
|
@ -1,10 +1,11 @@
|
|||
use crate::audit::AuditScope;
|
||||
use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryValid};
|
||||
use crate::filter::{Filter, FilterValid};
|
||||
use crate::proto_v1::Entry as ProtoEntry;
|
||||
use crate::proto_v1::{
|
||||
AuthRequest, AuthResponse, AuthStatus, CreateRequest, DeleteRequest, ModifyRequest,
|
||||
OperationResponse, ReviveRecycledRequest, SearchRequest, SearchResponse,
|
||||
use crate::proto::v1::Entry as ProtoEntry;
|
||||
use crate::proto::v1::{
|
||||
AuthAllowed, AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest,
|
||||
DeleteRequest, ModifyRequest, OperationResponse, ReviveRecycledRequest, SearchRequest,
|
||||
SearchResponse, UserAuthToken, WhoamiRequest, WhoamiResponse,
|
||||
};
|
||||
// use error::OperationError;
|
||||
use crate::error::OperationError;
|
||||
|
@ -12,6 +13,8 @@ use crate::modify::{ModifyList, ModifyValid};
|
|||
use crate::server::{
|
||||
QueryServerReadTransaction, QueryServerTransaction, QueryServerWriteTransaction,
|
||||
};
|
||||
|
||||
use crate::proto::v1::messages::AuthMessage;
|
||||
// Bring in schematransaction trait for validate
|
||||
// use crate::schema::SchemaTransaction;
|
||||
|
||||
|
@ -21,9 +24,10 @@ use crate::filter::FilterInvalid;
|
|||
#[cfg(test)]
|
||||
use crate::modify::ModifyInvalid;
|
||||
#[cfg(test)]
|
||||
use crate::proto_v1::SearchRecycledRequest;
|
||||
use crate::proto::v1::SearchRecycledRequest;
|
||||
|
||||
use actix::prelude::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
// Should the event Result have the log items?
|
||||
// FIXME: Remove seralising here - each type should
|
||||
|
@ -48,13 +52,13 @@ pub struct SearchResult {
|
|||
impl SearchResult {
|
||||
pub fn new(entries: Vec<Entry<EntryValid, EntryCommitted>>) -> Self {
|
||||
SearchResult {
|
||||
// FIXME: Can we consume this iter?
|
||||
entries: entries
|
||||
.iter()
|
||||
.map(|e| {
|
||||
// FIXME: The issue here is this probably is applying transforms
|
||||
// like access control ... May need to change.
|
||||
e.into()
|
||||
// All the needed transforms for this result are done
|
||||
// in search_ext. This is just an entry -> protoentry
|
||||
// step.
|
||||
e.into_pe()
|
||||
})
|
||||
.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(
|
||||
audit: &mut AuditScope,
|
||||
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.
|
||||
#[cfg(test)]
|
||||
pub unsafe fn new_impersonate_entry_ser(e: &str, filter: Filter<FilterInvalid>) -> Self {
|
||||
|
@ -329,7 +366,7 @@ impl CreateEvent {
|
|||
entries: Vec<Entry<EntryInvalid, EntryNew>>,
|
||||
) -> Self {
|
||||
CreateEvent {
|
||||
event: unsafe { Event::from_impersonate_entry_ser(e) },
|
||||
event: Event::from_impersonate_entry_ser(e),
|
||||
entries: entries,
|
||||
}
|
||||
}
|
||||
|
@ -519,22 +556,131 @@ impl ModifyEvent {
|
|||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AuthEvent {
|
||||
// pub event: Event,
|
||||
pub struct AuthEventStepInit {
|
||||
pub name: String,
|
||||
pub appid: Option<String>,
|
||||
}
|
||||
|
||||
impl AuthEvent {
|
||||
pub fn from_request(_request: AuthRequest) -> Self {
|
||||
AuthEvent {}
|
||||
#[derive(Debug)]
|
||||
pub struct AuthEventStepCreds {
|
||||
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 {
|
||||
pub fn response(self) -> 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
use crate::audit::AuditScope;
|
||||
use crate::error::{OperationError, SchemaError};
|
||||
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::server::{
|
||||
QueryServerReadTransaction, QueryServerTransaction, QueryServerWriteTransaction,
|
||||
|
|
119
src/lib/idm/account.rs
Normal file
119
src/lib/idm/account.rs
Normal 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
188
src/lib/idm/authsession.rs
Normal 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
56
src/lib/idm/macros.rs
Normal 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
9
src/lib/idm/mod.rs
Normal 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
263
src/lib/idm/server.rs
Normal 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.
|
||||
}
|
|
@ -3,7 +3,7 @@ use std::time::Duration;
|
|||
|
||||
use crate::constants::PURGE_TIMEOUT;
|
||||
use crate::event::{PurgeRecycledEvent, PurgeTombstoneEvent};
|
||||
use crate::proto_v1_actors::QueryServerV1;
|
||||
use crate::proto::v1::actors::QueryServerV1;
|
||||
|
||||
pub struct IntervalActor {
|
||||
// Store any addresses we require
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
#[macro_use]
|
||||
|
@ -32,7 +34,7 @@ extern crate concread;
|
|||
#[macro_use]
|
||||
mod macros;
|
||||
#[macro_use]
|
||||
mod log;
|
||||
mod async_log;
|
||||
#[macro_use]
|
||||
mod audit;
|
||||
mod be;
|
||||
|
@ -40,18 +42,18 @@ mod be;
|
|||
pub mod constants;
|
||||
mod entry;
|
||||
mod event;
|
||||
mod identity;
|
||||
// TODO: Does this need pub?
|
||||
mod filter;
|
||||
mod interval;
|
||||
mod modify;
|
||||
#[macro_use]
|
||||
mod plugins;
|
||||
mod access;
|
||||
mod idm;
|
||||
mod schema;
|
||||
mod server;
|
||||
|
||||
pub mod config;
|
||||
pub mod core;
|
||||
pub mod error;
|
||||
pub mod filter;
|
||||
pub mod proto_v1;
|
||||
mod proto_v1_actors;
|
||||
pub mod proto;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -5,7 +5,10 @@ macro_rules! run_test {
|
|||
use crate::be::Backend;
|
||||
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");
|
||||
|
||||
|
@ -18,7 +21,7 @@ macro_rules! run_test {
|
|||
.expect("Failed to bootstrap 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();
|
||||
|
@ -81,7 +84,7 @@ macro_rules! filter {
|
|||
#[allow(unused_imports)]
|
||||
use crate::filter::FC;
|
||||
#[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)
|
||||
}};
|
||||
}
|
||||
|
@ -96,7 +99,7 @@ macro_rules! filter_rec {
|
|||
#[allow(unused_imports)]
|
||||
use crate::filter::FC;
|
||||
#[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)
|
||||
}};
|
||||
}
|
||||
|
@ -111,7 +114,7 @@ macro_rules! filter_all {
|
|||
#[allow(unused_imports)]
|
||||
use crate::filter::FC;
|
||||
#[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)
|
||||
}};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::audit::AuditScope;
|
||||
use crate::proto_v1::Modify as ProtoModify;
|
||||
use crate::proto_v1::ModifyList as ProtoModifyList;
|
||||
use crate::proto::v1::Modify as ProtoModify;
|
||||
use crate::proto::v1::ModifyList as ProtoModifyList;
|
||||
|
||||
use crate::error::{OperationError, SchemaError};
|
||||
use crate::schema::SchemaTransaction;
|
||||
|
|
|
@ -13,7 +13,7 @@ macro_rules! setup_test {
|
|||
schema.bootstrap_core($au).expect("Failed to init 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() {
|
||||
let qs_write = qs.write();
|
||||
|
@ -42,7 +42,6 @@ macro_rules! run_create_test {
|
|||
use crate::event::CreateEvent;
|
||||
use crate::schema::Schema;
|
||||
use crate::server::QueryServer;
|
||||
use std::sync::Arc;
|
||||
|
||||
let mut au = AuditScope::new("run_create_test");
|
||||
audit_segment!(au, || {
|
||||
|
@ -96,7 +95,6 @@ macro_rules! run_modify_test {
|
|||
use crate::event::ModifyEvent;
|
||||
use crate::schema::Schema;
|
||||
use crate::server::QueryServer;
|
||||
use std::sync::Arc;
|
||||
|
||||
let mut au = AuditScope::new("run_modify_test");
|
||||
audit_segment!(au, || {
|
||||
|
@ -149,7 +147,6 @@ macro_rules! run_delete_test {
|
|||
use crate::event::DeleteEvent;
|
||||
use crate::schema::Schema;
|
||||
use crate::server::QueryServer;
|
||||
use std::sync::Arc;
|
||||
|
||||
let mut au = AuditScope::new("run_delete_test");
|
||||
audit_segment!(au, || {
|
||||
|
|
1
src/lib/proto/mod.rs
Normal file
1
src/lib/proto/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod v1;
|
|
@ -4,24 +4,29 @@ use std::sync::Arc;
|
|||
use crate::audit::AuditScope;
|
||||
use crate::be::Backend;
|
||||
|
||||
use crate::async_log::EventLog;
|
||||
use crate::error::OperationError;
|
||||
use crate::event::{
|
||||
CreateEvent, DeleteEvent, ModifyEvent, PurgeRecycledEvent, PurgeTombstoneEvent, SearchEvent,
|
||||
SearchResult,
|
||||
AuthEvent, AuthResult, CreateEvent, DeleteEvent, ModifyEvent, PurgeRecycledEvent,
|
||||
PurgeTombstoneEvent, SearchEvent, SearchResult, WhoamiResult,
|
||||
};
|
||||
use crate::log::EventLog;
|
||||
use crate::schema::{Schema, SchemaTransaction};
|
||||
|
||||
use crate::constants::UUID_ANONYMOUS;
|
||||
use crate::idm::server::IdmServer;
|
||||
use crate::server::{QueryServer, QueryServerTransaction};
|
||||
|
||||
use crate::proto_v1::{
|
||||
AuthRequest, CreateRequest, DeleteRequest, ModifyRequest, OperationResponse, SearchRequest,
|
||||
SearchResponse,
|
||||
use crate::proto::v1::{
|
||||
AuthRequest, AuthResponse, AuthState, CreateRequest, DeleteRequest, ModifyRequest,
|
||||
OperationResponse, SearchRequest, SearchResponse, UserAuthToken, WhoamiRequest, WhoamiResponse,
|
||||
};
|
||||
|
||||
use crate::proto::v1::messages::{AuthMessage, WhoamiMessage};
|
||||
|
||||
pub struct QueryServerV1 {
|
||||
log: actix::Addr<EventLog>,
|
||||
qs: QueryServer,
|
||||
idms: Arc<IdmServer>,
|
||||
}
|
||||
|
||||
impl Actor for QueryServerV1 {
|
||||
|
@ -33,14 +38,19 @@ impl Actor for 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 ...");
|
||||
QueryServerV1 {
|
||||
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(
|
||||
log: actix::Addr<EventLog>,
|
||||
path: &str,
|
||||
|
@ -53,9 +63,8 @@ impl QueryServerV1 {
|
|||
// Create "just enough" schema for us to be able to load from
|
||||
// disk ... Schema loading is one time where we validate the
|
||||
// entries as we read them, so we need this here.
|
||||
// FIXME: Handle results in start correctly
|
||||
let schema = match Schema::new(&mut audit) {
|
||||
Ok(s) => Arc::new(s),
|
||||
Ok(s) => s,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
|
@ -103,10 +112,14 @@ impl QueryServerV1 {
|
|||
}
|
||||
}
|
||||
|
||||
// Create a temporary query_server implementation
|
||||
let query_server = QueryServer::new(be.clone(), schema.clone());
|
||||
// Create a query_server implementation
|
||||
// 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");
|
||||
// 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();
|
||||
match query_server_write.initialise(&mut audit_qsc).and_then(|_| {
|
||||
audit_segment!(audit_qsc, || query_server_write.commit(&mut audit_qsc))
|
||||
|
@ -115,11 +128,17 @@ impl QueryServerV1 {
|
|||
Ok(_) => {}
|
||||
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);
|
||||
|
||||
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)
|
||||
});
|
||||
|
@ -249,14 +268,39 @@ impl Handler<DeleteRequest> for QueryServerV1 {
|
|||
}
|
||||
}
|
||||
|
||||
impl Handler<AuthRequest> for QueryServerV1 {
|
||||
type Result = Result<OperationResponse, OperationError>;
|
||||
// Need an auth session storage. LRU?
|
||||
// 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 res = audit_segment!(&mut audit, || {
|
||||
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.
|
||||
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 {
|
||||
type Result = ();
|
||||
|
26
src/lib/proto/v1/client.rs
Normal file
26
src/lib/proto/v1/client.rs
Normal 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() -> () {}
|
||||
}
|
45
src/lib/proto/v1/messages.rs
Normal file
45
src/lib/proto/v1/messages.rs
Normal 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>;
|
||||
}
|
|
@ -3,9 +3,71 @@
|
|||
use crate::error::OperationError;
|
||||
use actix::prelude::*;
|
||||
use std::collections::BTreeMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) mod actors;
|
||||
pub mod client;
|
||||
pub(crate) mod messages;
|
||||
|
||||
// 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
|
||||
// server core entry into. We also need to get from proto
|
||||
// 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
|
||||
// generated. The cookie can be shared between servers.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum AuthCredential {
|
||||
Anonymous,
|
||||
Password(String),
|
||||
// TOTP(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum AuthState {
|
||||
Init(String, Vec<String>),
|
||||
pub enum AuthStep {
|
||||
// name, application id?
|
||||
Init(String, Option<String>),
|
||||
/*
|
||||
Step(
|
||||
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?
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AuthRequest {
|
||||
pub state: AuthState,
|
||||
pub user_uuid: String,
|
||||
}
|
||||
|
||||
impl Message for AuthRequest {
|
||||
type Result = Result<OperationResponse, OperationError>;
|
||||
pub step: AuthStep,
|
||||
}
|
||||
|
||||
// Respond with the list of auth types and nonce, etc.
|
||||
// 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)]
|
||||
pub enum AuthStatus {
|
||||
Begin(String), // uuid of this session.
|
||||
// Continue, // Keep going, here are the things you could still provide ...
|
||||
// Go away, you made a mistake somewhere.
|
||||
// Provide reason?
|
||||
// Denied(String),
|
||||
// Welcome friend.
|
||||
// On success provide entry "self", for group assertions?
|
||||
// We also provide the "cookie"/token?
|
||||
// Success(String, Entry),
|
||||
pub enum AuthState {
|
||||
// Everything is good, your cookie has been issued, and a token is set here
|
||||
// for the client to view.
|
||||
Success(UserAuthToken),
|
||||
// Something was bad, your session is terminated and no cookie.
|
||||
Denied,
|
||||
// Continue to auth, allowed mechanisms listed.
|
||||
Continue(Vec<AuthAllowed>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
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 */
|
||||
|
@ -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)]
|
||||
mod tests {
|
||||
use crate::proto_v1::Filter as ProtoFilter;
|
||||
use crate::proto::v1::Filter as ProtoFilter;
|
||||
#[test]
|
||||
fn test_protofilter_simple() {
|
||||
let pf: ProtoFilter = ProtoFilter::Pres("class".to_string());
|
|
@ -1,7 +1,7 @@
|
|||
use crate::audit::AuditScope;
|
||||
use crate::constants::*;
|
||||
use crate::error::{ConsistencyError, OperationError, SchemaError};
|
||||
use crate::proto_v1::Filter as ProtoFilter;
|
||||
use crate::proto::v1::Filter as ProtoFilter;
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
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
|
||||
// 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
|
||||
// needs account lock, timeout, policy?
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ use crate::access::{
|
|||
};
|
||||
use crate::constants::{
|
||||
JSON_ADMIN_V1, JSON_ANONYMOUS_V1, JSON_IDM_ADMINS_ACP_REVIVE_V1, JSON_IDM_ADMINS_ACP_SEARCH_V1,
|
||||
JSON_IDM_ADMINS_V1, JSON_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::error::{ConsistencyError, OperationError, SchemaError};
|
||||
|
@ -554,6 +554,7 @@ impl<'a> QueryServerTransaction for QueryServerWriteTransaction<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct QueryServer {
|
||||
// log: actix::Addr<EventLog>,
|
||||
be: Backend,
|
||||
|
@ -562,11 +563,11 @@ pub struct 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 ...");
|
||||
QueryServer {
|
||||
be: be,
|
||||
schema: schema,
|
||||
schema: Arc::new(schema),
|
||||
accesscontrols: Arc::new(AccessControls::new()),
|
||||
}
|
||||
}
|
||||
|
@ -1300,16 +1301,11 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
}
|
||||
|
||||
// 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.
|
||||
let mut audit_an = AuditScope::new("start_idm_admins");
|
||||
let res = self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_ADMINS_V1);
|
||||
let mut audit_an = AuditScope::new("start_idm_admin_migrations");
|
||||
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);
|
||||
if res.is_err() {
|
||||
return res;
|
||||
|
@ -1318,15 +1314,15 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
// Create any system default schema entries.
|
||||
|
||||
// Create any system default access profile entries.
|
||||
let mut audit_an = AuditScope::new("start_idm_admins_acp");
|
||||
let res = self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_ADMINS_ACP_SEARCH_V1);
|
||||
audit.append_scope(audit_an);
|
||||
if res.is_err() {
|
||||
return res;
|
||||
}
|
||||
|
||||
let mut audit_an = AuditScope::new("start_idm_admins_acp");
|
||||
let res = self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_ADMINS_ACP_REVIVE_V1);
|
||||
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)
|
||||
.and_then(|_| {
|
||||
self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_ADMINS_ACP_REVIVE_V1)
|
||||
})
|
||||
.and_then(|_| {
|
||||
self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_SELF_ACP_READ_V1)
|
||||
});
|
||||
audit.append_scope(audit_an);
|
||||
if res.is_err() {
|
||||
return res;
|
||||
|
@ -1459,31 +1455,15 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
|
||||
#[cfg(test)]
|
||||
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::entry::{Entry, EntryInvalid, EntryNew};
|
||||
use crate::error::{OperationError, SchemaError};
|
||||
use crate::event::{CreateEvent, DeleteEvent, ModifyEvent, ReviveRecycledEvent, SearchEvent};
|
||||
use crate::modify::{Modify, ModifyList};
|
||||
use crate::proto_v1::Filter as ProtoFilter;
|
||||
use crate::proto_v1::Modify as ProtoModify;
|
||||
use crate::proto_v1::ModifyList as ProtoModifyList;
|
||||
use crate::proto_v1::{DeleteRequest, ModifyRequest, ReviveRecycledRequest};
|
||||
use crate::proto::v1::Filter as ProtoFilter;
|
||||
use crate::proto::v1::Modify as ProtoModify;
|
||||
use crate::proto::v1::ModifyList as ProtoModifyList;
|
||||
use crate::proto::v1::{DeleteRequest, ModifyRequest, ReviveRecycledRequest};
|
||||
use crate::server::QueryServerTransaction;
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
extern crate actix;
|
||||
extern crate env_logger;
|
||||
|
||||
extern crate rsidm;
|
||||
|
||||
|
@ -6,12 +7,14 @@ use rsidm::config::Configuration;
|
|||
use rsidm::core::create_server_core;
|
||||
|
||||
fn main() {
|
||||
// read the 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
|
||||
// Read our config (if any)
|
||||
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");
|
||||
|
||||
create_server_core(config);
|
||||
|
|
|
@ -5,7 +5,10 @@ extern crate rsidm;
|
|||
use rsidm::config::Configuration;
|
||||
use rsidm::constants::UUID_ADMIN;
|
||||
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;
|
||||
|
||||
|
@ -13,50 +16,60 @@ extern crate futures;
|
|||
// use futures::future;
|
||||
// use futures::future::Future;
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
|
||||
extern crate env_logger;
|
||||
extern crate tokio;
|
||||
|
||||
static PORT_ALLOC: AtomicUsize = AtomicUsize::new(8080);
|
||||
|
||||
// Test external behaviorus of the service.
|
||||
|
||||
macro_rules! run_test {
|
||||
($test_fn:expr) => {{
|
||||
let (tx, rx) = mpsc::channel();
|
||||
fn run_test(test_fn: fn(reqwest::Client, &str) -> ()) {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let port = PORT_ALLOC.fetch_add(1, Ordering::SeqCst);
|
||||
let mut config = Configuration::new();
|
||||
config.address = format!("127.0.0.1:{}", port);
|
||||
// Setup the config ...
|
||||
|
||||
thread::spawn(|| {
|
||||
// setup
|
||||
// Create a server config in memory for use - use test settings
|
||||
// Create a log: In memory - for now it's just stdout
|
||||
thread::spawn(move || {
|
||||
// Spawn a thread for the test runner, this should have a unique
|
||||
// port....
|
||||
System::run(move || {
|
||||
create_server_core(config);
|
||||
|
||||
System::run(move || {
|
||||
let config = Configuration::new();
|
||||
create_server_core(config);
|
||||
|
||||
// This appears to be bind random ...
|
||||
// let srv = srv.bind("127.0.0.1:0").unwrap();
|
||||
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?
|
||||
// Yes probably, but they'll need to be futures as well ...
|
||||
// later we could accept fixture as it's own future for re-use
|
||||
$test_fn();
|
||||
// Do we need any fixtures?
|
||||
// Yes probably, but they'll need to be futures as well ...
|
||||
// later we could accept fixture as it's own future for re-use
|
||||
|
||||
// We DO NOT need teardown, as sqlite is in mem
|
||||
// let the tables hit the floor
|
||||
let _ = sys.stop();
|
||||
}};
|
||||
// Setup the client, and the address we selected.
|
||||
let client = reqwest::Client::builder()
|
||||
.cookie_store(true)
|
||||
.build()
|
||||
.expect("Unexpected reqwest builder failure!");
|
||||
let addr = format!("http://127.0.0.1:{}", port);
|
||||
|
||||
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]
|
||||
fn test_server_proto() {
|
||||
run_test!(|| {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
run_test(|client: reqwest::Client, addr: &str| {
|
||||
let e: Entry = serde_json::from_str(
|
||||
r#"{
|
||||
"attrs": {
|
||||
|
@ -74,8 +87,10 @@ fn test_server_proto() {
|
|||
user_uuid: UUID_ADMIN.to_string(),
|
||||
};
|
||||
|
||||
let dest = format!("{}/v1/create", addr);
|
||||
|
||||
let mut response = client
|
||||
.post("http://127.0.0.1:8080/v1/create")
|
||||
.post(dest.as_str())
|
||||
.body(serde_json::to_string(&c).unwrap())
|
||||
.send()
|
||||
.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]
|
||||
fn test_be_create_user() {
|
||||
|
|
Loading…
Reference in a new issue