diff --git a/radicle-cli/examples/rad-block.md b/radicle-cli/examples/rad-block.md index d1d8503c..267229c7 100644 --- a/radicle-cli/examples/rad-block.md +++ b/radicle-cli/examples/rad-block.md @@ -35,9 +35,9 @@ And a 'block' policy was added: ``` $ rad seed -╭──────────────────────────────────────────────────────────────╮ -│ Repository Name Policy Scope │ -├──────────────────────────────────────────────────────────────┤ -│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji block followed │ -╰──────────────────────────────────────────────────────────────╯ +╭───────────────────────────────────────────────────────────╮ +│ Repository Name Policy Scope │ +├───────────────────────────────────────────────────────────┤ +│ rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji block all │ +╰───────────────────────────────────────────────────────────╯ ``` diff --git a/radicle-cli/examples/rad-config.md b/radicle-cli/examples/rad-config.md index a8ed742b..e47467da 100644 --- a/radicle-cli/examples/rad-config.md +++ b/radicle-cli/examples/rad-config.md @@ -21,14 +21,10 @@ $ rad config "alias": "alice", "listen": [], "peers": { - "type": "dynamic", - "target": 8 + "type": "dynamic" }, "connect": [], "externalAddresses": [], - "db": { - "journalMode": "rollback" - }, "network": "main", "log": "INFO", "relay": "auto", @@ -54,8 +50,9 @@ $ rad config } }, "workers": 8, - "policy": "block", - "scope": "all" + "seedingPolicy": { + "default": "block" + } } } ``` diff --git a/radicle-cli/src/commands/auth.rs b/radicle-cli/src/commands/auth.rs index 38bfe3f9..fe13ac88 100644 --- a/radicle-cli/src/commands/auth.rs +++ b/radicle-cli/src/commands/auth.rs @@ -169,6 +169,12 @@ pub fn authenticate(options: Options, profile: &Profile) -> anyhow::Result<()> { term::success!("Authenticated as {}", term::format::tertiary(profile.id())); return Ok(()); } + for (key, _) in &profile.config.node.extra { + term::warning(format!( + "unused or deprecated configuration attribute {:?}", + key + )); + } // If our key is encrypted, we try to authenticate with SSH Agent and // register it; only if it is running. diff --git a/radicle-cli/src/commands/inspect.rs b/radicle-cli/src/commands/inspect.rs index 689ae9ec..dabd81e6 100644 --- a/radicle-cli/src/commands/inspect.rs +++ b/radicle-cli/src/commands/inspect.rs @@ -9,7 +9,7 @@ use chrono::prelude::*; use radicle::identity::RepoId; use radicle::identity::{DocAt, Identity}; -use radicle::node::policy::Policy; +use radicle::node::policy::SeedingPolicy; use radicle::node::AliasStore as _; use radicle::storage::git::{Repository, Storage}; use radicle::storage::refs::RefsAt; @@ -175,15 +175,15 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { let policies = profile.policies()?; let seed = policies.seed_policy(&rid)?; match seed.policy { - Policy::Allow => { + SeedingPolicy::Allow { scope } => { println!( "Repository {} is {} with scope {}", term::format::tertiary(&rid), term::format::positive("being seeded"), - term::format::dim(format!("`{}`", seed.scope)) + term::format::dim(format!("`{scope}`")) ); } - Policy::Block => { + SeedingPolicy::Block => { println!( "Repository {} is {}", term::format::tertiary(&rid), diff --git a/radicle-cli/src/commands/seed.rs b/radicle-cli/src/commands/seed.rs index 9c98b322..f95a31ef 100644 --- a/radicle-cli/src/commands/seed.rs +++ b/radicle-cli/src/commands/seed.rs @@ -3,7 +3,7 @@ use std::ffi::OsString; use anyhow::anyhow; use radicle::node::policy; -use radicle::node::policy::Scope; +use radicle::node::policy::{Policy, Scope}; use radicle::node::Handle; use radicle::{prelude::*, storage, Node}; use radicle_term::Element as _; @@ -164,15 +164,15 @@ pub fn seeding(profile: &Profile) -> anyhow::Result<()> { ]); t.divider(); - for policy::SeedPolicy { rid, scope, policy } in store.seed_policies()? { + for policy::SeedPolicy { rid, policy } in store.seed_policies()? { let id = rid.to_string(); let name = storage .repository(rid) .map_err(storage::RepositoryError::from) .and_then(|repo| repo.project().map(|proj| proj.name().to_string())) .unwrap_or_default(); - let scope = scope.to_string(); - let policy = term::format::policy(&policy); + let scope = policy.scope().unwrap_or_default().to_string(); + let policy = term::format::policy(&Policy::from(policy)); t.push([ term::format::tertiary(id), diff --git a/radicle-cli/tests/commands.rs b/radicle-cli/tests/commands.rs index 2e77393e..dbf44bea 100644 --- a/radicle-cli/tests/commands.rs +++ b/radicle-cli/tests/commands.rs @@ -16,7 +16,7 @@ use radicle::storage::{ReadStorage, RefUpdate, RemoteRepository}; use radicle::test::fixtures; use radicle_cli_test::TestFormula; -use radicle_node::service::policy::{Policy, Scope}; +use radicle_node::service::policy::{Scope, SeedingPolicy}; use radicle_node::service::Event; use radicle_node::test::environment::{Config, Environment, Node}; #[allow(unused_imports)] @@ -1188,7 +1188,7 @@ fn rad_unseed() { fn rad_block() { let mut environment = Environment::new(); let alice = environment.node(Config { - policy: Policy::Allow, + seeding_policy: SeedingPolicy::permissive(), ..Config::test(Alias::new("alice")) }); let working = tempfile::tempdir().unwrap(); @@ -1491,7 +1491,7 @@ fn rad_init_sync_preferred() { let mut environment = Environment::new(); let mut alice = environment .node(Config { - policy: Policy::Allow, + seeding_policy: SeedingPolicy::permissive(), ..Config::test(Alias::new("alice")) }) .spawn(); @@ -1523,7 +1523,7 @@ fn rad_init_sync_timeout() { let mut environment = Environment::new(); let mut alice = environment .node(Config { - policy: Policy::Block, + seeding_policy: SeedingPolicy::Block, ..Config::test(Alias::new("alice")) }) .spawn(); @@ -1866,8 +1866,7 @@ fn test_replication_via_seed() { let alice = environment.node(config::relay("alice")); let bob = environment.node(config::relay("bob")); let seed = environment.node(Config { - policy: Policy::Allow, - scope: Scope::All, + seeding_policy: SeedingPolicy::permissive(), ..config::relay("seed") }); let working = environment.tmp().join("working"); @@ -2115,8 +2114,7 @@ fn rad_patch_open_explore() { let mut environment = Environment::new(); let seed = environment .node(Config { - policy: Policy::Allow, - scope: Scope::All, + seeding_policy: SeedingPolicy::permissive(), ..config::seed("seed") }) .spawn(); diff --git a/radicle-fetch/src/policy.rs b/radicle-fetch/src/policy.rs index 5cf7e7d6..508b5d65 100644 --- a/radicle-fetch/src/policy.rs +++ b/radicle-fetch/src/policy.rs @@ -5,7 +5,7 @@ use radicle::node::policy::config::Config; use radicle::node::policy::store::Read; use radicle::prelude::RepoId; -pub use radicle::node::policy::{Policy, Scope}; +pub use radicle::node::policy::{Policy, Scope, SeedingPolicy}; #[derive(Clone, Debug)] pub enum Allowed { @@ -19,23 +19,23 @@ impl Allowed { .seed_policy(&rid) .map_err(|err| error::Policy::FailedPolicy { rid, err })?; match entry.policy { - Policy::Block => { + SeedingPolicy::Block => { log::error!(target: "fetch", "Attempted to fetch non-seeded repo {rid}"); Err(error::Policy::BlockedPolicy { rid }) } - Policy::Allow => match entry.scope { - Scope::All => Ok(Self::All), - Scope::Followed => { - let nodes = config - .follow_policies() - .map_err(|err| error::Policy::FailedNodes { rid, err })?; - let followed: HashSet<_> = nodes - .filter_map(|node| (node.policy == Policy::Allow).then_some(node.nid)) - .collect(); + SeedingPolicy::Allow { scope: Scope::All } => Ok(Self::All), + SeedingPolicy::Allow { + scope: Scope::Followed, + } => { + let nodes = config + .follow_policies() + .map_err(|err| error::Policy::FailedNodes { rid, err })?; + let followed: HashSet<_> = nodes + .filter_map(|node| (node.policy == Policy::Allow).then_some(node.nid)) + .collect(); - Ok(Allowed::Followed { remotes: followed }) - } - }, + Ok(Allowed::Followed { remotes: followed }) + } } } } diff --git a/radicle-httpd/src/api/v1/node.rs b/radicle-httpd/src/api/v1/node.rs index 2e42b587..3e238238 100644 --- a/radicle-httpd/src/api/v1/node.rs +++ b/radicle-httpd/src/api/v1/node.rs @@ -8,7 +8,10 @@ use serde_json::json; use radicle::identity::RepoId; use radicle::node::routing::Store; -use radicle::node::{policy, AliasStore, Handle, NodeId, DEFAULT_TIMEOUT}; +use radicle::node::{ + policy::{Policy, SeedPolicy}, + AliasStore, Handle, NodeId, DEFAULT_TIMEOUT, +}; use radicle::Node; use crate::api::error::Error; @@ -84,16 +87,11 @@ async fn node_policies_repos_handler(State(ctx): State) -> impl IntoRes let policies = ctx.profile.policies()?; let mut repos = Vec::new(); - for policy::SeedPolicy { - rid: id, - scope, - policy, - } in policies.seed_policies()? - { + for SeedPolicy { rid: id, policy } in policies.seed_policies()? { repos.push(json!({ "id": id, - "scope": scope, - "policy": policy, + "scope": policy.scope().unwrap_or_default(), + "policy": Policy::from(policy), })); } diff --git a/radicle-httpd/src/api/v1/profile.rs b/radicle-httpd/src/api/v1/profile.rs index 123cbe15..6b869af1 100644 --- a/radicle-httpd/src/api/v1/profile.rs +++ b/radicle-httpd/src/api/v1/profile.rs @@ -83,13 +83,9 @@ mod routes { "node": { "alias": "seed", "listen": [], - "peers": { - "type": "dynamic", - "target": 8 - }, + "peers": { "type": "dynamic" }, "connect": [], "externalAddresses": [], - "db": { "journalMode": "rollback" }, "network": "main", "log": "INFO", "relay": "auto", @@ -115,8 +111,9 @@ mod routes { } }, "workers": 8, - "policy": "block", - "scope": "all" + "seedingPolicy": { + "default": "block", + } } }, "home": seed.profile.path() diff --git a/radicle-node/src/runtime.rs b/radicle-node/src/runtime.rs index 9f94a93a..90cbb460 100644 --- a/radicle-node/src/runtime.rs +++ b/radicle-node/src/runtime.rs @@ -119,16 +119,20 @@ impl Runtime { let clock = LocalTime::now(); let timestamp = clock.into(); let storage = Storage::open(home.storage(), git::UserInfo { alias, key: id })?; - let scope = config.scope; - let policy = config.policy; + let policy = config.seeding_policy; + for (key, _) in &config.extra { + log::warn!(target: "node", "Unused or deprecated configuration attribute {:?}", key); + } log::info!(target: "node", "Opening node database.."); - let db = home.database_mut()?.journal_mode(config.db.journal_mode)?; + let db = home + .database_mut()? + .journal_mode(node::db::JournalMode::default())?; let mut stores: service::Stores<_> = db.clone().into(); log::info!(target: "node", "Opening policy database.."); let policies = home.policies_mut()?; - let policies = policy::Config::new(policy, scope, policies); + let policies = policy::Config::new(policy, policies); let notifications = home.notifications_mut()?; let cobs_cache = cob::cache::Store::open(home.cobs().join(cob::cache::COBS_DB_FILE))?; @@ -234,7 +238,6 @@ impl Runtime { storage: storage.clone(), fetch, policy, - scope, policies_db: home.node().join(node::POLICIES_DB_FILE), }, )?; diff --git a/radicle-node/src/service.rs b/radicle-node/src/service.rs index dd6a0a20..ea9c4fdd 100644 --- a/radicle-node/src/service.rs +++ b/radicle-node/src/service.rs @@ -34,6 +34,7 @@ use radicle::node::seed::Store as _; use radicle::node::{ConnectOptions, Penalty, Severity}; use radicle::storage::refs::SIGREFS_BRANCH; use radicle::storage::RepositoryError; +use radicle_fetch::policy::SeedingPolicy; use crate::crypto; use crate::crypto::{Signer, Verified}; @@ -49,7 +50,7 @@ use crate::service::gossip::Store as _; use crate::service::message::{ Announcement, AnnouncementMessage, Info, NodeAnnouncement, Ping, RefsAnnouncement, RefsStatus, }; -use crate::service::policy::{store::Write, Policy, Scope}; +use crate::service::policy::{store::Write, Scope}; use crate::storage; use crate::storage::{refs::RefsAt, Namespaces, ReadStorage}; use crate::worker::fetch; @@ -102,6 +103,8 @@ pub const MAX_RECONNECTION_DELTA: LocalDuration = LocalDuration::from_mins(60); pub const CONNECTION_RETRY_DELTA: LocalDuration = LocalDuration::from_mins(10); /// How long to wait for a fetch to stall before aborting, default is 3s. pub const FETCH_TIMEOUT: time::Duration = time::Duration::from_secs(3); +/// Target number of peers to maintain connections to. +pub const TARGET_OUTBOUND_PEERS: usize = 8; /// Maximum external address limit imposed by message size limits. pub use message::ADDRESS_LIMIT; @@ -560,7 +563,7 @@ where self.filter = Filter::new( self.policies .seed_policies()? - .filter_map(|t| (t.policy == Policy::Allow).then_some(t.rid)), + .filter_map(|t| (t.policy.is_allow()).then_some(t.rid)), ); Ok(updated) } @@ -735,7 +738,7 @@ where self.filter = Filter::new( self.policies .seed_policies()? - .filter_map(|t| (t.policy == Policy::Allow).then_some(t.rid)), + .filter_map(|t| (t.policy.is_allow()).then_some(t.rid)), ); // Connect to configured peers. let addrs = self.config.connect.clone(); @@ -1216,9 +1219,12 @@ where .policies .seed_policy(&rid) .expect("Service::dequeue_fetch: error accessing repo seeding configuration"); - + let SeedingPolicy::Allow { scope } = repo_entry.policy else { + debug!(target: "service", "Repository {rid} is no longer seeded, skipping.."); + continue; + }; // Keep dequeueing if there was nothing to fetch, otherwise break. - if self.fetch_refs_at(rid, from, refs, repo_entry.scope, timeout, channel) { + if self.fetch_refs_at(rid, from, refs, scope, timeout, channel) { break; } } else { @@ -1625,14 +1631,14 @@ where let repo_entry = self.policies.seed_policy(&message.rid).expect( "Service::handle_announcement: error accessing repo seeding configuration", ); - if repo_entry.policy != Policy::Allow { + let SeedingPolicy::Allow { scope } = repo_entry.policy else { debug!( target: "service", "Ignoring refs announcement from {announcer}: repository {} isn't seeded (t={timestamp})", message.rid ); return Ok(None); - } + }; // Refs can be relayed by peers who don't have the data in storage, // therefore we only check whether we are connected to the *announcer*, // which is required by the protocol to only announce refs it has. @@ -1645,14 +1651,8 @@ where return Ok(relay); }; // Finally, start the fetch. - self.fetch_refs_at( - message.rid, - remote.id, - refs, - repo_entry.scope, - FETCH_TIMEOUT, - None, - ); + self.fetch_refs_at(message.rid, remote.id, refs, scope, FETCH_TIMEOUT, None); + return Ok(relay); } AnnouncementMessage::Node( @@ -2202,7 +2202,7 @@ where /// Return a new filter object, based on our seeding policy. fn filter(&self) -> Filter { - if self.config.policy == Policy::Allow { + if self.config.seeding_policy.is_allow() { // TODO: Remove bits for blocked repos. Filter::default() } else { @@ -2361,7 +2361,7 @@ where let missing = self .policies .seed_policies()? - .filter_map(|t| (t.policy == Policy::Allow).then_some(t.rid)) + .filter_map(|t| (t.policy.is_allow()).then_some(t.rid)) .filter(|rid| !inventory.contains(rid)); for rid in missing { @@ -2412,11 +2412,12 @@ where /// Try to maintain a target number of connections. fn maintain_connections(&mut self) { - let PeerConfig::Dynamic { target } = self.config.peers else { + let PeerConfig::Dynamic = self.config.peers else { return; }; trace!(target: "service", "Maintaining connections.."); + let target = TARGET_OUTBOUND_PEERS; let now = self.clock; let outbound = self .sessions diff --git a/radicle-node/src/test/peer.rs b/radicle-node/src/test/peer.rs index 106d2ff6..20e3dd10 100644 --- a/radicle-node/src/test/peer.rs +++ b/radicle-node/src/test/peer.rs @@ -24,7 +24,7 @@ use crate::runtime::Emitter; use crate::service; use crate::service::io::Io; use crate::service::message::*; -use crate::service::policy::{Policy, Scope}; +use crate::service::policy::{Scope, SeedingPolicy}; use crate::service::*; use crate::storage::git::transport::remote; use crate::storage::Inventory; @@ -101,8 +101,7 @@ pub struct Config { pub config: service::Config, pub db: Stores, pub local_time: LocalTime, - pub policy: Policy, - pub scope: Scope, + pub policy: SeedingPolicy, pub signer: G, pub rng: fastrand::Rng, pub tmp: tempfile::TempDir, @@ -121,8 +120,7 @@ impl Default for Config { config: service::Config::test(Alias::from_str("mocky").unwrap()), db, local_time: LocalTime::now(), - policy: Policy::default(), - scope: Scope::default(), + policy: SeedingPolicy::default(), signer, rng, tmp, @@ -162,7 +160,7 @@ where mut config: Config, ) -> Self { let policies = policy::Store::::memory().unwrap(); - let mut policies = policy::Config::new(config.policy, config.scope, policies); + let mut policies = policy::Config::new(config.policy, policies); let id = *config.signer.public_key(); let ip = ip.into(); let local_addr = net::SocketAddr::new(ip, config.rng.u16(..)); diff --git a/radicle-node/src/worker.rs b/radicle-node/src/worker.rs index eff250f9..ac3f5e3b 100644 --- a/radicle-node/src/worker.rs +++ b/radicle-node/src/worker.rs @@ -20,7 +20,7 @@ use radicle_fetch::FetchLimit; use crate::runtime::{thread, Emitter, Handle}; use crate::service::policy; -use crate::service::policy::Policy; +use crate::service::policy::SeedingPolicy; use crate::wire::StreamId; pub use channels::{ChannelEvent, Channels}; @@ -34,9 +34,7 @@ pub struct Config { /// Configuration for performing fetched. pub fetch: FetchConfig, /// Default policy, if a policy for a specific node or repository was not found. - pub policy: Policy, - /// Default scope, if a scope for a specific repository was not found. - pub scope: policy::Scope, + pub policy: SeedingPolicy, /// Path to the policies database. pub policies_db: PathBuf, } @@ -284,7 +282,7 @@ impl Worker { let policy = self.policies.seed_policy(&rid)?.policy; let repo = self.storage.repository(rid)?; let doc = repo.identity_doc()?; - if !doc.is_visible_to(&remote) || policy == Policy::Block { + if !doc.is_visible_to(&remote) || policy.is_block() { Err(UploadError::Unauthorized(remote, rid)) } else { Ok(()) @@ -357,11 +355,8 @@ impl Pool { ) -> Result { let mut pool = Vec::with_capacity(config.capacity); for i in 0..config.capacity { - let policies = policy::Config::new( - config.policy, - config.scope, - policy::Store::reader(&config.policies_db)?, - ); + let policies = + policy::Config::new(config.policy, policy::Store::reader(&config.policies_db)?); let worker = Worker { nid, tasks: tasks.clone(), diff --git a/radicle/src/node/config.rs b/radicle/src/node/config.rs index 68fb8da9..e51978af 100644 --- a/radicle/src/node/config.rs +++ b/radicle/src/node/config.rs @@ -4,13 +4,12 @@ use std::ops::Deref; use cyphernet::addr::PeerAddr; use localtime::LocalDuration; +use serde_json as json; use crate::node; -use crate::node::policy::{Policy, Scope}; -use crate::node::{db, Address, Alias, NodeId}; +use crate::node::policy::SeedingPolicy; +use crate::node::{Address, Alias, NodeId}; -/// Target number of peers to maintain connections to. -pub const TARGET_OUTBOUND_PEERS: usize = 8; /// Default number of workers to spawn. pub const DEFAULT_WORKERS: usize = 8; @@ -134,8 +133,7 @@ impl Default for Limits { pub struct ConnectionLimits { /// Max inbound connections. pub inbound: usize, - /// Max outbound connections. Note that this is higher than the *target* number - /// in [`TARGET_OUTBOUND_PEERS`]. + /// Max outbound connections. Note that this can be higher than the *target* number. pub outbound: usize, } @@ -223,14 +221,12 @@ pub enum PeerConfig { /// Static peer set. Connect to the configured peers and maintain the connections. Static, /// Dynamic peer set. - Dynamic { target: usize }, + Dynamic, } impl Default for PeerConfig { fn default() -> Self { - Self::Dynamic { - target: TARGET_OUTBOUND_PEERS, - } + Self::Dynamic } } @@ -261,14 +257,6 @@ pub enum AddressConfig { Forward, } -/// Database configuration. -#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DbConfig { - #[serde(default)] - pub journal_mode: db::JournalMode, -} - /// Service configuration. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] @@ -288,9 +276,6 @@ pub struct Config { /// Specify the node's public addresses #[serde(default)] pub external_addresses: Vec
, - /// Database config. - #[serde(default)] - pub db: DbConfig, /// Global proxy. #[serde(default, skip_serializing_if = "Option::is_none")] pub proxy: Option, @@ -315,10 +300,10 @@ pub struct Config { pub workers: usize, /// Default seeding policy. #[serde(default)] - pub policy: Policy, - /// Default seeding scope. - #[serde(default)] - pub scope: Scope, + pub seeding_policy: SeedingPolicy, + /// Extra fields that aren't supported. + #[serde(flatten, skip_serializing)] + pub extra: json::Map, } impl Config { @@ -336,7 +321,6 @@ impl Config { listen: vec![], connect: HashSet::default(), external_addresses: vec![], - db: DbConfig::default(), network: Network::default(), proxy: None, onion: None, @@ -344,8 +328,8 @@ impl Config { limits: Limits::default(), workers: DEFAULT_WORKERS, log: defaults::log(), - policy: Policy::default(), - scope: Scope::default(), + seeding_policy: SeedingPolicy::default(), + extra: json::Map::default(), } } diff --git a/radicle/src/node/db.rs b/radicle/src/node/db.rs index fc6aa774..c5727477 100644 --- a/radicle/src/node/db.rs +++ b/radicle/src/node/db.rs @@ -48,10 +48,10 @@ pub enum Error { pub enum JournalMode { /// "WAL" mode. Good for concurrent reads & writes, but keeps some extra files around. #[serde(rename = "wal")] + #[default] WriteAheadLog, /// Default "rollback" mode. Certain writes may block reads. #[serde(alias = "rollback")] - #[default] Rollback, } diff --git a/radicle/src/node/policy.rs b/radicle/src/node/policy.rs index 8befe8be..66aa10b6 100644 --- a/radicle/src/node/policy.rs +++ b/radicle/src/node/policy.rs @@ -15,8 +15,15 @@ pub use super::{Alias, NodeId}; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct SeedPolicy { pub rid: RepoId, - pub scope: Scope, - pub policy: Policy, + pub policy: SeedingPolicy, +} + +impl std::ops::Deref for SeedPolicy { + type Target = SeedingPolicy; + + fn deref(&self) -> &Self::Target { + &self.policy + } } /// Node following policy. @@ -27,6 +34,66 @@ pub struct FollowPolicy { pub policy: Policy, } +/// Default seeding policy. Applies when no repository policies for the given repo are found. +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase", tag = "default")] +pub enum SeedingPolicy { + /// Allow seeding. + Allow { + /// Seeding scope. + #[serde(default)] + scope: Scope, + }, + /// Block seeding. + #[default] + Block, +} + +impl SeedingPolicy { + /// Seed everything from anyone. + pub fn permissive() -> Self { + Self::Allow { scope: Scope::All } + } + + /// Is this an "allow" policy. + pub fn is_allow(&self) -> bool { + matches!(self, Self::Allow { .. }) + } + + /// Is this a "block" policy. + pub fn is_block(&self) -> bool { + !self.is_allow() + } + + /// Scope, if any. + pub fn scope(&self) -> Option { + match self { + Self::Allow { scope } => Some(*scope), + Self::Block => None, + } + } +} + +impl From for Policy { + fn from(p: SeedingPolicy) -> Self { + match p { + SeedingPolicy::Block => Policy::Block, + SeedingPolicy::Allow { .. } => Policy::Allow, + } + } +} + +impl std::fmt::Display for SeedingPolicy { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} ({})", + Policy::from(*self), + self.scope().unwrap_or_default() + ) + } +} + /// Resource policy. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/radicle/src/node/policy/config.rs b/radicle/src/node/policy/config.rs index 9e690a86..7a34a90e 100644 --- a/radicle/src/node/policy/config.rs +++ b/radicle/src/node/policy/config.rs @@ -6,13 +6,13 @@ use log::error; use thiserror::Error; use crate::crypto::PublicKey; -use crate::prelude::{NodeId, RepoId}; +use crate::prelude::RepoId; use crate::storage::{Namespaces, ReadRepository as _, ReadStorage, RepositoryError}; pub use crate::node::policy::store; pub use crate::node::policy::store::Error; pub use crate::node::policy::store::Store; -pub use crate::node::policy::{Alias, FollowPolicy, Policy, Scope, SeedPolicy}; +pub use crate::node::policy::{Alias, FollowPolicy, Policy, Scope, SeedPolicy, SeedingPolicy}; #[derive(Debug, Error)] pub enum NamespacesError { @@ -45,9 +45,7 @@ pub enum NamespacesError { /// Policies configuration. pub struct Config { /// Default policy, if a policy for a specific node or repository was not found. - policy: Policy, - /// Default scope, if a scope for a specific repository was not found. - scope: Scope, + policy: SeedingPolicy, /// Underlying configuration store. store: Store, } @@ -58,7 +56,6 @@ impl fmt::Debug for Config { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Config") .field("policy", &self.policy) - .field("scope", &self.scope) .field("store", &self.store) .finish() } @@ -66,34 +63,13 @@ impl fmt::Debug for Config { impl Config { /// Create a new policy configuration. - pub fn new(policy: Policy, scope: Scope, store: Store) -> Self { - Self { - policy, - scope, - store, - } + pub fn new(policy: SeedingPolicy, store: Store) -> Self { + Self { policy, store } } /// Check if a repository is seeded. pub fn is_seeding(&self, rid: &RepoId) -> Result { - self.seed_policy(rid) - .map(|entry| entry.policy == Policy::Allow) - } - - /// Check if a node is followed. - pub fn is_following(&self, nid: &NodeId) -> Result { - self.follow_policy(nid) - .map(|entry| entry.policy == Policy::Allow) - } - - /// Get a node's following information. - /// Returns the default policy if the node isn't found. - pub fn follow_policy(&self, nid: &NodeId) -> Result { - Ok(self.store.follow_policy(nid)?.unwrap_or(FollowPolicy { - nid: *nid, - alias: None, - policy: self.policy, - })) + self.seed_policy(rid).map(|entry| entry.policy.is_allow()) } /// Get a repository's seeding information. @@ -101,7 +77,6 @@ impl Config { pub fn seed_policy(&self, rid: &RepoId) -> Result { Ok(self.store.seed_policy(rid)?.unwrap_or(SeedPolicy { rid: *rid, - scope: self.scope, policy: self.policy, })) } @@ -120,37 +95,37 @@ impl Config { .seed_policy(rid) .map_err(|err| FailedPolicy { rid: *rid, err })?; match entry.policy { - Policy::Block => { + SeedingPolicy::Block => { error!(target: "service", "Attempted to fetch untracked repo {rid}"); Err(NamespacesError::BlockedPolicy { rid: *rid }) } - Policy::Allow => match entry.scope { - Scope::All => Ok(Namespaces::All), - Scope::Followed => { - let nodes = self - .follow_policies() - .map_err(|err| FailedNodes { rid: *rid, err })?; - let mut followed: HashSet<_> = nodes - .filter_map(|node| (node.policy == Policy::Allow).then_some(node.nid)) - .collect(); + SeedingPolicy::Allow { scope: Scope::All } => Ok(Namespaces::All), + SeedingPolicy::Allow { + scope: Scope::Followed, + } => { + let nodes = self + .follow_policies() + .map_err(|err| FailedNodes { rid: *rid, err })?; + let mut followed: HashSet<_> = nodes + .filter_map(|node| (node.policy == Policy::Allow).then_some(node.nid)) + .collect(); - if let Ok(repo) = storage.repository(*rid) { - let delegates = repo - .delegates() - .map_err(|err| FailedDelegates { rid: *rid, err })? - .map(PublicKey::from); - followed.extend(delegates); - }; - if followed.is_empty() { - // Nb. returning All here because the - // fetching logic will correctly determine - // followed and delegate remotes. - Ok(Namespaces::All) - } else { - Ok(Namespaces::Followed(followed)) - } + if let Ok(repo) = storage.repository(*rid) { + let delegates = repo + .delegates() + .map_err(|err| FailedDelegates { rid: *rid, err })? + .map(PublicKey::from); + followed.extend(delegates); + }; + if followed.is_empty() { + // Nb. returning All here because the + // fetching logic will correctly determine + // followed and delegate remotes. + Ok(Namespaces::All) + } else { + Ok(Namespaces::Followed(followed)) } - }, + } } } } diff --git a/radicle/src/node/policy/store.rs b/radicle/src/node/policy/store.rs index 9a302aaf..76352d3a 100644 --- a/radicle/src/node/policy/store.rs +++ b/radicle/src/node/policy/store.rs @@ -9,7 +9,7 @@ use thiserror::Error; use crate::node::{Alias, AliasStore}; use crate::prelude::{NodeId, RepoId}; -use super::{FollowPolicy, Policy, Scope, SeedPolicy}; +use super::{FollowPolicy, Policy, Scope, SeedPolicy, SeedingPolicy}; /// How long to wait for the database lock to be released before failing a read. const DB_READ_TIMEOUT: time::Duration = time::Duration::from_secs(3); @@ -218,10 +218,8 @@ impl Store { pub fn is_seeding(&self, id: &RepoId) -> Result { Ok(matches!( self.seed_policy(id)?, - Some(SeedPolicy { - policy: Policy::Allow, - .. - }) + Some(SeedPolicy { policy, .. }) + if policy.is_allow() )) } @@ -260,11 +258,13 @@ impl Store { stmt.bind((1, id))?; if let Some(Ok(row)) = stmt.into_iter().next() { - return Ok(Some(SeedPolicy { - rid: *id, - scope: row.read::("scope"), - policy: row.read::("policy"), - })); + let policy = match row.read::("policy") { + Policy::Allow => SeedingPolicy::Allow { + scope: row.read::("scope"), + }, + Policy::Block => SeedingPolicy::Block, + }; + return Ok(Some(SeedPolicy { rid: *id, policy })); } Ok(None) } @@ -307,14 +307,13 @@ impl Store { while let Some(Ok(row)) = stmt.next() { let id = row.read("id"); - let scope = row.read("scope"); - let policy = row.read::("policy"); - - entries.push(SeedPolicy { - rid: id, - scope, - policy, - }); + let policy = match row.read::("policy") { + Policy::Allow => SeedingPolicy::Allow { + scope: row.read::("scope"), + }, + Policy::Block => SeedingPolicy::Block, + }; + entries.push(SeedPolicy { rid: id, policy }); } Ok(Box::new(entries.into_iter())) } @@ -415,9 +414,15 @@ mod test { let mut db = Store::open(":memory:").unwrap(); assert!(db.seed(&id, Scope::All).unwrap()); - assert_eq!(db.seed_policy(&id).unwrap().unwrap().scope, Scope::All); + assert_eq!( + db.seed_policy(&id).unwrap().unwrap().scope(), + Some(Scope::All) + ); assert!(db.seed(&id, Scope::Followed).unwrap()); - assert_eq!(db.seed_policy(&id).unwrap().unwrap().scope, Scope::Followed); + assert_eq!( + db.seed_policy(&id).unwrap().unwrap().scope(), + Some(Scope::Followed) + ); } #[test] @@ -426,9 +431,10 @@ mod test { let mut db = Store::open(":memory:").unwrap(); assert!(db.seed(&id, Scope::All).unwrap()); - assert_eq!(db.seed_policy(&id).unwrap().unwrap().policy, Policy::Allow); + assert!(db.seed_policy(&id).unwrap().unwrap().is_allow()); assert!(db.set_seed_policy(&id, Policy::Block).unwrap()); - assert_eq!(db.seed_policy(&id).unwrap().unwrap().policy, Policy::Block); + assert!(!db.seed_policy(&id).unwrap().unwrap().is_allow()); + assert_eq!(db.seed_policy(&id).unwrap().unwrap().scope(), None); } #[test] diff --git a/radicle/src/profile.rs b/radicle/src/profile.rs index 25076414..171375ca 100644 --- a/radicle/src/profile.rs +++ b/radicle/src/profile.rs @@ -15,6 +15,7 @@ use std::path::{Path, PathBuf}; use std::{fs, io}; use serde::Serialize; +use serde_json as json; use thiserror::Error; use crate::crypto::ssh::agent::Agent; @@ -22,13 +23,16 @@ use crate::crypto::ssh::{keystore, Keystore, Passphrase}; use crate::crypto::{PublicKey, Signer}; use crate::explorer::Explorer; use crate::node::policy::config::store::Read; -use crate::node::{notifications, policy, Alias, AliasStore}; -use crate::prelude::Did; -use crate::prelude::NodeId; +use crate::node::{ + notifications, policy, + policy::{Policy, Scope, SeedingPolicy}, + Alias, AliasStore, +}; +use crate::prelude::{Did, NodeId}; use crate::storage::git::transport; use crate::storage::git::Storage; -use crate::storage::{self, ReadRepository}; -use crate::{cli, cob, git, node, web}; +use crate::storage::ReadRepository; +use crate::{cli, cob, git, node, storage, web}; /// Environment variables used by radicle. pub mod env { @@ -229,12 +233,28 @@ impl Config { /// Load a configuration from the given path. pub fn load(path: &Path) -> Result { - match fs::File::open(path) { + let mut cfg: Self = match fs::File::open(path) { Ok(cfg) => { - serde_json::from_reader(cfg).map_err(|e| ConfigError::Load(path.to_path_buf(), e)) + json::from_reader(cfg).map_err(|e| ConfigError::Load(path.to_path_buf(), e))? + } + Err(e) => return Err(ConfigError::Io(path.to_path_buf(), e)), + }; + + // Handle deprecated policy configuration. + // Nb. This will override "seedingPolicy" if set! This code should be removed after 1.0. + if let (Some(p), Some(s)) = (cfg.node.extra.get("policy"), cfg.node.extra.get("scope")) { + if let (Ok(policy), Ok(scope)) = ( + json::from_value::(p.clone()), + json::from_value::(s.clone()), + ) { + log::warn!(target: "radicle", "Overwriting `seedingPolicy` configuration"); + cfg.node.seeding_policy = match policy { + Policy::Allow => SeedingPolicy::Allow { scope }, + Policy::Block => SeedingPolicy::Block, + } } - Err(e) => Err(ConfigError::Io(path.to_path_buf(), e)), } + Ok(cfg) } /// Write configuration to disk. @@ -243,8 +263,8 @@ impl Config { .create_new(true) .write(true) .open(path)?; - let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); - let mut serializer = serde_json::Serializer::with_formatter(&file, formatter); + let formatter = json::ser::PrettyFormatter::with_indent(b" "); + let mut serializer = json::Serializer::with_formatter(&file, formatter); self.serialize(&mut serializer)?; file.write_all(b"\n")?; @@ -315,7 +335,6 @@ impl Profile { key: public_key, }, )?; - transport::local::register(storage.clone()); Ok(Profile { @@ -382,8 +401,7 @@ impl Profile { pub fn policies(&self) -> Result, policy::store::Error> { let path = self.node().join(node::POLICIES_DB_FILE); let config = policy::config::Config::new( - self.config.node.policy, - self.config.node.scope, + self.config.node.seeding_policy, policy::store::Store::reader(path)?, ); Ok(config) @@ -629,10 +647,9 @@ impl Home { #[cfg(test)] #[cfg(not(target_os = "macos"))] mod test { + use super::*; use std::fs; - use super::Home; - // Checks that if we have: // '/run/user/1000/.tmpqfK6ih/../.tmpqfK6ih/Radicle/Home' // @@ -656,4 +673,55 @@ mod test { assert_eq!(home.path, path); } + + #[test] + fn test_config() { + let cfg = json::from_value::(json::json!({ + "publicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path", + "preferredSeeds": [], + "web": { + "pinned": { + "repositories": [ + "rad:z3TajuiHXifEDEX4qbJxe8nXr9ufi", + "rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5" + ] + } + }, + "cli": { "hints": true }, + "node": { + "alias": "seed.radicle.xyz", + "listen": [], + "peers": { "type": "dynamic", "target": 8 }, + "connect": [ + "z6Mkmqogy2qEM2ummccUthFEaaHvyYmYBYh3dbe9W4ebScxo@ash.radicle.garden:8776", + "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@seed.radicle.garden:8776" + ], + "externalAddresses": [ "seed.radicle.xyz:8776" ], + "db": { "journalMode": "wal" }, + "network": "main", + "log": "INFO", + "relay": "always", + "limits": { + "routingMaxSize": 1000, + "routingMaxAge": 604800, + "gossipMaxAge": 604800, + "fetchConcurrency": 1, + "maxOpenFiles": 4096, + "rate": { + "inbound": { "fillRate": 10.0, "capacity": 2048 }, + "outbound": { "fillRate": 10.0, "capacity": 2048 } + }, + "connection": { "inbound": 128, "outbound": 16 } + }, + "workers": 32, + "policy": "allow", + "scope": "all" + } + })) + .unwrap(); + + assert!(cfg.node.extra.contains_key("db")); + assert!(cfg.node.extra.contains_key("policy")); + assert!(cfg.node.extra.contains_key("scope")); + } }