mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
Add plugin links,
This commit is contained in:
parent
b888d9036f
commit
db024258f2
|
@ -12,7 +12,7 @@ macro_rules! audit_log {
|
||||||
($audit:expr, $($arg:tt)*) => ({
|
($audit:expr, $($arg:tt)*) => ({
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
if cfg!(test) || cfg!(debug_assertions) {
|
if cfg!(test) || cfg!(debug_assertions) {
|
||||||
print!("DEBUG AUDIT -> ");
|
print!("DEBUG AUDIT ({})-> ", $audit.id());
|
||||||
println!($($arg)*)
|
println!($($arg)*)
|
||||||
}
|
}
|
||||||
$audit.log_event(
|
$audit.log_event(
|
||||||
|
@ -101,6 +101,10 @@ impl AuditScope {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> &str {
|
||||||
|
self.name.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
// Given a new audit event, append it in.
|
// Given a new audit event, append it in.
|
||||||
pub fn append_scope(&mut self, scope: AuditScope) {
|
pub fn append_scope(&mut self, scope: AuditScope) {
|
||||||
self.events.push(AuditEvent::scope(scope))
|
self.events.push(AuditEvent::scope(scope))
|
||||||
|
|
|
@ -5,7 +5,11 @@ use error::OperationError;
|
||||||
use event::CreateEvent;
|
use event::CreateEvent;
|
||||||
use schema::Schema;
|
use schema::Schema;
|
||||||
|
|
||||||
|
mod uuid;
|
||||||
|
|
||||||
trait Plugin {
|
trait Plugin {
|
||||||
|
fn id() -> &'static str;
|
||||||
|
|
||||||
fn pre_create(
|
fn pre_create(
|
||||||
be: &mut Backend,
|
be: &mut Backend,
|
||||||
au: &mut AuditScope,
|
au: &mut AuditScope,
|
||||||
|
@ -45,7 +49,48 @@ trait Plugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod uuid;
|
pub struct Plugins{}
|
||||||
|
|
||||||
|
macro_rules! run_pre_create_plugin {
|
||||||
|
(
|
||||||
|
$be:ident,
|
||||||
|
$au:ident,
|
||||||
|
$cand:ident,
|
||||||
|
$ce:ident,
|
||||||
|
$schema:ident,
|
||||||
|
$target_plugin:ty
|
||||||
|
) => {{
|
||||||
|
let mut audit_scope = AuditScope::new(<($target_plugin)>::id());
|
||||||
|
let r = audit_segment!(audit_scope, || {
|
||||||
|
<($target_plugin)>::pre_create(
|
||||||
|
$be, &mut audit_scope, $cand, $ce, $schema
|
||||||
|
)
|
||||||
|
});
|
||||||
|
$au.append_scope(audit_scope);
|
||||||
|
r
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugins {
|
||||||
|
pub fn run_pre_create(
|
||||||
|
be: &mut Backend,
|
||||||
|
au: &mut AuditScope,
|
||||||
|
cand: &mut Vec<Entry>,
|
||||||
|
ce: &CreateEvent,
|
||||||
|
schema: &Schema,
|
||||||
|
) -> Result<(), OperationError> {
|
||||||
|
audit_segment!(audit_plugin_pre, || {
|
||||||
|
|
||||||
|
// map chain?
|
||||||
|
let uuid_res = run_pre_create_plugin!(be, au, cand, ce, schema, uuid::UUID);
|
||||||
|
|
||||||
|
|
||||||
|
// TODO, actually return the right thing ...
|
||||||
|
uuid_res
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// We should define the order that plugins should run
|
// We should define the order that plugins should run
|
||||||
|
|
||||||
|
|
|
@ -9,9 +9,12 @@ use event::CreateEvent;
|
||||||
use filter::Filter;
|
use filter::Filter;
|
||||||
use schema::Schema;
|
use schema::Schema;
|
||||||
|
|
||||||
struct UUID {}
|
pub struct UUID {}
|
||||||
|
|
||||||
impl Plugin for UUID {
|
impl Plugin for UUID {
|
||||||
|
fn id() -> &'static str {
|
||||||
|
"UUID"
|
||||||
|
}
|
||||||
// Need to be given the backend(for testing ease)
|
// Need to be given the backend(for testing ease)
|
||||||
// audit
|
// audit
|
||||||
// the mut set of entries to create
|
// the mut set of entries to create
|
||||||
|
@ -30,17 +33,36 @@ impl Plugin for UUID {
|
||||||
for entry in cand.iter_mut() {
|
for entry in cand.iter_mut() {
|
||||||
let name_uuid = String::from("uuid");
|
let name_uuid = String::from("uuid");
|
||||||
|
|
||||||
|
audit_log!(au, "UUID check on entry: {:?}", entry);
|
||||||
|
|
||||||
// if they don't have uuid, create it.
|
// if they don't have uuid, create it.
|
||||||
// TODO: get_ava should have a str version for effeciency?
|
// TODO: get_ava should have a str version for effeciency?
|
||||||
let mut c_uuid = match entry.get_ava(&name_uuid) {
|
let mut c_uuid = match entry.get_ava(&name_uuid) {
|
||||||
Some(u) => {
|
Some(u) => {
|
||||||
// Actually check we have a value, could be empty array ...
|
// Actually check we have a value, could be empty array ...
|
||||||
let v = u.first().unwrap();
|
if u.len() > 1 {
|
||||||
|
audit_log!(au, "Entry defines uuid attr, but multiple values.");
|
||||||
|
return Err(OperationError::Plugin)
|
||||||
|
};
|
||||||
|
|
||||||
|
let v = match u.first() {
|
||||||
|
Some(v) => v,
|
||||||
|
None => {
|
||||||
|
audit_log!(au, "Entry defines uuid attr, but no value.");
|
||||||
|
return Err(OperationError::Plugin)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// This could actually fail, so we probably need to handle
|
// This could actually fail, so we probably need to handle
|
||||||
// this better ....
|
// this better ....
|
||||||
|
// TODO: Make this a SCHEMA check, not a manual one.
|
||||||
|
//
|
||||||
match Uuid::parse_str(v.as_str()) {
|
match Uuid::parse_str(v.as_str()) {
|
||||||
Ok(up) => up,
|
Ok(up) => up,
|
||||||
Err(_) => return Err(OperationError::Plugin),
|
Err(_) => {
|
||||||
|
audit_log!(au, "Entry contains invalid UUID content, rejecting out of principle.");
|
||||||
|
return Err(OperationError::Plugin)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => Uuid::new_v4(),
|
None => Uuid::new_v4(),
|
||||||
|
@ -48,7 +70,6 @@ impl Plugin for UUID {
|
||||||
|
|
||||||
// Make it a string, so we can filter.
|
// Make it a string, so we can filter.
|
||||||
let str_uuid = format!("{}", c_uuid);
|
let str_uuid = format!("{}", c_uuid);
|
||||||
println!("{}", str_uuid);
|
|
||||||
|
|
||||||
let mut au_be = AuditScope::new("be_exist");
|
let mut au_be = AuditScope::new("be_exist");
|
||||||
|
|
||||||
|
@ -58,27 +79,25 @@ impl Plugin for UUID {
|
||||||
let r = be.exists(&mut au_be, &filt);
|
let r = be.exists(&mut au_be, &filt);
|
||||||
|
|
||||||
au.append_scope(au_be);
|
au.append_scope(au_be);
|
||||||
// end the scope?
|
// end the scope for the be operation.
|
||||||
|
|
||||||
match r {
|
match r {
|
||||||
Ok(b) => {
|
Ok(b) => {
|
||||||
if b == true {
|
if b == true {
|
||||||
|
audit_log!(au, "UUID already exists, rejecting.");
|
||||||
return Err(OperationError::Plugin);
|
return Err(OperationError::Plugin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => return Err(OperationError::Plugin),
|
Err(e) => {
|
||||||
|
audit_log!(au, "Backend error occured checking UUID existance.");
|
||||||
|
return Err(OperationError::Plugin)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check that the uuid is unique in the be (even if one is provided
|
|
||||||
// we especially need to check that)
|
|
||||||
|
|
||||||
// if not unique, generate another, and try again.
|
|
||||||
|
|
||||||
// If it's okay, now put it into the entry.
|
|
||||||
// we may need to inject the base OC required for all objects in our
|
|
||||||
// server to support this?
|
|
||||||
let str_uuid = format!("{}", c_uuid);
|
let str_uuid = format!("{}", c_uuid);
|
||||||
|
audit_log!(au, "Set UUID {} to entry", str_uuid);
|
||||||
let ava_uuid: Vec<String> = vec![str_uuid];
|
let ava_uuid: Vec<String> = vec![str_uuid];
|
||||||
|
|
||||||
entry.set_avas(name_uuid, ava_uuid);
|
entry.set_avas(name_uuid, ava_uuid);
|
||||||
}
|
}
|
||||||
// done!
|
// done!
|
||||||
|
@ -142,9 +161,6 @@ mod tests {
|
||||||
// Check empty create
|
// Check empty create
|
||||||
#[test]
|
#[test]
|
||||||
fn test_pre_create_empty() {
|
fn test_pre_create_empty() {
|
||||||
// Need a macro to create all the bits here ...
|
|
||||||
// Macro needs preload entries, the create entries
|
|
||||||
// schema, identity for create event (later)
|
|
||||||
let preload: Vec<Entry> = Vec::new();
|
let preload: Vec<Entry> = Vec::new();
|
||||||
let mut create: Vec<Entry> = Vec::new();
|
let mut create: Vec<Entry> = Vec::new();
|
||||||
run_pre_create_test!(
|
run_pre_create_test!(
|
||||||
|
@ -169,9 +185,6 @@ mod tests {
|
||||||
// check create where no uuid
|
// check create where no uuid
|
||||||
#[test]
|
#[test]
|
||||||
fn test_pre_create_no_uuid() {
|
fn test_pre_create_no_uuid() {
|
||||||
// Need a macro to create all the bits here ...
|
|
||||||
// Macro needs preload entries, the create entries
|
|
||||||
// schema, identity for create event (later)
|
|
||||||
let preload: Vec<Entry> = Vec::new();
|
let preload: Vec<Entry> = Vec::new();
|
||||||
|
|
||||||
let e: Entry = serde_json::from_str(
|
let e: Entry = serde_json::from_str(
|
||||||
|
@ -208,16 +221,81 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
// check unparseable uuid
|
// check unparseable uuid
|
||||||
|
#[test]
|
||||||
|
fn test_pre_create_uuid_invalid() {
|
||||||
|
let preload: Vec<Entry> = Vec::new();
|
||||||
|
|
||||||
|
let e: Entry = serde_json::from_str(
|
||||||
|
r#"{
|
||||||
|
"attrs": {
|
||||||
|
"class": ["person"],
|
||||||
|
"name": ["testperson"],
|
||||||
|
"description": ["testperson"],
|
||||||
|
"displayname": ["testperson"],
|
||||||
|
"uuid": ["xxxxxx"]
|
||||||
|
}
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut create = vec![e.clone()];
|
||||||
|
|
||||||
|
run_pre_create_test!(
|
||||||
|
preload,
|
||||||
|
create,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
|be: &mut Backend,
|
||||||
|
au: &mut AuditScope,
|
||||||
|
cand: &mut Vec<Entry>,
|
||||||
|
ce: &CreateEvent,
|
||||||
|
schema: &Schema| {
|
||||||
|
let r = UUID::pre_create(be, au, cand, ce, schema);
|
||||||
|
assert!(r.is_err());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// check entry where uuid is empty list
|
// check entry where uuid is empty list
|
||||||
|
#[test]
|
||||||
|
fn test_pre_create_uuid_empty() {
|
||||||
|
let preload: Vec<Entry> = Vec::new();
|
||||||
|
|
||||||
|
let e: Entry = serde_json::from_str(
|
||||||
|
r#"{
|
||||||
|
"attrs": {
|
||||||
|
"class": ["person"],
|
||||||
|
"name": ["testperson"],
|
||||||
|
"description": ["testperson"],
|
||||||
|
"displayname": ["testperson"],
|
||||||
|
"uuid": []
|
||||||
|
}
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut create = vec![e.clone()];
|
||||||
|
|
||||||
|
run_pre_create_test!(
|
||||||
|
preload,
|
||||||
|
create,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
|be: &mut Backend,
|
||||||
|
au: &mut AuditScope,
|
||||||
|
cand: &mut Vec<Entry>,
|
||||||
|
ce: &CreateEvent,
|
||||||
|
schema: &Schema| {
|
||||||
|
let r = UUID::pre_create(be, au, cand, ce, schema);
|
||||||
|
assert!(r.is_err());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// check create where provided uuid is valid. It should be unchanged.
|
// check create where provided uuid is valid. It should be unchanged.
|
||||||
|
|
||||||
// check create where uuid already exists.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_pre_create_uuid_exist() {
|
fn test_pre_create_uuid_valid() {
|
||||||
// Need a macro to create all the bits here ...
|
|
||||||
// Macro needs preload entries, the create entries
|
|
||||||
// schema, identity for create event (later)
|
|
||||||
let preload: Vec<Entry> = Vec::new();
|
let preload: Vec<Entry> = Vec::new();
|
||||||
|
|
||||||
let e: Entry = serde_json::from_str(
|
let e: Entry = serde_json::from_str(
|
||||||
|
@ -234,7 +312,78 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut create = vec![e.clone()];
|
let mut create = vec![e.clone()];
|
||||||
let mut preload = vec![e];
|
|
||||||
|
run_pre_create_test!(
|
||||||
|
preload,
|
||||||
|
create,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
|be: &mut Backend,
|
||||||
|
au: &mut AuditScope,
|
||||||
|
cand: &mut Vec<Entry>,
|
||||||
|
ce: &CreateEvent,
|
||||||
|
schema: &Schema| {
|
||||||
|
let r = UUID::pre_create(be, au, cand, ce, schema);
|
||||||
|
assert!(r.is_ok());
|
||||||
|
let ue = cand.first().unwrap();
|
||||||
|
assert!(ue.attribute_equality("uuid", "79724141-3603-4060-b6bb-35c72772611d"));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pre_create_uuid_valid_multi() {
|
||||||
|
let preload: Vec<Entry> = Vec::new();
|
||||||
|
|
||||||
|
let e: Entry = serde_json::from_str(
|
||||||
|
r#"{
|
||||||
|
"attrs": {
|
||||||
|
"class": ["person"],
|
||||||
|
"name": ["testperson"],
|
||||||
|
"description": ["testperson"],
|
||||||
|
"displayname": ["testperson"],
|
||||||
|
"uuid": ["79724141-3603-4060-b6bb-35c72772611d", "79724141-3603-4060-b6bb-35c72772611d"]
|
||||||
|
}
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut create = vec![e.clone()];
|
||||||
|
|
||||||
|
run_pre_create_test!(
|
||||||
|
preload,
|
||||||
|
create,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
|be: &mut Backend,
|
||||||
|
au: &mut AuditScope,
|
||||||
|
cand: &mut Vec<Entry>,
|
||||||
|
ce: &CreateEvent,
|
||||||
|
schema: &Schema| {
|
||||||
|
let r = UUID::pre_create(be, au, cand, ce, schema);
|
||||||
|
assert!(r.is_err());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check create where uuid already exists.
|
||||||
|
#[test]
|
||||||
|
fn test_pre_create_uuid_exist() {
|
||||||
|
let e: Entry = serde_json::from_str(
|
||||||
|
r#"{
|
||||||
|
"attrs": {
|
||||||
|
"class": ["person"],
|
||||||
|
"name": ["testperson"],
|
||||||
|
"description": ["testperson"],
|
||||||
|
"displayname": ["testperson"],
|
||||||
|
"uuid": ["79724141-3603-4060-b6bb-35c72772611d"]
|
||||||
|
}
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut create = vec![e.clone()];
|
||||||
|
let preload = vec![e];
|
||||||
|
|
||||||
run_pre_create_test!(
|
run_pre_create_test!(
|
||||||
preload,
|
preload,
|
||||||
|
|
|
@ -668,21 +668,6 @@ impl Schema {
|
||||||
.map(|s| (s.clone(), self.attributes.get(s).unwrap()))
|
.map(|s| (s.clone(), self.attributes.get(s).unwrap()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
/*
|
|
||||||
let may: HashMap<String, &SchemaAttribute> = classes
|
|
||||||
.iter()
|
|
||||||
// Join our class systemmmust + must into one iter
|
|
||||||
.flat_map(|(_, cls)| {
|
|
||||||
cls.systemmust
|
|
||||||
.iter()
|
|
||||||
.chain(cls.must.iter())
|
|
||||||
.chain(cls.systemmay.iter())
|
|
||||||
.chain(cls.may.iter())
|
|
||||||
})
|
|
||||||
.map(|s| (s.clone(), self.attributes.get(s).unwrap()))
|
|
||||||
.collect();
|
|
||||||
*/
|
|
||||||
|
|
||||||
// FIXME: Error needs to say what is missing
|
// FIXME: Error needs to say what is missing
|
||||||
// We need to return *all* missing attributes.
|
// We need to return *all* missing attributes.
|
||||||
|
|
||||||
|
@ -822,7 +807,9 @@ impl Schema {
|
||||||
// Normalise *does not* validate.
|
// Normalise *does not* validate.
|
||||||
// Normalise just fixes some possible common issues, but it
|
// Normalise just fixes some possible common issues, but it
|
||||||
// can't fix *everything* possibly wrong ...
|
// can't fix *everything* possibly wrong ...
|
||||||
pub fn normalise_filter(&mut self) {}
|
pub fn normalise_filter(&mut self) {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -7,7 +7,7 @@ use entry::Entry;
|
||||||
use error::OperationError;
|
use error::OperationError;
|
||||||
use event::{CreateEvent, OpResult, SearchEvent, SearchResult};
|
use event::{CreateEvent, OpResult, SearchEvent, SearchResult};
|
||||||
use log::EventLog;
|
use log::EventLog;
|
||||||
use plugins;
|
use plugins::Plugins;
|
||||||
use schema::Schema;
|
use schema::Schema;
|
||||||
|
|
||||||
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> {
|
||||||
|
@ -96,16 +96,26 @@ impl QueryServer {
|
||||||
// based on request size in the frontend?
|
// based on request size in the frontend?
|
||||||
|
|
||||||
// Copy the entries to a writeable form.
|
// Copy the entries to a writeable form.
|
||||||
let mut candidates: Vec<_> = ce.entries.iter().collect();
|
let mut candidates: Vec<Entry> = ce.entries.iter()
|
||||||
|
.map(|er| er.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
// Start a txn
|
// Start a txn
|
||||||
|
|
||||||
// run any pre plugins, giving them the list of mutable candidates.
|
// run any pre plugins, giving them the list of mutable candidates.
|
||||||
|
// pre-plugins are defined here in their correct order of calling!
|
||||||
|
// I have no intent to make these dynamic or configurable.
|
||||||
|
|
||||||
// Run any pre checks
|
let mut audit_plugin_pre = AuditScope::new("plugin_pre_create");
|
||||||
// FIXME: Normalise all entries incoming
|
let plug_pre_res = Plugins::run_pre_create(&mut self.be, &mut audit_plugin_pre, &mut candidates, ce, &self.schema);
|
||||||
|
au.append_scope(audit_plugin_pre);
|
||||||
|
|
||||||
let r = ce.entries.iter().fold(Ok(()), |acc, e| {
|
if plug_pre_res.is_err() {
|
||||||
|
audit_log!(au, "Create operation failed (plugin), {:?}", plug_pre_res);
|
||||||
|
return plug_pre_res;
|
||||||
|
}
|
||||||
|
|
||||||
|
let r = candidates.iter().fold(Ok(()), |acc, e| {
|
||||||
if acc.is_ok() {
|
if acc.is_ok() {
|
||||||
self.schema
|
self.schema
|
||||||
.validate_entry(e)
|
.validate_entry(e)
|
||||||
|
@ -115,26 +125,35 @@ impl QueryServer {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if r.is_err() {
|
if r.is_err() {
|
||||||
|
audit_log!(au, "Create operation failed (schema), {:?}", r);
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: Normalise all entries now.
|
||||||
|
|
||||||
let mut audit_be = AuditScope::new("backend_create");
|
let mut audit_be = AuditScope::new("backend_create");
|
||||||
// We may change from ce.entries later to something else?
|
// We may change from ce.entries later to something else?
|
||||||
let res = self
|
let res = self
|
||||||
.be
|
.be
|
||||||
.create(&mut audit_be, &ce.entries)
|
.create(&mut audit_be, &candidates)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(|e| match e {
|
.map_err(|e| match e {
|
||||||
BackendError::EmptyRequest => OperationError::EmptyRequest,
|
BackendError::EmptyRequest => OperationError::EmptyRequest,
|
||||||
_ => OperationError::Backend,
|
_ => OperationError::Backend,
|
||||||
});
|
});
|
||||||
|
au.append_scope(audit_be);
|
||||||
|
|
||||||
|
if res.is_err() {
|
||||||
|
audit_log!(au, "Create operation failed (backend), {:?}", r);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
// Run any post plugins
|
// Run any post plugins
|
||||||
|
|
||||||
// Commit/Abort the txn
|
// Commit the txn
|
||||||
|
|
||||||
// We are complete, finalise logging and return
|
// We are complete, finalise logging and return
|
||||||
au.append_scope(audit_be);
|
|
||||||
|
audit_log!(au, "Create operation success");
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue