radicle-heartwood-lfs/radicle/src/node/policy/config.rs

167 lines
5.0 KiB
Rust

use core::fmt;
use std::collections::HashSet;
use std::ops;
use log::error;
use thiserror::Error;
use crate::crypto::PublicKey;
use crate::prelude::{Id, NodeId};
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, Node, Policy, Repo, Scope};
#[derive(Debug, Error)]
pub enum NamespacesError {
#[error("failed to find policy for {rid}")]
FailedPolicy {
rid: Id,
#[source]
err: Error,
},
#[error("cannot fetch {rid} as it is not seeded")]
BlockedPolicy { rid: Id },
#[error("failed to get node policies for {rid}")]
FailedNodes {
rid: Id,
#[source]
err: Error,
},
#[error("failed to get delegates for {rid}")]
FailedDelegates {
rid: Id,
#[source]
err: RepositoryError,
},
#[error(transparent)]
Git(#[from] crate::git::raw::Error),
#[error("could not find any followed nodes for {rid}")]
NoFollowed { rid: Id },
}
/// Policies configuration.
pub struct Config<T> {
/// 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,
/// Underlying configuration store.
store: Store<T>,
}
// N.b. deriving `Debug` will require `T: Debug` so we manually
// implement it here.
impl<T> fmt::Debug for Config<T> {
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()
}
}
impl<T> Config<T> {
/// Create a new policy configuration.
pub fn new(policy: Policy, scope: Scope, store: Store<T>) -> Self {
Self {
policy,
scope,
store,
}
}
/// Check if a repository is seeded.
pub fn is_seeding(&self, id: &Id) -> Result<bool, Error> {
self.repo_policy(id)
.map(|entry| entry.policy == Policy::Allow)
}
/// Check if a node is followed.
pub fn is_following(&self, id: &NodeId) -> Result<bool, Error> {
self.node_policy(id)
.map(|entry| entry.policy == Policy::Allow)
}
/// Get a node's following information.
/// Returns the default policy if the node isn't found.
pub fn node_policy(&self, id: &NodeId) -> Result<Node, Error> {
Ok(self.store.follow_policy(id)?.unwrap_or(Node {
id: *id,
alias: None,
policy: self.policy,
}))
}
/// Get a repository's seediing information.
/// Returns the default policy if the repo isn't found.
pub fn repo_policy(&self, id: &Id) -> Result<Repo, Error> {
Ok(self.store.seed_policy(id)?.unwrap_or(Repo {
id: *id,
scope: self.scope,
policy: self.policy,
}))
}
pub fn namespaces_for<S>(&self, storage: &S, rid: &Id) -> Result<Namespaces, NamespacesError>
where
S: ReadStorage,
{
use NamespacesError::*;
let entry = self
.repo_policy(rid)
.map_err(|err| FailedPolicy { rid: *rid, err })?;
match entry.policy {
Policy::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.id))
.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))
}
}
},
}
}
}
impl<T> ops::Deref for Config<T> {
type Target = Store<T>;
fn deref(&self) -> &Self::Target {
&self.store
}
}
impl<T> ops::DerefMut for Config<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.store
}
}