mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-16 03:40:37 +08:00
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:
committed by
Sebastian Martinez
parent
b105d06fae
commit
c60b6451d3
+1
-1
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Generated
+3
-3
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
0.14.0
|
||||
0.15.0
|
||||
Reference in New Issue
Block a user