From c60b6451d3f22756face50c823a5a278da90f8d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C5=ABdolfs=20O=C5=A1i=C5=86=C5=A1?= Date: Tue, 16 Jul 2024 17:59:15 +0200 Subject: [PATCH] http: Remove all authentication related code 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`. We bump the API and crate versions since this is a breaking change. squash or drop!: Small suggestion for improvement --- config/default.json | 2 +- http-client/index.ts | 6 +- http-client/lib/profile.ts | 31 ------- radicle-httpd/Cargo.lock | 6 +- radicle-httpd/Cargo.toml | 4 +- radicle-httpd/src/api.rs | 14 +--- radicle-httpd/src/api/error.rs | 5 -- radicle-httpd/src/api/v1.rs | 7 -- radicle-httpd/src/api/v1/node.rs | 53 ++++++++++-- radicle-httpd/src/api/v1/profile.rs | 123 ---------------------------- radicle-httpd/src/test.rs | 14 +--- tests/support/radicle-httpd-release | 2 +- 12 files changed, 62 insertions(+), 205 deletions(-) delete mode 100644 http-client/lib/profile.ts delete mode 100644 radicle-httpd/src/api/v1/profile.rs diff --git a/config/default.json b/config/default.json index 64bf5aefa..f6e43023d 100644 --- a/config/default.json +++ b/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", diff --git a/http-client/index.ts b/http-client/index.ts index c67c4d9ed..ea4eb62d9 100644 --- a/http-client/index.ts +++ b/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; @@ -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 { diff --git a/http-client/lib/profile.ts b/http-client/lib/profile.ts deleted file mode 100644 index 43f077660..000000000 --- a/http-client/lib/profile.ts +++ /dev/null @@ -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; - -export class Client { - #fetcher: Fetcher; - - public constructor(fetcher: Fetcher) { - this.#fetcher = fetcher; - } - - public async getProfile(options?: RequestOptions): Promise { - return this.#fetcher.fetchOk( - { - method: "GET", - path: `profile`, - options, - }, - profileSchema, - ); - } -} diff --git a/radicle-httpd/Cargo.lock b/radicle-httpd/Cargo.lock index 5d12e60bd..c2146bd5e 100644 --- a/radicle-httpd/Cargo.lock +++ b/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", diff --git a/radicle-httpd/Cargo.toml b/radicle-httpd/Cargo.toml index fb08cae19..d7f176273 100644 --- a/radicle-httpd/Cargo.toml +++ b/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 "] 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" diff --git a/radicle-httpd/src/api.rs b/radicle-httpd/src/api.rs index 86671b493..8c5084396 100644 --- a/radicle-httpd/src/api.rs +++ b/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]), ) } diff --git a/radicle-httpd/src/api/error.rs b/radicle-httpd/src/api/error.rs index 378c634c8..547385669 100644 --- a/radicle-httpd/src/api/error.rs +++ b/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())) diff --git a/radicle-httpd/src/api/v1.rs b/radicle-httpd/src/api/v1.rs index c827b8cf4..0c0090fa8 100644 --- a/radicle-httpd/src/api/v1.rs +++ b/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)); @@ -52,11 +50,6 @@ async fn root_handler(State(ctx): State) -> impl IntoResponse { "rel": "projects", "type": "GET" }, - { - "href": "/profile", - "rel": "profile", - "type": "GET" - }, { "href": "/stats", "rel": "stats", diff --git a/radicle-httpd/src/api/v1/node.rs b/radicle-httpd/src/api/v1/node.rs index e1a7a0d73..26c271602 100644 --- a/radicle-httpd/src/api/v1/node.rs +++ b/radicle-httpd/src/api/v1/node.rs @@ -2,13 +2,15 @@ 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::web; use radicle::Node; use crate::api::error::Error; @@ -25,6 +27,41 @@ pub fn router(ctx: Context) -> Router { .with_state(ctx) } +#[derive(Clone, Debug, Default, Serialize)] +#[serde(rename_all = "camelCase")] +struct Response { + id: String, + agent: Option, + config: Option, + state: String, + #[serde(skip_serializing_if = "Option::is_none")] + avatar_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + banner_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, +} + +impl Response { + fn new( + nid: NodeId, + agent: Option, + config: Option, + state: String, + web_config: web::Config, + ) -> Self { + Response { + id: nid.to_string(), + agent, + config, + state, + avatar_url: web_config.avatar_url, + banner_url: web_config.banner_url, + description: web_config.description, + } + } +} + /// Return local node information. /// `GET /node` async fn node_handler(State(ctx): State) -> impl IntoResponse { @@ -46,14 +83,14 @@ async fn node_handler(State(ctx): State) -> impl IntoResponse { None } }; - let response = json!({ - "id": node_id.to_string(), - "agent": agent, - "config": config, - "state": node_state, - }); - Ok::<_, Error>(Json(response)) + Ok::<_, Error>(Json(Response::new( + node_id, + agent, + config, + node_state.to_string(), + ctx.profile.config.web.clone(), + ))) } /// Return stored information about other nodes. diff --git a/radicle-httpd/src/api/v1/profile.rs b/radicle-httpd/src/api/v1/profile.rs deleted file mode 100644 index 6b869af1d..000000000 --- a/radicle-httpd/src/api/v1/profile.rs +++ /dev/null @@ -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, - ConnectInfo(addr): ConnectInfo, -) -> 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() - }) - ); - } -} diff --git a/radicle-httpd/src/test.rs b/radicle-httpd/src/test.rs index be267cc8e..ebb8d8a92 100644 --- a/radicle-httpd/src/test.rs +++ b/radicle-httpd/src/test.rs @@ -326,25 +326,17 @@ fn seed_with_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, - auth: Option, -) -> Request { - let mut request = Request::builder() +fn request(path: impl ToString, method: Method, body: Option) -> Request { + 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() } diff --git a/tests/support/radicle-httpd-release b/tests/support/radicle-httpd-release index 0548fb4e9..7092c7c46 100644 --- a/tests/support/radicle-httpd-release +++ b/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