mirror of
https://gitlab.com/fabinfra/fabaccess/bffh.git
synced 2024-11-22 23:07:56 +01:00
Actually make compile for once.
This commit is contained in:
parent
5c5a59a75c
commit
7956616891
12
src/api.rs
12
src/api.rs
@ -38,18 +38,10 @@ impl connection_capnp::bootstrap::Server for Bootstrap {
|
|||||||
// Forbid mutltiple authentication for now
|
// Forbid mutltiple authentication for now
|
||||||
// TODO: When should we allow multiple auth and how do me make sure that does not leak
|
// TODO: When should we allow multiple auth and how do me make sure that does not leak
|
||||||
// priviledges (e.g. due to previously issues caps)?
|
// priviledges (e.g. due to previously issues caps)?
|
||||||
let session = self.session.clone();
|
|
||||||
let check_perm_future = session.check_permission(&builtin::AUTH_PERM);
|
|
||||||
let f = async {
|
|
||||||
let r = check_perm_future.await.unwrap();
|
|
||||||
if r {
|
|
||||||
res.get().set_auth(capnp_rpc::new_client(auth::Auth::new(session.clone())))
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
res.get().set_auth(capnp_rpc::new_client(auth::Auth::new(self.session.clone())));
|
||||||
};
|
|
||||||
|
|
||||||
Promise::from_future(f)
|
Promise::ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn permissions(&mut self,
|
fn permissions(&mut self,
|
||||||
|
42
src/builtin.rs
Normal file
42
src/builtin.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use crate::db::access::{
|
||||||
|
Permission,
|
||||||
|
PermissionBuf,
|
||||||
|
PermRule,
|
||||||
|
RoleIdentifier,
|
||||||
|
Role,
|
||||||
|
};
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref AUTH_PERM: &'static Permission = Permission::new("bffh.auth");
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// lazy_static! {
|
||||||
|
// pub static ref AUTH_ROLE: RoleIdentifier = {
|
||||||
|
// RoleIdentifier::Local {
|
||||||
|
// name: "mayauth".to_string(),
|
||||||
|
// source: "builtin".to_string(),
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// lazy_static! {
|
||||||
|
// pub static ref DEFAULT_ROLEIDS: [RoleIdentifier; 1] = {
|
||||||
|
// [ AUTH_ROLE.clone(), ]
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// pub static ref DEFAULT_ROLES: HashMap<RoleIdentifier, Role> = {
|
||||||
|
// let mut m = HashMap::new();
|
||||||
|
// m.insert(AUTH_ROLE.clone(),
|
||||||
|
// Role {
|
||||||
|
// parents: vec![],
|
||||||
|
// permissions: vec![
|
||||||
|
// PermRule::Base(PermissionBuf::from_perm(AUTH_PERM)),
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
// m
|
||||||
|
// };
|
||||||
|
// }
|
@ -14,7 +14,7 @@ use crate::schema::connection_capnp;
|
|||||||
|
|
||||||
use crate::db::Databases;
|
use crate::db::Databases;
|
||||||
use crate::db::access::{AccessControl, Permission};
|
use crate::db::access::{AccessControl, Permission};
|
||||||
use crate::db::user::AuthzContext;
|
use crate::db::user::User;
|
||||||
use crate::builtin;
|
use crate::builtin;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -23,23 +23,23 @@ use crate::builtin;
|
|||||||
pub struct Session {
|
pub struct Session {
|
||||||
// Session-spezific log
|
// Session-spezific log
|
||||||
pub log: Logger,
|
pub log: Logger,
|
||||||
authz_data: Option<AuthzContext>,
|
user: Option<User>,
|
||||||
accessdb: Arc<AccessControl>,
|
accessdb: Arc<AccessControl>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
pub fn new(log: Logger, accessdb: Arc<AccessControl>) -> Self {
|
pub fn new(log: Logger, accessdb: Arc<AccessControl>) -> Self {
|
||||||
let authz_data = None;
|
let user = None;
|
||||||
|
|
||||||
Session { log, authz_data, accessdb }
|
Session { log, user, accessdb }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the current session has a certain permission
|
/// Check if the current session has a certain permission
|
||||||
pub async fn check_permission<P: AsRef<Permission>>(&self, perm: &P) -> Result<bool> {
|
pub async fn check_permission<P: AsRef<Permission>>(&self, perm: &P) -> Result<bool> {
|
||||||
if let Some(user) = self.authz_data.as_ref() {
|
if let Some(user) = self.user.as_ref() {
|
||||||
self.accessdb.check(user, perm).await
|
self.accessdb.check(&user.data, perm).await
|
||||||
} else {
|
} else {
|
||||||
self.accessdb.check_roles(builtin::DEFAULT_ROLEIDS, perm).await
|
Ok(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ use crate::error::Result;
|
|||||||
|
|
||||||
pub mod internal;
|
pub mod internal;
|
||||||
|
|
||||||
use crate::db::user::AuthzContext;
|
use crate::db::user::UserData;
|
||||||
pub use internal::init;
|
pub use internal::init;
|
||||||
|
|
||||||
pub struct AccessControl {
|
pub struct AccessControl {
|
||||||
@ -49,7 +49,7 @@ impl AccessControl {
|
|||||||
self.sources.insert(name, source);
|
self.sources.insert(name, source);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check<P: AsRef<Permission>>(&self, user: &AuthzContext, perm: &P) -> Result<bool> {
|
pub async fn check<P: AsRef<Permission>>(&self, user: &UserData, perm: &P) -> Result<bool> {
|
||||||
for v in self.sources.values() {
|
for v in self.sources.values() {
|
||||||
if v.check(user, perm.as_ref())? {
|
if v.check(user, perm.as_ref())? {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
@ -74,7 +74,7 @@ impl AccessControl {
|
|||||||
|
|
||||||
impl fmt::Debug for AccessControl {
|
impl fmt::Debug for AccessControl {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
let b = f.debug_struct("AccessControl");
|
let mut b = f.debug_struct("AccessControl");
|
||||||
for (name, roledb) in self.sources.iter() {
|
for (name, roledb) in self.sources.iter() {
|
||||||
b.field(name, &roledb.get_type_name().to_string());
|
b.field(name, &roledb.get_type_name().to_string());
|
||||||
}
|
}
|
||||||
@ -91,7 +91,7 @@ pub trait RoleDB {
|
|||||||
///
|
///
|
||||||
/// Default implementation which adapter may overwrite with more efficient specialized
|
/// Default implementation which adapter may overwrite with more efficient specialized
|
||||||
/// implementations.
|
/// implementations.
|
||||||
fn check(&self, user: &AuthzContext, perm: &Permission) -> Result<bool> {
|
fn check(&self, user: &UserData, perm: &Permission) -> Result<bool> {
|
||||||
self.check_roles(&user.roles, perm)
|
self.check_roles(&user.roles, perm)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -394,7 +394,7 @@ pub struct Permission {
|
|||||||
inner: str
|
inner: str
|
||||||
}
|
}
|
||||||
impl Permission {
|
impl Permission {
|
||||||
pub const fn new<S: AsRef<str> + ?Sized>(s: &S) -> &Permission {
|
pub fn new<S: AsRef<str> + ?Sized>(s: &S) -> &Permission {
|
||||||
unsafe { &*(s.as_ref() as *const str as *const Permission) }
|
unsafe { &*(s.as_ref() as *const str as *const Permission) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ use crate::config::Settings;
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
|
||||||
use crate::db::access::{Permission, Role, RoleIdentifier, RoleDB};
|
use crate::db::access::{Permission, Role, RoleIdentifier, RoleDB};
|
||||||
use crate::db::user::AuthzContext;
|
use crate::db::user::{User, UserData};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Internal {
|
pub struct Internal {
|
||||||
@ -34,7 +34,7 @@ impl Internal {
|
|||||||
|
|
||||||
/// Check if a given user has the given permission
|
/// Check if a given user has the given permission
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub fn _check<T: Transaction, P: AsRef<Permission>>(&self, txn: &T, user: &AuthzContext, perm: &P)
|
pub fn _check<T: Transaction, P: AsRef<Permission>>(&self, txn: &T, user: &UserData, perm: &P)
|
||||||
-> Result<bool>
|
-> Result<bool>
|
||||||
{
|
{
|
||||||
// Tally all roles. Makes dependent roles easier
|
// Tally all roles. Makes dependent roles easier
|
||||||
@ -154,7 +154,7 @@ impl RoleDB for Internal {
|
|||||||
"Internal"
|
"Internal"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check(&self, user: &AuthzContext, perm: &Permission) -> Result<bool> {
|
fn check(&self, user: &UserData, perm: &Permission) -> Result<bool> {
|
||||||
let txn = self.env.begin_ro_txn()?;
|
let txn = self.env.begin_ro_txn()?;
|
||||||
self._check(&txn, user, &perm)
|
self._check(&txn, user, &perm)
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,8 @@ use crate::registries::StatusSignal;
|
|||||||
|
|
||||||
use crate::machine::MachineDescription;
|
use crate::machine::MachineDescription;
|
||||||
|
|
||||||
|
use crate::db::user::UserId;
|
||||||
|
|
||||||
pub mod internal;
|
pub mod internal;
|
||||||
use internal::Internal;
|
use internal::Internal;
|
||||||
|
|
||||||
@ -42,15 +44,15 @@ pub enum Status {
|
|||||||
/// Not currently used by anybody
|
/// Not currently used by anybody
|
||||||
Free,
|
Free,
|
||||||
/// Used by somebody
|
/// Used by somebody
|
||||||
InUse(UserIdentifier),
|
InUse(UserId),
|
||||||
/// Was used by somebody and now needs to be checked for cleanliness
|
/// Was used by somebody and now needs to be checked for cleanliness
|
||||||
ToCheck(UserIdentifier),
|
ToCheck(UserId),
|
||||||
/// Not used by anybody but also can not be used. E.g. down for maintenance
|
/// Not used by anybody but also can not be used. E.g. down for maintenance
|
||||||
Blocked(UserIdentifier),
|
Blocked(UserId),
|
||||||
/// Disabled for some other reason
|
/// Disabled for some other reason
|
||||||
Disabled,
|
Disabled,
|
||||||
/// Reserved
|
/// Reserved
|
||||||
Reserved(UserIdentifier),
|
Reserved(UserId),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uuid_from_api(uuid: crate::schema::api_capnp::u_u_i_d::Reader) -> Uuid {
|
pub fn uuid_from_api(uuid: crate::schema::api_capnp::u_u_i_d::Reader) -> Uuid {
|
||||||
|
40
src/db/pass.rs
Normal file
40
src/db/pass.rs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use argon2;
|
||||||
|
use lmdb::{Environment, Transaction, RwTransaction, Cursor};
|
||||||
|
use rand::prelude::*;
|
||||||
|
use slog::Logger;
|
||||||
|
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
pub struct PassDB {
|
||||||
|
log: Logger,
|
||||||
|
env: Arc<Environment>,
|
||||||
|
db: lmdb::Database,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PassDB {
|
||||||
|
pub fn new(log: Logger, env: Arc<Environment>, db: lmdb::Database) -> Self {
|
||||||
|
Self { log, env, db }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check<T: Transaction>(&self, txn: &T, authcid: &str, password: &[u8]) -> Result<Option<bool>> {
|
||||||
|
match txn.get(self.db, &authcid.as_bytes()) {
|
||||||
|
Ok(bytes) => {
|
||||||
|
let encoded = unsafe { std::str::from_utf8_unchecked(bytes) };
|
||||||
|
let res = argon2::verify_encoded(encoded, password)?;
|
||||||
|
Ok(Some(res))
|
||||||
|
},
|
||||||
|
Err(lmdb::Error::NotFound) => { Ok(None) },
|
||||||
|
Err(e) => { Err(e.into()) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn store(&self, txn: &mut RwTransaction, authcid: &str, password: &[u8]) -> Result<()> {
|
||||||
|
let config = argon2::Config::default();
|
||||||
|
let salt: [u8; 16] = rand::random();
|
||||||
|
let hash = argon2::hash_encoded(password, &salt, &config)?;
|
||||||
|
txn.put(self.db, &authcid.as_bytes(), &hash.as_bytes(), lmdb::WriteFlags::empty())
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,25 @@
|
|||||||
|
//! UserDB does two kinds of lookups:
|
||||||
|
//! 1. "I have this here username, what user is that"
|
||||||
|
//! 2. "I have this here user, what are their roles (and other associated data)"
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use crate::db::access::RoleIdentifier;
|
use crate::db::access::RoleIdentifier;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
mod internal;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct User {
|
||||||
|
pub id: UserId,
|
||||||
|
pub data: UserData,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
/// Authorization Identity
|
/// Authorization Identity
|
||||||
///
|
///
|
||||||
/// This identity is internal to FabAccess and completely independent from the authentication
|
/// This identity is internal to FabAccess and completely independent from the authentication
|
||||||
/// method or source
|
/// method or source
|
||||||
struct AuthZId {
|
pub struct UserId {
|
||||||
/// Main User ID. Generally an user name or similar
|
/// Main User ID. Generally an user name or similar
|
||||||
uid: String,
|
uid: String,
|
||||||
/// Sub user ID.
|
/// Sub user ID.
|
||||||
@ -16,20 +27,36 @@ struct AuthZId {
|
|||||||
/// Can change scopes for permissions, e.g. having a +admin account with more permissions than
|
/// Can change scopes for permissions, e.g. having a +admin account with more permissions than
|
||||||
/// the default account and +dashboard et.al. accounts that have restricted permissions for
|
/// the default account and +dashboard et.al. accounts that have restricted permissions for
|
||||||
/// their applications
|
/// their applications
|
||||||
subuid: String,
|
subuid: Option<String>,
|
||||||
/// Realm this account originates.
|
/// Realm this account originates.
|
||||||
///
|
///
|
||||||
/// The Realm is usually described by a domain name but local policy may dictate an unrelated
|
/// The Realm is usually described by a domain name but local policy may dictate an unrelated
|
||||||
/// mapping
|
/// mapping
|
||||||
realm: String,
|
realm: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A Person, from the Authorization perspective
|
impl UserId {
|
||||||
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
|
pub fn new(uid: String, subuid: Option<String>, realm: Option<String>) -> Self {
|
||||||
pub struct AuthzContext {
|
Self { uid, subuid, realm }
|
||||||
/// The identification of this user.
|
}
|
||||||
pub id: AuthZId,
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for UserId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let r = write!(f, "{}", self.uid);
|
||||||
|
if let Some(ref s) = self.subuid {
|
||||||
|
write!(f, "+{}", s)?;
|
||||||
|
}
|
||||||
|
if let Some(ref l) = self.realm {
|
||||||
|
write!(f, "@{}", l)?;
|
||||||
|
}
|
||||||
|
r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
|
||||||
|
/// A Person, from the Authorization perspective
|
||||||
|
pub struct UserData {
|
||||||
/// A Person has N ≥ 0 roles.
|
/// A Person has N ≥ 0 roles.
|
||||||
/// Persons are only ever given roles, not permissions directly
|
/// Persons are only ever given roles, not permissions directly
|
||||||
pub roles: Vec<RoleIdentifier>,
|
pub roles: Vec<RoleIdentifier>,
|
||||||
@ -47,7 +74,7 @@ mod tests {
|
|||||||
fn format_uid_test() {
|
fn format_uid_test() {
|
||||||
let uid = "testuser".to_string();
|
let uid = "testuser".to_string();
|
||||||
let suid = "testsuid".to_string();
|
let suid = "testsuid".to_string();
|
||||||
let location = "testloc".to_string();
|
let realm = "testloc".to_string();
|
||||||
|
|
||||||
assert_eq!("testuser",
|
assert_eq!("testuser",
|
||||||
format!("{}", UserIdentifier::new(uid.clone(), None, None)));
|
format!("{}", UserIdentifier::new(uid.clone(), None, None)));
|
||||||
@ -56,6 +83,6 @@ mod tests {
|
|||||||
assert_eq!("testuser+testsuid",
|
assert_eq!("testuser+testsuid",
|
||||||
format!("{}", UserIdentifier::new(uid.clone(), Some(suid.clone()), None)));
|
format!("{}", UserIdentifier::new(uid.clone(), Some(suid.clone()), None)));
|
||||||
assert_eq!("testuser+testsuid@testloc",
|
assert_eq!("testuser+testsuid@testloc",
|
||||||
format!("{}", UserIdentifier::new(uid, Some(suid), Some(location))));
|
format!("{}", UserIdentifier::new(uid, Some(suid), Some(realm))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
49
src/db/user/internal.rs
Normal file
49
src/db/user/internal.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use slog::Logger;
|
||||||
|
use lmdb::{Environment, Transaction, RwTransaction, Cursor};
|
||||||
|
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Internal {
|
||||||
|
log: Logger,
|
||||||
|
env: Arc<Environment>,
|
||||||
|
db: lmdb::Database,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Internal {
|
||||||
|
pub fn new(log: Logger, env: Arc<Environment>, db: lmdb::Database) -> Self {
|
||||||
|
Self { log, env, db }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_user_txn<T: Transaction>(&self, txn: &T, uid: &str) -> Result<Option<User>> {
|
||||||
|
match txn.get(self.db, &uid.as_bytes()) {
|
||||||
|
Ok(bytes) => {
|
||||||
|
Ok(Some(flexbuffers::from_slice(bytes)?))
|
||||||
|
},
|
||||||
|
Err(lmdb::Error::NotFound) => Ok(None),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn get_user(&self, uid: &str) -> Result<Option<User>> {
|
||||||
|
let txn = self.env.begin_ro_txn()?;
|
||||||
|
self.get_user_txn(&txn, uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn put_user_txn(&self, txn: &mut RwTransaction, uid: &str, user: &User) -> Result<()> {
|
||||||
|
let bytes = flexbuffers::to_vec(user)?;
|
||||||
|
txn.put(self.db, &uid.as_bytes(), &bytes, lmdb::WriteFlags::empty())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn put_user(&self, uid: &str, user: &User) -> Result<()> {
|
||||||
|
let mut txn = self.env.begin_rw_txn()?;
|
||||||
|
self.put_user_txn(&mut txn, uid, user)?;
|
||||||
|
txn.commit()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -66,6 +66,9 @@ impl fmt::Display for Error {
|
|||||||
Error::Config(e) => {
|
Error::Config(e) => {
|
||||||
write!(f, "Failed to parse config: {}", e)
|
write!(f, "Failed to parse config: {}", e)
|
||||||
}
|
}
|
||||||
|
Error::Argon2(e) => {
|
||||||
|
write!(f, "Argon2 en/decoding failure: {}", e)
|
||||||
|
}
|
||||||
Error::BadVersion((major,minor)) => {
|
Error::BadVersion((major,minor)) => {
|
||||||
write!(f, "Peer uses API version {}.{} which is incompatible!", major, minor)
|
write!(f, "Peer uses API version {}.{} which is incompatible!", major, minor)
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ use crate::error::Result;
|
|||||||
|
|
||||||
use crate::db::access;
|
use crate::db::access;
|
||||||
use crate::db::machine::{MachineIdentifier, Status, MachineState};
|
use crate::db::machine::{MachineIdentifier, Status, MachineState};
|
||||||
|
use crate::db::user::User;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
/// Internal machine representation
|
/// Internal machine representation
|
||||||
@ -66,7 +67,7 @@ impl Machine {
|
|||||||
) -> Result<bool>
|
) -> Result<bool>
|
||||||
{
|
{
|
||||||
// TODO: Check different levels
|
// TODO: Check different levels
|
||||||
if access.check(who, &self.desc.privs.write).await? {
|
if access.check(&who.data, &self.desc.privs.write).await? {
|
||||||
self.state.set(MachineState { state: Status::InUse(who.id.clone()) });
|
self.state.set(MachineState { state: Status::InUse(who.id.clone()) });
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
} else {
|
} else {
|
||||||
|
0
src/user.rs
Normal file
0
src/user.rs
Normal file
Loading…
Reference in New Issue
Block a user