mirror of
https://gitlab.com/fabinfra/fabaccess/bffh.git
synced 2024-11-25 16:17:56 +01:00
More API implementation
This commit is contained in:
parent
b8a9b64953
commit
e5903961d1
21
src/api.rs
21
src/api.rs
@ -6,6 +6,7 @@ use crate::schema::connection_capnp;
|
||||
use crate::connection::Session;
|
||||
|
||||
use crate::db::Databases;
|
||||
use crate::db::user::UserId;
|
||||
|
||||
use crate::network::Network;
|
||||
|
||||
@ -55,11 +56,27 @@ impl connection_capnp::bootstrap::Server for Bootstrap {
|
||||
_: MachineSystemParams,
|
||||
mut res: MachineSystemResults
|
||||
) -> Promise<(), capnp::Error> {
|
||||
let session = self.session.clone();
|
||||
let accessdb = self.db.access.clone();
|
||||
let nw = self.nw.clone();
|
||||
let f = async move {
|
||||
// Ensure the lock is dropped as soon as possible
|
||||
if let Some(user) = { session.user.lock().await.clone() } {
|
||||
let perms = accessdb.collect_permrules(&user.data)
|
||||
.map_err(|e| capnp::Error::failed(format!("AccessDB lookup failed: {}", e)))?;
|
||||
|
||||
// TODO actual permission check and stuff
|
||||
let c = capnp_rpc::new_client(Machines::new(self.session.clone(), self.db.clone(), self.nw.clone()));
|
||||
// Right now we only check that the user has authenticated at all.
|
||||
let c = capnp_rpc::new_client(Machines::new(user.id, perms, nw));
|
||||
res.get().set_machine_system(c);
|
||||
}
|
||||
|
||||
Promise::ok(())
|
||||
// Promise is Ok either way, just the machine system may not be set, indicating as
|
||||
// usual a lack of permission.
|
||||
Ok(())
|
||||
};
|
||||
|
||||
Promise::from_future(f)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,6 +64,7 @@ impl Callback<AppData, SessionData> for CB {
|
||||
if b {
|
||||
if let Some(s) = cap_session {
|
||||
if let Ok(Some(user)) = appdata.userdb.get_user(authid) {
|
||||
// FIXME: This should set the userid outside the callback
|
||||
s.user.try_lock().unwrap().replace(user);
|
||||
}
|
||||
}
|
||||
@ -116,7 +117,7 @@ impl authentication_system::Server for Auth {
|
||||
_: authentication_system::MechanismsParams,
|
||||
mut res: authentication_system::MechanismsResults
|
||||
) -> Promise<(), capnp::Error> {
|
||||
let mechs = match self.ctx.server_mech_list() {
|
||||
/*let mechs = match self.ctx.server_mech_list() {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
return Promise::err(capnp::Error {
|
||||
@ -131,7 +132,10 @@ impl authentication_system::Server for Auth {
|
||||
let mut res_mechs = res.get().init_mechs(mechvec.len() as u32);
|
||||
for (i, m) in mechvec.into_iter().enumerate() {
|
||||
res_mechs.set(i as u32, m);
|
||||
}
|
||||
}*/
|
||||
// For now, only PLAIN
|
||||
let mut res_mechs = res.get().init_mechs(1);
|
||||
res_mechs.set(0, "PLAIN");
|
||||
|
||||
Promise::ok(())
|
||||
}
|
||||
@ -146,6 +150,13 @@ impl authentication_system::Server for Auth {
|
||||
// Extract the MECHANISM the client wants to use and start a session.
|
||||
// Or fail at that and thrown an exception TODO: return Outcome
|
||||
let mech = pry!(req.get_mechanism());
|
||||
if pry!(req.get_mechanism()) != "PLAIN" {
|
||||
return Promise::err(capnp::Error {
|
||||
kind: capnp::ErrorKind::Failed,
|
||||
description: format!("Invalid SASL mech"),
|
||||
})
|
||||
}
|
||||
|
||||
let mut session = match self.ctx.server_start(mech) {
|
||||
Ok(s) => s,
|
||||
Err(e) =>
|
||||
|
@ -1,137 +1,115 @@
|
||||
use std::sync::Arc;
|
||||
use std::ops::Deref;
|
||||
|
||||
use capnp::capability::Promise;
|
||||
use capnp::Error;
|
||||
|
||||
use futures::FutureExt;
|
||||
|
||||
use crate::schema::machine_capnp::machine::*;
|
||||
use crate::connection::Session;
|
||||
use crate::db::Databases;
|
||||
use crate::db::machine::{Status, MachineState};
|
||||
use crate::machine::{Machine as NwMachine, ReturnToken};
|
||||
use crate::db::access::{PrivilegesBuf, PermRule};
|
||||
|
||||
use crate::db::machine::Status;
|
||||
use crate::machine::Machine as NwMachine;
|
||||
use crate::schema::machine_capnp::machine::*;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Perms {
|
||||
pub disclose: bool,
|
||||
pub read: bool,
|
||||
pub write: bool,
|
||||
pub manage: bool,
|
||||
}
|
||||
|
||||
impl Perms {
|
||||
pub fn get_for<'a, I: Iterator<Item=&'a PermRule>>(privs: &'a PrivilegesBuf, rules: I) -> Self {
|
||||
let mut disclose = false;
|
||||
let mut read = false;
|
||||
let mut write = false;
|
||||
let mut manage = false;
|
||||
for rule in rules {
|
||||
if rule.match_perm(&privs.disclose) {
|
||||
disclose = true;
|
||||
}
|
||||
if rule.match_perm(&privs.read) {
|
||||
read = true;
|
||||
}
|
||||
if rule.match_perm(&privs.write) {
|
||||
write = true;
|
||||
}
|
||||
if rule.match_perm(&privs.manage) {
|
||||
manage = true;
|
||||
}
|
||||
}
|
||||
|
||||
Self { disclose, read, write, manage }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Machine {
|
||||
session: Arc<Session>,
|
||||
perms: Perms,
|
||||
machine: NwMachine,
|
||||
db: Databases,
|
||||
}
|
||||
|
||||
impl Machine {
|
||||
pub fn new(session: Arc<Session>, machine: NwMachine, db: Databases) -> Self {
|
||||
Machine { session, machine, db }
|
||||
}
|
||||
|
||||
pub fn fill(self: Arc<Self>, builder: &mut Builder) {
|
||||
builder.set_manage(capnp_rpc::new_client(Manage(self.clone())));
|
||||
builder.set_admin(capnp_rpc::new_client(Admin(self.clone())));
|
||||
pub fn new(perms: Perms, machine: NwMachine) -> Self {
|
||||
Self { perms, machine }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Read(Arc<Machine>);
|
||||
impl info::Server for Machine {
|
||||
fn get_machine_info_extended(
|
||||
&mut self,
|
||||
_: info::GetMachineInfoExtendedParams,
|
||||
_results: info::GetMachineInfoExtendedResults,
|
||||
) -> capnp::capability::Promise<(), capnp::Error> {
|
||||
/*if self.perms.manage {
|
||||
let mut builder = results.get();
|
||||
let mut extinfo = builder.init_machine_info_extended();
|
||||
let mut current = extinfo.init_current_user();
|
||||
// FIXME fill user
|
||||
}
|
||||
Promise::ok(())*/
|
||||
|
||||
impl Read {
|
||||
pub fn new(inner: Arc<Machine>) -> Self {
|
||||
Self(inner)
|
||||
Promise::err(capnp::Error::unimplemented("Extended Infos are unavailable".to_string()))
|
||||
}
|
||||
|
||||
fn get_reservation_list(
|
||||
&mut self,
|
||||
_: info::GetReservationListParams,
|
||||
mut results: info::GetReservationListResults,
|
||||
) -> capnp::capability::Promise<(), capnp::Error> {
|
||||
Promise::err(capnp::Error::unimplemented("Reservations are unavailable".to_string()))
|
||||
}
|
||||
|
||||
fn get_property_list(
|
||||
&mut self,
|
||||
_: info::GetPropertyListParams,
|
||||
mut results: info::GetPropertyListResults,
|
||||
) -> capnp::capability::Promise<(), capnp::Error> {
|
||||
Promise::err(capnp::Error::unimplemented("Extended Properties are unavailable".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl info::Server for Read {
|
||||
}
|
||||
|
||||
struct Write(Arc<Machine>);
|
||||
|
||||
impl use_::Server for Write {
|
||||
fn use_(&mut self,
|
||||
_params: use_::UseParams,
|
||||
mut results: use_::UseResults)
|
||||
-> Promise<(), Error>
|
||||
{
|
||||
let uid = self.0.session.user.try_lock().unwrap().as_ref().map(|u| u.id.clone());
|
||||
let new_state = MachineState::used(uid.clone());
|
||||
let this = self.0.clone();
|
||||
let f = async move {
|
||||
let res_token = this.machine.request_state_change(
|
||||
this.session.user.try_lock().unwrap().as_ref(),
|
||||
new_state
|
||||
).await;
|
||||
|
||||
match res_token {
|
||||
// TODO: Do something with the token we get returned
|
||||
Ok(tok) => {
|
||||
return Ok(());
|
||||
},
|
||||
Err(e) => Err(capnp::Error::failed(format!("State change request returned {}", e))),
|
||||
}
|
||||
};
|
||||
|
||||
Promise::from_future(f)
|
||||
}
|
||||
|
||||
fn reserve(&mut self,
|
||||
_params: use_::ReserveParams,
|
||||
_results: use_::ReserveResults)
|
||||
-> Promise<(), Error>
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl in_use::Server for Write {
|
||||
fn give_back(&mut self,
|
||||
_params: in_use::GiveBackParams,
|
||||
mut results: in_use::GiveBackResults)
|
||||
-> Promise<(), Error>
|
||||
{
|
||||
let this = self.0.clone();
|
||||
|
||||
let f = async move {
|
||||
let status = this.machine.get_status().await;
|
||||
let sess = this.session.clone();
|
||||
|
||||
match status {
|
||||
Status::InUse(Some(uid)) => {
|
||||
let user = sess.user.lock().await;
|
||||
if let Some(u) = user.as_ref() {
|
||||
if u.id == uid {
|
||||
}
|
||||
}
|
||||
},
|
||||
// Machine not in use
|
||||
_ => {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Promise::from_future(f.map(|_| Ok(())))
|
||||
impl use_::Server for Machine {
|
||||
fn use_(
|
||||
&mut self,
|
||||
_: use_::UseParams,
|
||||
_: use_::UseResults
|
||||
) -> capnp::capability::Promise<(), capnp::Error> {
|
||||
Promise::ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct Manage(Arc<Machine>);
|
||||
|
||||
impl manage::Server for Manage {
|
||||
impl in_use::Server for Machine {
|
||||
}
|
||||
|
||||
struct Admin(Arc<Machine>);
|
||||
|
||||
impl admin::Server for Admin {
|
||||
fn force_set_state(&mut self,
|
||||
_params: admin::ForceSetStateParams,
|
||||
_results: admin::ForceSetStateResults)
|
||||
-> Promise<(), Error>
|
||||
{
|
||||
unimplemented!()
|
||||
impl transfer::Server for Machine {
|
||||
}
|
||||
|
||||
fn force_set_user(&mut self,
|
||||
_params: admin::ForceSetUserParams,
|
||||
_results: admin::ForceSetUserResults)
|
||||
-> Promise<(), Error>
|
||||
{
|
||||
unimplemented!()
|
||||
impl check::Server for Machine {
|
||||
}
|
||||
|
||||
impl manage::Server for Machine {
|
||||
}
|
||||
|
||||
impl admin::Server for Machine {
|
||||
}
|
||||
|
@ -3,35 +3,39 @@ use std::sync::Arc;
|
||||
use capnp::capability::Promise;
|
||||
use capnp::Error;
|
||||
|
||||
use crate::db::machine::Status;
|
||||
use crate::api::machine::*;
|
||||
use crate::schema::machine_capnp::machine::MachineState;
|
||||
use crate::schema::machinesystem_capnp::machine_system;
|
||||
use crate::schema::machinesystem_capnp::machine_system::info as machines;
|
||||
use crate::connection::Session;
|
||||
|
||||
use crate::db::Databases;
|
||||
|
||||
use crate::network::Network;
|
||||
|
||||
use super::machine::*;
|
||||
use crate::db::user::UserId;
|
||||
use crate::db::access::{PermRule, admin_perm};
|
||||
|
||||
/// An implementation of the `Machines` API
|
||||
#[derive(Clone)]
|
||||
pub struct Machines {
|
||||
/// A reference to the connection — as long as at least one API endpoint is
|
||||
/// still alive the session has to be as well.
|
||||
session: Arc<Session>,
|
||||
|
||||
db: Databases,
|
||||
user: UserId,
|
||||
permissions: Vec<PermRule>,
|
||||
network: Arc<Network>,
|
||||
}
|
||||
|
||||
impl Machines {
|
||||
pub fn new(session: Arc<Session>, db: Databases, network: Arc<Network>) -> Self {
|
||||
info!(session.log, "Machines created");
|
||||
Self { session, db, network }
|
||||
pub fn new(user: UserId, permissions: Vec<PermRule>, network: Arc<Network>) -> Self {
|
||||
Self { user, permissions, network }
|
||||
}
|
||||
}
|
||||
|
||||
impl machine_system::Server for Machines {
|
||||
|
||||
// This function shouldn't exist. See fabaccess-api issue #16
|
||||
fn info(&mut self,
|
||||
_:machine_system::InfoParams,
|
||||
mut results: machine_system::InfoResults
|
||||
) -> capnp::capability::Promise<(), capnp::Error>
|
||||
{
|
||||
results.get().set_info(capnp_rpc::new_client(self.clone()));
|
||||
Promise::ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl machines::Server for Machines {
|
||||
@ -41,18 +45,61 @@ impl machines::Server for Machines {
|
||||
-> Promise<(), Error>
|
||||
{
|
||||
let v: Vec<(String, crate::machine::Machine)> = self.network.machines.iter()
|
||||
.filter(|(_name, machine)| {
|
||||
let required_disclose = &machine.desc.privs.disclose;
|
||||
for perm_rule in self.permissions.iter() {
|
||||
if perm_rule.match_perm(required_disclose) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
})
|
||||
.map(|(n,m)| (n.clone(), m.clone()))
|
||||
.collect();
|
||||
|
||||
/*let mut machines = results.get().init_machines(v.len() as u32);
|
||||
let permissions = self.permissions.clone();
|
||||
|
||||
let f = async move {
|
||||
let mut machines = results.get().init_machine_list(v.len() as u32);
|
||||
for (i, (name, machine)) in v.into_iter().enumerate() {
|
||||
trace!(self.session.log, "Adding machine #{} {}: {:?}", i, name, machine);
|
||||
let machine = Arc::new(Machine::new(self.session.clone(), machine, self.db.clone()));
|
||||
let mut builder = machines.reborrow().get(i as u32);
|
||||
machine.fill(&mut builder);
|
||||
}*/
|
||||
let perms = Perms::get_for(&machine.desc.privs, permissions.iter());
|
||||
|
||||
Promise::ok(())
|
||||
let mut builder = machines.reborrow().get(i as u32);
|
||||
builder.set_name(&name);
|
||||
if let Some(ref desc) = machine.desc.description {
|
||||
builder.set_description(desc);
|
||||
}
|
||||
|
||||
let s = match machine.get_status().await {
|
||||
Status::Free => MachineState::Free,
|
||||
Status::Disabled => MachineState::Disabled,
|
||||
Status::Blocked(_) => MachineState::Blocked,
|
||||
Status::InUse(_) => MachineState::InUse,
|
||||
Status::Reserved(_) => MachineState::Reserved,
|
||||
Status::ToCheck(_) => MachineState::ToCheck,
|
||||
};
|
||||
builder.set_state(s);
|
||||
|
||||
if perms.write {
|
||||
builder.set_use(capnp_rpc::new_client(Machine::new(perms, machine.clone())));
|
||||
builder.set_inuse(capnp_rpc::new_client(Machine::new(perms, machine.clone())));
|
||||
}
|
||||
if perms.manage {
|
||||
builder.set_transfer(capnp_rpc::new_client(Machine::new(perms, machine.clone())));
|
||||
builder.set_check(capnp_rpc::new_client(Machine::new(perms, machine.clone())));
|
||||
builder.set_manage(capnp_rpc::new_client(Machine::new(perms, machine.clone())));
|
||||
}
|
||||
if permissions.iter().any(|r| r.match_perm(&admin_perm())) {
|
||||
builder.set_admin(capnp_rpc::new_client(Machine::new(perms, machine.clone())));
|
||||
}
|
||||
|
||||
builder.set_info(capnp_rpc::new_client(Machine::new(perms, machine)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
Promise::from_future(f)
|
||||
}
|
||||
}
|
||||
|
@ -22,10 +22,15 @@ pub struct AccessControl {
|
||||
pub internal: Internal,
|
||||
}
|
||||
|
||||
pub const ADMINPERM: &'static str = "bffh.admin";
|
||||
pub fn admin_perm() -> &'static Permission {
|
||||
Permission::new(ADMINPERM)
|
||||
}
|
||||
|
||||
impl AccessControl {
|
||||
pub fn new(internal: Internal) -> Self {
|
||||
Self {
|
||||
internal: internal,
|
||||
internal,
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,6 +52,10 @@ impl AccessControl {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
pub fn collect_permrules(&self, user: &UserData) -> Result<Vec<PermRule>> {
|
||||
self.internal.collect_permrules(user)
|
||||
}
|
||||
|
||||
pub fn dump_roles(&self) -> Result<Vec<(RoleIdentifier, Role)>> {
|
||||
self.internal.dump_roles()
|
||||
}
|
||||
@ -120,6 +129,22 @@ pub trait RoleDB {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_permrules(&self, user: &UserData) -> Result<Vec<PermRule>> {
|
||||
let mut roleset = HashMap::new();
|
||||
for role_id in user.roles.iter() {
|
||||
self.tally_role(&mut roleset, role_id)?;
|
||||
}
|
||||
|
||||
let mut output = Vec::new();
|
||||
|
||||
// Iter all unique role->permissions we've found and early return on match.
|
||||
for (_roleid, role) in roleset.iter() {
|
||||
output.extend(role.permissions.iter().cloned())
|
||||
}
|
||||
|
||||
return Ok(output);
|
||||
}
|
||||
}
|
||||
|
||||
/// A "Role" from the Authorization perspective
|
||||
@ -310,7 +335,7 @@ impl PermissionBuf {
|
||||
self.inner.push_str(perm.as_str())
|
||||
}
|
||||
|
||||
pub fn from_string(inner: String) -> Self {
|
||||
pub const fn from_string(inner: String) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
@ -429,7 +454,7 @@ pub enum PermRule {
|
||||
|
||||
impl PermRule {
|
||||
// Does this rule match that permission
|
||||
fn match_perm<P: AsRef<Permission>>(&self, perm: &P) -> bool {
|
||||
pub fn match_perm<P: AsRef<Permission>>(&self, perm: &P) -> bool {
|
||||
match self {
|
||||
PermRule::Base(ref base) => base.as_permission() == perm.as_ref(),
|
||||
PermRule::Children(ref parent) => parent.as_permission() > perm.as_ref() ,
|
||||
|
@ -77,21 +77,19 @@ impl Index {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Machine {
|
||||
pub id: uuid::Uuid,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub desc: MachineDescription,
|
||||
|
||||
inner: Arc<Mutex<Inner>>,
|
||||
access: Arc<access::AccessControl>,
|
||||
}
|
||||
|
||||
impl Machine {
|
||||
pub fn new(inner: Inner, access: Arc<access::AccessControl>) -> Self {
|
||||
pub fn new(inner: Inner, desc: MachineDescription, access: Arc<access::AccessControl>) -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::default(),
|
||||
name: "".to_string(),
|
||||
description: "".to_string(),
|
||||
inner: Arc::new(Mutex::new(inner)),
|
||||
access: access,
|
||||
access,
|
||||
desc,
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,7 +100,7 @@ impl Machine {
|
||||
, access: Arc<access::AccessControl>
|
||||
) -> Machine
|
||||
{
|
||||
Self::new(Inner::new(id, desc, state), access)
|
||||
Self::new(Inner::new(id, state), desc, access)
|
||||
}
|
||||
|
||||
pub fn from_file<P: AsRef<Path>>(path: P, access: Arc<access::AccessControl>)
|
||||
@ -129,14 +127,14 @@ impl Machine {
|
||||
|
||||
let f = async move {
|
||||
if let Some(udata) = udata {
|
||||
let mut guard = this.inner.try_lock().unwrap();
|
||||
if this.access.check(&udata, &guard.desc.privs.write).await? {
|
||||
if this.access.check(&udata, &this.desc.privs.write).await? {
|
||||
let mut guard = this.inner.lock().await;
|
||||
guard.do_state_change(new_state);
|
||||
return Ok(ReturnToken::new(this.inner.clone()))
|
||||
}
|
||||
} else {
|
||||
if new_state == MachineState::free() {
|
||||
let mut guard = this.inner.try_lock().unwrap();
|
||||
let mut guard = this.inner.lock().await;
|
||||
guard.do_state_change(new_state);
|
||||
return Ok(ReturnToken::new(this.inner.clone()));
|
||||
}
|
||||
@ -148,6 +146,20 @@ impl Machine {
|
||||
Box::pin(f)
|
||||
}
|
||||
|
||||
pub fn do_state_change(&self, new_state: MachineState)
|
||||
-> BoxFuture<'static, Result<ReturnToken>>
|
||||
{
|
||||
let this = self.clone();
|
||||
|
||||
let f = async move {
|
||||
let mut guard = this.inner.lock().await;
|
||||
guard.do_state_change(new_state);
|
||||
return Ok(ReturnToken::new(this.inner.clone()))
|
||||
};
|
||||
|
||||
Box::pin(f)
|
||||
}
|
||||
|
||||
pub fn create_token(&self) -> ReturnToken {
|
||||
ReturnToken::new(self.inner.clone())
|
||||
}
|
||||
@ -161,6 +173,10 @@ impl Machine {
|
||||
let guard = self.inner.try_lock().unwrap();
|
||||
guard.signal()
|
||||
}
|
||||
|
||||
pub fn get_inner(&self) -> Arc<Mutex<Inner>> {
|
||||
self.inner.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Machine {
|
||||
@ -182,9 +198,6 @@ pub struct Inner {
|
||||
/// Globally unique machine readable identifier
|
||||
pub id: MachineIdentifier,
|
||||
|
||||
/// Descriptor of the machine
|
||||
pub desc: MachineDescription,
|
||||
|
||||
/// The state of the machine as bffh thinks the machine *should* be in.
|
||||
///
|
||||
/// This is a Signal generator. Subscribers to this signal will be notified of changes. In the
|
||||
@ -195,13 +208,11 @@ pub struct Inner {
|
||||
|
||||
impl Inner {
|
||||
pub fn new ( id: MachineIdentifier
|
||||
, desc: MachineDescription
|
||||
, state: MachineState
|
||||
) -> Inner
|
||||
{
|
||||
Inner {
|
||||
id: id,
|
||||
desc: desc,
|
||||
id,
|
||||
state: Mutable::new(state),
|
||||
reset: None,
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user