mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
Working modify internal and migrations
This commit is contained in:
parent
764695db4a
commit
3b3f5dc6d5
|
@ -156,7 +156,7 @@ impl Drop for BackendTransaction {
|
||||||
impl BackendTransaction {
|
impl BackendTransaction {
|
||||||
pub fn new(conn: r2d2::PooledConnection<SqliteConnectionManager>) -> Self {
|
pub fn new(conn: r2d2::PooledConnection<SqliteConnectionManager>) -> Self {
|
||||||
// Start the transaction
|
// Start the transaction
|
||||||
println!("Starting txn ...");
|
println!("Starting RO txn ...");
|
||||||
// TODO: Way to flag that this will be a read?
|
// TODO: Way to flag that this will be a read?
|
||||||
conn.execute("BEGIN TRANSACTION", NO_PARAMS).unwrap();
|
conn.execute("BEGIN TRANSACTION", NO_PARAMS).unwrap();
|
||||||
BackendTransaction {
|
BackendTransaction {
|
||||||
|
@ -196,7 +196,7 @@ impl BackendReadTransaction for BackendWriteTransaction {
|
||||||
impl BackendWriteTransaction {
|
impl BackendWriteTransaction {
|
||||||
pub fn new(conn: r2d2::PooledConnection<SqliteConnectionManager>) -> Self {
|
pub fn new(conn: r2d2::PooledConnection<SqliteConnectionManager>) -> Self {
|
||||||
// Start the transaction
|
// Start the transaction
|
||||||
println!("Starting txn ...");
|
println!("Starting WR txn ...");
|
||||||
// TODO: Way to flag that this will be a write?
|
// TODO: Way to flag that this will be a write?
|
||||||
conn.execute("BEGIN TRANSACTION", NO_PARAMS).unwrap();
|
conn.execute("BEGIN TRANSACTION", NO_PARAMS).unwrap();
|
||||||
BackendWriteTransaction {
|
BackendWriteTransaction {
|
||||||
|
@ -273,8 +273,51 @@ impl BackendWriteTransaction {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn modify() -> Result<(), BackendError> {
|
pub fn modify(&self, au: &mut AuditScope, entries: &Vec<Entry>) -> Result<(), BackendError> {
|
||||||
unimplemented!()
|
if entries.is_empty() {
|
||||||
|
// TODO: Better error
|
||||||
|
return Err(BackendError::EmptyRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert the Id's exist on the entry, and serialise them.
|
||||||
|
let ser_entries: Vec<IdEntry> = entries
|
||||||
|
.iter()
|
||||||
|
.filter_map(|e| {
|
||||||
|
match e.id {
|
||||||
|
Some(id) => {
|
||||||
|
Some(IdEntry {
|
||||||
|
id: id,
|
||||||
|
// TODO: Should we do better than unwrap?
|
||||||
|
data: serde_json::to_string(&e).unwrap(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
None => None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
audit_log!(au, "serialising: {:?}", ser_entries);
|
||||||
|
|
||||||
|
// Simple: If the list of id's is not the same as the input list, we are missing id's
|
||||||
|
// TODO: This check won't be needed once I rebuild the entry state types.
|
||||||
|
if entries.len() != ser_entries.len() {
|
||||||
|
return Err(BackendError::EntryMissingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, given the list of id's, update them
|
||||||
|
{
|
||||||
|
// TODO: ACTUALLY HANDLE THIS ERROR WILLIAM YOU LAZY SHIT.
|
||||||
|
let mut stmt = self.conn.prepare("UPDATE id2entry SET data = :data WHERE id = :id").unwrap();
|
||||||
|
|
||||||
|
ser_entries.iter().for_each(|ser_ent| {
|
||||||
|
stmt.execute_named(&[
|
||||||
|
(":id", &ser_ent.id),
|
||||||
|
(":data", &ser_ent.data),
|
||||||
|
]).unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(&self, au: &mut AuditScope, entries: &Vec<Entry>) -> Result<(), BackendError> {
|
pub fn delete(&self, au: &mut AuditScope, entries: &Vec<Entry>) -> Result<(), BackendError> {
|
||||||
|
@ -282,7 +325,6 @@ impl BackendWriteTransaction {
|
||||||
|
|
||||||
if entries.is_empty() {
|
if entries.is_empty() {
|
||||||
// TODO: Better error
|
// TODO: Better error
|
||||||
// End the timer
|
|
||||||
return Err(BackendError::EmptyRequest);
|
return Err(BackendError::EmptyRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -294,12 +336,11 @@ impl BackendWriteTransaction {
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Simple: If the list of id's is not the same as the input list, we are missing id's
|
// Simple: If the list of id's is not the same as the input list, we are missing id's
|
||||||
|
// TODO: This check won't be needed once I rebuild the entry state types.
|
||||||
if entries.len() != id_list.len() {
|
if entries.len() != id_list.len() {
|
||||||
return Err(BackendError::EntryMissingId);
|
return Err(BackendError::EntryMissingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Now, given the list of id's, delete them.
|
// Now, given the list of id's, delete them.
|
||||||
{
|
{
|
||||||
// SQL doesn't say if the thing "does or does not exist anymore". As a result,
|
// SQL doesn't say if the thing "does or does not exist anymore". As a result,
|
||||||
|
@ -521,6 +562,19 @@ mod tests {
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! entry_attr_pres {
|
||||||
|
($audit:expr, $be:expr, $ent:expr, $attr:expr) => {{
|
||||||
|
let filt = $ent.filter_from_attrs(&vec![String::from("userid")]).unwrap();
|
||||||
|
let entries = $be.search($audit, &filt).unwrap();
|
||||||
|
match entries.first() {
|
||||||
|
Some(ent) => {
|
||||||
|
ent.attribute_pres($attr)
|
||||||
|
}
|
||||||
|
None => false
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_simple_create() {
|
fn test_simple_create() {
|
||||||
run_test!(|audit: &mut AuditScope, be: &BackendWriteTransaction| {
|
run_test!(|audit: &mut AuditScope, be: &BackendWriteTransaction| {
|
||||||
|
@ -553,6 +607,44 @@ mod tests {
|
||||||
fn test_simple_modify() {
|
fn test_simple_modify() {
|
||||||
run_test!(|audit: &mut AuditScope, be: &BackendWriteTransaction| {
|
run_test!(|audit: &mut AuditScope, be: &BackendWriteTransaction| {
|
||||||
audit_log!(audit, "Simple Modify");
|
audit_log!(audit, "Simple Modify");
|
||||||
|
// First create some entries (3?)
|
||||||
|
let mut e1: Entry = Entry::new();
|
||||||
|
e1.add_ava(String::from("userid"), String::from("william"));
|
||||||
|
|
||||||
|
let mut e2: Entry = Entry::new();
|
||||||
|
e2.add_ava(String::from("userid"), String::from("alice"));
|
||||||
|
|
||||||
|
assert!(be.create(audit, &vec![e1.clone(), e2.clone()]).is_ok());
|
||||||
|
assert!(entry_exists!(audit, be, e1));
|
||||||
|
assert!(entry_exists!(audit, be, e2));
|
||||||
|
|
||||||
|
// You need to now retrieve the entries back out to get the entry id's
|
||||||
|
let mut results = be.search(audit, &Filter::Pres(String::from("userid"))).unwrap();
|
||||||
|
|
||||||
|
// Get these out to usable entries.
|
||||||
|
let mut r1 = results.remove(0);
|
||||||
|
let mut r2 = results.remove(0);
|
||||||
|
|
||||||
|
// Modify no id (err)
|
||||||
|
assert!(be.modify(audit, &vec![e1.clone()]).is_err());
|
||||||
|
// Modify none
|
||||||
|
assert!(be.modify(audit, &vec![]).is_err());
|
||||||
|
|
||||||
|
// Make some changes to r1, r2.
|
||||||
|
r1.add_ava(String::from("desc"), String::from("modified"));
|
||||||
|
r2.add_ava(String::from("desc"), String::from("modified"));
|
||||||
|
|
||||||
|
// Modify single
|
||||||
|
assert!(be.modify(audit, &vec![r1.clone()]).is_ok());
|
||||||
|
// Assert no other changes
|
||||||
|
assert!(entry_attr_pres!(audit, be, r1, "desc"));
|
||||||
|
assert!(! entry_attr_pres!(audit, be, r2, "desc"));
|
||||||
|
|
||||||
|
// Modify both
|
||||||
|
assert!(be.modify(audit, &vec![r1.clone(), r2.clone()]).is_ok());
|
||||||
|
|
||||||
|
assert!(entry_attr_pres!(audit, be, r1, "desc"));
|
||||||
|
assert!(entry_attr_pres!(audit, be, r2, "desc"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
131
src/lib/entry.rs
131
src/lib/entry.rs
|
@ -4,6 +4,8 @@ use filter::Filter;
|
||||||
use std::collections::btree_map::{Iter as BTreeIter, IterMut as BTreeIterMut};
|
use std::collections::btree_map::{Iter as BTreeIter, IterMut as BTreeIterMut};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::slice::Iter as SliceIter;
|
use std::slice::Iter as SliceIter;
|
||||||
|
use modify::{Modify, ModifyList};
|
||||||
|
use schema::SchemaReadTransaction;
|
||||||
|
|
||||||
// make a trait entry for everything to adhere to?
|
// make a trait entry for everything to adhere to?
|
||||||
// * How to get indexs out?
|
// * How to get indexs out?
|
||||||
|
@ -94,9 +96,23 @@ impl<'a> Iterator for EntryAvasMut<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a BE concept, so move it there!
|
// This is a BE concept, so move it there!
|
||||||
|
|
||||||
|
// Entry should have a lifecycle of types. THis is Raw (modifiable) and Entry (verified).
|
||||||
|
// This way, we can move between them, but only certain actions are possible on either
|
||||||
|
// This means modifications happen on Raw, but to move to Entry, you schema normalise.
|
||||||
|
// Vice versa, you can for free, move to Raw, but you lose the validation.
|
||||||
|
|
||||||
|
// Because this is type system it's "free" in the end, and means we force validation
|
||||||
|
// at the correct and required points of the entries life.
|
||||||
|
|
||||||
|
// This is specifically important for the commit to the backend, as we only want to
|
||||||
|
// commit validated types.
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct Entry {
|
pub struct Entry {
|
||||||
pub id: Option<i64>,
|
pub id: Option<i64>,
|
||||||
|
// Flag if we have been schema checked or not.
|
||||||
|
// pub schema_validated: bool,
|
||||||
attrs: BTreeMap<String, Vec<String>>,
|
attrs: BTreeMap<String, Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,6 +121,9 @@ impl Entry {
|
||||||
Entry {
|
Entry {
|
||||||
// This means NEVER COMMITED
|
// This means NEVER COMMITED
|
||||||
id: None,
|
id: None,
|
||||||
|
// TODO: Make this only on cfg(test/debug_assertions) builds.
|
||||||
|
// ALTERNATE: Convert to a different entry type on validate/normalise?
|
||||||
|
// CONVERT TO A TYPE
|
||||||
attrs: BTreeMap::new(),
|
attrs: BTreeMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,6 +142,7 @@ impl Entry {
|
||||||
// Here we need to actually do a check/binary search ...
|
// Here we need to actually do a check/binary search ...
|
||||||
// FIXME: Because map_err is lazy, this won't do anything on release
|
// FIXME: Because map_err is lazy, this won't do anything on release
|
||||||
match v.binary_search(&value) {
|
match v.binary_search(&value) {
|
||||||
|
// It already exists, done!
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(idx) => {
|
Err(idx) => {
|
||||||
// This cloning is to fix a borrow issue with the or_insert below.
|
// This cloning is to fix a borrow issue with the or_insert below.
|
||||||
|
@ -134,6 +154,29 @@ impl Entry {
|
||||||
.or_insert(vec![value]);
|
.or_insert(vec![value]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remove_ava(&mut self, attr: &String, value: &String) {
|
||||||
|
self.attrs
|
||||||
|
// TODO: Fix this clone ...
|
||||||
|
.entry(attr.clone())
|
||||||
|
.and_modify(|v| {
|
||||||
|
// Here we need to actually do a check/binary search ...
|
||||||
|
// FIXME: Because map_err is lazy, this won't do anything on release
|
||||||
|
match v.binary_search(&value) {
|
||||||
|
// It exists, rm it.
|
||||||
|
Ok(idx) => {
|
||||||
|
v.remove(idx);
|
||||||
|
}
|
||||||
|
// It does not exist, move on.
|
||||||
|
Err(_) => {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn purge_ava(&mut self, attr: &String) {
|
||||||
|
self.attrs.remove(attr);
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: Should this collect from iter instead?
|
// FIXME: Should this collect from iter instead?
|
||||||
/// Overwrite the existing avas.
|
/// Overwrite the existing avas.
|
||||||
pub fn set_avas(&mut self, attr: String, values: Vec<String>) {
|
pub fn set_avas(&mut self, attr: String, values: Vec<String>) {
|
||||||
|
@ -250,6 +293,72 @@ impl Entry {
|
||||||
attrs: self.attrs.clone(),
|
attrs: self.attrs.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn gen_modlist_assert(&self, schema: &SchemaReadTransaction) -> Result<ModifyList, ()>
|
||||||
|
{
|
||||||
|
// Create a modlist from this entry. We make this assuming we want the entry
|
||||||
|
// to have this one as a subset of values. This means if we have single
|
||||||
|
// values, we'll replace, if they are multivalue, we present them.
|
||||||
|
//
|
||||||
|
// We assume the schema validaty of the entry is already checked, and
|
||||||
|
// normalisation performed.
|
||||||
|
let mut mods = ModifyList::new();
|
||||||
|
|
||||||
|
for (k, vs) in self.attrs.iter() {
|
||||||
|
// Get the schema attribute type out.
|
||||||
|
match schema.is_multivalue(k) {
|
||||||
|
Ok(r) => {
|
||||||
|
if !r {
|
||||||
|
// As this is single value, purge then present to maintain this
|
||||||
|
// invariant
|
||||||
|
mods.push_mod(Modify::Purged(k.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for v in vs {
|
||||||
|
mods.push_mod(Modify::Present(k.clone(), v.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(mods)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should this be schemaless, relying on checks of the modlist, and the entry validate after?
|
||||||
|
pub fn apply_modlist(&self, modlist: &ModifyList) -> Result<Entry, ()> {
|
||||||
|
// Apply a modlist, generating a new entry that conforms to the changes.
|
||||||
|
// This is effectively clone-and-transform
|
||||||
|
|
||||||
|
// clone the entry
|
||||||
|
let mut ne = self.clone();
|
||||||
|
|
||||||
|
// mutate
|
||||||
|
for modify in modlist.mods.iter() {
|
||||||
|
match modify {
|
||||||
|
Modify::Present(a, v) => {
|
||||||
|
ne.add_ava(a.clone(), v.clone())
|
||||||
|
}
|
||||||
|
Modify::Removed(a, v) => {
|
||||||
|
ne.remove_ava(a, v)
|
||||||
|
}
|
||||||
|
Modify::Purged(a) => {
|
||||||
|
ne.purge_ava(a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return it
|
||||||
|
Ok(ne)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clone_no_attrs(&self) -> Entry {
|
||||||
|
Entry {
|
||||||
|
id: self.id,
|
||||||
|
attrs: BTreeMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for Entry {
|
impl Clone for Entry {
|
||||||
|
@ -318,6 +427,7 @@ struct User {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{Entry, User};
|
use super::{Entry, User};
|
||||||
|
use modify::{Modify, ModifyList};
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -349,7 +459,6 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_entry_pres() {
|
fn test_entry_pres() {
|
||||||
let mut e: Entry = Entry::new();
|
let mut e: Entry = Entry::new();
|
||||||
|
|
||||||
e.add_ava(String::from("userid"), String::from("william"));
|
e.add_ava(String::from("userid"), String::from("william"));
|
||||||
|
|
||||||
assert!(e.attribute_pres("userid"));
|
assert!(e.attribute_pres("userid"));
|
||||||
|
@ -367,4 +476,24 @@ mod tests {
|
||||||
assert!(!e.attribute_equality("nonexist", "william"));
|
assert!(!e.attribute_equality("nonexist", "william"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_entry_apply_modlist() {
|
||||||
|
// Test application of changes to an entry.
|
||||||
|
let mut e: Entry = Entry::new();
|
||||||
|
e.add_ava(String::from("userid"), String::from("william"));
|
||||||
|
|
||||||
|
let mods = ModifyList::new_list(vec![
|
||||||
|
Modify::Present(String::from("attr"), String::from("value")),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let ne = e.apply_modlist(&mods).unwrap();
|
||||||
|
|
||||||
|
// Assert the changes are there
|
||||||
|
assert!(ne.attribute_equality("attr", "value"));
|
||||||
|
|
||||||
|
|
||||||
|
// Assert present for multivalue
|
||||||
|
// Assert purge on single/multi/empty value
|
||||||
|
// Assert removed on value that exists and doesn't exist
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ pub enum SchemaError {
|
||||||
pub enum OperationError {
|
pub enum OperationError {
|
||||||
EmptyRequest,
|
EmptyRequest,
|
||||||
Backend,
|
Backend,
|
||||||
|
NoMatchingEntries,
|
||||||
SchemaViolation,
|
SchemaViolation,
|
||||||
Plugin,
|
Plugin,
|
||||||
FilterGeneration,
|
FilterGeneration,
|
||||||
|
|
|
@ -4,6 +4,7 @@ use super::proto_v1::{CreateRequest, Response, SearchRequest, SearchResponse};
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
use entry::Entry;
|
use entry::Entry;
|
||||||
use error::OperationError;
|
use error::OperationError;
|
||||||
|
use modify::ModifyList;
|
||||||
|
|
||||||
// Should the event Result have the log items?
|
// Should the event Result have the log items?
|
||||||
// FIXME: Remove seralising here - each type should
|
// FIXME: Remove seralising here - each type should
|
||||||
|
@ -70,6 +71,14 @@ impl SearchEvent {
|
||||||
class: (),
|
class: (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_internal(filter: Filter) -> Self {
|
||||||
|
SearchEvent {
|
||||||
|
internal: true,
|
||||||
|
filter: filter,
|
||||||
|
class: (),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Represents the decoded entries from the protocol -> internal entry representation
|
// Represents the decoded entries from the protocol -> internal entry representation
|
||||||
|
@ -156,3 +165,24 @@ impl DeleteEvent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ModifyEvent {
|
||||||
|
pub filter: Filter,
|
||||||
|
pub modlist: ModifyList,
|
||||||
|
pub internal: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message for ModifyEvent {
|
||||||
|
type Result = Result<OpResult, OperationError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModifyEvent {
|
||||||
|
pub fn new_internal(filter: Filter, modlist: ModifyList) -> Self {
|
||||||
|
ModifyEvent {
|
||||||
|
filter: filter,
|
||||||
|
modlist: modlist,
|
||||||
|
internal: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,10 @@ pub enum Filter {
|
||||||
Not(Box<Filter>),
|
Not(Box<Filter>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Change this so you have RawFilter and Filter. RawFilter is the "builder", and then
|
||||||
|
// given a "schema" you can emit a Filter. For us internally, we can create Filter
|
||||||
|
// directly still ...
|
||||||
|
|
||||||
impl Filter {
|
impl Filter {
|
||||||
// Does this need mut self? Aren't we returning
|
// Does this need mut self? Aren't we returning
|
||||||
// a new copied filter?
|
// a new copied filter?
|
||||||
|
|
|
@ -42,6 +42,7 @@ mod identity;
|
||||||
mod plugins;
|
mod plugins;
|
||||||
mod schema;
|
mod schema;
|
||||||
mod server;
|
mod server;
|
||||||
|
mod modify;
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod core;
|
pub mod core;
|
||||||
|
|
|
@ -20,7 +20,7 @@ pub struct Base {}
|
||||||
|
|
||||||
impl Plugin for Base {
|
impl Plugin for Base {
|
||||||
fn id() -> &'static str {
|
fn id() -> &'static str {
|
||||||
"Base"
|
"plugin_base"
|
||||||
}
|
}
|
||||||
// Need to be given the backend(for testing ease)
|
// Need to be given the backend(for testing ease)
|
||||||
// audit
|
// audit
|
||||||
|
|
|
@ -9,13 +9,12 @@ use regex::Regex;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use modify::ModifyList;
|
||||||
|
|
||||||
use concread::cowcell::{CowCell, CowCellReadTxn, CowCellWriteTxn};
|
use concread::cowcell::{CowCell, CowCellReadTxn, CowCellWriteTxn};
|
||||||
|
|
||||||
// representations of schema that confines object types, classes
|
// representations of schema that confines object types, classes
|
||||||
// and attributes. This ties in deeply with "Entry".
|
// and attributes. This ties in deeply with "Entry".
|
||||||
// This only defines the types, and how they are represented. For
|
|
||||||
// application and validation of the schema, see "Entry".
|
|
||||||
//
|
//
|
||||||
// In the future this will parse/read it's schema from the db
|
// In the future this will parse/read it's schema from the db
|
||||||
// but we have to bootstrap with some core types.
|
// but we have to bootstrap with some core types.
|
||||||
|
@ -32,6 +31,7 @@ use concread::cowcell::{CowCell, CowCellReadTxn, CowCellWriteTxn};
|
||||||
// probably just protection from delete and modify, except systemmay/systemmust/index?
|
// probably just protection from delete and modify, except systemmay/systemmust/index?
|
||||||
|
|
||||||
// TODO: Schema types -> Entry conversion
|
// TODO: Schema types -> Entry conversion
|
||||||
|
// TODO: Entry -> Schema given class. This is for loading from the db.
|
||||||
|
|
||||||
// TODO: prefix on all schema types that are system?
|
// TODO: prefix on all schema types that are system?
|
||||||
|
|
||||||
|
@ -300,6 +300,34 @@ pub struct SchemaInner {
|
||||||
attributes: HashMap<String, SchemaAttribute>,
|
attributes: HashMap<String, SchemaAttribute>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait SchemaReadTransaction {
|
||||||
|
fn get_inner(&self) -> &SchemaInner;
|
||||||
|
|
||||||
|
fn validate(&self, audit: &mut AuditScope) -> Result<(), ()> {
|
||||||
|
self.get_inner().validate(audit)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_entry(&self, entry: &Entry) -> Result<(), SchemaError> {
|
||||||
|
self.get_inner().validate_entry(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_filter(&self, filt: &Filter) -> Result<(), SchemaError> {
|
||||||
|
self.get_inner().validate_filter(filt)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalise_entry(&self, entry: &Entry) -> Entry {
|
||||||
|
self.get_inner().normalise_entry(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalise_modlist(&self, modlist: &ModifyList) -> ModifyList {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_multivalue(&self, attr: &str) -> Result<bool, SchemaError> {
|
||||||
|
self.get_inner().is_multivalue(attr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SchemaInner {
|
impl SchemaInner {
|
||||||
pub fn new(audit: &mut AuditScope) -> Result<Self, ()> {
|
pub fn new(audit: &mut AuditScope) -> Result<Self, ()> {
|
||||||
let mut au = AuditScope::new("schema_new");
|
let mut au = AuditScope::new("schema_new");
|
||||||
|
@ -941,13 +969,13 @@ impl SchemaInner {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub fn normalise_entry(&mut self, entry: &mut Entry) -> Result<(), SchemaError> {
|
// TODO: Restructure this when we change entry lifecycle types.
|
||||||
pub fn normalise_entry(&self, entry: &Entry) -> Entry {
|
pub fn normalise_entry(&self, entry: &Entry) -> Entry {
|
||||||
// We duplicate the entry here, because we can't
|
// We duplicate the entry here, because we can't
|
||||||
// modify what we got on the protocol level. It also
|
// modify what we got on the protocol level. It also
|
||||||
// lets us extend and change things.
|
// lets us extend and change things.
|
||||||
|
|
||||||
let mut entry_new: Entry = Entry::new();
|
let mut entry_new: Entry = entry.clone_no_attrs();
|
||||||
// Better hope we have the attribute type ...
|
// Better hope we have the attribute type ...
|
||||||
let schema_attr_name = self.attributes.get("name").unwrap();
|
let schema_attr_name = self.attributes.get("name").unwrap();
|
||||||
// For each ava
|
// For each ava
|
||||||
|
@ -972,7 +1000,10 @@ impl SchemaInner {
|
||||||
// now push those to the new entry.
|
// now push those to the new entry.
|
||||||
entry_new.set_avas(attr_name_normal, avas_normal);
|
entry_new.set_avas(attr_name_normal, avas_normal);
|
||||||
}
|
}
|
||||||
|
// Mark it is valid
|
||||||
|
// entry_new.schema_validated = true;
|
||||||
// Done!
|
// Done!
|
||||||
|
// TODO: Convert the entry type here to a validated type.
|
||||||
entry_new
|
entry_new
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1035,6 +1066,17 @@ impl SchemaInner {
|
||||||
pub fn normalise_filter(&mut self) {
|
pub fn normalise_filter(&mut self) {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_multivalue(&self, attr_name: &str) -> Result<bool, SchemaError> {
|
||||||
|
match self.attributes.get(attr_name) {
|
||||||
|
Some(a_schema) => {
|
||||||
|
Ok(a_schema.multivalue)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
return Err(SchemaError::InvalidAttribute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// type Schema = CowCell<SchemaInner>;
|
// type Schema = CowCell<SchemaInner>;
|
||||||
|
@ -1052,14 +1094,6 @@ impl<'a> SchemaWriteTransaction<'a> {
|
||||||
self.inner.bootstrap_core(audit)
|
self.inner.bootstrap_core(audit)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_entry(&self, entry: &Entry) -> Result<(), SchemaError> {
|
|
||||||
self.inner.validate_entry(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn normalise_entry(&self, entry: &Entry) -> Entry {
|
|
||||||
self.inner.normalise_entry(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Schema probably needs to be part of the backend, so that commits are wholly atomic
|
// TODO: Schema probably needs to be part of the backend, so that commits are wholly atomic
|
||||||
// but in the current design, we need to open be first, then schema, but we have to commit be
|
// but in the current design, we need to open be first, then schema, but we have to commit be
|
||||||
// first, then schema to ensure that the be content matches our schema. Saying this, if your
|
// first, then schema to ensure that the be content matches our schema. Saying this, if your
|
||||||
|
@ -1068,13 +1102,12 @@ impl<'a> SchemaWriteTransaction<'a> {
|
||||||
pub fn commit(self) {
|
pub fn commit(self) {
|
||||||
self.inner.commit();
|
self.inner.commit();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn validate_filter(&self, filt: &Filter) -> Result<(), SchemaError> {
|
impl<'a> SchemaReadTransaction for SchemaWriteTransaction<'a> {
|
||||||
self.inner.validate_filter(filt)
|
fn get_inner(&self) -> &SchemaInner {
|
||||||
}
|
// Does this deref the CowCell for us?
|
||||||
|
&self.inner
|
||||||
pub fn validate(&self, audit: &mut AuditScope) -> Result<(), ()> {
|
|
||||||
self.inner.validate(audit)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1082,17 +1115,10 @@ pub struct SchemaTransaction {
|
||||||
inner: CowCellReadTxn<SchemaInner>,
|
inner: CowCellReadTxn<SchemaInner>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SchemaTransaction {
|
impl SchemaReadTransaction for SchemaTransaction {
|
||||||
pub fn validate(&self, audit: &mut AuditScope) -> Result<(), ()> {
|
fn get_inner(&self) -> &SchemaInner {
|
||||||
self.inner.validate(audit)
|
// Does this deref the CowCell for us?
|
||||||
}
|
&self.inner
|
||||||
|
|
||||||
pub fn validate_entry(&self, entry: &Entry) -> Result<(), SchemaError> {
|
|
||||||
self.inner.validate_entry(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_filter(&self, filt: &Filter) -> Result<(), SchemaError> {
|
|
||||||
self.inner.validate_filter(filt)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1124,6 +1150,7 @@ mod tests {
|
||||||
use super::super::error::SchemaError;
|
use super::super::error::SchemaError;
|
||||||
use super::super::filter::Filter;
|
use super::super::filter::Filter;
|
||||||
use super::{IndexType, Schema, SchemaAttribute, SchemaClass, SyntaxType};
|
use super::{IndexType, Schema, SchemaAttribute, SchemaClass, SyntaxType};
|
||||||
|
use schema::SchemaReadTransaction;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
@ -1373,10 +1400,11 @@ mod tests {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
let res = schema.validate_entry(&e_attr_invalid);
|
||||||
schema.validate_entry(&e_attr_invalid),
|
assert!(match res {
|
||||||
Err(SchemaError::MissingMustAttribute(String::from("system")))
|
Err(SchemaError::MissingMustAttribute(_)) => true,
|
||||||
);
|
_ => false,
|
||||||
|
});
|
||||||
|
|
||||||
let e_attr_invalid_may: Entry = serde_json::from_str(
|
let e_attr_invalid_may: Entry = serde_json::from_str(
|
||||||
r#"{
|
r#"{
|
||||||
|
|
|
@ -12,11 +12,13 @@ use be::{
|
||||||
use constants::{JSON_ANONYMOUS_V1, JSON_SYSTEM_INFO_V1};
|
use constants::{JSON_ANONYMOUS_V1, JSON_SYSTEM_INFO_V1};
|
||||||
use entry::Entry;
|
use entry::Entry;
|
||||||
use error::OperationError;
|
use error::OperationError;
|
||||||
use event::{CreateEvent, ExistsEvent, OpResult, SearchEvent, SearchResult, DeleteEvent};
|
use event::{CreateEvent, ExistsEvent, OpResult, SearchEvent, SearchResult, DeleteEvent, ModifyEvent};
|
||||||
use filter::Filter;
|
use filter::Filter;
|
||||||
use log::EventLog;
|
use log::EventLog;
|
||||||
use plugins::Plugins;
|
use plugins::Plugins;
|
||||||
use schema::{Schema, SchemaTransaction, SchemaWriteTransaction};
|
use schema::{Schema, SchemaTransaction, SchemaReadTransaction, SchemaWriteTransaction};
|
||||||
|
use modify::ModifyList;
|
||||||
|
|
||||||
|
|
||||||
pub fn start(log: actix::Addr<EventLog>, path: &str, threads: usize) -> actix::Addr<QueryServer> {
|
pub fn start(log: actix::Addr<EventLog>, path: &str, threads: usize) -> actix::Addr<QueryServer> {
|
||||||
let mut audit = AuditScope::new("server_start");
|
let mut audit = AuditScope::new("server_start");
|
||||||
|
@ -139,8 +141,13 @@ pub trait QueryServerReadTransaction {
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
fn internal_search(&self, _au: &mut AuditScope, _filter: Filter) -> Result<Vec<Entry>, OperationError> {
|
fn internal_search(&self, audit: &mut AuditScope, filter: Filter) -> Result<Vec<Entry>, OperationError> {
|
||||||
unimplemented!()
|
|
||||||
|
let mut audit_int = AuditScope::new("internal_search");
|
||||||
|
let se = SearchEvent::new_internal(filter);
|
||||||
|
let res = self.search(&mut audit_int, &se);
|
||||||
|
audit.append_scope(audit_int);
|
||||||
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -313,6 +320,90 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn modify(&self, au: &mut AuditScope, me: &ModifyEvent) -> Result<(), OperationError> {
|
||||||
|
// Get the candidates.
|
||||||
|
// Modify applies a modlist to a filter, so we need to internal search
|
||||||
|
// then apply.
|
||||||
|
// WARNING! Check access controls here!!!!
|
||||||
|
// How can we do the search with the permissions of the caller?
|
||||||
|
|
||||||
|
// TODO: Fix this filter clone ....
|
||||||
|
// Likely this will be fixed if search takes &filter, and then clone
|
||||||
|
// to normalise, instead of attempting to mut the filter on norm.
|
||||||
|
let pre_candidates = match self.internal_search(au, me.filter.clone()) {
|
||||||
|
Ok(results) => results,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
|
if pre_candidates.len() == 0 {
|
||||||
|
return Err(OperationError::NoMatchingEntries)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clone a set of writeables.
|
||||||
|
// Apply the modlist -> Remember, we have a set of origs
|
||||||
|
// and the new modified ents.
|
||||||
|
let mut candidates: Vec<Entry> = pre_candidates.iter()
|
||||||
|
.map(|er| {
|
||||||
|
er.apply_modlist(&me.modlist).unwrap()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
audit_log!(au, "modify: candidates -> {:?}", candidates);
|
||||||
|
|
||||||
|
// Pre mod plugins
|
||||||
|
|
||||||
|
// Schema validate
|
||||||
|
let r = candidates.iter().fold(Ok(()), |acc, e| {
|
||||||
|
if acc.is_ok() {
|
||||||
|
self.schema
|
||||||
|
.validate_entry(e)
|
||||||
|
.map_err(|err| {
|
||||||
|
audit_log!(au, "Schema Violation: {:?}", err);
|
||||||
|
OperationError::SchemaViolation
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
acc
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if r.is_err() {
|
||||||
|
audit_log!(au, "Modify operation failed (schema), {:?}", r);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalise all the data now it's validated.
|
||||||
|
// FIXME: This normalisation COPIES everything, which may be
|
||||||
|
// slow.
|
||||||
|
let norm_cand: Vec<Entry> = candidates
|
||||||
|
.iter()
|
||||||
|
.map(|e| self.schema.normalise_entry(&e))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Backend Modify
|
||||||
|
let mut audit_be = AuditScope::new("backend_modify");
|
||||||
|
|
||||||
|
let res = self
|
||||||
|
.be_txn
|
||||||
|
.modify(&mut audit_be, &norm_cand)
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|e| match e {
|
||||||
|
BackendError::EmptyRequest => OperationError::EmptyRequest,
|
||||||
|
BackendError::EntryMissingId => OperationError::InvalidRequestState,
|
||||||
|
});
|
||||||
|
au.append_scope(audit_be);
|
||||||
|
|
||||||
|
if res.is_err() {
|
||||||
|
// be_txn is dropped, ie aborted here.
|
||||||
|
audit_log!(au, "Modify operation failed (backend), {:?}", r);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post Plugins
|
||||||
|
|
||||||
|
// return
|
||||||
|
audit_log!(au, "Modify operation success");
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
// These are where searches and other actions are actually implemented. This
|
// These are where searches and other actions are actually implemented. This
|
||||||
// is the "internal" version, where we define the event as being internal
|
// is the "internal" version, where we define the event as being internal
|
||||||
// only, allowing certain plugin by passes etc.
|
// only, allowing certain plugin by passes etc.
|
||||||
|
@ -343,6 +434,19 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn internal_modify(
|
||||||
|
&self,
|
||||||
|
audit: &mut AuditScope,
|
||||||
|
filter: Filter,
|
||||||
|
modlist: ModifyList
|
||||||
|
) -> Result<(), OperationError> {
|
||||||
|
let mut audit_int = AuditScope::new("internal_modify");
|
||||||
|
let me = ModifyEvent::new_internal(filter, modlist);
|
||||||
|
let res = self.modify(&mut audit_int, &me);
|
||||||
|
audit.append_scope(audit_int);
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
// internal server operation types.
|
// internal server operation types.
|
||||||
// These just wrap the fn create/search etc, but they allow
|
// These just wrap the fn create/search etc, but they allow
|
||||||
// creating the needed create event with the correct internal flags
|
// creating the needed create event with the correct internal flags
|
||||||
|
@ -370,20 +474,32 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
// few attributes.
|
// few attributes.
|
||||||
//
|
//
|
||||||
// This will extra classes an attributes alone!
|
// This will extra classes an attributes alone!
|
||||||
let filt = match e.filter_from_attrs(&vec![String::from("name"), String::from("uuid")]) {
|
let filt = match e.filter_from_attrs(&vec![String::from("uuid")]) {
|
||||||
Some(f) => f,
|
Some(f) => f,
|
||||||
None => return Err(OperationError::FilterGeneration),
|
None => return Err(OperationError::FilterGeneration),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Does it exist? (TODO: Should be search, not exists ...)
|
match self.internal_search(audit, filt.clone()) {
|
||||||
match self.internal_exists(audit, filt) {
|
Ok(results) => {
|
||||||
Ok(true) => {
|
if results.len() == 0 {
|
||||||
// it exists. We need to ensure the content now.
|
// It does not exist. Create it.
|
||||||
unimplemented!()
|
self.internal_create(audit, vec![e])
|
||||||
}
|
} else if results.len() == 1 {
|
||||||
Ok(false) => {
|
// If the thing is subset, pass
|
||||||
// It does not exist. Create it.
|
match e.gen_modlist_assert(&self.schema) {
|
||||||
self.internal_create(audit, vec![e])
|
Ok(modlist) => {
|
||||||
|
// Apply to &results[0]
|
||||||
|
audit_log!(audit, "Generated modlist -> {:?}", modlist);
|
||||||
|
self.internal_modify(audit, filt, modlist)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
unimplemented!()
|
||||||
|
// No action required.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(OperationError::InvalidDBState)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// An error occured. pass it back up.
|
// An error occured. pass it back up.
|
||||||
|
@ -403,7 +519,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
// attributes and classes.
|
// attributes and classes.
|
||||||
|
|
||||||
// Create a filter from the entry for assertion.
|
// Create a filter from the entry for assertion.
|
||||||
let filt = match e.filter_from_attrs(&vec![String::from("name"), String::from("uuid")]) {
|
let filt = match e.filter_from_attrs(&vec![String::from("uuid")]) {
|
||||||
Some(f) => f,
|
Some(f) => f,
|
||||||
None => return Err(OperationError::FilterGeneration),
|
None => return Err(OperationError::FilterGeneration),
|
||||||
};
|
};
|
||||||
|
@ -441,6 +557,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
let e: Entry = serde_json::from_str(JSON_SYSTEM_INFO_V1).unwrap();
|
let e: Entry = serde_json::from_str(JSON_SYSTEM_INFO_V1).unwrap();
|
||||||
self.internal_assert_or_create(audit, e)
|
self.internal_assert_or_create(audit, e)
|
||||||
});
|
});
|
||||||
|
audit_log!(audit_si, "start_system_info -> result {:?}", res);
|
||||||
audit.append_scope(audit_si);
|
audit.append_scope(audit_si);
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
if res.is_err() {
|
if res.is_err() {
|
||||||
|
@ -453,6 +570,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
let e: Entry = serde_json::from_str(JSON_ANONYMOUS_V1).unwrap();
|
let e: Entry = serde_json::from_str(JSON_ANONYMOUS_V1).unwrap();
|
||||||
self.internal_migrate_or_create(audit, e)
|
self.internal_migrate_or_create(audit, e)
|
||||||
});
|
});
|
||||||
|
audit_log!(audit_an, "start_anonymous -> result {:?}", res);
|
||||||
audit.append_scope(audit_an);
|
audit.append_scope(audit_an);
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
if res.is_err() {
|
if res.is_err() {
|
||||||
|
|
Loading…
Reference in a new issue