diff --git a/radicle-httpd/src/api.rs b/radicle-httpd/src/api.rs index 81c00fcb..a21da03d 100644 --- a/radicle-httpd/src/api.rs +++ b/radicle-httpd/src/api.rs @@ -11,6 +11,7 @@ use axum::routing::get; use axum::Router; use radicle::issue::cache::Issues as _; use radicle::patch::cache::Patches as _; +use radicle::storage::git::Repository; use serde::{Deserialize, Serialize}; use serde_json::json; use tokio::sync::RwLock; @@ -54,16 +55,19 @@ impl Context { } } - pub fn project_info(&self, id: RepoId) -> Result { - let storage = &self.profile.storage; - let repo = storage.repository(id)?; + pub fn project_info( + &self, + repo: &R, + doc: DocAt, + ) -> Result { let (_, head) = repo.head()?; - let DocAt { doc, .. } = repo.identity_doc()?; + let DocAt { doc, .. } = doc; + let id = repo.id(); let payload = doc.project()?; let delegates = doc.delegates; - let issues = self.profile.issues(&repo)?.counts()?; - let patches = self.profile.patches(&repo)?.counts()?; + let issues = self.profile.issues(repo)?.counts()?; + let patches = self.profile.patches(repo)?.counts()?; let db = &self.profile.database()?; let seeding = db.count(&id).unwrap_or_default(); @@ -79,6 +83,17 @@ impl Context { }) } + /// Get a repository by RID, checking to make sure we're allowed to view it. + pub fn repo(&self, rid: RepoId) -> Result<(Repository, DocAt), error::Error> { + let repo = self.profile.storage.repository(rid)?; + let doc = repo.identity_doc()?; + // Don't allow accessing private repos. + if doc.visibility.is_private() { + return Err(Error::NotFound); + } + Ok((repo, doc)) + } + #[cfg(test)] pub fn profile(&self) -> &Arc { &self.profile diff --git a/radicle-httpd/src/api/v1/delegates.rs b/radicle-httpd/src/api/v1/delegates.rs index 96d536f4..fc5d27c2 100644 --- a/radicle-httpd/src/api/v1/delegates.rs +++ b/radicle-httpd/src/api/v1/delegates.rs @@ -1,11 +1,9 @@ -use std::net::SocketAddr; - -use axum::extract::{ConnectInfo, State}; +use axum::extract::State; use axum::response::IntoResponse; use axum::routing::get; use axum::{Json, Router}; -use radicle::identity::{Did, Visibility}; +use radicle::identity::Did; use radicle::issue::cache::Issues as _; use radicle::node::routing::Store; use radicle::patch::cache::Patches as _; @@ -29,7 +27,6 @@ pub fn router(ctx: Context) -> Router { /// List all projects which delegate is a part of. /// `GET /delegates/:delegate/projects` async fn delegates_projects_handler( - ConnectInfo(addr): ConnectInfo, State(ctx): State, Path(delegate): Path, Query(qs): Query, @@ -48,10 +45,7 @@ async fn delegates_projects_handler( ProjectQuery::All => storage .repositories()? .into_iter() - .filter(|repo| match &repo.doc.visibility { - Visibility::Private { .. } => addr.ip().is_loopback(), - Visibility::Public => true, - }) + .filter(|repo| repo.doc.visibility.is_public()) .collect::>(), ProjectQuery::Pinned => storage.repositories_by_id(pinned.repositories.iter())?, }; @@ -137,30 +131,6 @@ mod routes { assert_eq!( response.json().await, json!([ - { - "name": "hello-world-private", - "description": "Private Rad repository for tests", - "defaultBranch": "master", - "delegates": [ - "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi", - ], - "visibility": { - "type": "private", - }, - "head": "d26ed310ed140fbef2a066aa486cf59a0f9f7812", - "patches": { - "open": 0, - "draft": 0, - "archived": 0, - "merged": 0, - }, - "issues": { - "open": 0, - "closed": 0, - }, - "id": "rad:zLuTzcmoWMcdK37xqArS8eckp9vK", - "seeding": 0, - }, { "name": "hello-world", "description": "Rad repository for tests", diff --git a/radicle-httpd/src/api/v1/projects.rs b/radicle-httpd/src/api/v1/projects.rs index 5049289c..177d5782 100644 --- a/radicle-httpd/src/api/v1/projects.rs +++ b/radicle-httpd/src/api/v1/projects.rs @@ -1,7 +1,6 @@ use std::collections::{BTreeMap, HashMap}; -use std::net::SocketAddr; -use axum::extract::{ConnectInfo, DefaultBodyLimit, State}; +use axum::extract::{DefaultBodyLimit, State}; use axum::handler::Handler; use axum::http::{header, HeaderValue}; use axum::response::IntoResponse; @@ -18,10 +17,9 @@ use radicle::cob::{ issue, issue::cache::Issues as _, patch, patch::cache::Patches as _, resolve_embed, Embed, Label, Uri, }; -use radicle::identity::{Did, RepoId, Visibility}; +use radicle::identity::{Did, RepoId}; use radicle::node::routing::Store; use radicle::node::{AliasStore, Node, NodeId}; -use radicle::storage::git::paths; use radicle::storage::{ReadRepository, ReadStorage, RemoteRepository, WriteRepository}; use radicle_surf::{diff, Glob, Oid, Repository}; @@ -78,7 +76,6 @@ pub fn router(ctx: Context) -> Router { /// List all projects. /// `GET /projects` async fn project_root_handler( - ConnectInfo(addr): ConnectInfo, State(ctx): State, Query(qs): Query, ) -> impl IntoResponse { @@ -98,18 +95,12 @@ async fn project_root_handler( ProjectQuery::All => storage .repositories()? .into_iter() - .filter(|repo| match &repo.doc.visibility { - Visibility::Private { .. } => addr.ip().is_loopback(), - Visibility::Public => true, - }) + .filter(|repo| repo.doc.visibility.is_public()) .collect::>(), ProjectQuery::Pinned => storage .repositories_by_id(pinned.repositories.iter())? .into_iter() - .filter(|repo| match &repo.doc.visibility { - Visibility::Private { .. } => addr.ip().is_loopback(), - Visibility::Public => true, - }) + .filter(|repo| repo.doc.visibility.is_public()) .collect::>(), }; projects.sort_by_key(|p| p.rid); @@ -164,8 +155,9 @@ async fn project_root_handler( /// Get project metadata. /// `GET /projects/:project` -async fn project_handler(State(ctx): State, Path(id): Path) -> impl IntoResponse { - let info = ctx.project_info(id)?; +async fn project_handler(State(ctx): State, Path(rid): Path) -> impl IntoResponse { + let (repo, doc) = ctx.repo(rid)?; + let info = ctx.project_info(&repo, doc)?; Ok::<_, Error>(Json(info)) } @@ -184,9 +176,10 @@ pub struct CommitsQueryString { /// `GET /projects/:project/commits?parent=` async fn history_handler( State(ctx): State, - Path(project): Path, + Path(rid): Path, Query(qs): Query, ) -> impl IntoResponse { + let (repo, doc) = ctx.repo(rid)?; let CommitsQueryString { since, until, @@ -202,15 +195,9 @@ async fn history_handler( let sha = match parent { Some(commit) => commit, - None => { - let info = ctx.project_info(project)?; - - info.head.to_string() - } + None => ctx.project_info(&repo, doc)?.head.to_string(), }; - - let storage = &ctx.profile.storage; - let repo = Repository::open(paths::repository(storage, &project))?; + let repo = Repository::open(repo.path())?; // If a pagination is defined, we do not want to paginate the commits, and we return all of them on the first page. let page = page.unwrap_or(0); @@ -276,8 +263,8 @@ async fn commit_handler( State(ctx): State, Path((project, sha)): Path<(RepoId, Oid)>, ) -> impl IntoResponse { - let storage = &ctx.profile.storage; - let repo = Repository::open(paths::repository(storage, &project))?; + let (repo, _) = ctx.repo(project)?; + let repo = Repository::open(repo.path())?; let commit = repo.commit(sha)?; let diff = repo.diff_commit(commit.id)?; @@ -342,8 +329,8 @@ async fn diff_handler( State(ctx): State, Path((project, base, oid)): Path<(RepoId, Oid, Oid)>, ) -> impl IntoResponse { - let storage = &ctx.profile.storage; - let repo = Repository::open(paths::repository(storage, &project))?; + let (repo, _) = ctx.repo(project)?; + let repo = Repository::open(repo.path())?; let base = repo.commit(base)?; let commit = repo.commit(oid)?; let diff = repo.diff(base.id, commit.id)?; @@ -409,10 +396,10 @@ async fn activity_handler( State(ctx): State, Path(project): Path, ) -> impl IntoResponse { + let (repo, _) = ctx.repo(project)?; let current_date = chrono::Utc::now().timestamp(); let one_year_ago = chrono::Duration::weeks(52); - let storage = &ctx.profile.storage; - let repo = Repository::open(paths::repository(storage, &project))?; + let repo = Repository::open(repo.path())?; let head = repo.head()?; let timestamps = repo .history(head)? @@ -434,9 +421,9 @@ async fn activity_handler( /// `GET /projects/:project/tree/:sha/` async fn tree_handler_root( State(ctx): State, - Path((project, sha)): Path<(RepoId, Oid)>, + Path((rid, sha)): Path<(RepoId, Oid)>, ) -> impl IntoResponse { - tree_handler(State(ctx), Path((project, sha, String::new()))).await + tree_handler(State(ctx), Path((rid, sha, String::new()))).await } /// Get project source tree. @@ -445,6 +432,8 @@ async fn tree_handler( State(ctx): State, Path((project, sha, path)): Path<(RepoId, Oid, String)>, ) -> impl IntoResponse { + let (repo, _) = ctx.repo(project)?; + if let Some(ref cache) = ctx.cache { let cache = &mut cache.tree.lock().await; if let Some(response) = cache.get(&(project, sha, path.clone())) { @@ -452,13 +441,12 @@ async fn tree_handler( } } - let storage = &ctx.profile.storage; - let repo = Repository::open(paths::repository(storage, &project))?; + let repo = Repository::open(repo.path())?; let tree = repo.tree(sha, &path)?; let stats = repo.stats_from(&sha)?; let response = api::json::tree(&tree, &path, &stats); - if let Some(cache) = ctx.cache { + if let Some(cache) = &ctx.cache { let cache = &mut cache.tree.lock().await; cache.put((project, sha, path.clone()), response.clone()); } @@ -472,9 +460,8 @@ async fn remotes_handler( State(ctx): State, Path(project): Path, ) -> impl IntoResponse { - let storage = &ctx.profile.storage; - let repo = storage.repository(project)?; - let delegates = repo.delegates()?; + let (repo, doc) = ctx.repo(project)?; + let delegates = &doc.delegates; let aliases = &ctx.profile.aliases(); let remotes = repo .remotes()? @@ -515,9 +502,8 @@ async fn remote_handler( State(ctx): State, Path((project, node_id)): Path<(RepoId, NodeId)>, ) -> impl IntoResponse { - let storage = &ctx.profile.storage; - let repo = storage.repository(project)?; - let delegates = repo.delegates()?; + let (repo, doc) = ctx.repo(project)?; + let delegates = &doc.delegates; let remote = repo.remote(&node_id)?; let refs = remote .refs @@ -543,8 +529,8 @@ async fn blob_handler( State(ctx): State, Path((project, sha, path)): Path<(RepoId, Oid, String)>, ) -> impl IntoResponse { - let storage = &ctx.profile.storage; - let repo = Repository::open(paths::repository(storage, &project))?; + let (repo, _) = ctx.repo(project)?; + let repo = Repository::open(repo.path())?; let blob = repo.blob(sha, &path)?; let response = api::json::blob(&blob, &path); @@ -557,8 +543,8 @@ async fn readme_handler( State(ctx): State, Path((project, sha)): Path<(RepoId, Oid)>, ) -> impl IntoResponse { - let storage = &ctx.profile.storage; - let repo = Repository::open(paths::repository(storage, &project))?; + let (repo, _) = ctx.repo(project)?; + let repo = Repository::open(repo.path())?; let paths = [ "README", "README.md", @@ -589,6 +575,7 @@ async fn issues_handler( Path(project): Path, Query(qs): Query>, ) -> impl IntoResponse { + let (repo, _) = ctx.repo(project)?; let CobsQuery { page, per_page, @@ -597,8 +584,6 @@ async fn issues_handler( let page = page.unwrap_or(0); let per_page = per_page.unwrap_or(10); let state = state.unwrap_or_default(); - let storage = &ctx.profile.storage; - let repo = storage.repository(project)?; let issues = ctx.profile.issues(&repo)?; let mut issues: Vec<_> = issues .list()? @@ -638,13 +623,13 @@ async fn issue_create_handler( Json(issue): Json, ) -> impl IntoResponse { api::auth::validate(&ctx, &token).await?; + + let (repo, _) = ctx.repo(project)?; let node = Node::new(ctx.profile.socket()); - let storage = &ctx.profile.storage; let signer = ctx .profile .signer() .map_err(|_| Error::Auth("Unauthorized"))?; - let repo = storage.repository(project)?; let embeds: Vec = issue .embeds .into_iter() @@ -680,10 +665,10 @@ async fn issue_update_handler( Json(action): Json, ) -> impl IntoResponse { api::auth::validate(&ctx, &token).await?; + + let (repo, _) = ctx.repo(project)?; let node = Node::new(ctx.profile.socket()); - let storage = &ctx.profile.storage; let signer = ctx.profile.signer()?; - let repo = storage.repository(project)?; let mut issues = ctx.profile.issues_mut(&repo)?; let mut issue = issues.get_mut(&issue_id.into())?; @@ -733,8 +718,7 @@ async fn issue_handler( State(ctx): State, Path((project, issue_id)): Path<(RepoId, Oid)>, ) -> impl IntoResponse { - let storage = &ctx.profile.storage; - let repo = storage.repository(project)?; + let (repo, _) = ctx.repo(project)?; let issue = ctx .profile .issues(&repo)? @@ -763,13 +747,13 @@ async fn patch_create_handler( Json(patch): Json, ) -> impl IntoResponse { api::auth::validate(&ctx, &token).await?; + let node = Node::new(ctx.profile.socket()); - let storage = &ctx.profile.storage; let signer = ctx .profile .signer() .map_err(|_| Error::Auth("Unauthorized"))?; - let repo = storage.repository(project)?; + let (repo, _) = ctx.repo(project)?; let mut patches = ctx.profile.patches_mut(&repo)?; let base_oid = repo.raw().merge_base(*patch.target, *patch.oid)?; @@ -802,13 +786,13 @@ async fn patch_update_handler( Json(action): Json, ) -> impl IntoResponse { api::auth::validate(&ctx, &token).await?; + let node = Node::new(ctx.profile.socket()); - let storage = &ctx.profile.storage; let signer = ctx .profile .signer() .map_err(|_| Error::Auth("Unauthorized"))?; - let repo = storage.repository(project)?; + let (repo, _) = ctx.repo(project)?; let mut patches = ctx.profile.patches_mut(&repo)?; let mut patch = patches.get_mut(&patch_id.into())?; let id = match action { @@ -941,9 +925,10 @@ async fn patch_update_handler( /// `GET /projects/:project/patches` async fn patches_handler( State(ctx): State, - Path(project): Path, + Path(rid): Path, Query(qs): Query>, ) -> impl IntoResponse { + let (repo, _) = ctx.repo(rid)?; let CobsQuery { page, per_page, @@ -952,8 +937,6 @@ async fn patches_handler( let page = page.unwrap_or(0); let per_page = per_page.unwrap_or(10); let state = state.unwrap_or_default(); - let storage = &ctx.profile.storage; - let repo = storage.repository(project)?; let patches = ctx.profile.patches(&repo)?; let mut patches = patches .list()? @@ -978,10 +961,9 @@ async fn patches_handler( /// `GET /projects/:project/patches/:id` async fn patch_handler( State(ctx): State, - Path((project, patch_id)): Path<(RepoId, Oid)>, + Path((rid, patch_id)): Path<(RepoId, Oid)>, ) -> impl IntoResponse { - let storage = &ctx.profile.storage; - let repo = storage.repository(project)?; + let (repo, _) = ctx.repo(rid)?; let patches = ctx.profile.patches(&repo)?; let patch = patches.get(&patch_id.into())?.ok_or(Error::NotFound)?; let aliases = ctx.profile.aliases(); @@ -1002,6 +984,7 @@ mod routes { use axum::extract::connect_info::MockConnectInfo; use axum::http::StatusCode; use pretty_assertions::assert_eq; + use radicle::storage::ReadStorage; use serde_json::json; use crate::test::*; @@ -1018,30 +1001,6 @@ mod routes { assert_eq!( response.json().await, json!([ - { - "name": "hello-world-private", - "description": "Private Rad repository for tests", - "defaultBranch": "master", - "delegates": [ - "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi", - ], - "visibility": { - "type": "private", - }, - "head": "d26ed310ed140fbef2a066aa486cf59a0f9f7812", - "patches": { - "open": 0, - "draft": 0, - "archived": 0, - "merged": 0, - }, - "issues": { - "open": 0, - "closed": 0, - }, - "id": "rad:zLuTzcmoWMcdK37xqArS8eckp9vK", - "seeding": 0, - }, { "name": "hello-world", "description": "Rad repository for tests", @@ -3800,4 +3759,32 @@ mod routes { }) ); } + + #[tokio::test] + async fn test_projects_private() { + let tmp = tempfile::tempdir().unwrap(); + let ctx = seed(tmp.path()); + let app = super::router(ctx.to_owned()); + + // Check that the repo exists. + ctx.profile() + .storage + .repository(RID_PRIVATE.parse().unwrap()) + .unwrap(); + + let response = get(&app, format!("/projects/{RID_PRIVATE}")).await; + assert_eq!(response.status(), StatusCode::NOT_FOUND); + + let response = get(&app, format!("/projects/{RID_PRIVATE}/patches")).await; + assert_eq!(response.status(), StatusCode::NOT_FOUND); + + let response = get(&app, format!("/projects/{RID_PRIVATE}/issues")).await; + assert_eq!(response.status(), StatusCode::NOT_FOUND); + + let response = get(&app, format!("/projects/{RID_PRIVATE}/commits")).await; + assert_eq!(response.status(), StatusCode::NOT_FOUND); + + let response = get(&app, format!("/projects/{RID_PRIVATE}/remotes")).await; + assert_eq!(response.status(), StatusCode::NOT_FOUND); + } } diff --git a/radicle-httpd/src/error.rs b/radicle-httpd/src/error.rs index f3bb4a29..1dc002a9 100644 --- a/radicle-httpd/src/error.rs +++ b/radicle-httpd/src/error.rs @@ -22,6 +22,14 @@ pub enum GitError { #[error("invalid radicle identifier: {0}")] Id(#[from] radicle::identity::IdError), + /// Storage error. + #[error("storage: {0}")] + Storage(#[from] radicle::storage::Error), + + /// Repository error. + #[error("repository: {0}")] + Repository(#[from] radicle::storage::RepositoryError), + /// Git backend error. #[error("git-http-backend: exited with code {0}")] BackendExited(ExitStatus), @@ -73,6 +81,10 @@ pub enum RawError { #[error(transparent)] Storage(#[from] radicle::storage::Error), + /// Repository error. + #[error(transparent)] + Repository(#[from] radicle::storage::RepositoryError), + /// Http Headers error. #[error(transparent)] Headers(#[from] http::header::InvalidHeaderValue), @@ -80,12 +92,16 @@ pub enum RawError { /// Surf file error. #[error(transparent)] SurfFile(#[from] radicle_surf::fs::error::File), + + /// The entity was not found. + #[error("not found")] + NotFound, } impl RawError { pub fn status(&self) -> http::StatusCode { match self { - RawError::SurfFile(_) => http::StatusCode::NOT_FOUND, + RawError::SurfFile(_) | RawError::NotFound => http::StatusCode::NOT_FOUND, _ => http::StatusCode::INTERNAL_SERVER_ERROR, } } diff --git a/radicle-httpd/src/git.rs b/radicle-httpd/src/git.rs index 43d725b9..c2f0de8f 100644 --- a/radicle-httpd/src/git.rs +++ b/radicle-httpd/src/git.rs @@ -18,6 +18,7 @@ use hyper::body::Buf as _; use radicle::identity::RepoId; use radicle::profile::Profile; +use radicle::storage::{ReadRepository, ReadStorage}; use crate::error::GitError as Error; @@ -82,6 +83,12 @@ async fn git_http_backend( "" }; + // Don't allow cloning of private repositories. + let doc = profile.storage.repository(id)?.identity_doc()?; + if doc.visibility.is_private() { + return Err(Error::NotFound); + } + // Reject push requests. match (path, query.as_str()) { ("git-receive-pack", _) | (_, "service=git-receive-pack") => { diff --git a/radicle-httpd/src/raw.rs b/radicle-httpd/src/raw.rs index 074e5b73..3aa2dad6 100644 --- a/radicle-httpd/src/raw.rs +++ b/radicle-httpd/src/raw.rs @@ -107,11 +107,17 @@ pub fn router(profile: Arc) -> Router { } async fn file_by_path_handler( - Path((project, sha, path)): Path<(RepoId, Oid, String)>, + Path((rid, sha, path)): Path<(RepoId, Oid, String)>, State(profile): State>, ) -> impl IntoResponse { let storage = &profile.storage; - let repo = storage.repository(project)?; + let repo = storage.repository(rid)?; + + // Don't allow downloading raw files for private repos. + if repo.identity_doc()?.visibility.is_private() { + return Err(Error::NotFound); + } + let mut response_headers = HeaderMap::new(); let repo: Repository = repo.backend.into(); let blob = repo.blob(sha, &path)?; @@ -134,12 +140,18 @@ async fn file_by_path_handler( } async fn file_by_oid_handler( - Path((project, oid)): Path<(RepoId, Oid)>, + Path((rid, oid)): Path<(RepoId, Oid)>, State(profile): State>, Query(qs): Query, ) -> impl IntoResponse { let storage = &profile.storage; - let repo = storage.repository(project)?; + let repo = storage.repository(rid)?; + + // Don't allow downloading raw files for private repos. + if repo.identity_doc()?.visibility.is_private() { + return Err(Error::NotFound); + } + let blob = repo.blob(oid)?; let mut response_headers = HeaderMap::new(); @@ -159,7 +171,8 @@ async fn file_by_oid_handler( mod routes { use axum::http::StatusCode; - use crate::test::{self, get, HEAD, RID}; + use crate::test::{self, get, HEAD, RID, RID_PRIVATE}; + use radicle::storage::{ReadRepository, ReadStorage}; #[tokio::test] async fn test_file_handler() { @@ -171,5 +184,16 @@ mod routes { assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.body().await, "Hello World from dir1!\n"); + + // Make sure the repo exists in storage. + let repo = ctx + .profile() + .storage + .repository(RID_PRIVATE.parse().unwrap()) + .unwrap(); + let (_, head) = repo.head().unwrap(); + + let response = get(&app, format!("/{RID_PRIVATE}/{head}/README")).await; + assert_eq!(response.status(), StatusCode::NOT_FOUND); } } diff --git a/radicle-httpd/src/test.rs b/radicle-httpd/src/test.rs index 7ac99fa7..43d8fb27 100644 --- a/radicle-httpd/src/test.rs +++ b/radicle-httpd/src/test.rs @@ -26,6 +26,7 @@ use radicle_crypto::test::signer::MockSigner; use crate::api::{auth, Context}; pub const RID: &str = "rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp"; +pub const RID_PRIVATE: &str = "rad:zLuTzcmoWMcdK37xqArS8eckp9vK"; pub const HEAD: &str = "e8c676b9e3b42308dc9d218b70faa5408f8e58ca"; pub const PARENT: &str = "ee8d6a29304623a78ebfa5eeed5af674d0e58f83"; pub const INITIAL_COMMIT: &str = "f604ce9fd5b7cc77b7609beda45ea8760bee78f7"; @@ -138,6 +139,7 @@ fn seed_with_signer(dir: &Path, profile: radicle::Profile, signer: &G &profile.storage, ) .unwrap(); + policies.seed(&rid, node::policy::Scope::All).unwrap(); let workdir = dir.join("hello-world"); diff --git a/radicle-node/src/worker.rs b/radicle-node/src/worker.rs index 6c97020e..3868deeb 100644 --- a/radicle-node/src/worker.rs +++ b/radicle-node/src/worker.rs @@ -273,7 +273,7 @@ impl Worker { fn is_authorized(&self, remote: NodeId, rid: RepoId) -> Result<(), UploadError> { let policy = self.policies.seed_policy(&rid)?.policy; let repo = self.storage.repository(rid)?; - let doc = repo.canonical_identity_doc()?; + let doc = repo.identity_doc()?; if !doc.is_visible_to(&remote) || policy == Policy::Block { Err(UploadError::Unauthorized(remote, rid)) } else {