This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

knot2: serve legacy basic-auth admin endpoint if secret set

Lewis: May this revision serve well! <did:plc:3fwecdnvtcscjnrx2p4n7alz>

author did:plc:3fwecdnvtcscjnrx2p4n7a… date (Jul 27, 2026, 2:31 PM +0300) commit 4ce2be78 parent ae5220ef change-id uwvwrwrm
+446 -47
+2
Cargo.lock
··· 4967 4967 "serde", 4968 4968 "serde_json", 4969 4969 "sha2 0.11.0", 4970 + "subtle", 4970 4971 "tempfile", 4971 4972 "thiserror 2.0.18", 4972 4973 "tokio", ··· 4977 4978 "tower-http 0.7.0", 4978 4979 "tracing", 4979 4980 "url", 4981 + "zeroize", 4980 4982 ] 4981 4983 4982 4984 [[package]]
+1
Cargo.toml
··· 144 144 base64 = "0.22" 145 145 bs58 = "0.5" 146 146 zeroize = { version = "1", features = ["derive"] } 147 + subtle = "2.6" 147 148 148 149 wiremock = "0.6" 149 150 tempfile = "3"
+27 -1
knot2/crates/knot-config/src/lib.rs
··· 53 53 pub struct AclConfig { 54 54 #[config(env = "KNOT_ADMISSION", default = "closed")] 55 55 pub admission: AdmissionPolicy, 56 + 57 + #[config(env = "KNOT_LEGACY_ADMIN_SECRET_ENV")] 58 + pub legacy_admin_secret_env: Option<String>, 56 59 } 57 60 58 61 #[derive(Debug, Config)] ··· 800 803 .as_ref() 801 804 .filter(|header| !is_http_token(header)) 802 805 .map(|_| "xrpc.trusted_proxy_header isn't valid HTTP header name".to_string()), 806 + self.acl 807 + .legacy_admin_secret_env 808 + .as_deref() 809 + .filter(|name| !is_env_var_name(name)) 810 + .map(|_| { 811 + "acl.legacy_admin_secret_env must be valid environment variable name" 812 + .to_string() 813 + }), 803 814 match self.homepage.source() { 804 815 HomepageSource::File(path) if !path.is_absolute() => { 805 816 Some("homepage.path must be absolute path".to_string()) ··· 1094 1105 assert_eq!( 1095 1106 template(), 1096 1107 include_str!("../../../example.toml"), 1097 - "regenerate example.toml from knot_config::template() after changing config" 1108 + "regenerate example.toml with `just gen-config` after changing config" 1098 1109 ); 1099 1110 } 1100 1111 ··· 1133 1144 }, 1134 1145 acl: AclConfig { 1135 1146 admission: AdmissionPolicy::Closed, 1147 + legacy_admin_secret_env: None, 1136 1148 }, 1137 1149 repo: RepoConfig { 1138 1150 scan_path: PathBuf::from("/srv/git"), ··· 1266 1278 false, 1267 1279 false, 1268 1280 ), 1281 + ( 1282 + "a_legacy_admin_secret_env_var", 1283 + |config| { 1284 + config.acl.legacy_admin_secret_env = 1285 + Some("KNOT_LEGACY_ADMIN_SECRET".to_string()) 1286 + }, 1287 + false, 1288 + false, 1289 + ), 1269 1290 ]; 1270 1291 cases 1271 1292 .iter() ··· 1346 1367 "empty_admin_list", 1347 1368 |config| config.server.admins = Vec::new(), 1348 1369 "admins", 1370 + ), 1371 + ( 1372 + "a_malformed_legacy_admin_secret_env_var_name", 1373 + |config| config.acl.legacy_admin_secret_env = Some("9_NOT_A_VAR".to_string()), 1374 + "acl.legacy_admin_secret_env", 1349 1375 ), 1350 1376 ( 1351 1377 "a_zero_maintenance_interval",
+1 -10
knot2/crates/knot-lexicons/src/lib.rs
··· 2 2 3 3 #[path = "_lex/lib.rs"] 4 4 #[allow(non_snake_case, unused_imports, unused_extern_crates)] 5 - #[allow( 6 - clippy::absurd_extreme_comparisons, 7 - clippy::collapsible_if, 8 - clippy::manual_strip, 9 - clippy::needless_update, 10 - clippy::new_ret_no_self, 11 - clippy::new_without_default, 12 - clippy::should_implement_trait, 13 - clippy::type_complexity 14 - )] 5 + #[allow(clippy::all)] 15 6 #[rustfmt::skip] 16 7 mod _lex; 17 8
+51 -3
knot2/crates/knot-server/src/main.rs
··· 74 74 .init(); 75 75 } 76 76 77 + fn subcommand(name: &str) -> Option<anyhow::Result<()>> { 78 + match name { 79 + "config-template" => { 80 + print!("{}", knot_config::template()); 81 + Some(Ok(())) 82 + } 83 + "validate" => Some( 84 + knot_config::load(std::env::args().nth(2).map(PathBuf::from).as_deref()) 85 + .context("load configuration") 86 + .and_then(|config| { 87 + config 88 + .verify_environment() 89 + .context("verify runtime environment") 90 + }) 91 + .map(|()| println!("configuration is valid")), 92 + ), 93 + _ => None, 94 + } 95 + } 96 + 77 97 #[tokio::main] 78 98 async fn main() -> anyhow::Result<()> { 79 99 #[cfg(target_os = "linux")] 80 100 rustix::process::set_dumpable_behavior(rustix::process::DumpableBehavior::NotDumpable) 81 101 .context("disable core dumps and ptrace attachment")?; 82 102 83 - if std::env::args().nth(1).as_deref() == Some("config-template") { 84 - print!("{}", knot_config::template()); 85 - return Ok(()); 103 + if let Some(result) = std::env::args().nth(1).as_deref().and_then(subcommand) { 104 + return result; 86 105 } 87 106 88 107 init_tracing(); ··· 414 433 let catalog = Arc::new( 415 434 knot_messages::Catalog::parse(&config.messages).context("parse message templates")?, 416 435 ); 436 + let legacy_admin = legacy_admin_secret(&config); 417 437 418 438 knot_config::init(config); 419 439 ··· 526 546 xrpc_state.knot_hostname.clone(), 527 547 Arc::new(SystemClock), 528 548 ); 549 + let legacy_admin_routes = legacy_admin.map(|secret| { 550 + tracing::warn!( 551 + route = knot_xrpc::legacy_admin::ADD_MEMBER_ROUTE, 552 + "serving the legacy basic-auth admin route" 553 + ); 554 + knot_xrpc::legacy_admin::router(Arc::clone(&xrpc_state), secret) 555 + }); 529 556 let base_router = write_routes.merge(knot_xrpc::router(xrpc_state)).route( 530 557 "/.well-known/did.json", 531 558 get(move || { ··· 533 560 async move { Json(document) } 534 561 }), 535 562 ); 563 + let base_router = match legacy_admin_routes { 564 + Some(routes) => base_router.merge(routes), 565 + None => base_router, 566 + }; 536 567 let base_router = match homepage { 537 568 HomepageSource::Disabled => base_router, 538 569 HomepageSource::Default => base_router.route("/", get(|| async { Html(DEFAULT_HOMEPAGE) })), ··· 622 653 Edge(Result<Result<(), knot_edge::EdgeError>, tokio::task::JoinError>), 623 654 Ssh(Result<Result<(), knot_ssh::SshError>, tokio::task::JoinError>), 624 655 Signal, 656 + } 657 + 658 + fn legacy_admin_secret( 659 + config: &knot_config::Validated, 660 + ) -> Option<knot_xrpc::legacy_admin::LegacyAdminSecret> { 661 + let name = config.acl.legacy_admin_secret_env.as_deref()?; 662 + let value = zeroize::Zeroizing::new(std::env::var(name).unwrap_or_default()); 663 + match knot_xrpc::legacy_admin::LegacyAdminSecret::new(&value) { 664 + Ok(secret) => Some(secret), 665 + Err(_) => { 666 + tracing::warn!( 667 + secret_env = name, 668 + "the environment variable in acl.legacy_admin_secret_env is unset or empty, so we won't serve the admin route" 669 + ); 670 + None 671 + } 672 + } 625 673 } 626 674 627 675 fn build_tls_setup(
+2
knot2/crates/knot-xrpc/Cargo.toml
··· 43 43 chrono = { workspace = true } 44 44 tempfile = { workspace = true } 45 45 sha2 = { workspace = true } 46 + subtle = { workspace = true } 47 + zeroize = { workspace = true } 46 48 url = { workspace = true } 47 49 48 50 [dev-dependencies]
+147
knot2/crates/knot-xrpc/src/legacy_admin.rs
··· 1 + use std::sync::Arc; 2 + 3 + use axum::Router; 4 + use axum::body::Bytes; 5 + use axum::extract::{DefaultBodyLimit, State}; 6 + use axum::middleware::from_fn_with_state; 7 + use axum::response::Response; 8 + use axum::routing::post; 9 + use http::HeaderMap; 10 + use subtle::ConstantTimeEq; 11 + use zeroize::Zeroizing; 12 + 13 + use knot_cobs::Grant; 14 + use knot_runtime::{Clock, HttpTransport}; 15 + 16 + use crate::error::XrpcError; 17 + use crate::members::{SubjectInput, grant_membership}; 18 + use crate::{XrpcState, basic_credentials, decode, enforce_pre_auth_limit}; 19 + 20 + pub const ADD_MEMBER_ROUTE: &str = "/admin/addMember"; 21 + 22 + const BASIC_USER: &str = "admin"; 23 + 24 + #[derive(Debug, thiserror::Error)] 25 + #[error("legacy admin secret mustn't be empty")] 26 + pub struct EmptySecret; 27 + 28 + pub struct LegacyAdminSecret(Zeroizing<String>); 29 + 30 + impl LegacyAdminSecret { 31 + pub fn new(value: &str) -> Result<Self, EmptySecret> { 32 + let value = value.trim(); 33 + match value.is_empty() { 34 + true => Err(EmptySecret), 35 + false => Ok(Self(Zeroizing::new(value.to_string()))), 36 + } 37 + } 38 + 39 + fn authorize(&self, headers: &HeaderMap) -> Result<(), XrpcError> { 40 + let denied = || XrpcError::auth_required("invalid admin credentials"); 41 + let credentials = headers 42 + .get(http::header::AUTHORIZATION) 43 + .and_then(|value| value.to_str().ok()) 44 + .and_then(basic_credentials) 45 + .ok_or_else(denied)?; 46 + let admitted = credentials.user.matches(BASIC_USER) 47 + && bool::from(credentials.password.as_bytes().ct_eq(self.0.as_bytes())); 48 + match admitted { 49 + true => Ok(()), 50 + false => Err(denied()), 51 + } 52 + } 53 + } 54 + 55 + struct LegacyAdmin<H, C> { 56 + state: Arc<XrpcState<H, C>>, 57 + secret: LegacyAdminSecret, 58 + } 59 + 60 + pub fn router<H: HttpTransport, C: Clock>( 61 + state: Arc<XrpcState<H, C>>, 62 + secret: LegacyAdminSecret, 63 + ) -> Router { 64 + let limits = state.byte_limits.body.get(); 65 + let limiter = Arc::clone(&state); 66 + Router::new() 67 + .route(ADD_MEMBER_ROUTE, post(add_member::<H, C>)) 68 + .layer(DefaultBodyLimit::max(limits)) 69 + .layer(from_fn_with_state(limiter, enforce_pre_auth_limit::<H, C>)) 70 + .with_state(Arc::new(LegacyAdmin { state, secret })) 71 + } 72 + 73 + async fn add_member<H: HttpTransport, C: Clock>( 74 + State(admin): State<Arc<LegacyAdmin<H, C>>>, 75 + headers: HeaderMap, 76 + body: Bytes, 77 + ) -> Result<Response, XrpcError> { 78 + admin.secret.authorize(&headers)?; 79 + let SubjectInput { subject } = decode(&body)?; 80 + tracing::warn!( 81 + route = ADD_MEMBER_ROUTE, 82 + %subject, 83 + "legacy admin route authorized a member grant" 84 + ); 85 + grant_membership( 86 + &admin.state, 87 + Grant { 88 + subject, 89 + added_by: admin.state.service_owner.clone(), 90 + created_at: admin.state.now(), 91 + }, 92 + ) 93 + .await 94 + } 95 + 96 + #[cfg(test)] 97 + mod tests { 98 + use super::*; 99 + 100 + use base64::Engine; 101 + 102 + fn header(value: &str) -> HeaderMap { 103 + let mut headers = HeaderMap::new(); 104 + headers.insert( 105 + http::header::AUTHORIZATION, 106 + value.parse().expect("header is ascii"), 107 + ); 108 + headers 109 + } 110 + 111 + fn basic(scheme: &str, user: &str, password: &str) -> HeaderMap { 112 + let encoded = base64::engine::general_purpose::STANDARD 113 + .encode(format!("{user}:{password}").as_bytes()); 114 + header(&format!("{scheme} {encoded}")) 115 + } 116 + 117 + #[test] 118 + fn a_trimmed_secret_admits_only_the_admin_user_sending_it_exactly() { 119 + assert!(LegacyAdminSecret::new(" ").is_err()); 120 + let secret = LegacyAdminSecret::new("\tnekomilk2\n").expect("secret is non-empty"); 121 + 122 + let admitted = 123 + ["Basic", "basic", "BASIC"].map(|scheme| basic(scheme, "admin", "nekomilk2")); 124 + let refused = [ 125 + basic("Basic", "admin", "\tnekomilk2\n"), 126 + basic("Basic", "admin", "nope"), 127 + basic("Basic", "root", "nekomilk2"), 128 + basic("Basic", "admin", ""), 129 + header("Basic !!!not-base64!!!"), 130 + header("Bearer nekomilk2"), 131 + header("nekomilk2"), 132 + HeaderMap::new(), 133 + ]; 134 + assert!( 135 + admitted 136 + .iter() 137 + .all(|headers| secret.authorize(headers).is_ok()), 138 + "authorize admits the trimmed secret under any case of the Basic scheme" 139 + ); 140 + assert!( 141 + refused 142 + .iter() 143 + .all(|headers| secret.authorize(headers).is_err()), 144 + "authorize refuses an untrimmed, wrong, empty or malformed credential" 145 + ); 146 + } 147 + }
+35 -4
knot2/crates/knot-xrpc/src/lib.rs
··· 6 6 mod error; 7 7 mod events; 8 8 mod forks; 9 + pub mod legacy_admin; 9 10 mod lfs; 10 11 mod lists; 11 12 mod locks; ··· 329 330 .with_state(state) 330 331 } 331 332 332 - async fn enforce_pre_auth_limit<H: HttpTransport, C: Clock>( 333 + pub(crate) async fn enforce_pre_auth_limit<H: HttpTransport, C: Clock>( 333 334 State(state): State<Arc<XrpcState<H, C>>>, 334 335 socket: SocketPeer, 335 336 request: Request, ··· 392 393 .ok_or_else(|| XrpcError::auth_required("missing or malformed Bearer authorization header")) 393 394 } 394 395 395 - fn strip_basic(value: &str) -> Option<String> { 396 + pub(crate) struct BasicUser(String); 397 + 398 + impl BasicUser { 399 + pub(crate) fn matches(&self, expected: &str) -> bool { 400 + self.0 == expected 401 + } 402 + } 403 + 404 + pub(crate) struct BasicPassword(String); 405 + 406 + impl BasicPassword { 407 + pub(crate) fn as_bytes(&self) -> &[u8] { 408 + self.0.as_bytes() 409 + } 410 + } 411 + 412 + pub(crate) struct BasicCredentials { 413 + pub(crate) user: BasicUser, 414 + pub(crate) password: BasicPassword, 415 + } 416 + 417 + pub(crate) fn basic_credentials(value: &str) -> Option<BasicCredentials> { 396 418 let (scheme, rest) = value.split_once(' ')?; 397 419 if !scheme.eq_ignore_ascii_case("Basic") { 398 420 return None; ··· 401 423 .decode(rest.trim()) 402 424 .ok()?; 403 425 let text = String::from_utf8(decoded).ok()?; 404 - let (_user, password) = text.split_once(':')?; 405 - (!password.is_empty()).then(|| password.to_string()) 426 + let (user, password) = text.split_once(':')?; 427 + Some(BasicCredentials { 428 + user: BasicUser(user.to_string()), 429 + password: BasicPassword(password.to_string()), 430 + }) 431 + } 432 + 433 + fn strip_basic(value: &str) -> Option<String> { 434 + basic_credentials(value) 435 + .map(|credentials| credentials.password.0) 436 + .filter(|password| !password.is_empty()) 406 437 } 407 438 408 439 fn push_credential(headers: &HeaderMap) -> Result<ServiceJwt, XrpcError> {
+21 -11
knot2/crates/knot-xrpc/src/members.rs
··· 23 23 pub(crate) const REMOVE_ROUTE: &str = "/xrpc/sh.tangled.knot.removeMember"; 24 24 25 25 #[derive(Deserialize)] 26 - struct SubjectInput { 27 - subject: AccountDid, 26 + pub(crate) struct SubjectInput { 27 + pub(crate) subject: AccountDid, 28 28 } 29 29 30 30 pub(crate) async fn add_member<H: HttpTransport, C: Clock>( ··· 40 40 } 41 41 42 42 let SubjectInput { subject } = decode(&body)?; 43 - if state.admins.contains(&subject) 44 - || matches!(state.index.is_member(&subject), Resolved::Ready(true)) 43 + grant_membership( 44 + &state, 45 + Grant { 46 + subject, 47 + added_by: actor, 48 + created_at: state.now(), 49 + }, 50 + ) 51 + .await 52 + } 53 + 54 + pub(crate) async fn grant_membership<H: HttpTransport, C: Clock>( 55 + state: &Arc<XrpcState<H, C>>, 56 + grant: Grant, 57 + ) -> Result<Response, XrpcError> { 58 + if state.admins.contains(&grant.subject) 59 + || matches!(state.index.is_member(&grant.subject), Resolved::Ready(true)) 45 60 { 46 61 return Ok(ok_empty()); 47 62 } 48 63 49 - let now = state.now(); 50 - let event_subject = subject.clone(); 51 - let grant = Grant { 52 - subject, 53 - added_by: actor, 54 - created_at: now, 55 - }; 64 + let now = grant.created_at; 65 + let event_subject = grant.subject.clone(); 56 66 let signer = state.secrets.signer(&state.knot_did)?; 57 67 let meta_path = state.meta_path.clone(); 58 68 let index = Arc::clone(&state.index);
+149
knot2/crates/knot-xrpc/src/tests.rs
··· 3042 3042 assert_eq!(fork.find_ref(&main_ref()).unwrap(), Some(new_tip)); 3043 3043 } 3044 3044 } 3045 + 3046 + mod legacy_admin_route { 3047 + use super::*; 3048 + use crate::legacy_admin::{ADD_MEMBER_ROUTE, LegacyAdminSecret}; 3049 + use tower::ServiceExt; 3050 + 3051 + const SECRET: &str = "nekomilk2"; 3052 + 3053 + async fn call(router: &axum::Router, user: &str, password: &str, subject: &str) -> StatusCode { 3054 + let encoded = base64::engine::general_purpose::STANDARD 3055 + .encode(format!("{user}:{password}").as_bytes()); 3056 + let request = http::Request::builder() 3057 + .method("POST") 3058 + .uri(ADD_MEMBER_ROUTE) 3059 + .header(AUTHORIZATION, format!("Basic {encoded}")) 3060 + .header(http::header::CONTENT_TYPE, "application/json") 3061 + .body(axum::body::Body::from( 3062 + json!({ "subject": subject }).to_string(), 3063 + )) 3064 + .unwrap(); 3065 + router.clone().oneshot(request).await.unwrap().status() 3066 + } 3067 + 3068 + #[tokio::test] 3069 + async fn the_legacy_route_admits_a_member_only_with_the_configured_credentials() { 3070 + let world = World::new(); 3071 + let router = crate::router(Arc::clone(&world.state)).merge(crate::legacy_admin::router( 3072 + Arc::clone(&world.state), 3073 + LegacyAdminSecret::new(SECRET).unwrap(), 3074 + )); 3075 + let subject = format!("did:web:{MEMBER_HOST}"); 3076 + 3077 + let version = http::Request::builder() 3078 + .method("GET") 3079 + .uri(crate::service::VERSION_ROUTE) 3080 + .body(axum::body::Body::empty()) 3081 + .unwrap(); 3082 + assert_eq!( 3083 + router.clone().oneshot(version).await.unwrap().status(), 3084 + StatusCode::OK, 3085 + "merging the legacy route leaves the xrpc routes reachable" 3086 + ); 3087 + 3088 + let refused = futures::future::join_all( 3089 + [("admin", "nope"), ("root", SECRET), ("admin", "")] 3090 + .map(|(user, password)| call(&router, user, password, &subject)), 3091 + ) 3092 + .await; 3093 + assert!( 3094 + refused 3095 + .iter() 3096 + .all(|status| *status == StatusCode::UNAUTHORIZED), 3097 + "the knot refuses a wrong user or secret, got {refused:?}" 3098 + ); 3099 + assert_eq!( 3100 + call( 3101 + &router, 3102 + "admin", 3103 + SECRET, 3104 + &"n".repeat(world.state.byte_limits.body.get() + 1) 3105 + ) 3106 + .await, 3107 + StatusCode::PAYLOAD_TOO_LARGE 3108 + ); 3109 + assert_eq!( 3110 + world.state.index.is_member(&account(MEMBER_HOST)), 3111 + Resolved::Ready(false), 3112 + "a refused call grants nothing" 3113 + ); 3114 + 3115 + assert_eq!( 3116 + call(&router, "admin", SECRET, &subject).await, 3117 + StatusCode::OK 3118 + ); 3119 + assert_eq!( 3120 + world.state.index.is_member(&account(MEMBER_HOST)), 3121 + Resolved::Ready(true) 3122 + ); 3123 + let added = last_event(&world, "sh.tangled.knot.memberUpdate"); 3124 + assert_eq!(added.payload["op"], "add"); 3125 + assert_eq!(added.payload["subject"], account(MEMBER_HOST).to_string()); 3126 + let Resolved::Ready(members) = world.state.index.member_entries() else { 3127 + panic!("the member roster is warm in this test"); 3128 + }; 3129 + assert_eq!( 3130 + members 3131 + .iter() 3132 + .find(|grant| grant.subject == account(MEMBER_HOST)) 3133 + .expect("the member is in the roster") 3134 + .added_by, 3135 + world.state.service_owner, 3136 + "the legacy grant records the service owner as the granter" 3137 + ); 3138 + 3139 + let baseline = event_count(&world); 3140 + assert_eq!( 3141 + call(&router, "admin", SECRET, &subject).await, 3142 + StatusCode::OK, 3143 + "the legacy route is idempotent, matching the Go knot" 3144 + ); 3145 + assert_eq!( 3146 + event_count(&world), 3147 + baseline, 3148 + "re-adding an existing member emits no event" 3149 + ); 3150 + } 3151 + 3152 + #[tokio::test] 3153 + async fn the_legacy_route_sheds_a_pre_auth_flood_from_one_peer() { 3154 + use axum::extract::ConnectInfo; 3155 + use std::net::SocketAddr; 3156 + 3157 + let world = World::new(); 3158 + let router = crate::legacy_admin::router( 3159 + Arc::clone(&world.state), 3160 + LegacyAdminSecret::new(SECRET).unwrap(), 3161 + ); 3162 + let peer = SocketAddr::from(([203, 0, 113, 9], 5555)); 3163 + 3164 + let statuses: Vec<StatusCode> = futures::stream::iter(0..22) 3165 + .then(|_| { 3166 + let router = router.clone(); 3167 + async move { 3168 + let mut request = http::Request::builder() 3169 + .method("POST") 3170 + .uri(ADD_MEMBER_ROUTE) 3171 + .body(axum::body::Body::empty()) 3172 + .unwrap(); 3173 + request.extensions_mut().insert(ConnectInfo(peer)); 3174 + router.oneshot(request).await.unwrap().status() 3175 + } 3176 + }) 3177 + .collect() 3178 + .await; 3179 + 3180 + assert!( 3181 + statuses[..20] 3182 + .iter() 3183 + .all(|status| *status == StatusCode::UNAUTHORIZED), 3184 + "the knot admits the per-peer burst and then fails it on the missing credentials, got {statuses:?}" 3185 + ); 3186 + assert!( 3187 + statuses[20..] 3188 + .iter() 3189 + .all(|status| *status == StatusCode::TOO_MANY_REQUESTS), 3190 + "past the burst the knot sheds the guess flood before it reaches the secret comparison, got {statuses:?}" 3191 + ); 3192 + } 3193 + }
+3
knot2/example.toml
··· 107 107 # Default value: "closed" 108 108 #admission = "closed" 109 109 110 + # Can also be specified via environment variable `KNOT_LEGACY_ADMIN_SECRET_ENV`. 111 + #legacy_admin_secret_env = 112 + 110 113 [repo] 111 114 # Can also be specified via environment variable `KNOT_SCAN_PATH`. 112 115 # Required! This value must be specified.
+7 -18
knot2/justfile
··· 7 7 cargo run -p knot-server -- config-template > example.toml 8 8 9 9 fmt: 10 - cargo fmt 10 + cargo fmt --all 11 11 12 12 fmt-check: 13 - cargo fmt --check 13 + cargo fmt --all --check 14 14 15 15 clippy: 16 16 cargo clippy -p 'knot-*' --all-targets -- -D warnings ··· 45 45 46 46 ci: fmt-check clippy test gates bench-gate fuzz-ci 47 47 48 - gates: gate-no-subprocess gate-no-sql gate-no-native-git gate-no-string-ids gate-no-unguarded-receive gate-fuzz-targets-enumerated 48 + gates: gate-no-subprocess (gate-no-banned-deps "no-sql" "an embedded database" "rusqlite|libsqlite3-sys|sqlx|sqlx-core|sled|fjall|redb") (gate-no-banned-deps "no-native-git" "a native git or TLS shim" "git2|libgit2-sys|openssl-sys|zlib-ng|zlib-ng-sys") gate-no-string-ids gate-no-unguarded-receive gate-fuzz-targets-enumerated 49 49 50 50 gate-no-subprocess: 51 51 #!/usr/bin/env bash ··· 58 58 fi 59 59 echo "ok: no process spawning in server source" 60 60 61 - gate-no-sql: 61 + gate-no-banned-deps name subject pattern: 62 62 #!/usr/bin/env bash 63 63 set -euo pipefail 64 - hits=$(grep -inE '^name = "(rusqlite|libsqlite3-sys|sqlx|sqlx-core|sled|fjall|redb)"' ../Cargo.lock || true) 64 + hits=$(cargo tree -p knot-server --edges normal,build --prefix none | sort -u | grep -iE '^({{pattern}}) v' || true) 65 65 if [ -n "$hits" ]; then 66 - echo "no-sql gate failed: an embedded database is in the dependency tree" >&2 66 + echo "{{name}} gate failed: {{subject}} is in the knot-server dependency tree" >&2 67 67 echo "$hits" >&2 68 68 exit 1 69 69 fi 70 - echo "ok: no embedded database in the dependency tree" 71 - 72 - gate-no-native-git: 73 - #!/usr/bin/env bash 74 - set -euo pipefail 75 - hits=$(grep -inE '^name = "(git2|libgit2-sys|openssl-sys|zlib-ng|zlib-ng-sys)"' ../Cargo.lock || true) 76 - if [ -n "$hits" ]; then 77 - echo "no-native-git gate failed: a native git or TLS shim is in the dependency tree" >&2 78 - echo "$hits" >&2 79 - exit 1 80 - fi 81 - echo "ok: no native git or TLS shim in the dependency tree" 70 + echo "ok: {{subject}} isn't in the knot-server dependency tree" 82 71 83 72 gate-no-string-ids: 84 73 #!/usr/bin/env bash