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
This commit is contained in:
Rūdolfs Ošiņš
2024-07-19 08:11:47 -03:00
committed by Sebastian Martinez
parent b105d06fae
commit c60b6451d3
12 changed files with 62 additions and 205 deletions
+1 -1
View File
@@ -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",
+3 -3
View File
@@ -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 {
-31
View File
@@ -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,
);
}
}
+3 -3
View File
@@ -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",
+2 -2
View File
@@ -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"
+4 -10
View File
@@ -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]),
)
}
-5
View File
@@ -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()))
-7
View File
@@ -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<Context>) -> impl IntoResponse {
"rel": "projects",
"type": "GET"
},
{
"href": "/profile",
"rel": "profile",
"type": "GET"
},
{
"href": "/stats",
"rel": "stats",
+45 -8
View File
@@ -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<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>,
}
impl Response {
fn new(
nid: NodeId,
agent: Option<UserAgent>,
config: Option<Config>,
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<Context>) -> impl IntoResponse {
@@ -46,14 +83,14 @@ 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))
Ok::<_, Error>(Json(Response::new(
node_id,
agent,
config,
node_state.to_string(),
ctx.profile.config.web.clone(),
)))
}
/// Return stored information about other nodes.
-123
View File
@@ -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()
})
);
}
}
+3 -11
View File
@@ -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()
}
+1 -1
View File
@@ -1 +1 @@
0.14.0
0.15.0