feat: added basic ACL functionality

This commit is contained in:
NinekoTheCat 2023-12-24 11:03:02 +01:00
parent 6a9f8dfa6f
commit 7562925aeb
No known key found for this signature in database
GPG key ID: 700DB3F678A4AB66
13 changed files with 183 additions and 4 deletions

2
Cargo.lock generated
View file

@ -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]]

View file

@ -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

View file

@ -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
View 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>>
}

View file

@ -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>,
} }

View 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())
}
}

View file

@ -11,3 +11,5 @@ mod sending;
mod transaction_ids; mod transaction_ids;
mod uiaa; mod uiaa;
mod users; mod users;
mod acl;

View file

@ -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
View 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
View 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
}
}
}
}

View file

@ -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 {

View file

@ -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,

View file

@ -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 {