mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
Add a lot of client support, clean up warnings, and clean up output for clients
This commit is contained in:
parent
d436291eff
commit
4ba9508a31
|
@ -9,10 +9,10 @@ log = "0.4"
|
|||
env_logger = "0.6"
|
||||
reqwest = "0.9"
|
||||
rsidm_proto = { path = "../rsidm_proto" }
|
||||
serde_json = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = "0.1"
|
||||
actix = "0.7"
|
||||
rsidm = { path = "../rsidmd" }
|
||||
futures = "0.1"
|
||||
serde_json = "1.0"
|
||||
|
|
|
@ -1,8 +1,27 @@
|
|||
// #![deny(warnings)]
|
||||
#![deny(warnings)]
|
||||
#![warn(unused_extern_crates)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use serde_json;
|
||||
|
||||
use reqwest;
|
||||
|
||||
use rsidm_proto::v1::{
|
||||
AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, Entry,
|
||||
OperationResponse, UserAuthToken, WhoamiResponse,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ClientError {
|
||||
Unauthorized,
|
||||
Http(reqwest::StatusCode),
|
||||
Transport(reqwest::Error),
|
||||
AuthenticationFailed,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RsidmClient {
|
||||
client: reqwest::Client,
|
||||
addr: String,
|
||||
|
@ -20,10 +39,156 @@ impl RsidmClient {
|
|||
}
|
||||
}
|
||||
|
||||
fn auth_step_init(&self, ident: &str, appid: Option<&str>) -> Result<AuthState, ClientError> {
|
||||
// TODO: Way to avoid formatting so much?
|
||||
let auth_dest = format!("{}/v1/auth", self.addr);
|
||||
|
||||
let auth_init = AuthRequest {
|
||||
step: AuthStep::Init(ident.to_string(), appid.map(|s| s.to_string())),
|
||||
};
|
||||
|
||||
// Handle this!
|
||||
let mut response = self
|
||||
.client
|
||||
.post(auth_dest.as_str())
|
||||
.body(serde_json::to_string(&auth_init).expect("Generated invalid initstep?!"))
|
||||
.send()
|
||||
.map_err(|e| ClientError::Transport(e))?;
|
||||
|
||||
match response.status() {
|
||||
reqwest::StatusCode::OK => {}
|
||||
unexpect => return Err(ClientError::Http(unexpect)),
|
||||
}
|
||||
// Check that we got the next step
|
||||
let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap();
|
||||
|
||||
Ok(r.state)
|
||||
}
|
||||
|
||||
// auth
|
||||
pub fn auth_anonymous(&self) -> Result<UserAuthToken, ClientError> {
|
||||
let _state = match self.auth_step_init("anonymous", None) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
// TODO: Avoid creating this so much?
|
||||
let auth_dest = format!("{}/v1/auth", self.addr);
|
||||
|
||||
// Check state for auth continue contains anonymous.
|
||||
|
||||
let auth_anon = AuthRequest {
|
||||
step: AuthStep::Creds(vec![AuthCredential::Anonymous]),
|
||||
};
|
||||
|
||||
let mut response = self
|
||||
.client
|
||||
.post(auth_dest.as_str())
|
||||
.body(serde_json::to_string(&auth_anon).unwrap())
|
||||
.send()
|
||||
.map_err(|e| ClientError::Transport(e))?;
|
||||
|
||||
match response.status() {
|
||||
reqwest::StatusCode::OK => {}
|
||||
unexpect => return Err(ClientError::Http(unexpect)),
|
||||
}
|
||||
// Check that we got the next step
|
||||
let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap();
|
||||
|
||||
match r.state {
|
||||
AuthState::Success(uat) => {
|
||||
debug!("==> Authed as uat; {:?}", uat);
|
||||
Ok(uat)
|
||||
}
|
||||
_ => Err(ClientError::AuthenticationFailed),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn auth_simple_password(
|
||||
&self,
|
||||
ident: &str,
|
||||
password: &str,
|
||||
) -> Result<UserAuthToken, ClientError> {
|
||||
// TODO: Way to avoid formatting so much?
|
||||
let auth_dest = format!("{}/v1/auth", self.addr);
|
||||
|
||||
let _state = match self.auth_step_init(ident, None) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
// Send the credentials required now
|
||||
let auth_req = AuthRequest {
|
||||
step: AuthStep::Creds(vec![AuthCredential::Password(password.to_string())]),
|
||||
};
|
||||
|
||||
let mut response = self
|
||||
.client
|
||||
.post(auth_dest.as_str())
|
||||
.body(serde_json::to_string(&auth_req).unwrap())
|
||||
.send()
|
||||
.map_err(|e| ClientError::Transport(e))?;
|
||||
|
||||
match response.status() {
|
||||
reqwest::StatusCode::OK => {}
|
||||
unexpect => return Err(ClientError::Http(unexpect)),
|
||||
}
|
||||
|
||||
let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap();
|
||||
|
||||
match r.state {
|
||||
AuthState::Success(uat) => {
|
||||
debug!("==> Authed as uat; {:?}", uat);
|
||||
Ok(uat)
|
||||
}
|
||||
_ => Err(ClientError::AuthenticationFailed),
|
||||
}
|
||||
}
|
||||
|
||||
// whoami
|
||||
pub fn whoami(&self) -> Result<Option<(Entry, UserAuthToken)>, ClientError> {
|
||||
let whoami_dest = format!("{}/v1/whoami", self.addr);
|
||||
let mut response = self.client.get(whoami_dest.as_str()).send().unwrap();
|
||||
// https://docs.rs/reqwest/0.9.15/reqwest/struct.Response.html
|
||||
|
||||
match response.status() {
|
||||
// Continue to process.
|
||||
reqwest::StatusCode::OK => {}
|
||||
reqwest::StatusCode::UNAUTHORIZED => return Ok(None),
|
||||
unexpect => return Err(ClientError::Http(unexpect)),
|
||||
}
|
||||
|
||||
let r: WhoamiResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap();
|
||||
|
||||
Ok(Some((r.youare, r.uat)))
|
||||
}
|
||||
|
||||
// search
|
||||
// create
|
||||
pub fn create(&self, entries: Vec<Entry>) -> Result<(), ClientError> {
|
||||
let c = CreateRequest { entries: entries };
|
||||
|
||||
// TODO: Avoid formatting this so much!
|
||||
let dest = format!("{}/v1/create", self.addr);
|
||||
|
||||
let mut response = self
|
||||
.client
|
||||
.post(dest.as_str())
|
||||
.body(serde_json::to_string(&c).unwrap())
|
||||
.send()
|
||||
.map_err(|e| ClientError::Transport(e))?;
|
||||
|
||||
match response.status() {
|
||||
reqwest::StatusCode::OK => {}
|
||||
unexpect => return Err(ClientError::Http(unexpect)),
|
||||
}
|
||||
|
||||
// TODO: What about errors
|
||||
let _r: OperationResponse =
|
||||
serde_json::from_str(response.text().unwrap().as_str()).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// modify
|
||||
//
|
||||
}
|
||||
|
|
|
@ -7,16 +7,15 @@ extern crate actix;
|
|||
use actix::prelude::*;
|
||||
|
||||
extern crate rsidm;
|
||||
extern crate rsidm_client;
|
||||
extern crate rsidm_proto;
|
||||
extern crate serde_json;
|
||||
|
||||
use rsidm_client::RsidmClient;
|
||||
|
||||
use rsidm::config::{Configuration, IntegrationTestConfig};
|
||||
use rsidm::constants::UUID_ADMIN;
|
||||
use rsidm::core::create_server_core;
|
||||
use rsidm_proto::v1::{
|
||||
AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, Entry,
|
||||
OperationResponse,
|
||||
};
|
||||
use rsidm_proto::v1::Entry;
|
||||
|
||||
extern crate reqwest;
|
||||
|
||||
|
@ -36,7 +35,8 @@ static ADMIN_TEST_PASSWORD: &'static str = "integration test admin password";
|
|||
|
||||
// Test external behaviorus of the service.
|
||||
|
||||
fn run_test(test_fn: fn(reqwest::Client, &str) -> ()) {
|
||||
fn run_test(test_fn: fn(RsidmClient) -> ()) {
|
||||
// ::std::env::set_var("RUST_LOG", "actix_web=debug,rsidm=debug");
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let port = PORT_ALLOC.fetch_add(1, Ordering::SeqCst);
|
||||
|
@ -70,13 +70,10 @@ fn run_test(test_fn: fn(reqwest::Client, &str) -> ()) {
|
|||
// later we could accept fixture as it's own future for re-use
|
||||
|
||||
// Setup the client, and the address we selected.
|
||||
let client = reqwest::Client::builder()
|
||||
.cookie_store(true)
|
||||
.build()
|
||||
.expect("Unexpected reqwest builder failure!");
|
||||
let addr = format!("http://127.0.0.1:{}", port);
|
||||
let rsclient = RsidmClient::new(addr.as_str());
|
||||
|
||||
test_fn(client, addr.as_str());
|
||||
test_fn(rsclient);
|
||||
|
||||
// We DO NOT need teardown, as sqlite is in mem
|
||||
// let the tables hit the floor
|
||||
|
@ -84,8 +81,8 @@ fn run_test(test_fn: fn(reqwest::Client, &str) -> ()) {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_server_proto() {
|
||||
run_test(|client: reqwest::Client, addr: &str| {
|
||||
fn test_server_create() {
|
||||
run_test(|rsclient: RsidmClient| {
|
||||
let e: Entry = serde_json::from_str(
|
||||
r#"{
|
||||
"attrs": {
|
||||
|
@ -98,168 +95,54 @@ fn test_server_proto() {
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let c = CreateRequest {
|
||||
entries: vec![e],
|
||||
user_uuid: UUID_ADMIN.to_string(),
|
||||
};
|
||||
// Not logged in - should fail!
|
||||
let res = rsclient.create(vec![e.clone()]);
|
||||
assert!(res.is_err());
|
||||
|
||||
let dest = format!("{}/v1/create", addr);
|
||||
let a_res = rsclient.auth_simple_password("admin", ADMIN_TEST_PASSWORD);
|
||||
assert!(a_res.is_ok());
|
||||
|
||||
let mut response = client
|
||||
.post(dest.as_str())
|
||||
.body(serde_json::to_string(&c).unwrap())
|
||||
.send()
|
||||
.unwrap();
|
||||
|
||||
println!("{:?}", response);
|
||||
let r: OperationResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap();
|
||||
|
||||
println!("{:?}", r);
|
||||
|
||||
// deserialise the response here
|
||||
// check it's valid.
|
||||
|
||||
()
|
||||
let res = rsclient.create(vec![e]);
|
||||
assert!(res.is_ok());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_server_whoami_anonymous() {
|
||||
run_test(|client: reqwest::Client, addr: &str| {
|
||||
run_test(|rsclient: RsidmClient| {
|
||||
// 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);
|
||||
let pre_res = rsclient.whoami();
|
||||
// This means it was okay whoami, but no uat attached.
|
||||
assert!(pre_res.unwrap().is_none());
|
||||
|
||||
// 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();
|
||||
debug!("{}", response.status());
|
||||
assert!(response.status() == reqwest::StatusCode::OK);
|
||||
// Check that we got the next step
|
||||
let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap();
|
||||
println!("==> AUTHRESPONSE ==> {:?}", r);
|
||||
|
||||
assert!(match &r.state {
|
||||
AuthState::Success(uat) => {
|
||||
println!("==> Authed as uat; {:?}", uat);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
let res = rsclient.auth_anonymous();
|
||||
assert!(res.is_ok());
|
||||
|
||||
// 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()
|
||||
let post_res = rsclient.whoami().unwrap();
|
||||
assert!(post_res.is_some());
|
||||
// TODO: Now unwrap and ensure anony
|
||||
println!("{:?}", post_res);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_server_whoami_admin_simple_password() {
|
||||
run_test(|client: reqwest::Client, addr: &str| {
|
||||
run_test(|rsclient: RsidmClient| {
|
||||
// First show we are un-authenticated.
|
||||
let whoami_dest = format!("{}/v1/whoami", addr);
|
||||
let auth_dest = format!("{}/v1/auth", addr);
|
||||
// Now login as admin
|
||||
let pre_res = rsclient.whoami();
|
||||
// This means it was okay whoami, but no uat attached.
|
||||
assert!(pre_res.unwrap().is_none());
|
||||
|
||||
// Setup the auth initialisation
|
||||
let auth_init = AuthRequest {
|
||||
step: AuthStep::Init("admin".to_string(), None),
|
||||
};
|
||||
|
||||
let mut response = client
|
||||
.post(auth_dest.as_str())
|
||||
.body(serde_json::to_string(&auth_init).unwrap())
|
||||
.send()
|
||||
.unwrap();
|
||||
assert!(response.status() == reqwest::StatusCode::OK);
|
||||
// Check that we got the next step
|
||||
let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap();
|
||||
println!("==> AUTHRESPONSE ==> {:?}", r);
|
||||
|
||||
assert!(match &r.state {
|
||||
AuthState::Continue(_all_list) => {
|
||||
// Check anonymous is present? It will fail on next step if not ...
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
|
||||
// Send the credentials required now
|
||||
let auth_admin = AuthRequest {
|
||||
step: AuthStep::Creds(vec![AuthCredential::Password(
|
||||
ADMIN_TEST_PASSWORD.to_string(),
|
||||
)]),
|
||||
};
|
||||
|
||||
let mut response = client
|
||||
.post(auth_dest.as_str())
|
||||
.body(serde_json::to_string(&auth_admin).unwrap())
|
||||
.send()
|
||||
.unwrap();
|
||||
debug!("{}", response.status());
|
||||
assert!(response.status() == reqwest::StatusCode::OK);
|
||||
// Check that we got the next step
|
||||
let r: AuthResponse = serde_json::from_str(response.text().unwrap().as_str()).unwrap();
|
||||
println!("==> AUTHRESPONSE ==> {:?}", r);
|
||||
|
||||
assert!(match &r.state {
|
||||
AuthState::Success(uat) => {
|
||||
println!("==> Authed as uat; {:?}", uat);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
let res = rsclient.auth_simple_password("admin", ADMIN_TEST_PASSWORD);
|
||||
assert!(res.is_ok());
|
||||
|
||||
// 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()
|
||||
let post_res = rsclient.whoami().unwrap();
|
||||
assert!(post_res.is_some());
|
||||
// TODO: Now unwrap and ensure anony
|
||||
debug!("{:?}", post_res);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -4,9 +4,6 @@ version = "0.1.0"
|
|||
authors = ["William Brown <william@blackhats.net.au>"]
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
rsidm_internal = ["actix"]
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(feature = "rsidm_internal")]
|
||||
use actix::prelude::*;
|
||||
|
||||
// These proto implementations are here because they have public definitions
|
||||
|
||||
/* ===== errors ===== */
|
||||
|
@ -120,6 +118,16 @@ pub struct UserAuthToken {
|
|||
// Should we allow supplemental ava's to be added on request?
|
||||
}
|
||||
|
||||
impl fmt::Display for UserAuthToken {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "name: {}", self.name)?;
|
||||
writeln!(f, "display: {}", self.displayname)?;
|
||||
writeln!(f, "uuid: {}", self.uuid)?;
|
||||
writeln!(f, "groups: {:?}", self.groups)?;
|
||||
writeln!(f, "claims: {:?}", self.claims)
|
||||
}
|
||||
}
|
||||
|
||||
// UAT will need a downcast to Entry, which adds in the claims to the entry
|
||||
// for the purpose of filtering.
|
||||
|
||||
|
@ -178,22 +186,13 @@ impl OperationResponse {
|
|||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SearchRequest {
|
||||
pub filter: Filter,
|
||||
pub user_uuid: String,
|
||||
}
|
||||
|
||||
impl SearchRequest {
|
||||
pub fn new(filter: Filter, user_uuid: &str) -> Self {
|
||||
SearchRequest {
|
||||
filter: filter,
|
||||
user_uuid: user_uuid.to_string(),
|
||||
pub fn new(filter: Filter) -> Self {
|
||||
SearchRequest { filter: filter }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rsidm_internal")]
|
||||
impl Message for SearchRequest {
|
||||
type Result = Result<SearchResponse, OperationError>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SearchResponse {
|
||||
|
@ -209,66 +208,41 @@ impl SearchResponse {
|
|||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CreateRequest {
|
||||
pub entries: Vec<Entry>,
|
||||
pub user_uuid: String,
|
||||
}
|
||||
|
||||
impl CreateRequest {
|
||||
pub fn new(entries: Vec<Entry>, user_uuid: &str) -> Self {
|
||||
CreateRequest {
|
||||
entries: entries,
|
||||
user_uuid: user_uuid.to_string(),
|
||||
pub fn new(entries: Vec<Entry>) -> Self {
|
||||
CreateRequest { entries: entries }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rsidm_internal")]
|
||||
impl Message for CreateRequest {
|
||||
type Result = Result<OperationResponse, OperationError>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct DeleteRequest {
|
||||
pub filter: Filter,
|
||||
pub user_uuid: String,
|
||||
}
|
||||
|
||||
impl DeleteRequest {
|
||||
pub fn new(filter: Filter, user_uuid: &str) -> Self {
|
||||
DeleteRequest {
|
||||
filter: filter,
|
||||
user_uuid: user_uuid.to_string(),
|
||||
pub fn new(filter: Filter) -> Self {
|
||||
DeleteRequest { filter: filter }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rsidm_internal")]
|
||||
impl Message for DeleteRequest {
|
||||
type Result = Result<OperationResponse, OperationError>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ModifyRequest {
|
||||
// Probably needs a modlist?
|
||||
pub filter: Filter,
|
||||
pub modlist: ModifyList,
|
||||
pub user_uuid: String,
|
||||
}
|
||||
|
||||
impl ModifyRequest {
|
||||
pub fn new(filter: Filter, modlist: ModifyList, user_uuid: &str) -> Self {
|
||||
pub fn new(filter: Filter, modlist: ModifyList) -> Self {
|
||||
ModifyRequest {
|
||||
filter: filter,
|
||||
modlist: modlist,
|
||||
user_uuid: user_uuid.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rsidm_internal")]
|
||||
impl Message for ModifyRequest {
|
||||
type Result = Result<OperationResponse, OperationError>;
|
||||
}
|
||||
|
||||
// Login is a multi-step process potentially. First the client says who they
|
||||
// want to request
|
||||
//
|
||||
|
@ -340,15 +314,11 @@ pub struct AuthResponse {
|
|||
|
||||
pub struct SearchRecycledRequest {
|
||||
pub filter: Filter,
|
||||
pub user_uuid: String,
|
||||
}
|
||||
|
||||
impl SearchRecycledRequest {
|
||||
pub fn new(filter: Filter, user_uuid: &str) -> Self {
|
||||
SearchRecycledRequest {
|
||||
filter: filter,
|
||||
user_uuid: user_uuid.to_string(),
|
||||
}
|
||||
pub fn new(filter: Filter) -> Self {
|
||||
SearchRecycledRequest { filter: filter }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -356,15 +326,11 @@ impl SearchRecycledRequest {
|
|||
|
||||
pub struct ReviveRecycledRequest {
|
||||
pub filter: Filter,
|
||||
pub user_uuid: String,
|
||||
}
|
||||
|
||||
impl ReviveRecycledRequest {
|
||||
pub fn new(filter: Filter, user_uuid: &str) -> Self {
|
||||
ReviveRecycledRequest {
|
||||
filter: filter,
|
||||
user_uuid: user_uuid.to_string(),
|
||||
}
|
||||
pub fn new(filter: Filter) -> Self {
|
||||
ReviveRecycledRequest { filter: filter }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -382,11 +348,15 @@ impl WhoamiRequest {
|
|||
pub struct WhoamiResponse {
|
||||
// Should we just embed the entry? Or destructure it?
|
||||
pub youare: Entry,
|
||||
pub uat: UserAuthToken,
|
||||
}
|
||||
|
||||
impl WhoamiResponse {
|
||||
pub fn new(e: Entry) -> Self {
|
||||
WhoamiResponse { youare: e }
|
||||
pub fn new(e: Entry, uat: UserAuthToken) -> Self {
|
||||
WhoamiResponse {
|
||||
youare: e,
|
||||
uat: uat,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,4 +10,6 @@ path = "src/main.rs"
|
|||
|
||||
[dependencies]
|
||||
rsidm_client = { path = "../rsidm_client" }
|
||||
rpassword = "0.4"
|
||||
structopt = { version = "0.2", default-features = false }
|
||||
|
||||
|
|
|
@ -1,3 +1,54 @@
|
|||
fn main() {
|
||||
println!("Hello kanidm");
|
||||
extern crate structopt;
|
||||
use rsidm_client::RsidmClient;
|
||||
use structopt::StructOpt;
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
struct CommonOpt {
|
||||
#[structopt(short = "H", long = "url")]
|
||||
addr: String,
|
||||
#[structopt(short = "D", long = "name")]
|
||||
username: String,
|
||||
}
|
||||
|
||||
impl CommonOpt {
|
||||
fn to_client(&self) -> RsidmClient {
|
||||
RsidmClient::new(self.addr.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
enum ClientOpt {
|
||||
#[structopt(name = "whoami")]
|
||||
Whoami(CommonOpt),
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let opt = ClientOpt::from_args();
|
||||
|
||||
match opt {
|
||||
ClientOpt::Whoami(copt) => {
|
||||
let client = copt.to_client();
|
||||
let r = if copt.username == "anonymous" {
|
||||
client.auth_anonymous()
|
||||
} else {
|
||||
let password = rpassword::prompt_password_stderr("Enter password: ").unwrap();
|
||||
client.auth_simple_password(copt.username.as_str(), password.as_str())
|
||||
};
|
||||
|
||||
if r.is_err() {
|
||||
println!("Error during authentication phase: {:?}", r);
|
||||
return;
|
||||
}
|
||||
|
||||
match client.whoami() {
|
||||
Ok(o_ent) => match o_ent {
|
||||
Some((_ent, uat)) => {
|
||||
println!("{}", uat);
|
||||
}
|
||||
None => println!("Unauthenticated"),
|
||||
},
|
||||
Err(e) => println!("Error: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
extern crate reqwest;
|
||||
extern crate rsidm;
|
||||
|
||||
use rsidm::proto::v1::{WhoamiRequest, WhoamiResponse};
|
||||
|
||||
fn main() {
|
||||
println!("Hello whoami");
|
||||
|
||||
// Given the current ~/.rsidm/cookie (or none)
|
||||
// we should check who we are plus show the auth token that the server
|
||||
// would generate for us.
|
||||
|
||||
let whoami_req = WhoamiRequest {};
|
||||
|
||||
// FIXME TODO: Make this url configurable!!!
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let mut response = client
|
||||
.get("http://127.0.0.1:8080/v1/whoami")
|
||||
.send()
|
||||
.unwrap();
|
||||
|
||||
println!("{:?}", response);
|
||||
|
||||
// Parse it if desire.
|
||||
// let r: Response = serde_json::from_str(response.text().unwrap().as_str()).unwrap();
|
||||
// println!("{:?}", r);
|
||||
}
|
|
@ -17,7 +17,7 @@ path = "src/server/main.rs"
|
|||
|
||||
|
||||
[dependencies]
|
||||
rsidm_proto = { path = "../rsidm_proto", features = ["rsidm_internal"] }
|
||||
rsidm_proto = { path = "../rsidm_proto" }
|
||||
|
||||
actix = "0.7"
|
||||
actix-web = "0.7"
|
||||
|
|
|
@ -858,7 +858,7 @@ pub trait AccessControlsTransaction {
|
|||
})
|
||||
.collect();
|
||||
|
||||
audit_log!(audit, "Related acs -> {:?}", related_acp);
|
||||
audit_log!(audit, "Related acc -> {:?}", related_acp);
|
||||
|
||||
// For each entry
|
||||
let r = entries.iter().fold(true, |acc, e| {
|
||||
|
|
|
@ -61,6 +61,66 @@ impl Message for AuthMessage {
|
|||
type Result = Result<AuthResponse, OperationError>;
|
||||
}
|
||||
|
||||
pub struct CreateMessage {
|
||||
pub uat: Option<UserAuthToken>,
|
||||
pub req: CreateRequest,
|
||||
}
|
||||
|
||||
impl CreateMessage {
|
||||
pub fn new(uat: Option<UserAuthToken>, req: CreateRequest) -> Self {
|
||||
CreateMessage { uat: uat, req: req }
|
||||
}
|
||||
}
|
||||
|
||||
impl Message for CreateMessage {
|
||||
type Result = Result<OperationResponse, OperationError>;
|
||||
}
|
||||
|
||||
pub struct DeleteMessage {
|
||||
pub uat: Option<UserAuthToken>,
|
||||
pub req: DeleteRequest,
|
||||
}
|
||||
|
||||
impl DeleteMessage {
|
||||
pub fn new(uat: Option<UserAuthToken>, req: DeleteRequest) -> Self {
|
||||
DeleteMessage { uat: uat, req: req }
|
||||
}
|
||||
}
|
||||
|
||||
impl Message for DeleteMessage {
|
||||
type Result = Result<OperationResponse, OperationError>;
|
||||
}
|
||||
|
||||
pub struct ModifyMessage {
|
||||
pub uat: Option<UserAuthToken>,
|
||||
pub req: ModifyRequest,
|
||||
}
|
||||
|
||||
impl ModifyMessage {
|
||||
pub fn new(uat: Option<UserAuthToken>, req: ModifyRequest) -> Self {
|
||||
ModifyMessage { uat: uat, req: req }
|
||||
}
|
||||
}
|
||||
|
||||
impl Message for ModifyMessage {
|
||||
type Result = Result<OperationResponse, OperationError>;
|
||||
}
|
||||
|
||||
pub struct SearchMessage {
|
||||
pub uat: Option<UserAuthToken>,
|
||||
pub req: SearchRequest,
|
||||
}
|
||||
|
||||
impl SearchMessage {
|
||||
pub fn new(uat: Option<UserAuthToken>, req: SearchRequest) -> Self {
|
||||
SearchMessage { uat: uat, req: req }
|
||||
}
|
||||
}
|
||||
|
||||
impl Message for SearchMessage {
|
||||
type Result = Result<SearchResponse, OperationError>;
|
||||
}
|
||||
|
||||
pub struct QueryServerV1 {
|
||||
log: actix::Addr<EventLog>,
|
||||
qs: QueryServer,
|
||||
|
@ -98,22 +158,22 @@ impl QueryServerV1 {
|
|||
}
|
||||
}
|
||||
|
||||
// The server only recieves "Event" structures, which
|
||||
// The server only recieves "Message" structures, which
|
||||
// are whole self contained DB operations with all parsing
|
||||
// required complete. We still need to do certain validation steps, but
|
||||
// at this point our just is just to route to do_<action>
|
||||
|
||||
impl Handler<SearchRequest> for QueryServerV1 {
|
||||
impl Handler<SearchMessage> for QueryServerV1 {
|
||||
type Result = Result<SearchResponse, OperationError>;
|
||||
|
||||
fn handle(&mut self, msg: SearchRequest, _: &mut Self::Context) -> Self::Result {
|
||||
fn handle(&mut self, msg: SearchMessage, _: &mut Self::Context) -> Self::Result {
|
||||
let mut audit = AuditScope::new("search");
|
||||
let res = audit_segment!(&mut audit, || {
|
||||
// Begin a read
|
||||
let qs_read = self.qs.read();
|
||||
|
||||
// Make an event from the request
|
||||
let srch = match SearchEvent::from_request(&mut audit, msg, &qs_read) {
|
||||
let srch = match SearchEvent::from_message(&mut audit, msg, &qs_read) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
audit_log!(audit, "Failed to begin search: {:?}", e);
|
||||
|
@ -138,15 +198,15 @@ impl Handler<SearchRequest> for QueryServerV1 {
|
|||
}
|
||||
}
|
||||
|
||||
impl Handler<CreateRequest> for QueryServerV1 {
|
||||
impl Handler<CreateMessage> for QueryServerV1 {
|
||||
type Result = Result<OperationResponse, OperationError>;
|
||||
|
||||
fn handle(&mut self, msg: CreateRequest, _: &mut Self::Context) -> Self::Result {
|
||||
fn handle(&mut self, msg: CreateMessage, _: &mut Self::Context) -> Self::Result {
|
||||
let mut audit = AuditScope::new("create");
|
||||
let res = audit_segment!(&mut audit, || {
|
||||
let mut qs_write = self.qs.write();
|
||||
|
||||
let crt = match CreateEvent::from_request(&mut audit, msg, &qs_write) {
|
||||
let crt = match CreateEvent::from_message(&mut audit, msg, &qs_write) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
audit_log!(audit, "Failed to begin create: {:?}", e);
|
||||
|
@ -166,14 +226,14 @@ impl Handler<CreateRequest> for QueryServerV1 {
|
|||
}
|
||||
}
|
||||
|
||||
impl Handler<ModifyRequest> for QueryServerV1 {
|
||||
impl Handler<ModifyMessage> for QueryServerV1 {
|
||||
type Result = Result<OperationResponse, OperationError>;
|
||||
|
||||
fn handle(&mut self, msg: ModifyRequest, _: &mut Self::Context) -> Self::Result {
|
||||
fn handle(&mut self, msg: ModifyMessage, _: &mut Self::Context) -> Self::Result {
|
||||
let mut audit = AuditScope::new("modify");
|
||||
let res = audit_segment!(&mut audit, || {
|
||||
let mut qs_write = self.qs.write();
|
||||
let mdf = match ModifyEvent::from_request(&mut audit, msg, &qs_write) {
|
||||
let mdf = match ModifyEvent::from_message(&mut audit, msg, &qs_write) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
audit_log!(audit, "Failed to begin modify: {:?}", e);
|
||||
|
@ -192,15 +252,15 @@ impl Handler<ModifyRequest> for QueryServerV1 {
|
|||
}
|
||||
}
|
||||
|
||||
impl Handler<DeleteRequest> for QueryServerV1 {
|
||||
impl Handler<DeleteMessage> for QueryServerV1 {
|
||||
type Result = Result<OperationResponse, OperationError>;
|
||||
|
||||
fn handle(&mut self, msg: DeleteRequest, _: &mut Self::Context) -> Self::Result {
|
||||
fn handle(&mut self, msg: DeleteMessage, _: &mut Self::Context) -> Self::Result {
|
||||
let mut audit = AuditScope::new("delete");
|
||||
let res = audit_segment!(&mut audit, || {
|
||||
let mut qs_write = self.qs.write();
|
||||
|
||||
let del = match DeleteEvent::from_request(&mut audit, msg, &qs_write) {
|
||||
let del = match DeleteEvent::from_message(&mut audit, msg, &qs_write) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
audit_log!(audit, "Failed to begin delete: {:?}", e);
|
||||
|
@ -285,6 +345,8 @@ impl Handler<WhoamiMessage> for QueryServerV1 {
|
|||
// trigger the failure, but if we can manage to work out async
|
||||
// then move this to core.rs, and don't allow Option<UAT> to get
|
||||
// this far.
|
||||
let uat = msg.uat.clone().ok_or(OperationError::NotAuthenticated)?;
|
||||
|
||||
let srch = match SearchEvent::from_whoami_request(&mut audit, msg.uat, &qs_read) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
|
@ -303,7 +365,7 @@ impl Handler<WhoamiMessage> for QueryServerV1 {
|
|||
1 => {
|
||||
let e = entries.pop().expect("Entry length mismatch!!!");
|
||||
// Now convert to a response, and return
|
||||
let wr = WhoamiResult::new(e);
|
||||
let wr = WhoamiResult::new(e, uat);
|
||||
Ok(wr.response())
|
||||
}
|
||||
// Somehow we matched multiple, which should be impossible.
|
||||
|
|
|
@ -32,7 +32,8 @@ impl Configuration {
|
|||
// log type
|
||||
// log path
|
||||
// TODO #63: default true in prd
|
||||
secure_cookies: if cfg!(test) { false } else { true },
|
||||
// secure_cookies: if cfg!(test) { false } else { true },
|
||||
secure_cookies: false,
|
||||
cookie_key: [0; 32],
|
||||
server_id: [0; 4],
|
||||
integration_test_config: None,
|
||||
|
|
|
@ -24,7 +24,7 @@ pub static JSON_ADMIN_V1: &'static str = r#"{
|
|||
},
|
||||
"state": null,
|
||||
"attrs": {
|
||||
"class": ["account", "object"],
|
||||
"class": ["account", "memberof", "object"],
|
||||
"name": ["admin"],
|
||||
"uuid": ["00000000-0000-0000-0000-000000000000"],
|
||||
"description": ["Builtin Admin account."],
|
||||
|
@ -80,7 +80,7 @@ pub static JSON_IDM_ADMINS_ACP_SEARCH_V1: &'static str = r#"{
|
|||
"acp_targetscope": [
|
||||
"{\"Pres\":\"class\"}"
|
||||
],
|
||||
"acp_search_attr": ["name", "class", "uuid"]
|
||||
"acp_search_attr": ["name", "class", "uuid", "description", "displayname"]
|
||||
}
|
||||
}"#;
|
||||
|
||||
|
@ -129,6 +129,40 @@ pub static JSON_IDM_SELF_ACP_READ_V1: &'static str = r#"{
|
|||
}
|
||||
}"#;
|
||||
|
||||
pub static _UUID_IDM_ADMINS_ACP_MANAGE_V1: &'static str = "00000000-0000-0000-0000-ffffff000005";
|
||||
pub static JSON_IDM_ADMINS_ACP_MANAGE_V1: &'static str = r#"{
|
||||
"valid": {
|
||||
"uuid": "00000000-0000-0000-0000-ffffff000005"
|
||||
},
|
||||
"state": null,
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"access_control_profile",
|
||||
"access_control_modify",
|
||||
"access_control_create",
|
||||
"access_control_delete",
|
||||
"access_control_search"
|
||||
],
|
||||
"name": ["idm_admins_acp_manage"],
|
||||
"uuid": ["00000000-0000-0000-0000-ffffff000005"],
|
||||
"description": ["Builtin IDM Administrators Access Controls to manage the install."],
|
||||
"acp_enable": ["true"],
|
||||
"acp_receiver": [
|
||||
"{\"Eq\":[\"memberof\",\"00000000-0000-0000-0000-000000000001\"]}"
|
||||
],
|
||||
"acp_targetscope": [
|
||||
"{\"Pres\":\"class\"}"
|
||||
],
|
||||
"acp_search_attr": ["name", "class", "uuid", "classname", "attributename"],
|
||||
"acp_modify_class": ["person"],
|
||||
"acp_modify_removedattr": ["class", "displayname", "name", "description"],
|
||||
"acp_modify_presentattr": ["class", "displayname", "name", "description"],
|
||||
"acp_create_class": ["object", "person", "account"],
|
||||
"acp_create_attr": ["name", "class", "description", "displayname"]
|
||||
}
|
||||
}"#;
|
||||
|
||||
pub static JSON_ANONYMOUS_V1: &'static str = r#"{
|
||||
"valid": {
|
||||
"uuid": "00000000-0000-0000-0000-ffffffffffff"
|
||||
|
|
|
@ -13,7 +13,9 @@ use crate::config::Configuration;
|
|||
|
||||
// SearchResult
|
||||
use crate::actors::v1::QueryServerV1;
|
||||
use crate::actors::v1::{AuthMessage, WhoamiMessage};
|
||||
use crate::actors::v1::{
|
||||
AuthMessage, CreateMessage, DeleteMessage, ModifyMessage, SearchMessage, WhoamiMessage,
|
||||
};
|
||||
use crate::async_log;
|
||||
use crate::audit::AuditScope;
|
||||
use crate::be::{Backend, BackendTransaction};
|
||||
|
@ -46,12 +48,15 @@ fn get_current_user(req: &HttpRequest<AppState>) -> Option<UserAuthToken> {
|
|||
}
|
||||
|
||||
macro_rules! json_event_post {
|
||||
($req:expr, $state:expr, $event_type:ty, $message_type:ty) => {{
|
||||
($req:expr, $state:expr, $message_type:ty, $request_type:ty) => {{
|
||||
// This is copied every request. Is there a better way?
|
||||
// The issue is the fold move takes ownership of state if
|
||||
// we don't copy this here
|
||||
let max_size = $state.max_size;
|
||||
|
||||
// Get auth if any?
|
||||
let uat = get_current_user(&$req);
|
||||
|
||||
// HttpRequest::payload() is stream of Bytes objects
|
||||
$req.payload()
|
||||
// `Future::from_err` acts like `?` in that it coerces the error type from
|
||||
|
@ -73,20 +78,17 @@ macro_rules! json_event_post {
|
|||
.and_then(
|
||||
move |body| -> Box<dyn Future<Item = HttpResponse, Error = Error>> {
|
||||
// body is loaded, now we can deserialize serde-json
|
||||
// let r_obj = serde_json::from_slice::<SearchRequest>(&body);
|
||||
let r_obj = serde_json::from_slice::<$message_type>(&body);
|
||||
let r_obj = serde_json::from_slice::<$request_type>(&body);
|
||||
|
||||
// Send to the db for handling
|
||||
match r_obj {
|
||||
Ok(obj) => {
|
||||
// combine request + uat -> message.
|
||||
let m_obj = <($message_type)>::new(uat, obj);
|
||||
let res = $state
|
||||
.qe
|
||||
.send(
|
||||
// Could make this a .into_inner() and move?
|
||||
// event::SearchEvent::new(obj.filter),
|
||||
// <($event_type)>::from_request(obj),
|
||||
obj,
|
||||
)
|
||||
.send(m_obj)
|
||||
// What is from_err?
|
||||
.from_err()
|
||||
.and_then(|res| match res {
|
||||
Ok(event_result) => Ok(HttpResponse::Ok().json(event_result)),
|
||||
|
@ -106,7 +108,7 @@ macro_rules! json_event_post {
|
|||
}
|
||||
|
||||
macro_rules! json_event_get {
|
||||
($req:expr, $state:expr, $event_type:ty, $message_type:ty) => {{
|
||||
($req:expr, $state:expr, $message_type:ty) => {{
|
||||
// Get current auth data - remember, the QS checks if the
|
||||
// none/some is okay, because it's too hard to make it work here
|
||||
// with all the async parts.
|
||||
|
@ -132,32 +134,31 @@ macro_rules! json_event_get {
|
|||
fn create(
|
||||
(req, state): (HttpRequest<AppState>, State<AppState>),
|
||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||
json_event_post!(req, state, CreateEvent, CreateRequest)
|
||||
json_event_post!(req, state, CreateMessage, CreateRequest)
|
||||
}
|
||||
|
||||
fn modify(
|
||||
(req, state): (HttpRequest<AppState>, State<AppState>),
|
||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||
json_event_post!(req, state, ModifyEvent, ModifyRequest)
|
||||
json_event_post!(req, state, ModifyMessage, ModifyRequest)
|
||||
}
|
||||
|
||||
fn delete(
|
||||
(req, state): (HttpRequest<AppState>, State<AppState>),
|
||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||
json_event_post!(req, state, DeleteEvent, DeleteRequest)
|
||||
json_event_post!(req, state, DeleteMessage, DeleteRequest)
|
||||
}
|
||||
|
||||
fn search(
|
||||
(req, state): (HttpRequest<AppState>, State<AppState>),
|
||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||
json_event_post!(req, state, SearchEvent, SearchRequest)
|
||||
json_event_post!(req, state, SearchMessage, SearchRequest)
|
||||
}
|
||||
|
||||
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)
|
||||
json_event_get!(req, state, WhoamiMessage)
|
||||
}
|
||||
|
||||
// We probably need an extract auth or similar to handle the different
|
||||
|
|
|
@ -3,8 +3,8 @@ use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryReduced,
|
|||
use crate::filter::{Filter, FilterValid};
|
||||
use rsidm_proto::v1::Entry as ProtoEntry;
|
||||
use rsidm_proto::v1::{
|
||||
AuthCredential, AuthResponse, AuthState, AuthStep, CreateRequest, DeleteRequest, ModifyRequest,
|
||||
ReviveRecycledRequest, SearchRequest, SearchResponse, UserAuthToken, WhoamiResponse,
|
||||
AuthCredential, AuthResponse, AuthState, AuthStep, SearchResponse, UserAuthToken,
|
||||
WhoamiResponse,
|
||||
};
|
||||
// use error::OperationError;
|
||||
use crate::modify::{ModifyList, ModifyValid};
|
||||
|
@ -13,7 +13,7 @@ use crate::server::{
|
|||
};
|
||||
use rsidm_proto::v1::OperationError;
|
||||
|
||||
use crate::actors::v1::AuthMessage;
|
||||
use crate::actors::v1::{AuthMessage, CreateMessage, DeleteMessage, ModifyMessage, SearchMessage};
|
||||
// Bring in schematransaction trait for validate
|
||||
// use crate::schema::SchemaTransaction;
|
||||
|
||||
|
@ -22,8 +22,6 @@ use crate::actors::v1::AuthMessage;
|
|||
use crate::filter::FilterInvalid;
|
||||
#[cfg(test)]
|
||||
use crate::modify::ModifyInvalid;
|
||||
#[cfg(test)]
|
||||
use rsidm_proto::v1::SearchRecycledRequest;
|
||||
|
||||
use actix::prelude::*;
|
||||
use uuid::Uuid;
|
||||
|
@ -120,6 +118,27 @@ impl Event {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn from_rw_uat(
|
||||
audit: &mut AuditScope,
|
||||
qs: &QueryServerWriteTransaction,
|
||||
uat: Option<UserAuthToken>,
|
||||
) -> Result<Self, OperationError> {
|
||||
audit_log!(audit, "from_rw_uat -> {:?}", uat);
|
||||
let uat = uat.ok_or(OperationError::NotAuthenticated)?;
|
||||
let u = try_audit!(
|
||||
audit,
|
||||
Uuid::parse_str(uat.uuid.as_str()).map_err(|_| OperationError::InvalidUuid)
|
||||
);
|
||||
|
||||
let e = try_audit!(audit, qs.internal_search_uuid(audit, &u));
|
||||
// TODO #64: Now apply claims from the uat into the Entry
|
||||
// to allow filtering.
|
||||
|
||||
Ok(Event {
|
||||
origin: EventOrigin::User(e),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_rw_request(
|
||||
audit: &mut AuditScope,
|
||||
qs: &QueryServerWriteTransaction,
|
||||
|
@ -186,14 +205,14 @@ pub struct SearchEvent {
|
|||
}
|
||||
|
||||
impl SearchEvent {
|
||||
pub fn from_request(
|
||||
pub fn from_message(
|
||||
audit: &mut AuditScope,
|
||||
request: SearchRequest,
|
||||
msg: SearchMessage,
|
||||
qs: &QueryServerReadTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
match Filter::from_ro(audit, &request.filter, qs) {
|
||||
match Filter::from_ro(audit, &msg.req.filter, qs) {
|
||||
Ok(f) => Ok(SearchEvent {
|
||||
event: Event::from_ro_request(audit, qs, request.user_uuid.as_str())?,
|
||||
event: Event::from_ro_uat(audit, qs, msg.uat)?,
|
||||
// We do need to do this twice to account for the ignore_hidden
|
||||
// changes.
|
||||
filter: f
|
||||
|
@ -259,6 +278,7 @@ impl SearchEvent {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)]
|
||||
pub fn from_rec_request(
|
||||
|
@ -268,7 +288,7 @@ impl SearchEvent {
|
|||
) -> Result<Self, OperationError> {
|
||||
match Filter::from_ro(audit, &request.filter, qs) {
|
||||
Ok(f) => Ok(SearchEvent {
|
||||
event: Event::from_ro_request(audit, qs, request.user_uuid.as_str())?,
|
||||
event: Event::from_ro_uat(audit, qs, msg.uat)?,
|
||||
filter: f
|
||||
.clone()
|
||||
.to_recycled()
|
||||
|
@ -281,6 +301,7 @@ impl SearchEvent {
|
|||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
#[cfg(test)]
|
||||
/* Impersonate a request for recycled objects */
|
||||
|
@ -340,12 +361,13 @@ pub struct CreateEvent {
|
|||
}
|
||||
|
||||
impl CreateEvent {
|
||||
pub fn from_request(
|
||||
pub fn from_message(
|
||||
audit: &mut AuditScope,
|
||||
request: CreateRequest,
|
||||
msg: CreateMessage,
|
||||
qs: &QueryServerWriteTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
let rentries: Result<Vec<_>, _> = request
|
||||
let rentries: Result<Vec<_>, _> = msg
|
||||
.req
|
||||
.entries
|
||||
.iter()
|
||||
.map(|e| Entry::from_proto_entry(audit, e, qs))
|
||||
|
@ -355,7 +377,7 @@ impl CreateEvent {
|
|||
// From ProtoEntry -> Entry
|
||||
// What is the correct consuming iterator here? Can we
|
||||
// even do that?
|
||||
event: Event::from_rw_request(audit, qs, request.user_uuid.as_str())?,
|
||||
event: Event::from_rw_uat(audit, qs, msg.uat)?,
|
||||
entries: entries,
|
||||
}),
|
||||
Err(e) => Err(e),
|
||||
|
@ -421,14 +443,14 @@ pub struct DeleteEvent {
|
|||
}
|
||||
|
||||
impl DeleteEvent {
|
||||
pub fn from_request(
|
||||
pub fn from_message(
|
||||
audit: &mut AuditScope,
|
||||
request: DeleteRequest,
|
||||
msg: DeleteMessage,
|
||||
qs: &QueryServerWriteTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
match Filter::from_rw(audit, &request.filter, qs) {
|
||||
match Filter::from_rw(audit, &msg.req.filter, qs) {
|
||||
Ok(f) => Ok(DeleteEvent {
|
||||
event: Event::from_rw_request(audit, qs, request.user_uuid.as_str())?,
|
||||
event: Event::from_rw_uat(audit, qs, msg.uat)?,
|
||||
filter: f
|
||||
.clone()
|
||||
.to_ignore_hidden()
|
||||
|
@ -442,6 +464,18 @@ impl DeleteEvent {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub unsafe fn new_impersonate_entry(
|
||||
e: Entry<EntryValid, EntryCommitted>,
|
||||
filter: Filter<FilterInvalid>,
|
||||
) -> Self {
|
||||
DeleteEvent {
|
||||
event: Event::from_impersonate_entry(e),
|
||||
filter: filter.clone().to_valid(),
|
||||
filter_orig: filter.to_valid(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub unsafe fn new_impersonate_entry_ser(e: &str, filter: Filter<FilterInvalid>) -> Self {
|
||||
DeleteEvent {
|
||||
|
@ -480,15 +514,15 @@ pub struct ModifyEvent {
|
|||
}
|
||||
|
||||
impl ModifyEvent {
|
||||
pub fn from_request(
|
||||
pub fn from_message(
|
||||
audit: &mut AuditScope,
|
||||
request: ModifyRequest,
|
||||
msg: ModifyMessage,
|
||||
qs: &QueryServerWriteTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
match Filter::from_rw(audit, &request.filter, qs) {
|
||||
Ok(f) => match ModifyList::from(audit, &request.modlist, qs) {
|
||||
match Filter::from_rw(audit, &msg.req.filter, qs) {
|
||||
Ok(f) => match ModifyList::from(audit, &msg.req.modlist, qs) {
|
||||
Ok(m) => Ok(ModifyEvent {
|
||||
event: Event::from_rw_request(audit, qs, request.user_uuid.as_str())?,
|
||||
event: Event::from_rw_uat(audit, qs, msg.uat)?,
|
||||
filter: f
|
||||
.clone()
|
||||
.to_ignore_hidden()
|
||||
|
@ -544,6 +578,20 @@ impl ModifyEvent {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub unsafe fn new_impersonate_entry(
|
||||
e: Entry<EntryValid, EntryCommitted>,
|
||||
filter: Filter<FilterInvalid>,
|
||||
modlist: ModifyList<ModifyInvalid>,
|
||||
) -> Self {
|
||||
ModifyEvent {
|
||||
event: Event::from_impersonate_entry(e),
|
||||
filter: filter.clone().to_valid(),
|
||||
filter_orig: filter.to_valid(),
|
||||
modlist: modlist.to_valid(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_impersonate(
|
||||
event: &Event,
|
||||
filter: Filter<FilterValid>,
|
||||
|
@ -703,18 +751,21 @@ impl AuthResult {
|
|||
|
||||
pub struct WhoamiResult {
|
||||
youare: ProtoEntry,
|
||||
uat: UserAuthToken,
|
||||
}
|
||||
|
||||
impl WhoamiResult {
|
||||
pub fn new(e: Entry<EntryReduced, EntryCommitted>) -> Self {
|
||||
pub fn new(e: Entry<EntryReduced, EntryCommitted>, uat: UserAuthToken) -> Self {
|
||||
WhoamiResult {
|
||||
youare: e.into_pe(),
|
||||
uat: uat,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn response(self) -> WhoamiResponse {
|
||||
WhoamiResponse {
|
||||
youare: self.youare,
|
||||
uat: self.uat,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -769,14 +820,15 @@ impl Message for ReviveRecycledEvent {
|
|||
}
|
||||
|
||||
impl ReviveRecycledEvent {
|
||||
pub fn from_request(
|
||||
/*
|
||||
pub fn from_message(
|
||||
audit: &mut AuditScope,
|
||||
request: ReviveRecycledRequest,
|
||||
msg: ReviveRecycledMessage,
|
||||
qs: &QueryServerWriteTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
match Filter::from_rw(audit, &request.filter, qs) {
|
||||
match Filter::from_rw(audit, &msg.req.filter, qs) {
|
||||
Ok(f) => Ok(ReviveRecycledEvent {
|
||||
event: Event::from_rw_request(audit, qs, request.user_uuid.as_str())?,
|
||||
event: Event::from_rw_uat(audit, qs, msg.uat)?,
|
||||
filter: f
|
||||
.to_recycled()
|
||||
.validate(qs.get_schema())
|
||||
|
@ -785,4 +837,16 @@ impl ReviveRecycledEvent {
|
|||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
#[cfg(test)]
|
||||
pub unsafe fn new_impersonate_entry(
|
||||
e: Entry<EntryValid, EntryCommitted>,
|
||||
filter: Filter<FilterInvalid>,
|
||||
) -> Self {
|
||||
ReviveRecycledEvent {
|
||||
event: Event::from_impersonate_entry(e),
|
||||
filter: filter.to_valid(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// #![deny(warnings)]
|
||||
#![deny(warnings)]
|
||||
#![warn(unused_extern_crates)]
|
||||
|
||||
#[macro_use]
|
||||
|
|
|
@ -25,7 +25,11 @@ lazy_static! {
|
|||
m
|
||||
};
|
||||
static ref PVCLASS_SYSTEM: PartialValue = PartialValue::new_class("system");
|
||||
static ref PVCLASS_TOMBSTONE: PartialValue = PartialValue::new_class("tombstone");
|
||||
static ref PVCLASS_RECYCLED: PartialValue = PartialValue::new_class("recycled");
|
||||
static ref VCLASS_SYSTEM: Value = Value::new_class("system");
|
||||
static ref VCLASS_TOMBSTONE: Value = Value::new_class("tombstone");
|
||||
static ref VCLASS_RECYCLED: Value = Value::new_class("recycled");
|
||||
}
|
||||
|
||||
impl Plugin for Protected {
|
||||
|
@ -51,7 +55,10 @@ impl Plugin for Protected {
|
|||
cand.iter().fold(Ok(()), |acc, cand| match acc {
|
||||
Err(_) => acc,
|
||||
Ok(_) => {
|
||||
if cand.attribute_value_pres("class", &PVCLASS_SYSTEM) {
|
||||
if cand.attribute_value_pres("class", &PVCLASS_SYSTEM)
|
||||
|| cand.attribute_value_pres("class", &PVCLASS_TOMBSTONE)
|
||||
|| cand.attribute_value_pres("class", &PVCLASS_RECYCLED)
|
||||
{
|
||||
Err(OperationError::SystemProtectedObject)
|
||||
} else {
|
||||
acc
|
||||
|
@ -74,7 +81,7 @@ impl Plugin for Protected {
|
|||
);
|
||||
return Ok(());
|
||||
}
|
||||
// Prevent adding class: system
|
||||
// Prevent adding class: system, tombstone, or recycled.
|
||||
me.modlist.iter().fold(Ok(()), |acc, m| {
|
||||
if acc.is_err() {
|
||||
acc
|
||||
|
@ -82,7 +89,11 @@ impl Plugin for Protected {
|
|||
match m {
|
||||
Modify::Present(a, v) => {
|
||||
// TODO: Can we avoid this clone?
|
||||
if a == "class" && v == &(VCLASS_SYSTEM.clone()) {
|
||||
if a == "class"
|
||||
&& (v == &(VCLASS_SYSTEM.clone())
|
||||
|| v == &(VCLASS_TOMBSTONE.clone())
|
||||
|| v == &(VCLASS_RECYCLED.clone()))
|
||||
{
|
||||
Err(OperationError::SystemProtectedObject)
|
||||
} else {
|
||||
Ok(())
|
||||
|
@ -92,8 +103,22 @@ impl Plugin for Protected {
|
|||
}
|
||||
}
|
||||
})?;
|
||||
// if class: system, check the mods are "allowed"
|
||||
|
||||
// HARD block mods on tombstone or recycle.
|
||||
cand.iter().fold(Ok(()), |acc, cand| match acc {
|
||||
Err(_) => acc,
|
||||
Ok(_) => {
|
||||
if cand.attribute_value_pres("class", &PVCLASS_TOMBSTONE)
|
||||
|| cand.attribute_value_pres("class", &PVCLASS_RECYCLED)
|
||||
{
|
||||
Err(OperationError::SystemProtectedObject)
|
||||
} else {
|
||||
acc
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
// if class: system, check the mods are "allowed"
|
||||
let system_pres = cand.iter().fold(false, |acc, c| {
|
||||
if acc {
|
||||
acc
|
||||
|
@ -145,7 +170,10 @@ impl Plugin for Protected {
|
|||
cand.iter().fold(Ok(()), |acc, cand| match acc {
|
||||
Err(_) => acc,
|
||||
Ok(_) => {
|
||||
if cand.attribute_value_pres("class", &PVCLASS_SYSTEM) {
|
||||
if cand.attribute_value_pres("class", &PVCLASS_SYSTEM)
|
||||
|| cand.attribute_value_pres("class", &PVCLASS_TOMBSTONE)
|
||||
|| cand.attribute_value_pres("class", &PVCLASS_RECYCLED)
|
||||
{
|
||||
Err(OperationError::SystemProtectedObject)
|
||||
} else {
|
||||
acc
|
||||
|
|
|
@ -13,11 +13,11 @@ use crate::access::{
|
|||
AccessControlsWriteTransaction,
|
||||
};
|
||||
use crate::constants::{
|
||||
JSON_ADMIN_V1, JSON_ANONYMOUS_V1, JSON_IDM_ADMINS_ACP_REVIVE_V1, JSON_IDM_ADMINS_ACP_SEARCH_V1,
|
||||
JSON_IDM_ADMINS_V1, JSON_IDM_SELF_ACP_READ_V1, JSON_SCHEMA_ATTR_DISPLAYNAME,
|
||||
JSON_SCHEMA_ATTR_MAIL, JSON_SCHEMA_ATTR_PRIMARY_CREDENTIAL, JSON_SCHEMA_ATTR_SSH_PUBLICKEY,
|
||||
JSON_SCHEMA_CLASS_ACCOUNT, JSON_SCHEMA_CLASS_GROUP, JSON_SCHEMA_CLASS_PERSON,
|
||||
JSON_SYSTEM_INFO_V1, UUID_DOES_NOT_EXIST,
|
||||
JSON_ADMIN_V1, JSON_ANONYMOUS_V1, JSON_IDM_ADMINS_ACP_MANAGE_V1, JSON_IDM_ADMINS_ACP_REVIVE_V1,
|
||||
JSON_IDM_ADMINS_ACP_SEARCH_V1, JSON_IDM_ADMINS_V1, JSON_IDM_SELF_ACP_READ_V1,
|
||||
JSON_SCHEMA_ATTR_DISPLAYNAME, JSON_SCHEMA_ATTR_MAIL, JSON_SCHEMA_ATTR_PRIMARY_CREDENTIAL,
|
||||
JSON_SCHEMA_ATTR_SSH_PUBLICKEY, JSON_SCHEMA_CLASS_ACCOUNT, JSON_SCHEMA_CLASS_GROUP,
|
||||
JSON_SCHEMA_CLASS_PERSON, JSON_SYSTEM_INFO_V1, UUID_DOES_NOT_EXIST,
|
||||
};
|
||||
use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryReduced, EntryValid};
|
||||
use crate::event::{
|
||||
|
@ -944,6 +944,11 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
if ts.len() == 0 {
|
||||
audit_log!(au, "No Tombstones present - purge operation success");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// TODO #68: Has an appropriate amount of time/condition past (ie replication events?)
|
||||
|
||||
// Delete them
|
||||
|
@ -975,6 +980,11 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
if rc.len() == 0 {
|
||||
audit_log!(au, "No recycled present - purge operation success");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Modify them to strip all avas except uuid
|
||||
let tombstone_cand = rc.iter().map(|e| e.to_tombstone()).collect();
|
||||
|
||||
|
@ -1514,6 +1524,9 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
.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_ADMINS_ACP_MANAGE_V1)
|
||||
})
|
||||
.and_then(|_| {
|
||||
self.internal_migrate_or_create_str(&mut audit_an, JSON_IDM_SELF_ACP_READ_V1)
|
||||
});
|
||||
|
@ -1689,17 +1702,13 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::constants::{JSON_ADMIN_V1, STR_UUID_ADMIN, UUID_ADMIN};
|
||||
use crate::constants::{JSON_ADMIN_V1, UUID_ADMIN};
|
||||
use crate::credential::Credential;
|
||||
use crate::entry::{Entry, EntryInvalid, EntryNew};
|
||||
use crate::event::{CreateEvent, DeleteEvent, ModifyEvent, ReviveRecycledEvent, SearchEvent};
|
||||
use crate::modify::{Modify, ModifyList};
|
||||
use crate::server::QueryServerTransaction;
|
||||
use crate::value::{PartialValue, Value};
|
||||
use rsidm_proto::v1::Filter as ProtoFilter;
|
||||
use rsidm_proto::v1::Modify as ProtoModify;
|
||||
use rsidm_proto::v1::ModifyList as ProtoModifyList;
|
||||
use rsidm_proto::v1::{DeleteRequest, ModifyRequest, ReviveRecycledRequest};
|
||||
use rsidm_proto::v1::{OperationError, SchemaError};
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -2079,31 +2088,23 @@ mod tests {
|
|||
.internal_search_uuid(audit, &UUID_ADMIN)
|
||||
.expect("failed");
|
||||
|
||||
let filt_ts = ProtoFilter::Eq("class".to_string(), "tombstone".to_string());
|
||||
|
||||
let filt_i_ts = filter_all!(f_eq("class", PartialValue::new_class("tombstone")));
|
||||
|
||||
// Create fake external requests. Probably from admin later
|
||||
// Should we do this with impersonate instead of using the external
|
||||
let me_ts = ModifyEvent::from_request(
|
||||
audit,
|
||||
ModifyRequest::new(
|
||||
filt_ts.clone(),
|
||||
ProtoModifyList::new_list(vec![ProtoModify::Present(
|
||||
let me_ts = unsafe {
|
||||
ModifyEvent::new_impersonate_entry(
|
||||
admin.clone(),
|
||||
filt_i_ts.clone(),
|
||||
ModifyList::new_list(vec![Modify::Present(
|
||||
"class".to_string(),
|
||||
"tombstone".to_string(),
|
||||
Value::new_class("tombstone"),
|
||||
)]),
|
||||
STR_UUID_ADMIN,
|
||||
),
|
||||
&server_txn,
|
||||
)
|
||||
.expect("modify event create failed");
|
||||
let de_ts = DeleteEvent::from_request(
|
||||
audit,
|
||||
DeleteRequest::new(filt_ts.clone(), STR_UUID_ADMIN),
|
||||
&server_txn,
|
||||
)
|
||||
.expect("delete event create failed");
|
||||
};
|
||||
|
||||
let de_ts =
|
||||
unsafe { DeleteEvent::new_impersonate_entry(admin.clone(), filt_i_ts.clone()) };
|
||||
let se_ts = unsafe { SearchEvent::new_ext_impersonate_entry(admin, filt_i_ts.clone()) };
|
||||
|
||||
// First, create a tombstone
|
||||
|
@ -2163,8 +2164,6 @@ mod tests {
|
|||
.internal_search_uuid(audit, &UUID_ADMIN)
|
||||
.expect("failed");
|
||||
|
||||
let filt_rc = ProtoFilter::Eq("class".to_string(), "recycled".to_string());
|
||||
|
||||
let filt_i_rc = filter_all!(f_eq("class", PartialValue::new_class("recycled")));
|
||||
|
||||
let filt_i_ts = filter_all!(f_eq("class", PartialValue::new_class("tombstone")));
|
||||
|
@ -2172,40 +2171,32 @@ mod tests {
|
|||
let filt_i_per = filter_all!(f_eq("class", PartialValue::new_class("person")));
|
||||
|
||||
// Create fake external requests. Probably from admin later
|
||||
let me_rc = ModifyEvent::from_request(
|
||||
audit,
|
||||
ModifyRequest::new(
|
||||
filt_rc.clone(),
|
||||
ProtoModifyList::new_list(vec![ProtoModify::Present(
|
||||
let me_rc = unsafe {
|
||||
ModifyEvent::new_impersonate_entry(
|
||||
admin.clone(),
|
||||
filt_i_rc.clone(),
|
||||
ModifyList::new_list(vec![Modify::Present(
|
||||
"class".to_string(),
|
||||
"recycled".to_string(),
|
||||
Value::new_class("recycled"),
|
||||
)]),
|
||||
STR_UUID_ADMIN,
|
||||
),
|
||||
&server_txn,
|
||||
)
|
||||
.expect("modify event create failed");
|
||||
let de_rc = DeleteEvent::from_request(
|
||||
audit,
|
||||
DeleteRequest::new(filt_rc.clone(), STR_UUID_ADMIN),
|
||||
&server_txn,
|
||||
)
|
||||
.expect("delete event create failed");
|
||||
};
|
||||
|
||||
let de_rc =
|
||||
unsafe { DeleteEvent::new_impersonate_entry(admin.clone(), filt_i_rc.clone()) };
|
||||
|
||||
let se_rc =
|
||||
unsafe { SearchEvent::new_ext_impersonate_entry(admin.clone(), filt_i_rc.clone()) };
|
||||
|
||||
let sre_rc =
|
||||
unsafe { SearchEvent::new_rec_impersonate_entry(admin, filt_i_rc.clone()) };
|
||||
unsafe { SearchEvent::new_rec_impersonate_entry(admin.clone(), filt_i_rc.clone()) };
|
||||
|
||||
let rre_rc = ReviveRecycledEvent::from_request(
|
||||
audit,
|
||||
ReviveRecycledRequest::new(
|
||||
ProtoFilter::Eq("name".to_string(), "testperson1".to_string()),
|
||||
STR_UUID_ADMIN,
|
||||
),
|
||||
&server_txn,
|
||||
let rre_rc = unsafe {
|
||||
ReviveRecycledEvent::new_impersonate_entry(
|
||||
admin,
|
||||
filter_all!(f_eq("name", PartialValue::new_iutf8s("testperson1"))),
|
||||
)
|
||||
.expect("revive recycled create failed");
|
||||
};
|
||||
|
||||
// Create some recycled objects
|
||||
let e1: Entry<EntryInvalid, EntryNew> = Entry::unsafe_from_entry_str(
|
||||
|
|
Loading…
Reference in a new issue