Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
http: Remove any remaining authentication code
Open rudolfs opened 1 year ago

We also remove the /api/v1/profile endpoint because that was only supposed to be used with a local node and not publicly accessible. All fields that are still used in the UI were moved to /api/v1/node.

check check-visual check-unit-test check-http-client-unit-test check-radicle-httpd check-e2e check-build check-http 👉 Preview 👉 Workflow runs 👉 Branch on GitHub

12 files changed +48 -205 b105d06f 04d04191
modified config/default.json
@@ -1,7 +1,7 @@
{
  "nodes": {
    "fallbackPublicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path",
-
    "apiVersion": "3.0.0",
+
    "apiVersion": "4.0.0",
    "defaultHttpdPort": 443,
    "defaultHttpdHostname": "seed.radicle.garden",
    "defaultHttpdScheme": "https",
modified http-client/index.ts
@@ -42,7 +42,6 @@ import type { ZodSchema } from "zod";
import { z, array, literal, number, object, string, union } from "zod";

import * as project from "./lib/project.js";
-
import * as profile from "./lib/profile.js";
import { Fetcher } from "./lib/fetcher.js";
import {
  nodeConfigSchema,
@@ -93,6 +92,9 @@ const nodeSchema = object({
  agent: string(),
  config: nodeConfigSchema.nullable(),
  state: union([literal("running"), literal("stopped")]),
+
  avatarUrl: string().optional(),
+
  bannerUrl: string().optional(),
+
  description: string().optional(),
});

export type NodeInfo = z.infer<typeof nodeInfoSchema>;
@@ -144,14 +146,12 @@ export class HttpdClient {

  public baseUrl: BaseUrl;
  public project: project.Client;
-
  public profile: profile.Client;

  public constructor(baseUrl: BaseUrl) {
    this.baseUrl = baseUrl;
    this.#fetcher = new Fetcher(this.baseUrl);

    this.project = new project.Client(this.#fetcher);
-
    this.profile = new profile.Client(this.#fetcher);
  }

  public changePort(port: number): void {
deleted http-client/lib/profile.ts
@@ -1,31 +0,0 @@
-
import type { Fetcher, RequestOptions } from "./fetcher.js";
-
import type { z } from "zod";
-

-
import { object, string } from "zod";
-
import { configSchema } from "./shared.js";
-

-
const profileSchema = object({
-
  config: configSchema,
-
  home: string(),
-
});
-

-
export type Profile = z.infer<typeof profileSchema>;
-

-
export class Client {
-
  #fetcher: Fetcher;
-

-
  public constructor(fetcher: Fetcher) {
-
    this.#fetcher = fetcher;
-
  }
-

-
  public async getProfile(options?: RequestOptions): Promise<Profile> {
-
    return this.#fetcher.fetchOk(
-
      {
-
        method: "GET",
-
        path: `profile`,
-
        options,
-
      },
-
      profileSchema,
-
    );
-
  }
-
}
modified radicle-httpd/Cargo.lock
@@ -1614,9 +1614,9 @@ dependencies = [

[[package]]
name = "radicle"
-
version = "0.11.1"
+
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "acd518b6fdbfb4355e261f91fa3acb9ab5f83c842714ea5e7a3f66762f17b816"
+
checksum = "e8dd9d6612b6248f99f98c9e20e84649982e08bb9c35b0fff5aa521dd8f2970a"
dependencies = [
 "amplify",
 "base64 0.21.7",
@@ -1708,7 +1708,7 @@ dependencies = [

[[package]]
name = "radicle-httpd"
-
version = "0.14.0"
+
version = "0.15.0"
dependencies = [
 "anyhow",
 "axum",
modified radicle-httpd/Cargo.toml
@@ -3,7 +3,7 @@ name = "radicle-httpd"
description = "Radicle HTTP daemon"
homepage = "https://radicle.xyz"
license = "MIT OR Apache-2.0"
-
version = "0.14.0"
+
version = "0.15.0"
authors = ["cloudhead <cloudhead@radicle.xyz>"]
edition = "2021"
default-run = "radicle-httpd"
@@ -46,7 +46,7 @@ ureq = { version = "2.9", default-features = false, features = ["json"] }
url = { version = "2.5.0" }

[dependencies.radicle]
-
version = "0.11.1"
+
version = "0.12.0"

[dependencies.radicle-term]
version = "0.10.0"
modified radicle-httpd/src/api.rs
@@ -1,7 +1,7 @@
use std::sync::Arc;
use std::time::Duration;

-
use axum::http::header::{AUTHORIZATION, CONTENT_TYPE};
+
use axum::http::header::CONTENT_TYPE;
use axum::http::Method;
use axum::response::{IntoResponse, Json};
use axum::routing::get;
@@ -31,7 +31,7 @@ use crate::Options;

pub const RADICLE_VERSION: &str = env!("RADICLE_VERSION");
// This version has to be updated on every breaking change to the radicle-httpd API.
-
pub const API_VERSION: &str = "3.0.0";
+
pub const API_VERSION: &str = "4.0.0";

#[derive(Clone)]
pub struct Context {
@@ -106,14 +106,8 @@ pub fn router(ctx: Context) -> Router {
            CorsLayer::new()
                .max_age(Duration::from_secs(86400))
                .allow_origin(cors::Any)
-
                .allow_methods([
-
                    Method::GET,
-
                    Method::POST,
-
                    Method::PATCH,
-
                    Method::PUT,
-
                    Method::DELETE,
-
                ])
-
                .allow_headers([CONTENT_TYPE, AUTHORIZATION]),
+
                .allow_methods([Method::GET])
+
                .allow_headers([CONTENT_TYPE]),
        )
}

modified radicle-httpd/src/api/error.rs
@@ -10,10 +10,6 @@ pub enum Error {
    #[error("entity not found")]
    NotFound,

-
    /// An error occurred during an authentication process.
-
    #[error("could not authenticate: {0}")]
-
    Auth(&'static str),
-

    /// An error occurred with env variables.
    #[error(transparent)]
    Env(#[from] std::env::VarError),
@@ -107,7 +103,6 @@ impl IntoResponse for Error {
            Error::CobStore(e @ radicle::cob::store::Error::NotFound(_, _)) => {
                (StatusCode::NOT_FOUND, Some(e.to_string()))
            }
-
            Error::Auth(msg) => (StatusCode::UNAUTHORIZED, Some(msg.to_string())),
            Error::Crypto(msg) => (StatusCode::BAD_REQUEST, Some(msg.to_string())),
            Error::Surf(radicle_surf::Error::Git(e)) if radicle::git::is_not_found_err(&e) => {
                (StatusCode::NOT_FOUND, Some(e.message().to_owned()))
modified radicle-httpd/src/api/v1.rs
@@ -1,6 +1,5 @@
mod delegates;
mod node;
-
mod profile;
mod projects;
mod stats;

@@ -20,7 +19,6 @@ pub fn router(ctx: Context) -> Router {
    let routes = Router::new()
        .merge(root_router)
        .merge(node::router(ctx.clone()))
-
        .merge(profile::router(ctx.clone()))
        .merge(delegates::router(ctx.clone()))
        .merge(projects::router(ctx.clone()))
        .merge(stats::router(ctx));
@@ -53,11 +51,6 @@ async fn root_handler(State(ctx): State<Context>) -> impl IntoResponse {
                "type": "GET"
            },
            {
-
                "href": "/profile",
-
                "rel": "profile",
-
                "type": "GET"
-
            },
-
            {
                "href": "/stats",
                "rel": "stats",
                "type": "GET"
modified radicle-httpd/src/api/v1/node.rs
@@ -2,13 +2,14 @@ use axum::extract::State;
use axum::response::IntoResponse;
use axum::routing::get;
use axum::{Json, Router};
+
use serde::Serialize;
use serde_json::json;

use radicle::crypto::ssh::fmt;
use radicle::identity::{Did, RepoId};
use radicle::node::address::Store as AddressStore;
use radicle::node::routing::Store;
-
use radicle::node::{AliasStore, Handle, NodeId};
+
use radicle::node::{AliasStore, Config, Handle, NodeId, UserAgent};
use radicle::Node;

use crate::api::error::Error;
@@ -25,6 +26,21 @@ pub fn router(ctx: Context) -> Router {
        .with_state(ctx)
}

+
#[derive(Clone, Debug, Default, Serialize)]
+
#[serde(rename_all = "camelCase")]
+
struct Response {
+
    id: String,
+
    agent: Option<UserAgent>,
+
    config: Option<Config>,
+
    state: String,
+
    #[serde(skip_serializing_if = "Option::is_none")]
+
    avatar_url: Option<String>,
+
    #[serde(skip_serializing_if = "Option::is_none")]
+
    banner_url: Option<String>,
+
    #[serde(skip_serializing_if = "Option::is_none")]
+
    description: Option<String>,
+
}
+

/// Return local node information.
/// `GET /node`
async fn node_handler(State(ctx): State<Context>) -> impl IntoResponse {
@@ -39,6 +55,9 @@ async fn node_handler(State(ctx): State<Context>) -> impl IntoResponse {
    } else {
        "stopped"
    };
+
    let avatar_url = ctx.profile.config.web.avatar_url.clone();
+
    let banner_url = ctx.profile.config.web.banner_url.clone();
+
    let description = ctx.profile.config.web.description.clone();
    let config = match node.config() {
        Ok(config) => Some(config),
        Err(err) => {
@@ -46,14 +65,18 @@ async fn node_handler(State(ctx): State<Context>) -> impl IntoResponse {
            None
        }
    };
-
    let response = json!({
-
        "id": node_id.to_string(),
-
        "agent": agent,
-
        "config": config,
-
        "state": node_state,
-
    });

-
    Ok::<_, Error>(Json(response))
+
    let response = Response {
+
        id: node_id.to_string(),
+
        agent,
+
        config,
+
        state: node_state.to_owned(),
+
        avatar_url,
+
        banner_url,
+
        description,
+
    };
+

+
    Ok::<_, Error>(Json(json!(response)))
}

/// Return stored information about other nodes.
deleted radicle-httpd/src/api/v1/profile.rs
@@ -1,123 +0,0 @@
-
use std::net::SocketAddr;
-

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

-
use crate::api::error::Error;
-
use crate::api::Context;
-

-
pub fn router(ctx: Context) -> Router {
-
    Router::new()
-
        .route("/profile", get(profile_handler))
-
        .with_state(ctx)
-
}
-

-
/// Return local profile information.
-
/// `GET /profile`
-
async fn profile_handler(
-
    State(ctx): State<Context>,
-
    ConnectInfo(addr): ConnectInfo<SocketAddr>,
-
) -> impl IntoResponse {
-
    if !addr.ip().is_loopback() {
-
        return Err(Error::Auth("Profile data is only shown for localhost"));
-
    }
-

-
    Ok::<_, Error>(Json(
-
        json!({ "config": ctx.profile.config, "home": ctx.profile.home.path() }),
-
    ))
-
}
-

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

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

-
    use crate::test::{self, get};
-

-
    #[tokio::test]
-
    async fn test_remote_profile() {
-
        let tmp = tempfile::tempdir().unwrap();
-
        let seed = test::seed(tmp.path());
-
        let app = super::router(seed.clone())
-
            .layer(MockConnectInfo(SocketAddr::from(([192, 168, 1, 1], 8080))));
-
        let response = get(&app, "/profile").await;
-

-
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
-
        assert_eq!(
-
            response.json().await,
-
            json!({
-
              "error": "Profile data is only shown for localhost",
-
              "code": 401
-
            })
-
        )
-
    }
-

-
    #[tokio::test]
-
    async fn test_profile() {
-
        let tmp = tempfile::tempdir().unwrap();
-
        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, "/profile").await;
-

-
        assert_eq!(response.status(), StatusCode::OK);
-
        assert_eq!(
-
            response.json().await,
-
            json!({
-
              "config": {
-
                "publicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path",
-
                "preferredSeeds": [
-
                  "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@seed.radicle.garden:8776",
-
                  "z6Mkmqogy2qEM2ummccUthFEaaHvyYmYBYh3dbe9W4ebScxo@ash.radicle.garden:8776"
-
                ],
-
                "web": { "pinned": { "repositories": [] } },
-
                "cli": {
-
                  "hints": true
-
                },
-
                "node": {
-
                  "alias": "seed",
-
                  "listen": [],
-
                  "peers": { "type": "dynamic" },
-
                  "connect": [],
-
                  "externalAddresses": [],
-
                  "network": "main",
-
                  "log": "INFO",
-
                  "relay": "auto",
-
                  "limits": {
-
                    "routingMaxSize": 1000,
-
                    "routingMaxAge": 604800,
-
                    "gossipMaxAge": 1209600,
-
                    "fetchConcurrency": 1,
-
                    "maxOpenFiles": 4096,
-
                    "rate": {
-
                      "inbound": {
-
                        "fillRate": 5.0,
-
                        "capacity": 1024
-
                      },
-
                      "outbound": {
-
                        "fillRate": 10.0,
-
                        "capacity": 2048
-
                      }
-
                    },
-
                    "connection": {
-
                      "inbound": 128,
-
                      "outbound": 16
-
                    }
-
                  },
-
                  "workers": 8,
-
                  "seedingPolicy": {
-
                      "default": "block",
-
                  }
-
                }
-
              },
-
              "home": seed.profile.path()
-
            })
-
        );
-
    }
-
}
modified radicle-httpd/src/test.rs
@@ -326,25 +326,17 @@ fn seed_with_signer<G: Signer>(dir: &Path, profile: radicle::Profile, signer: &G
pub async fn get(app: &Router, path: impl ToString) -> Response {
    Response(
        app.clone()
-
            .oneshot(request(path, Method::GET, None, None))
+
            .oneshot(request(path, Method::GET, None))
            .await
            .unwrap(),
    )
}

-
fn request(
-
    path: impl ToString,
-
    method: Method,
-
    body: Option<Body>,
-
    auth: Option<String>,
-
) -> Request<Body> {
-
    let mut request = Request::builder()
+
fn request(path: impl ToString, method: Method, body: Option<Body>) -> Request<Body> {
+
    let request = Request::builder()
        .method(method)
        .uri(path.to_string())
        .header("Content-Type", "application/json");
-
    if let Some(token) = auth {
-
        request = request.header("Authorization", format!("Bearer {token}"));
-
    }

    request.body(body.unwrap_or_else(Body::empty)).unwrap()
}
modified tests/support/radicle-httpd-release
@@ -1 +1 @@
-
0.14.0

\ No newline at end of file
+
0.15.0

\ No newline at end of file