mirror of
https://gitlab.com/fabinfra/fabaccess/bffh.git
synced 2024-11-11 01:53:23 +01:00
Stuff
This commit is contained in:
parent
0a9ae09984
commit
65830af01d
@ -49,6 +49,21 @@ impl AccessControl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn check<P: AsRef<Permission>>(&self, user: &UserData, perm: P) -> Result<bool> {
|
||||||
|
let mut roles = HashMap::new();
|
||||||
|
// Check all user roles by..
|
||||||
|
Ok(user.roles.iter().any(|role| {
|
||||||
|
// 1. Getting the whole tree down to a list of Roles applied
|
||||||
|
self.internal.tally_role(&mut roles, role)?;
|
||||||
|
|
||||||
|
// 2. Checking if any of the roles the user has give any permission granting the
|
||||||
|
// requested one.
|
||||||
|
roles.drain().any(|(rid, role)| {
|
||||||
|
role.permissions.iter().any(|rule| rule.match_perm(perm))
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn collect_permrules(&self, user: &UserData) -> Result<Vec<PermRule>> {
|
pub fn collect_permrules(&self, user: &UserData) -> Result<Vec<PermRule>> {
|
||||||
self.internal.collect_permrules(user)
|
self.internal.collect_permrules(user)
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::task::{Poll, Context};
|
use std::task::{Poll, Context};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
@ -7,14 +8,13 @@ use smol::Timer;
|
|||||||
|
|
||||||
use slog::Logger;
|
use slog::Logger;
|
||||||
|
|
||||||
use paho_mqtt::AsyncClient;
|
|
||||||
|
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
|
|
||||||
use futures_signals::signal::{Signal, Mutable, MutableSignalCloned};
|
use futures_signals::signal::{Signal, Mutable, MutableSignalCloned};
|
||||||
use crate::machine::{Machine, ReturnToken};
|
use crate::machine::{Machine, ReturnToken};
|
||||||
use crate::db::machine::MachineState;
|
use crate::db::machine::MachineState;
|
||||||
use crate::db::user::{User, UserId, UserData};
|
use crate::db::user::{User, UserId, UserData, Internal as UserDB};
|
||||||
|
use crate::db::access::AccessControl;
|
||||||
|
|
||||||
use crate::network::InitMap;
|
use crate::network::InitMap;
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ use crate::error::Result;
|
|||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
|
||||||
pub trait Sensor {
|
pub trait Sensor {
|
||||||
fn run_sensor(&mut self) -> BoxFuture<'static, (Option<User>, MachineState)>;
|
fn run_sensor(&mut self) -> BoxFuture<'static, (Option<UserId>, MachineState)>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type BoxSensor = Box<dyn Sensor + Send>;
|
type BoxSensor = Box<dyn Sensor + Send>;
|
||||||
@ -36,10 +36,13 @@ pub struct Initiator {
|
|||||||
state_change_fut: Option<BoxFuture<'static, Result<ReturnToken>>>,
|
state_change_fut: Option<BoxFuture<'static, Result<ReturnToken>>>,
|
||||||
token: Option<ReturnToken>,
|
token: Option<ReturnToken>,
|
||||||
sensor: BoxSensor,
|
sensor: BoxSensor,
|
||||||
|
|
||||||
|
userdb: UserDB,
|
||||||
|
access: AccessControl,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Initiator {
|
impl Initiator {
|
||||||
pub fn new(log: Logger, sensor: BoxSensor, signal: MutableSignalCloned<Option<Machine>>) -> Self {
|
pub fn new(log: Logger, sensor: BoxSensor, signal: MutableSignalCloned<Option<Machine>>, userdb: UserDB, access: AccessControl) -> Self {
|
||||||
Self {
|
Self {
|
||||||
log: log,
|
log: log,
|
||||||
signal: signal,
|
signal: signal,
|
||||||
@ -48,14 +51,16 @@ impl Initiator {
|
|||||||
state_change_fut: None,
|
state_change_fut: None,
|
||||||
token: None,
|
token: None,
|
||||||
sensor: sensor,
|
sensor: sensor,
|
||||||
|
userdb,
|
||||||
|
access,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn wrap(log: Logger, sensor: BoxSensor) -> (Mutable<Option<Machine>>, Self) {
|
pub fn wrap(log: Logger, sensor: BoxSensor, userdb: UserDB, access: Arc<AccessControl>) -> (Mutable<Option<Machine>>, Self) {
|
||||||
let m = Mutable::new(None);
|
let m = Mutable::new(None);
|
||||||
let s = m.signal_cloned();
|
let s = m.signal_cloned();
|
||||||
|
|
||||||
(m, Self::new(log, sensor, s))
|
(m, Self::new(log, sensor, s, userdb, access))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,12 +118,11 @@ impl Future for Initiator {
|
|||||||
None => {
|
None => {
|
||||||
this.future = Some(this.sensor.run_sensor());
|
this.future = Some(this.sensor.run_sensor());
|
||||||
},
|
},
|
||||||
Some(Poll::Ready((user, state))) => {
|
Some(Poll::Ready((uid, state))) => {
|
||||||
debug!(this.log, "Sensor returned a new state");
|
debug!(this.log, "Sensor returned a new state");
|
||||||
this.future.take();
|
this.future.take();
|
||||||
let f = this.machine.as_mut().map(|machine| {
|
let f = this.machine.as_mut().map(|machine| {
|
||||||
unimplemented!()
|
machine.request_state_change(state, this.access.clone(), user)
|
||||||
//machine.request_state_change(user.as_ref(), state)
|
|
||||||
});
|
});
|
||||||
this.state_change_fut = f;
|
this.state_change_fut = f;
|
||||||
}
|
}
|
||||||
@ -128,7 +132,7 @@ impl Future for Initiator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(log: &Logger, config: &Config) -> Result<(InitMap, Vec<Initiator>)> {
|
pub fn load(log: &Logger, config: &Config, userdb: UserDB, access: Arc<AccessControl>) -> Result<(InitMap, Vec<Initiator>)> {
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
|
|
||||||
let initiators = config.initiators.iter()
|
let initiators = config.initiators.iter()
|
||||||
@ -140,7 +144,7 @@ pub fn load(log: &Logger, config: &Config) -> Result<(InitMap, Vec<Initiator>)>
|
|||||||
|
|
||||||
let mut v = Vec::new();
|
let mut v = Vec::new();
|
||||||
for (name, initiator) in initiators {
|
for (name, initiator) in initiators {
|
||||||
let (m, i) = Initiator::wrap(log.new(o!("name" => name.clone())), initiator);
|
let (m, i) = Initiator::wrap(log.new(o!("name" => name.clone())), initiator, userdb.clone(), access.clone());
|
||||||
map.insert(name.clone(), m);
|
map.insert(name.clone(), m);
|
||||||
v.push(i);
|
v.push(i);
|
||||||
}
|
}
|
||||||
@ -180,7 +184,7 @@ impl Dummy {
|
|||||||
|
|
||||||
impl Sensor for Dummy {
|
impl Sensor for Dummy {
|
||||||
fn run_sensor(&mut self)
|
fn run_sensor(&mut self)
|
||||||
-> BoxFuture<'static, (Option<User>, MachineState)>
|
-> BoxFuture<'static, (Option<UserId>, MachineState)>
|
||||||
{
|
{
|
||||||
let step = self.step;
|
let step = self.step;
|
||||||
self.step = !step;
|
self.step = !step;
|
||||||
@ -192,12 +196,8 @@ impl Sensor for Dummy {
|
|||||||
if step {
|
if step {
|
||||||
return (None, MachineState::free());
|
return (None, MachineState::free());
|
||||||
} else {
|
} else {
|
||||||
let user = User::new(
|
let user = UserId::new("test".to_string(), None, None);
|
||||||
UserId::new("test".to_string(), None, None),
|
return (Some(user), MachineState::used(Some(user)));
|
||||||
UserData::new(vec![crate::db::access::RoleIdentifier::local_from_str("lmdb".to_string(), "testrole".to_string())], 0),
|
|
||||||
);
|
|
||||||
let id = user.id.clone();
|
|
||||||
return (Some(user), MachineState::used(Some(id)));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -22,9 +22,9 @@ use futures_signals::signal::{Mutable, ReadOnlyMutable};
|
|||||||
|
|
||||||
use crate::error::{Result, Error};
|
use crate::error::{Result, Error};
|
||||||
|
|
||||||
use crate::db::access;
|
use crate::db::access::{AccessControl, PrivilegesBuf, PermissionBuf};
|
||||||
use crate::db::machine::{MachineIdentifier, MachineState, Status};
|
use crate::db::machine::{MachineIdentifier, MachineState, Status};
|
||||||
use crate::db::user::{User, UserData};
|
use crate::db::user::{User, UserData, UserId};
|
||||||
|
|
||||||
use crate::network::MachineMap;
|
use crate::network::MachineMap;
|
||||||
use crate::space;
|
use crate::space;
|
||||||
@ -82,6 +82,52 @@ impl Machine {
|
|||||||
Self::new(Inner::new(id, state), desc)
|
Self::new(Inner::new(id, state), desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn match_perm(&self, status: &Status) -> Option<&PermissionBuf> {
|
||||||
|
let p = self.desc.privs;
|
||||||
|
match status {
|
||||||
|
// If you were allowed to use it you're allowed to give it back
|
||||||
|
Status::Free
|
||||||
|
| Status::ToCheck(_)
|
||||||
|
=> None,
|
||||||
|
|
||||||
|
Status::Blocked(_)
|
||||||
|
| Status::Disabled
|
||||||
|
| Status::Reserved(_)
|
||||||
|
=> Some(&p.manage),
|
||||||
|
|
||||||
|
Status::InUse(_) => Some(&p.write),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_state_change(&self, new_state: MachineState, access: AccessControl, user: &User)
|
||||||
|
-> BoxFuture<'static, Result<()>>
|
||||||
|
{
|
||||||
|
let this = self.clone();
|
||||||
|
let perm = self.match_perm(&new_state.state);
|
||||||
|
let grant = perm.map(|p| access.check(&user.data, p).unwrap_or(false));
|
||||||
|
|
||||||
|
let uid = user.id.clone();
|
||||||
|
// is it a return
|
||||||
|
let is_ret = new_state.state == Status::Free;
|
||||||
|
// is it a (normal) write /the user is allowed to do/?
|
||||||
|
let is_wri = new_state.state == Status::InUse(Some(uid))
|
||||||
|
&& access.check(&user.data, self.desc.privs.write).unwrap_or(false);
|
||||||
|
|
||||||
|
let f = async move {
|
||||||
|
let mut guard = this.inner.lock().await;
|
||||||
|
// either e.g. InUse(<myself>) => Free or I'm allowed to overwrite
|
||||||
|
if (is_ret && guard.is_self(uid))
|
||||||
|
|| (is_wri && guard.is_free())
|
||||||
|
|| grant.unwrap_or(false)
|
||||||
|
{
|
||||||
|
guard.do_state_change(new_state);
|
||||||
|
}
|
||||||
|
return Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
Box::pin(f)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn do_state_change(&self, new_state: MachineState)
|
pub fn do_state_change(&self, new_state: MachineState)
|
||||||
-> BoxFuture<'static, Result<()>>
|
-> BoxFuture<'static, Result<()>>
|
||||||
{
|
{
|
||||||
@ -126,6 +172,10 @@ impl Deref for Machine {
|
|||||||
/// A machine connects an event from a sensor to an actor activating/deactivating a real-world
|
/// A machine connects an event from a sensor to an actor activating/deactivating a real-world
|
||||||
/// machine, checking that the user who wants the machine (de)activated has the required
|
/// machine, checking that the user who wants the machine (de)activated has the required
|
||||||
/// permissions.
|
/// permissions.
|
||||||
|
///
|
||||||
|
/// Machines have a rather complex state machine since they have to be eventually consistent and
|
||||||
|
/// can fail at any point in time (e.g. because power cuts out suddenly, a different task on this
|
||||||
|
/// thread panics, some loaded code produces a segfault, ...)
|
||||||
pub struct Inner {
|
pub struct Inner {
|
||||||
/// Globally unique machine readable identifier
|
/// Globally unique machine readable identifier
|
||||||
pub id: MachineIdentifier,
|
pub id: MachineIdentifier,
|
||||||
@ -180,6 +230,20 @@ impl Inner {
|
|||||||
self.state.replace(state);
|
self.state.replace(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_self(&mut self, uid: UserId) -> bool {
|
||||||
|
match self.read_state().get_cloned().state {
|
||||||
|
Status::InUse(u) if u == uid => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_free(&mut self) -> bool {
|
||||||
|
match self.read_state().get_cloned().state {
|
||||||
|
Status::Free => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//pub type ReturnToken = futures::channel::oneshot::Sender<()>;
|
//pub type ReturnToken = futures::channel::oneshot::Sender<()>;
|
||||||
@ -228,7 +292,7 @@ pub struct MachineDescription {
|
|||||||
|
|
||||||
/// The permission required
|
/// The permission required
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub privs: access::PrivilegesBuf,
|
pub privs: PrivilegesBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MachineDescription {
|
impl MachineDescription {
|
||||||
|
@ -25,6 +25,8 @@ mod actor;
|
|||||||
mod initiator;
|
mod initiator;
|
||||||
mod space;
|
mod space;
|
||||||
|
|
||||||
|
mod resource;
|
||||||
|
|
||||||
use clap::{App, Arg};
|
use clap::{App, Arg};
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
@ -169,7 +171,7 @@ fn maybe(matches: clap::ArgMatches, log: Arc<Logger>) -> Result<(), Error> {
|
|||||||
|
|
||||||
let machines = machine::load(&config)?;
|
let machines = machine::load(&config)?;
|
||||||
let (actor_map, actors) = actor::load(&log, &config)?;
|
let (actor_map, actors) = actor::load(&log, &config)?;
|
||||||
let (init_map, initiators) = initiator::load(&log, &config)?;
|
let (init_map, initiators) = initiator::load(&log, &config, db.userdb.clone(), db.access.clone())?;
|
||||||
|
|
||||||
let mut network = network::Network::new(machines, actor_map, init_map);
|
let mut network = network::Network::new(machines, actor_map, init_map);
|
||||||
|
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
|
use std::io::{Read, Write};
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use smol::process::{Command, Child};
|
use smol::process::{Command, Child};
|
||||||
use smol::io::{AsyncWriteExt, AsyncReadExt};
|
use smol::io::{AsyncWrite, AsyncWriteExt, AsyncReadExt};
|
||||||
|
|
||||||
use futures::future::FutureExt;
|
use futures::future::{Future, FutureExt};
|
||||||
|
|
||||||
use crate::actor::Actuator;
|
use crate::actor::Actuator;
|
||||||
use crate::initiator::Sensor;
|
use crate::initiator::Sensor;
|
||||||
@ -19,13 +20,14 @@ use slog::Logger;
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
pub struct Batch {
|
pub struct Batch {
|
||||||
|
log: Logger,
|
||||||
userdb: UserDB,
|
userdb: UserDB,
|
||||||
name: String,
|
name: String,
|
||||||
cmd: String,
|
cmd: String,
|
||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
kill: bool,
|
kill: bool,
|
||||||
child: Child,
|
child: Child,
|
||||||
stdout: RefCell<Pin<Box<dyn AsyncWrite>>>,
|
stdout: Pin<Box<dyn AsyncWrite>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Batch {
|
impl Batch {
|
||||||
@ -39,24 +41,28 @@ impl Batch {
|
|||||||
.collect())
|
.collect())
|
||||||
.unwrap_or_else(Vec::new);
|
.unwrap_or_else(Vec::new);
|
||||||
|
|
||||||
let kill = params.get("kill_on_exit").and_then(|s|
|
let kill = params
|
||||||
s.parse()
|
.get("kill_on_exit")
|
||||||
.or_else(|| {
|
.and_then(|kill|
|
||||||
|
kill.parse()
|
||||||
|
.or_else(|_| {
|
||||||
warn!(log, "Can't parse `kill_on_exit` for {} set as {} as boolean. \
|
warn!(log, "Can't parse `kill_on_exit` for {} set as {} as boolean. \
|
||||||
Must be either \"True\" or \"False\".", &name, &s);
|
Must be either \"True\" or \"False\".", &name, &s);
|
||||||
false
|
Ok(false)
|
||||||
}));
|
})
|
||||||
|
.ok())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
info!(log, "Starting {} ({})…", &name, &cmd);
|
info!(log, "Starting {} ({})…", &name, &cmd);
|
||||||
let mut child = Self::start(&name, &cmd, &args)
|
let mut child = Self::start(&name, &cmd, &args)
|
||||||
.map_err(|err| error!(log, "Failed to spawn {} ({}): {}", &name, &cmd, err))
|
.map_err(|err| error!(log, "Failed to spawn {} ({}): {}", &name, &cmd, err))
|
||||||
.ok()?;
|
.ok()?;
|
||||||
let stdout = Self::get_stdin(&mut child);
|
let stdout = Self::get_stdout(&mut child);
|
||||||
|
|
||||||
Ok(Self { userdb, name, cmd, args, kill, child, stdout })
|
Ok(Self { log, userdb, name, cmd, args, kill, child, stdout })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_actor(name: &String, cmd: &String, args: &Vec<String>) -> Result<Child> {
|
fn start(name: &String, cmd: &String, args: &Vec<String>) -> std::io::Result<Child> {
|
||||||
let mut command = Command::new(cmd);
|
let mut command = Command::new(cmd);
|
||||||
command
|
command
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
@ -74,7 +80,7 @@ impl Batch {
|
|||||||
stdout.boxed_writer()
|
stdout.boxed_writer()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn maybe_restart(&mut self, f: &mut Option<impl Future<Item=()>>) -> bool {
|
fn maybe_restart(&mut self, f: &mut Option<BoxFuture<'static, ()>>) -> bool {
|
||||||
let stat = self.child.try_status();
|
let stat = self.child.try_status();
|
||||||
if stat.is_err() {
|
if stat.is_err() {
|
||||||
error!(self.log, "Can't check process for {} ({}) [{}]: {}",
|
error!(self.log, "Can't check process for {} ({}) [{}]: {}",
|
||||||
@ -87,22 +93,22 @@ impl Batch {
|
|||||||
let errlog = self.log.new(o!("pid" => self.child.id()));
|
let errlog = self.log.new(o!("pid" => self.child.id()));
|
||||||
// If we have any stderr try to log it
|
// If we have any stderr try to log it
|
||||||
if let Some(stderr) = self.child.stderr.take() {
|
if let Some(stderr) = self.child.stderr.take() {
|
||||||
f = Some(async move {
|
*f = Some(Box::pin(async move {
|
||||||
match stderr.into_stdio().await {
|
let mut out = String::new();
|
||||||
Err(err) => error!(errlog, "Failed to open actor process STDERR: ", err),
|
match stderr.read_to_string(&mut out).await {
|
||||||
Ok(err) => if !retv.stderr.is_empty() {
|
Err(e) => warn!(errlog, "Failed to read child stderr: {}", e),
|
||||||
let errstr = String::from_utf8_lossy(err);
|
Ok(n) => if n != 0 {
|
||||||
|
let errstr = String::from_utf8_lossy(out);
|
||||||
for line in errstr.lines() {
|
for line in errstr.lines() {
|
||||||
warn!(errlog, "{}", line);
|
warn!(errlog, "{}", line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
info!(self.log, "Attempting to re-start {}", &self.name);
|
info!(self.log, "Attempting to re-start {}", &self.name);
|
||||||
let mut child = Self::start(&self.name, &self.cmd, &self.args)
|
let mut child = Self::start(&self.name, &self.cmd, &self.args)
|
||||||
.map_err(|err| error!(log, "Failed to spawn {} ({}): {}", &self.name, &self.cmd, err))
|
.map_err(|err| error!(self.log, "Failed to spawn {} ({}): {}", &self.name, &self.cmd, err))
|
||||||
.ok();
|
.ok();
|
||||||
// Nothing else to do with the currect architecture. In reality we should fail here
|
// Nothing else to do with the currect architecture. In reality we should fail here
|
||||||
// because we *didn't apply* the change.
|
// because we *didn't apply* the change.
|
||||||
|
25
src/resource.rs
Normal file
25
src/resource.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
use core::sync::atomic;
|
||||||
|
|
||||||
|
/// A something BFFH holds internal state of
|
||||||
|
pub struct Resource {
|
||||||
|
// claims
|
||||||
|
strong: atomic::AtomicUsize,
|
||||||
|
weak: atomic::AtomicUsize,
|
||||||
|
max_strong: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A claim is taken in lieu of an user on a resource.
|
||||||
|
///
|
||||||
|
/// They come in two flavours: Weak, of which an infinite amount can exist, and Strong which may be
|
||||||
|
/// limited in number. Strong claims represent the right of the user to use this resource
|
||||||
|
/// "writable". A weak claim indicates co-usage of a resource and are mainly useful for notice and
|
||||||
|
/// information of the respective other ones. E.g. a space would be strongly claimed by keyholders
|
||||||
|
/// when they check in and released when they check out and weakly claimed by everybody else. In
|
||||||
|
/// that case the last strong claim could also fail to be released if there are outstanding weak
|
||||||
|
/// claims. Alternatively, releasing the last strong claim also releases all weak claims and sets
|
||||||
|
/// the resource to "Free" again.
|
||||||
|
///
|
||||||
|
/// Most importantly, claims can be released by *both* the claim holder and the resource.
|
||||||
|
pub struct Claim {
|
||||||
|
id: u128,
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user