mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2025-07-09 10:56:41 +02:00
feat: added basic ACL functionality
This commit is contained in:
parent
6a9f8dfa6f
commit
7562925aeb
13 changed files with 183 additions and 4 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -451,6 +451,7 @@ dependencies = [
|
||||||
"tracing-opentelemetry",
|
"tracing-opentelemetry",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"trust-dns-resolver",
|
"trust-dns-resolver",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3274,6 +3275,7 @@ dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"idna",
|
"idna",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -58,6 +58,8 @@ rand = "0.8.5"
|
||||||
# Used to hash passwords
|
# Used to hash passwords
|
||||||
rust-argon2 = { git = "https://github.com/sru-systems/rust-argon2", rev = "e6cb5bf99643e565f4f0d103960d655dac9f3097" }
|
rust-argon2 = { git = "https://github.com/sru-systems/rust-argon2", rev = "e6cb5bf99643e565f4f0d103960d655dac9f3097" }
|
||||||
reqwest = { version = "0.11.22", default-features = false, features = ["rustls-tls-native-roots", "socks"] }
|
reqwest = { version = "0.11.22", default-features = false, features = ["rustls-tls-native-roots", "socks"] }
|
||||||
|
# Used to validate hostnames, already included in reqwest however we need access to it
|
||||||
|
url = {version = "^2", features = ["serde"]}
|
||||||
# Used for conduit::Error type
|
# Used for conduit::Error type
|
||||||
thiserror = "1.0.51"
|
thiserror = "1.0.51"
|
||||||
# Used to generate thumbnails for images
|
# Used to generate thumbnails for images
|
||||||
|
|
|
@ -99,7 +99,7 @@ impl FedDest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hostname(&self) -> String {
|
pub(crate) fn hostname(&self) -> String {
|
||||||
match &self {
|
match &self {
|
||||||
Self::Literal(addr) => addr.ip().to_string(),
|
Self::Literal(addr) => addr.ip().to_string(),
|
||||||
Self::Named(host, _) => host.clone(),
|
Self::Named(host, _) => host.clone(),
|
||||||
|
@ -154,6 +154,14 @@ where
|
||||||
(result.0, result.1.into_uri_string())
|
(result.0, result.1.into_uri_string())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
debug!("Checking acl allowance for {}", destination);
|
||||||
|
|
||||||
|
if !services().acl.is_federation_with_allowed_fedi_dest(&actual_destination) {
|
||||||
|
debug!("blocked sending federation to {:?}", actual_destination);
|
||||||
|
|
||||||
|
return Err(Error::ACLBlock(destination.to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
let actual_destination_str = actual_destination.clone().into_https_string();
|
let actual_destination_str = actual_destination.clone().into_https_string();
|
||||||
|
|
||||||
let mut http_request = request
|
let mut http_request = request
|
||||||
|
|
11
src/config/acl.rs
Normal file
11
src/config/acl.rs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use url::Host;
|
||||||
|
#[derive(Deserialize,Debug, Default, Clone)]
|
||||||
|
pub struct AccessControlListConfig {
|
||||||
|
/// setting this explicitly enables allowlists
|
||||||
|
pub(crate)allow_list: Option<HashSet<Host<String>>>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate)block_list: HashSet<Host<String>>
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ use std::{
|
||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
fmt,
|
fmt,
|
||||||
net::{IpAddr, Ipv4Addr},
|
net::{IpAddr, Ipv4Addr},
|
||||||
path::PathBuf,
|
path::PathBuf, sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use figment::Figment;
|
use figment::Figment;
|
||||||
|
@ -10,9 +10,10 @@ use ruma::{OwnedServerName, RoomVersionId};
|
||||||
use serde::{de::IgnoredAny, Deserialize};
|
use serde::{de::IgnoredAny, Deserialize};
|
||||||
use tracing::{error, warn};
|
use tracing::{error, warn};
|
||||||
|
|
||||||
|
pub(crate) mod acl;
|
||||||
mod proxy;
|
mod proxy;
|
||||||
|
|
||||||
use self::proxy::ProxyConfig;
|
use self::{proxy::ProxyConfig, acl::AccessControlListConfig};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
@ -122,6 +123,9 @@ pub struct Config {
|
||||||
#[serde(default = "false_fn")]
|
#[serde(default = "false_fn")]
|
||||||
pub allow_guest_registration: bool,
|
pub allow_guest_registration: bool,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub acl: Arc<AccessControlListConfig>,
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub catchall: BTreeMap<String, IgnoredAny>,
|
pub catchall: BTreeMap<String, IgnoredAny>,
|
||||||
}
|
}
|
||||||
|
|
33
src/database/key_value/acl.rs
Normal file
33
src/database/key_value/acl.rs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use crate::{service::acl::{Data, AclDatabaseEntry, AclMode}, KeyValueDatabase};
|
||||||
|
|
||||||
|
impl Data for KeyValueDatabase {
|
||||||
|
fn check_acl(&self,host: &url::Host<String> ) -> crate::Result<Option<AclMode>> {
|
||||||
|
let thing = self.acl_list.get(host.to_string().as_bytes())?;
|
||||||
|
if let Some(thing) = thing {
|
||||||
|
match thing.first() {
|
||||||
|
Some(0x1) => Ok(Some(AclMode::Allow)),
|
||||||
|
Some(0x0) => Ok(Some(AclMode::Block)),
|
||||||
|
Some(invalid) => {
|
||||||
|
warn!("found invalid value for mode byte in value {}, probably db corruption", invalid);
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_acl(&self, acl: AclDatabaseEntry) -> crate::Result<()> {
|
||||||
|
self.acl_list.insert(acl.hostname.to_string().as_bytes(), match acl.mode {
|
||||||
|
AclMode::Block => &[0x0],
|
||||||
|
AclMode::Allow => &[0x1],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_acl(&self,host: url::Host<String>) -> crate::Result<()> {
|
||||||
|
self.acl_list.remove(host.to_string().as_bytes())
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,3 +11,5 @@ mod sending;
|
||||||
mod transaction_ids;
|
mod transaction_ids;
|
||||||
mod uiaa;
|
mod uiaa;
|
||||||
mod users;
|
mod users;
|
||||||
|
|
||||||
|
mod acl;
|
|
@ -172,6 +172,8 @@ pub struct KeyValueDatabase {
|
||||||
pub(super) appservice_in_room_cache: RwLock<HashMap<OwnedRoomId, HashMap<String, bool>>>,
|
pub(super) appservice_in_room_cache: RwLock<HashMap<OwnedRoomId, HashMap<String, bool>>>,
|
||||||
pub(super) lasttimelinecount_cache: Mutex<HashMap<OwnedRoomId, PduCount>>,
|
pub(super) lasttimelinecount_cache: Mutex<HashMap<OwnedRoomId, PduCount>>,
|
||||||
pub(super) presence_timer_sender: Arc<mpsc::UnboundedSender<(OwnedUserId, Duration)>>,
|
pub(super) presence_timer_sender: Arc<mpsc::UnboundedSender<(OwnedUserId, Duration)>>,
|
||||||
|
|
||||||
|
pub(super) acl_list: Arc<dyn KvTree>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeyValueDatabase {
|
impl KeyValueDatabase {
|
||||||
|
@ -281,6 +283,7 @@ impl KeyValueDatabase {
|
||||||
|
|
||||||
let db_raw = Box::new(Self {
|
let db_raw = Box::new(Self {
|
||||||
_db: builder.clone(),
|
_db: builder.clone(),
|
||||||
|
acl_list: builder.open_tree("acl")?,
|
||||||
userid_password: builder.open_tree("userid_password")?,
|
userid_password: builder.open_tree("userid_password")?,
|
||||||
userid_displayname: builder.open_tree("userid_displayname")?,
|
userid_displayname: builder.open_tree("userid_displayname")?,
|
||||||
userid_avatarurl: builder.open_tree("userid_avatarurl")?,
|
userid_avatarurl: builder.open_tree("userid_avatarurl")?,
|
||||||
|
|
25
src/service/acl/data.rs
Normal file
25
src/service/acl/data.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use url::Host;
|
||||||
|
|
||||||
|
|
||||||
|
pub trait Data: Send + Sync {
|
||||||
|
/// check if given host exists in Acls, if so return it
|
||||||
|
fn check_acl(&self,host: &Host<String> ) -> crate::Result<Option<AclMode>>;
|
||||||
|
|
||||||
|
/// add a given Acl entry to the database
|
||||||
|
fn add_acl(&self, acl: AclDatabaseEntry) -> crate::Result<()>;
|
||||||
|
/// remove a given Acl entry from the database
|
||||||
|
fn remove_acl(&self,host: Host<String>) -> crate::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize,Deserialize, Debug, Clone, Copy)]
|
||||||
|
pub enum AclMode{
|
||||||
|
Block,
|
||||||
|
Allow
|
||||||
|
}
|
||||||
|
#[derive(Serialize,Deserialize, Debug, Clone)]
|
||||||
|
|
||||||
|
pub struct AclDatabaseEntry {
|
||||||
|
pub(crate) mode: AclMode,
|
||||||
|
pub(crate) hostname: Host
|
||||||
|
}
|
72
src/service/acl/mod.rs
Normal file
72
src/service/acl/mod.rs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use ruma::ServerName;
|
||||||
|
use tracing::{warn, debug, error};
|
||||||
|
use url::Host;
|
||||||
|
|
||||||
|
use crate::{config::acl::AccessControlListConfig, api::server_server::FedDest};
|
||||||
|
|
||||||
|
pub use self::data::*;
|
||||||
|
mod data;
|
||||||
|
pub struct Service {
|
||||||
|
pub db: &'static dyn Data,
|
||||||
|
pub acl_config: Arc<AccessControlListConfig>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Service {
|
||||||
|
/// same as federation_with_allowed however it can work with the fedi_dest type
|
||||||
|
pub fn is_federation_with_allowed_fedi_dest(&self,fedi_dest: &FedDest) -> bool {
|
||||||
|
let hostname = if let Ok(name) = Host::parse(&fedi_dest.hostname()) {
|
||||||
|
name
|
||||||
|
} else {
|
||||||
|
warn!("cannot deserialise hostname for server with name {:?}",fedi_dest);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
return self.is_federation_with_allowed(hostname);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// same as federation_with_allowed however it can work with the fedi_dest type
|
||||||
|
pub fn is_federation_with_allowed_server_name(&self,srv: &ServerName) -> bool {
|
||||||
|
let hostname = if let Ok(name) = Host::parse(srv.host()) {
|
||||||
|
name
|
||||||
|
} else {
|
||||||
|
warn!("cannot deserialise hostname for server with name {:?}",srv);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
return self.is_federation_with_allowed(hostname);
|
||||||
|
}
|
||||||
|
/// is federation allowed with this particular server?
|
||||||
|
pub fn is_federation_with_allowed(&self,server_host_name: Host<String>) -> bool {
|
||||||
|
debug!("checking federation allowance for {}", server_host_name);
|
||||||
|
// check blocklist first
|
||||||
|
if self.acl_config.block_list.contains(&server_host_name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let mut allow_list_enabled = false;
|
||||||
|
// check allowlist
|
||||||
|
if let Some(list) = &self.acl_config.allow_list {
|
||||||
|
if list.contains(&server_host_name) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
allow_list_enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//check database
|
||||||
|
match self.db.check_acl(&server_host_name) {
|
||||||
|
Err(error) => {
|
||||||
|
error!("database failed with {}",error);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
Ok(None) => false,
|
||||||
|
Ok(Some(data::AclMode::Block)) => false,
|
||||||
|
Ok(Some(data::AclMode::Allow)) if allow_list_enabled => true,
|
||||||
|
Ok(Some(data::AclMode::Allow)) => {
|
||||||
|
warn!("allowlist value found in database for {} but allow list is not enabled, denied request", server_host_name);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ use std::{
|
||||||
use lru_cache::LruCache;
|
use lru_cache::LruCache;
|
||||||
|
|
||||||
use crate::{Config, Result};
|
use crate::{Config, Result};
|
||||||
|
pub mod acl;
|
||||||
pub mod account_data;
|
pub mod account_data;
|
||||||
pub mod admin;
|
pub mod admin;
|
||||||
pub mod appservice;
|
pub mod appservice;
|
||||||
|
@ -34,6 +34,7 @@ pub struct Services {
|
||||||
pub key_backups: key_backups::Service,
|
pub key_backups: key_backups::Service,
|
||||||
pub media: media::Service,
|
pub media: media::Service,
|
||||||
pub sending: Arc<sending::Service>,
|
pub sending: Arc<sending::Service>,
|
||||||
|
pub acl: acl::Service
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Services {
|
impl Services {
|
||||||
|
@ -49,11 +50,13 @@ impl Services {
|
||||||
+ key_backups::Data
|
+ key_backups::Data
|
||||||
+ media::Data
|
+ media::Data
|
||||||
+ sending::Data
|
+ sending::Data
|
||||||
|
+ acl::Data
|
||||||
+ 'static,
|
+ 'static,
|
||||||
>(
|
>(
|
||||||
db: &'static D,
|
db: &'static D,
|
||||||
config: Config,
|
config: Config,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
|
let acl_conf = config.acl.clone();
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
appservice: appservice::Service { db },
|
appservice: appservice::Service { db },
|
||||||
pusher: pusher::Service { db },
|
pusher: pusher::Service { db },
|
||||||
|
@ -118,6 +121,7 @@ impl Services {
|
||||||
sending: sending::Service::build(db, &config),
|
sending: sending::Service::build(db, &config),
|
||||||
|
|
||||||
globals: globals::Service::load(db, config)?,
|
globals: globals::Service::load(db, config)?,
|
||||||
|
acl: acl::Service { db: db, acl_config: acl_conf },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
fn memory_usage(&self) -> String {
|
fn memory_usage(&self) -> String {
|
||||||
|
|
|
@ -1645,6 +1645,17 @@ impl Service {
|
||||||
|
|
||||||
/// Returns Ok if the acl allows the server
|
/// Returns Ok if the acl allows the server
|
||||||
pub fn acl_check(&self, server_name: &ServerName, room_id: &RoomId) -> Result<()> {
|
pub fn acl_check(&self, server_name: &ServerName, room_id: &RoomId) -> Result<()> {
|
||||||
|
if !services().acl.is_federation_with_allowed_server_name(server_name) {
|
||||||
|
info!(
|
||||||
|
"Server {} was denied by server ACL in {}",
|
||||||
|
server_name, room_id
|
||||||
|
);
|
||||||
|
return Err(Error::BadRequest(
|
||||||
|
ErrorKind::Forbidden,
|
||||||
|
"Server was denied by Server ACL",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let acl_event = match services().rooms.state_accessor.room_state_get(
|
let acl_event = match services().rooms.state_accessor.room_state_get(
|
||||||
room_id,
|
room_id,
|
||||||
&StateEventType::RoomServerAcl,
|
&StateEventType::RoomServerAcl,
|
||||||
|
|
|
@ -84,6 +84,8 @@ pub enum Error {
|
||||||
RedactionError(OwnedServerName, ruma::canonical_json::RedactionError),
|
RedactionError(OwnedServerName, ruma::canonical_json::RedactionError),
|
||||||
#[error("{0} in {1}")]
|
#[error("{0} in {1}")]
|
||||||
InconsistentRoomState(&'static str, ruma::OwnedRoomId),
|
InconsistentRoomState(&'static str, ruma::OwnedRoomId),
|
||||||
|
#[error("blocked {0}")]
|
||||||
|
ACLBlock(OwnedServerName)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
|
|
Loading…
Add table
Reference in a new issue