Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
httpd: Show private projects in project listing
Sebastian Martinez committed 2 years ago
commit d894a8b68b3037dadf84483711fe06f2f340db6c
parent cd071f031cf37c9edbbc9bdb4cdf16954b3f857d
4 files changed +234 -35
modified radicle-httpd/src/api.rs
@@ -66,6 +66,7 @@ impl Context {
        Ok(project::Info {
            payload,
            delegates,
+
            visibility: doc.visibility,
            head,
            issues,
            patches,
@@ -185,7 +186,7 @@ mod project {
    use radicle::cob;
    use radicle::git::Oid;
    use radicle::identity::project::Project;
-
    use radicle::identity::Id;
+
    use radicle::identity::{Id, Visibility};
    use radicle::prelude::Did;

    /// Project info.
@@ -196,6 +197,7 @@ mod project {
        #[serde(flatten)]
        pub payload: Project,
        pub delegates: NonEmpty<Did>,
+
        pub visibility: Visibility,
        pub head: Oid,
        pub patches: cob::patch::PatchCounts,
        pub issues: cob::issue::IssueCounts,
modified radicle-httpd/src/api/v1/delegates.rs
@@ -1,11 +1,13 @@
-
use axum::extract::State;
+
use std::net::SocketAddr;
+

+
use axum::extract::{ConnectInfo, State};
use axum::response::IntoResponse;
use axum::routing::get;
use axum::{Json, Router};

use radicle::cob::issue::Issues;
use radicle::cob::patch::Patches;
-
use radicle::identity::{Did, DocAt};
+
use radicle::identity::{Did, Visibility};
use radicle::node::routing::Store;
use radicle::storage::{ReadRepository, ReadStorage};

@@ -27,6 +29,7 @@ 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<SocketAddr>,
    State(ctx): State<Context>,
    Path(delegate): Path<Did>,
    Query(qs): Query<PaginationQuery>,
@@ -36,27 +39,31 @@ async fn delegates_projects_handler(
    let per_page = per_page.unwrap_or(10);
    let storage = &ctx.profile.storage;
    let routing = &ctx.profile.routing()?;
-
    let projects = storage
-
        .inventory()?
+
    let mut projects = storage
+
        .repositories()?
+
        .into_iter()
+
        .filter(|id| match &id.doc.visibility {
+
            Visibility::Private { .. } => addr.ip().is_loopback(),
+
            Visibility::Public => true,
+
        })
+
        .collect::<Vec<_>>();
+
    projects.sort_by_key(|p| p.rid);
+

+
    let infos = projects
        .into_iter()
        .filter_map(|id| {
-
            let Ok(repo) = storage.repository(id) else {
+
            if !id.doc.delegates.iter().any(|d| *d == delegate) {
                return None;
-
            };
-
            let Ok((_, head)) = repo.head() else {
+
            }
+
            let Ok(repo) = storage.repository(id.rid) else {
                return None;
            };
-
            let Ok(DocAt { doc, .. }) = repo.identity_doc() else {
+
            let Ok((_, head)) = repo.head() else {
                return None;
            };
-
            let Ok(payload) = doc.project() else {
+
            let Ok(payload) = id.doc.project() else {
                return None;
            };
-

-
            if !doc.delegates.iter().any(|d| *d == delegate) {
-
                return None;
-
            }
-

            let Ok(issues) = Issues::open(&repo) else {
                return None;
            };
@@ -70,16 +77,17 @@ async fn delegates_projects_handler(
                return None;
            };

-
            let delegates = doc.delegates;
-
            let trackings = routing.count(&id).unwrap_or_default();
+
            let delegates = id.doc.delegates;
+
            let trackings = routing.count(&id.rid).unwrap_or_default();

            Some(Info {
                payload,
                delegates,
+
                visibility: id.doc.visibility,
                head,
                issues,
                patches,
-
                id,
+
                id: id.rid,
                trackings,
            })
        })
@@ -87,11 +95,14 @@ async fn delegates_projects_handler(
        .take(per_page)
        .collect::<Vec<_>>();

-
    Ok::<_, Error>(Json(projects))
+
    Ok::<_, Error>(Json(infos))
}

#[cfg(test)]
mod routes {
+
    use std::net::SocketAddr;
+

+
    use axum::extract::connect_info::MockConnectInfo;
    use axum::http::StatusCode;
    use serde_json::json;

@@ -100,7 +111,9 @@ mod routes {
    #[tokio::test]
    async fn test_delegates_projects() {
        let tmp = tempfile::tempdir().unwrap();
-
        let app = super::router(test::seed(tmp.path()));
+
        let seed = test::seed(tmp.path());
+
        let app = super::router(seed.clone())
+
            .layer(MockConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080))));
        let response = get(
            &app,
            "/delegates/did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/projects",
@@ -112,10 +125,76 @@ mod routes {
            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",
+
                "trackings": 0,
+
              },
+
              {
                "name": "hello-world",
                "description": "Rad repository for tests",
                "defaultBranch": "master",
                "delegates": ["did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"],
+
                "visibility": {
+
                  "type": "public"
+
                },
+
                "head": HEAD,
+
                "patches": {
+
                  "open": 1,
+
                  "draft": 0,
+
                  "archived": 0,
+
                  "merged": 0,
+
                },
+
                "issues": {
+
                  "open": 1,
+
                  "closed": 0,
+
                },
+
                "id": RID,
+
                "trackings": 0,
+
              },
+
            ])
+
        );
+

+
        let app = super::router(seed).layer(MockConnectInfo(SocketAddr::from((
+
            [192, 168, 13, 37],
+
            8080,
+
        ))));
+
        let response = get(
+
            &app,
+
            "/delegates/did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/projects",
+
        )
+
        .await;
+

+
        assert_eq!(response.status(), StatusCode::OK);
+
        assert_eq!(
+
            response.json().await,
+
            json!([
+
              {
+
                "name": "hello-world",
+
                "description": "Rad repository for tests",
+
                "defaultBranch": "master",
+
                "delegates": ["did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"],
+
                "visibility": {
+
                  "type": "public"
+
                },
                "head": HEAD,
                "patches": {
                  "open": 1,
modified radicle-httpd/src/api/v1/projects.rs
@@ -1,6 +1,7 @@
use std::collections::{BTreeMap, HashMap};
+
use std::net::SocketAddr;

-
use axum::extract::{DefaultBodyLimit, State};
+
use axum::extract::{ConnectInfo, DefaultBodyLimit, State};
use axum::handler::Handler;
use axum::http::{header, HeaderValue};
use axum::response::IntoResponse;
@@ -14,7 +15,7 @@ use serde_json::{json, Value};
use tower_http::set_header::SetResponseHeaderLayer;

use radicle::cob::{issue, patch, Embed, Label, Uri};
-
use radicle::identity::{Did, DocAt, Id};
+
use radicle::identity::{Did, Id, Visibility};
use radicle::node::routing::Store;
use radicle::node::AliasStore;
use radicle::node::NodeId;
@@ -75,6 +76,7 @@ pub fn router(ctx: Context) -> Router {
/// List all projects.
/// `GET /projects`
async fn project_root_handler(
+
    ConnectInfo(addr): ConnectInfo<SocketAddr>,
    State(ctx): State<Context>,
    Query(qs): Query<PaginationQuery>,
) -> impl IntoResponse {
@@ -83,21 +85,26 @@ async fn project_root_handler(
    let per_page = per_page.unwrap_or(10);
    let storage = &ctx.profile.storage;
    let routing = &ctx.profile.routing()?;
-
    let projects = storage
-
        .inventory()?
+
    let mut projects = storage
+
        .repositories()?
+
        .into_iter()
+
        .filter(|id| match &id.doc.visibility {
+
            Visibility::Private { .. } => addr.ip().is_loopback(),
+
            Visibility::Public => true,
+
        })
+
        .collect::<Vec<_>>();
+
    projects.sort_by_key(|p| p.rid);
+

+
    let infos = projects
        .into_iter()
        .filter_map(|id| {
-
            let Ok(repo) = storage.repository(id) else {
+
            let Ok(repo) = storage.repository(id.rid) else {
                return None;
            };
            let Ok((_, head)) = repo.head() else {
                return None;
            };
-
            let Ok(DocAt { doc, .. }) = repo.identity_doc() else {
-
                return None;
-
            };
-

-
            let Ok(payload) = doc.project() else {
+
            let Ok(payload) = id.doc.project() else {
                return None;
            };
            let Ok(issues) = issue::Issues::open(&repo) else {
@@ -112,16 +119,17 @@ async fn project_root_handler(
            let Ok(patches) = patches.counts() else {
                return None;
            };
-
            let delegates = doc.delegates;
-
            let trackings = routing.count(&id).unwrap_or_default();
+
            let delegates = id.doc.delegates;
+
            let trackings = routing.count(&id.rid).unwrap_or_default();

            Some(Info {
                payload,
                delegates,
                head,
+
                visibility: id.doc.visibility,
                issues,
                patches,
-
                id,
+
                id: id.rid,
                trackings,
            })
        })
@@ -129,7 +137,7 @@ async fn project_root_handler(
        .take(per_page)
        .collect::<Vec<_>>();

-
    Ok::<_, Error>(Json(projects))
+
    Ok::<_, Error>(Json(infos))
}

/// Get project metadata.
@@ -990,7 +998,10 @@ async fn patch_handler(

#[cfg(test)]
mod routes {
+
    use std::net::SocketAddr;
+

    use axum::body::Body;
+
    use axum::extract::connect_info::MockConnectInfo;
    use axum::http::StatusCode;
    use pretty_assertions::assert_eq;
    use serde_json::json;
@@ -1000,7 +1011,68 @@ mod routes {
    #[tokio::test]
    async fn test_projects_root() {
        let tmp = tempfile::tempdir().unwrap();
-
        let app = super::router(seed(tmp.path()));
+
        let seed = seed(tmp.path());
+
        let app = super::router(seed.clone())
+
            .layer(MockConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080))));
+
        let response = get(&app, "/projects").await;
+

+
        assert_eq!(response.status(), StatusCode::OK);
+
        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",
+
                "trackings": 0,
+
              },
+
              {
+
                "name": "hello-world",
+
                "description": "Rad repository for tests",
+
                "defaultBranch": "master",
+
                "delegates": [DID],
+
                "visibility": {
+
                  "type": "public"
+
                },
+
                "head": HEAD,
+
                "patches": {
+
                  "open": 1,
+
                  "draft": 0,
+
                  "archived": 0,
+
                  "merged": 0,
+
                },
+
                "issues": {
+
                  "open": 1,
+
                  "closed": 0,
+
                },
+
                "id": RID,
+
                "trackings": 0,
+
              },
+
            ])
+
        );
+

+
        let app = super::router(seed).layer(MockConnectInfo(SocketAddr::from((
+
            [192, 168, 13, 37],
+
            8080,
+
        ))));
        let response = get(&app, "/projects").await;

        assert_eq!(response.status(), StatusCode::OK);
@@ -1012,6 +1084,9 @@ mod routes {
                "description": "Rad repository for tests",
                "defaultBranch": "master",
                "delegates": [DID],
+
                "visibility": {
+
                  "type": "public"
+
                },
                "head": HEAD,
                "patches": {
                  "open": 1,
@@ -1044,6 +1119,9 @@ mod routes {
               "description": "Rad repository for tests",
               "defaultBranch": "master",
               "delegates": [DID],
+
               "visibility": {
+
                 "type": "public"
+
               },
               "head": HEAD,
               "patches": {
                 "open": 1,
modified radicle-httpd/src/test.rs
@@ -1,3 +1,4 @@
+
use std::collections::BTreeSet;
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
@@ -107,6 +108,45 @@ fn seed_with_signer<G: Signer>(dir: &Path, profile: radicle::Profile, signer: &G
    RoutingStore::Table::open(routing_db).unwrap();
    AddressStore::Book::open(addresses_db).unwrap();

+
    let workdir = dir.join("hello-world-private");
+
    fs::create_dir_all(&workdir).unwrap();
+

+
    // add commits to workdir (repo)
+
    let mut opts = git2::RepositoryInitOptions::new();
+
    opts.initial_head(DEFAULT_BRANCH);
+
    let repo = git2::Repository::init_opts(&workdir, &opts).unwrap();
+
    let tree = radicle::git::write_tree(
+
        Path::new("README"),
+
        "Hello Private World!\n".as_bytes(),
+
        &repo,
+
    )
+
    .unwrap();
+

+
    let sig_time = git2::Time::new(1673001014, 0);
+
    let sig = git2::Signature::new("Alice Liddell", "alice@radicle.xyz", &sig_time).unwrap();
+

+
    repo.commit(Some("HEAD"), &sig, &sig, "Initial commit\n", &tree, &[])
+
        .unwrap();
+

+
    // rad init
+
    let repo = git2::Repository::open(&workdir).unwrap();
+
    let name = "hello-world-private".to_string();
+
    let description = "Private Rad repository for tests".to_string();
+
    let branch = RefString::try_from(DEFAULT_BRANCH).unwrap();
+
    let visibility = Visibility::Private {
+
        allow: BTreeSet::default(),
+
    };
+
    radicle::rad::init(
+
        &repo,
+
        &name,
+
        &description,
+
        branch,
+
        visibility,
+
        signer,
+
        &profile.storage,
+
    )
+
    .unwrap();
+

    let workdir = dir.join("hello-world");

    env::set_var("RAD_COMMIT_TIME", TIMESTAMP.to_string());