fix: set and unset acting usr based on actor
This commit is contained in:
parent
dc27262328
commit
7e89edc30f
@ -161,7 +161,9 @@ fn handle_request<A>(app: &A, req: Addrd<Req<ToadT>>) -> Addrd<Message>
|
|||||||
let actor = dbg!(actor);
|
let actor = dbg!(actor);
|
||||||
|
|
||||||
let not_found = || {
|
let not_found = || {
|
||||||
log::info!("{:?} {} not found", req.data().msg().code, req.data().msg().path_string().unwrap());
|
log::info!("{:?} {} not found",
|
||||||
|
req.data().msg().code,
|
||||||
|
req.data().msg().path_string().unwrap());
|
||||||
Message::builder(Type::Ack, toad::resp::code::NOT_FOUND).token(req.data().msg().token)
|
Message::builder(Type::Ack, toad::resp::code::NOT_FOUND).token(req.data().msg().token)
|
||||||
.build()
|
.build()
|
||||||
};
|
};
|
||||||
|
@ -99,20 +99,20 @@ impl<Db> GroupRepo<DbError<Db>> for GroupRepoImpl<Db> where Db: Postgres + 'stat
|
|||||||
static QUERY: &'static str = "select g.tag :: text, g.uid :: text from public.usr_groups(($1 :: text) :: human_uuid.huid) as g";
|
static QUERY: &'static str = "select g.tag :: text, g.uid :: text from public.usr_groups(($1 :: text) :: human_uuid.huid) as g";
|
||||||
|
|
||||||
let grps = self.0
|
let grps = self.0
|
||||||
.with_client(|c| c.query(QUERY, &[&id.as_ref()]))
|
.with_client(actor, |c| c.query(QUERY, &[&id.as_ref()]))
|
||||||
.and_then(|rows| {
|
.and_then(|rows| {
|
||||||
rows.iter()
|
rows.iter()
|
||||||
.map(Group::unmarshal)
|
.map(Group::unmarshal)
|
||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
})
|
})
|
||||||
.map_err(GroupRepoError::Other)?;
|
.map_err(GroupRepoError::Other)?;
|
||||||
|
|
||||||
let paths = grps.iter().map(|g| Path::parse(format!("/groups/{}/members", g.uid))).collect::<Vec<_>>();
|
let paths = grps.iter()
|
||||||
|
.map(|g| Path::parse(format!("/groups/{}/members", g.uid)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
if !self.0
|
if !self.0
|
||||||
.authorized_all(&paths,
|
.authorized_all(&paths, Mode::Read, actor)
|
||||||
Mode::Read,
|
|
||||||
actor)
|
|
||||||
.map_err(GroupRepoError::Other)?
|
.map_err(GroupRepoError::Other)?
|
||||||
{
|
{
|
||||||
return Err(GroupRepoError::Unauthorized);
|
return Err(GroupRepoError::Unauthorized);
|
||||||
@ -134,7 +134,7 @@ impl<Db> GroupRepo<DbError<Db>> for GroupRepoImpl<Db> where Db: Postgres + 'stat
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.0
|
self.0
|
||||||
.with_client(|c| c.query(QUERY, &[&id.as_ref()]))
|
.with_client(actor, |c| c.query(QUERY, &[&id.as_ref()]))
|
||||||
.and_then(|rows| {
|
.and_then(|rows| {
|
||||||
rows.into_iter()
|
rows.into_iter()
|
||||||
.map(|row| row.try_get::<_, String>("u.uid").map(UserId::from))
|
.map(|row| row.try_get::<_, String>("u.uid").map(UserId::from))
|
||||||
@ -167,7 +167,7 @@ impl<Db> ReadOne for GroupRepoImpl<Db> where Db: Postgres + 'static
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.0
|
self.0
|
||||||
.with_client(|c| c.query_opt(QUERY, &[&id.as_ref()]))
|
.with_client(actor, |c| c.query_opt(QUERY, &[&id.as_ref()]))
|
||||||
.and_then(|opt| {
|
.and_then(|opt| {
|
||||||
opt.as_ref()
|
opt.as_ref()
|
||||||
.map(Group::unmarshal)
|
.map(Group::unmarshal)
|
||||||
@ -204,7 +204,7 @@ impl<Db> ReadMany for GroupRepoImpl<Db> where Db: Postgres + 'static
|
|||||||
|
|
||||||
let grps: Vec<Group> =
|
let grps: Vec<Group> =
|
||||||
self.0
|
self.0
|
||||||
.with_client(|c| match page.after {
|
.with_client(actor, |c| match page.after {
|
||||||
| Some(after) => {
|
| Some(after) => {
|
||||||
c.query(&QUERY_AFTER_LINES.iter()
|
c.query(&QUERY_AFTER_LINES.iter()
|
||||||
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
||||||
@ -252,9 +252,9 @@ impl<Db> Patch for GroupRepoImpl<Db> where Db: Postgres + 'static
|
|||||||
|
|
||||||
let paths =
|
let paths =
|
||||||
vec![Some(format!("/groups/{id}/name")).filter(|_| state.name.is_some())].into_iter()
|
vec![Some(format!("/groups/{id}/name")).filter(|_| state.name.is_some())].into_iter()
|
||||||
.filter_map(identity)
|
.filter_map(identity)
|
||||||
.map(Path::parse)
|
.map(Path::parse)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
if !self.0
|
if !self.0
|
||||||
.authorized_all(&paths, Mode::Write, actor)
|
.authorized_all(&paths, Mode::Write, actor)
|
||||||
.map_err(GroupRepoError::Other)?
|
.map_err(GroupRepoError::Other)?
|
||||||
@ -263,7 +263,7 @@ impl<Db> Patch for GroupRepoImpl<Db> where Db: Postgres + 'static
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.0
|
self.0
|
||||||
.with_client(|c| {
|
.with_client(actor, |c| {
|
||||||
c.execute(&QUERY_LINES.iter()
|
c.execute(&QUERY_LINES.iter()
|
||||||
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
||||||
&[&id.as_ref(), &state.name.as_ref().map(|n| n.as_ref())])
|
&[&id.as_ref(), &state.name.as_ref().map(|n| n.as_ref())])
|
||||||
@ -292,7 +292,7 @@ impl<Db> Insert for GroupRepoImpl<Db> where Db: Postgres + 'static
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.0
|
self.0
|
||||||
.with_client(|c| {
|
.with_client(actor, |c| {
|
||||||
c.query_one(&QUERY_LINES.iter()
|
c.query_one(&QUERY_LINES.iter()
|
||||||
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
||||||
&[&insert.name.as_ref()])
|
&[&insert.name.as_ref()])
|
||||||
@ -318,7 +318,7 @@ impl<Db> Del for GroupRepoImpl<Db> where Db: Postgres + 'static
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.0
|
self.0
|
||||||
.with_client(|c| c.execute(QUERY, &[&id.as_ref()]))
|
.with_client(actor, |c| c.execute(QUERY, &[&id.as_ref()]))
|
||||||
.map(|n| n == 1)
|
.map(|n| n == 1)
|
||||||
.map_err(GroupRepoError::Other)
|
.map_err(GroupRepoError::Other)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use postgres::{GenericClient, GenericRow};
|
use postgres::{GenericClient, GenericRow};
|
||||||
|
|
||||||
|
use super::Actor;
|
||||||
use crate::newtype;
|
use crate::newtype;
|
||||||
use crate::postgres::{DbError, Postgres};
|
use crate::postgres::{DbError, Postgres};
|
||||||
use crate::repo::Ext;
|
use crate::repo::Ext;
|
||||||
@ -10,7 +11,11 @@ newtype!(
|
|||||||
);
|
);
|
||||||
|
|
||||||
pub trait HashedTextExt: Ext {
|
pub trait HashedTextExt: Ext {
|
||||||
fn matches<S: AsRef<str>>(&self, this: &HashedText, other: S) -> Result<bool, Self::Error>;
|
fn matches<S: AsRef<str>>(&self,
|
||||||
|
actor: &Actor,
|
||||||
|
this: &HashedText,
|
||||||
|
other: S)
|
||||||
|
-> Result<bool, Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct HashedTextExtImpl<Db: Postgres>(pub &'static Db);
|
pub struct HashedTextExtImpl<Db: Postgres>(pub &'static Db);
|
||||||
@ -22,11 +27,17 @@ impl<Db> Ext for HashedTextExtImpl<Db> where Db: Postgres
|
|||||||
|
|
||||||
impl<Db> HashedTextExt for HashedTextExtImpl<Db> where Db: Postgres
|
impl<Db> HashedTextExt for HashedTextExtImpl<Db> where Db: Postgres
|
||||||
{
|
{
|
||||||
fn matches<S: AsRef<str>>(&self, this: &HashedText, other: S) -> Result<bool, DbError<Db>> {
|
fn matches<S: AsRef<str>>(&self,
|
||||||
|
actor: &Actor,
|
||||||
|
this: &HashedText,
|
||||||
|
other: S)
|
||||||
|
-> Result<bool, DbError<Db>> {
|
||||||
static QUERY: &str = "select public.hashed_text_matches($1, public.hashed_text_of_string($2))";
|
static QUERY: &str = "select public.hashed_text_matches($1, public.hashed_text_of_string($2))";
|
||||||
|
|
||||||
self.0
|
self.0
|
||||||
.with_client(|client| client.query_one(QUERY, &[&other.as_ref(), &this.0.as_str()]))
|
.with_client(actor, |client| {
|
||||||
|
client.query_one(QUERY, &[&other.as_ref(), &this.0.as_str()])
|
||||||
|
})
|
||||||
.and_then(|row| row.try_get(0))
|
.and_then(|row| row.try_get(0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -36,13 +47,18 @@ mod test {
|
|||||||
use postgres::types::Type;
|
use postgres::types::Type;
|
||||||
|
|
||||||
use super::{HashedText, HashedTextExt, HashedTextExtImpl};
|
use super::{HashedText, HashedTextExt, HashedTextExtImpl};
|
||||||
|
use crate::model::Actor;
|
||||||
use crate::postgres::test::{from_sql_owned, Client, Row};
|
use crate::postgres::test::{from_sql_owned, Client, Row};
|
||||||
use crate::postgres::{Postgres, PostgresImpl};
|
use crate::postgres::{Postgres, PostgresImpl};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hashed_text_matches_fn_call() {
|
fn hashed_text_matches_fn_call() {
|
||||||
let client = || Client { query_one: Box::new(|_, q, ps| {
|
let client = || Client { query_one: Box::new(|_, q, ps| {
|
||||||
assert_eq!(q.unwrap_str(), "select public.hashed_text_matches($1, public.hashed_text_of_string($2))");
|
let q = q.unwrap_str();
|
||||||
|
if q.contains("set_acting_usr") {
|
||||||
|
return Ok(Row::new(vec![]));
|
||||||
|
}
|
||||||
|
assert_eq!(q, "select public.hashed_text_matches($1, public.hashed_text_of_string($2))");
|
||||||
assert_eq!(from_sql_owned::<String>(ps[1]), String::from("XXX"));
|
assert_eq!(from_sql_owned::<String>(ps[1]), String::from("XXX"));
|
||||||
|
|
||||||
Ok(Row::new(vec![("",
|
Ok(Row::new(vec![("",
|
||||||
@ -57,9 +73,9 @@ mod test {
|
|||||||
std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&pg)
|
std::mem::transmute::<_, &'static PostgresImpl<Client<()>>>(&pg)
|
||||||
});
|
});
|
||||||
|
|
||||||
assert!(htext.matches(&HashedText(String::from("XXX")), "foo")
|
assert!(htext.matches(&Actor::default(), &HashedText(String::from("XXX")), "foo")
|
||||||
.unwrap());
|
.unwrap());
|
||||||
assert!(!htext.matches(&HashedText(String::from("XXX")), "foob")
|
assert!(!htext.matches(&Actor::default(), &HashedText(String::from("XXX")), "foob")
|
||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -128,7 +128,7 @@ impl<Db> ReadOne for UserRepoImpl<Db> where Db: Postgres + 'static
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.0
|
self.0
|
||||||
.with_client(|c| {
|
.with_client(actor, |c| {
|
||||||
c.query_opt(&QUERY_LINES.iter()
|
c.query_opt(&QUERY_LINES.iter()
|
||||||
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
||||||
&[&id.as_ref()])
|
&[&id.as_ref()])
|
||||||
@ -177,7 +177,7 @@ impl<Db> ReadMany for UserRepoImpl<Db> where Db: Postgres + 'static
|
|||||||
|
|
||||||
let usrs: Vec<User> =
|
let usrs: Vec<User> =
|
||||||
self.0
|
self.0
|
||||||
.with_client(|c| match page.after {
|
.with_client(actor, |c| match page.after {
|
||||||
| Some(after) => {
|
| Some(after) => {
|
||||||
c.query(&QUERY_AFTER_LINES.iter()
|
c.query(&QUERY_AFTER_LINES.iter()
|
||||||
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
||||||
@ -238,7 +238,7 @@ impl<Db> Patch for UserRepoImpl<Db> where Db: Postgres + 'static
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.0
|
self.0
|
||||||
.with_client(|c| {
|
.with_client(actor, |c| {
|
||||||
c.execute(&QUERY_LINES.iter()
|
c.execute(&QUERY_LINES.iter()
|
||||||
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
||||||
&[&id.as_ref(),
|
&[&id.as_ref(),
|
||||||
@ -272,7 +272,7 @@ impl<Db> Insert for UserRepoImpl<Db> where Db: Postgres + 'static
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.0
|
self.0
|
||||||
.with_client(|c| {
|
.with_client(actor, |c| {
|
||||||
c.query_one(&QUERY_LINES.iter()
|
c.query_one(&QUERY_LINES.iter()
|
||||||
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
||||||
&[&insert.tag,
|
&[&insert.tag,
|
||||||
@ -302,7 +302,7 @@ impl<Db> Del for UserRepoImpl<Db> where Db: Postgres + 'static
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.0
|
self.0
|
||||||
.with_client(|c| c.execute(QUERY, &[&id.as_ref()]))
|
.with_client(actor, |c| c.execute(QUERY, &[&id.as_ref()]))
|
||||||
.map(|n| n == 1)
|
.map(|n| n == 1)
|
||||||
.map_err(Other)
|
.map_err(Other)
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@ impl<Db, G> UserSessionExt for UserSessionExtImpl<Db, G>
|
|||||||
let expr = "public.usr_session_touch(public.usr_session_key_of_string($1))";
|
let expr = "public.usr_session_touch(public.usr_session_key_of_string($1))";
|
||||||
let query = format!("select {cols} from {expr} as u");
|
let query = format!("select {cols} from {expr} as u");
|
||||||
self.0
|
self.0
|
||||||
.with_client(|c| {
|
.with_client(&Actor::default(), |c| {
|
||||||
c.query_one(&query, &[&session.as_ref()])
|
c.query_one(&query, &[&session.as_ref()])
|
||||||
.and_then(|r| User::unmarshal(&r))
|
.and_then(|r| User::unmarshal(&r))
|
||||||
})
|
})
|
||||||
@ -118,7 +118,7 @@ impl<Db, G> UserSessionExt for UserSessionExtImpl<Db, G>
|
|||||||
" ) s"];
|
" ) s"];
|
||||||
|
|
||||||
self.0
|
self.0
|
||||||
.with_client(|c| {
|
.with_client(&Actor::default(), |c| {
|
||||||
c.query_one(&QUERY_LINES.iter()
|
c.query_one(&QUERY_LINES.iter()
|
||||||
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
.fold(String::new(), |b, a| format!("{b}{a}\n")),
|
||||||
&[&tag_or_email.as_str(),
|
&[&tag_or_email.as_str(),
|
||||||
|
@ -6,7 +6,7 @@ use postgres::types::FromSql;
|
|||||||
use postgres::{GenericClient, GenericRow};
|
use postgres::{GenericClient, GenericRow};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
|
||||||
use crate::model::{Actor, Mode, Path, Perm};
|
use crate::model::{Actor, Mode, Path, Perm, UserId};
|
||||||
|
|
||||||
pub type DbError<Pg> = <<Pg as Postgres>::Client as postgres::GenericClient>::Error;
|
pub type DbError<Pg> = <<Pg as Postgres>::Client as postgres::GenericClient>::Error;
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ pub trait Postgres
|
|||||||
fn try_new<F>(connect: F, pool_size: usize) -> Result<Self, DbError<Self>>
|
fn try_new<F>(connect: F, pool_size: usize) -> Result<Self, DbError<Self>>
|
||||||
where F: Fn() -> Result<Self::Client, DbError<Self>>;
|
where F: Fn() -> Result<Self::Client, DbError<Self>>;
|
||||||
|
|
||||||
fn with_client<F, R>(&self, f: F) -> Result<R, DbError<Self>>
|
fn with_client<F, R>(&self, a: &Actor, f: F) -> Result<R, DbError<Self>>
|
||||||
where F: FnOnce(&mut Self::Client) -> Result<R, DbError<Self>>;
|
where F: FnOnce(&mut Self::Client) -> Result<R, DbError<Self>>;
|
||||||
|
|
||||||
fn authorized(&self, path: &Path, wants_to: Mode, actor: &Actor) -> Result<bool, DbError<Self>> {
|
fn authorized(&self, path: &Path, wants_to: Mode, actor: &Actor) -> Result<bool, DbError<Self>> {
|
||||||
@ -91,7 +91,7 @@ pub trait Postgres
|
|||||||
"inner join public.usr ou on ou.id = p.owner_user",
|
"inner join public.usr ou on ou.id = p.owner_user",
|
||||||
"where path = $1 :: text",].into_iter()
|
"where path = $1 :: text",].into_iter()
|
||||||
.fold(String::new(), |b, a| format!("{b}\n{a}"));
|
.fold(String::new(), |b, a| format!("{b}\n{a}"));
|
||||||
let got_perm = self.with_client(|c| c.query_opt(&q, &[&path.to_string()]))?;
|
let got_perm = self.with_client(actor, |c| c.query_opt(&q, &[&path.to_string()]))?;
|
||||||
Ok(match got_perm {
|
Ok(match got_perm {
|
||||||
| None => {
|
| None => {
|
||||||
log::debug!("no such perm: {}", path);
|
log::debug!("no such perm: {}", path);
|
||||||
@ -133,9 +133,11 @@ pub trait Postgres
|
|||||||
"inner join public.grp og on og.id = p.owner_group",
|
"inner join public.grp og on og.id = p.owner_group",
|
||||||
"inner join public.usr ou on ou.id = p.owner_user",
|
"inner join public.usr ou on ou.id = p.owner_user",
|
||||||
format!("where path in ({set})").as_str(),].into_iter()
|
format!("where path in ({set})").as_str(),].into_iter()
|
||||||
.fold(String::new(), |b, a| format!("{b}\n{a}"));
|
.fold(String::new(), |b, a| {
|
||||||
|
format!("{b}\n{a}")
|
||||||
|
});
|
||||||
|
|
||||||
let pass = self.with_client(|c| c.query(&q, &[]))?
|
let pass = self.with_client(actor, |c| c.query(&q, &[]))?
|
||||||
.iter()
|
.iter()
|
||||||
.map(Perm::unmarshal)
|
.map(Perm::unmarshal)
|
||||||
.collect::<Result<Vec<Perm>, _>>()?
|
.collect::<Result<Vec<Perm>, _>>()?
|
||||||
@ -189,13 +191,22 @@ impl<C> Postgres for PostgresImpl<C>
|
|||||||
pool: Box::pin(pool) })
|
pool: Box::pin(pool) })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_client<F, R>(&self, f: F) -> Result<R, C::Error>
|
fn with_client<F, R>(&self, a: &Actor, f: F) -> Result<R, C::Error>
|
||||||
where F: FnOnce(&mut Self::Client) -> Result<R, C::Error>
|
where F: FnOnce(&mut Self::Client) -> Result<R, C::Error>
|
||||||
{
|
{
|
||||||
match self.unused_lock() {
|
let mut c = match self.unused_lock() {
|
||||||
| Some(mut lock) => f(lock.deref_mut()),
|
| Some(lock) => lock,
|
||||||
| None => f(self.block_for_next().deref_mut()),
|
| None => self.block_for_next(),
|
||||||
}
|
};
|
||||||
|
|
||||||
|
c.query_one("select public.set_acting_usr($1)",
|
||||||
|
&[&a.uid.as_ref().map(UserId::as_ref)])?;
|
||||||
|
|
||||||
|
let r = f(c.deref_mut())?;
|
||||||
|
|
||||||
|
c.query_one("select public.unset_acting_usr()", &[])?;
|
||||||
|
|
||||||
|
Ok(r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,7 +375,7 @@ pub mod test {
|
|||||||
perms: vec![] })
|
perms: vec![] })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_client<F, R>(&self, f: F) -> Result<R, super::DbError<Self>>
|
fn with_client<F, R>(&self, a: &Actor, f: F) -> Result<R, super::DbError<Self>>
|
||||||
where F: FnOnce(&mut Self::Client) -> Result<R, super::DbError<Self>>
|
where F: FnOnce(&mut Self::Client) -> Result<R, super::DbError<Self>>
|
||||||
{
|
{
|
||||||
f(self.client.lock().unwrap().deref_mut())
|
f(self.client.lock().unwrap().deref_mut())
|
||||||
@ -598,7 +609,8 @@ pub mod test {
|
|||||||
assert!(pg.unused_lock().is_some());
|
assert!(pg.unused_lock().is_some());
|
||||||
|
|
||||||
// with_client does not block when there are available clients
|
// with_client does not block when there are available clients
|
||||||
let row = pg.with_client(|c| c.query_one("", &[])).unwrap();
|
let row = pg.with_client(&Actor::default(), |c| c.query_one("", &[]))
|
||||||
|
.unwrap();
|
||||||
assert_eq!(row.get::<_, String>("foo"), String::from("bar"));
|
assert_eq!(row.get::<_, String>("foo"), String::from("bar"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user