From 5a0321b9f7ee8a6a24a5a9687546a6da700cafcb Mon Sep 17 00:00:00 2001 From: Lewis Date: Thu, 15 Oct 2026 17:15:08 +0300 Subject: [PATCH] knot2/xrpc: member CRUD goes thru atproto Lewis: May this revision serve well! --- Cargo.lock | 1 + api/org_tangled/serveraddMember.go | 30 -- api/org_tangled/serverbanUser.go | 30 -- api/org_tangled/serverremoveMember.go | 30 -- api/org_tangled/serverunbanUser.go | 30 -- appview/oauth/handler.go | 22 +- .../src/content/docs/reference/knot-config.md | 2 +- docs/src/content/docs/reference/records.md | 4 +- docs/src/content/docs/reference/xrpc.md | 3 +- knot2/crates/knot-config/src/lib.rs | 31 ++- knot2/crates/knot-server/Cargo.toml | 1 + knot2/crates/knot-server/src/admin.rs | 57 +++- knot2/crates/knot-server/src/main.rs | 12 +- knot2/crates/knot-sim/src/harness.rs | 1 + knot2/crates/knot-sim/tests/lfs_roundtrip.rs | 1 + knot2/crates/knot-ssh/tests/ssh_push.rs | 1 + knot2/crates/knot-xrpc/src/legacy_admin.rs | 160 +++++------ knot2/crates/knot-xrpc/src/lib.rs | 30 +- knot2/crates/knot-xrpc/src/social.rs | 28 +- knot2/crates/knot-xrpc/src/tests.rs | 258 +++++++++--------- knot2/crates/knot-xrpc/tests/common/mod.rs | 1 + .../org/tangled/temp/server/addMember.json | 26 -- lexicons/org/tangled/temp/server/banUser.json | 26 -- .../org/tangled/temp/server/removeMember.json | 26 -- .../org/tangled/temp/server/unbanUser.json | 26 -- web/src/lib/api/lexicons/index.ts | 4 - .../org/tangled/temp/server/addMember.ts | 35 --- .../types/org/tangled/temp/server/banUser.ts | 35 --- .../org/tangled/temp/server/removeMember.ts | 35 --- .../org/tangled/temp/server/unbanUser.ts | 35 --- 30 files changed, 352 insertions(+), 629 deletions(-) delete mode 100644 api/org_tangled/serveraddMember.go delete mode 100644 api/org_tangled/serverbanUser.go delete mode 100644 api/org_tangled/serverremoveMember.go delete mode 100644 api/org_tangled/serverunbanUser.go delete mode 100644 lexicons/org/tangled/temp/server/addMember.json delete mode 100644 lexicons/org/tangled/temp/server/banUser.json delete mode 100644 lexicons/org/tangled/temp/server/removeMember.json delete mode 100644 lexicons/org/tangled/temp/server/unbanUser.json delete mode 100644 web/src/lib/api/lexicons/types/org/tangled/temp/server/addMember.ts delete mode 100644 web/src/lib/api/lexicons/types/org/tangled/temp/server/banUser.ts delete mode 100644 web/src/lib/api/lexicons/types/org/tangled/temp/server/removeMember.ts delete mode 100644 web/src/lib/api/lexicons/types/org/tangled/temp/server/unbanUser.ts diff --git a/Cargo.lock b/Cargo.lock index af170eea5..97e68a672 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5726,6 +5726,7 @@ dependencies = [ "knot-messages", "knot-pack", "knot-postreceive", + "knot-record", "knot-resource", "knot-runtime", "knot-secrets", diff --git a/api/org_tangled/serveraddMember.go b/api/org_tangled/serveraddMember.go deleted file mode 100644 index 35293f25d..000000000 --- a/api/org_tangled/serveraddMember.go +++ /dev/null @@ -1,30 +0,0 @@ -// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. - -package org_tangled - -// schema: org.tangled.temp.server.addMember - -import ( - "context" - - "github.com/bluesky-social/indigo/lex/util" -) - -const ( - TempServerAddMemberNSID = "org.tangled.temp.server.addMember" -) - -// TempServerAddMember_Input is the input argument to a org.tangled.temp.server.addMember call. -type TempServerAddMember_Input struct { - // subject: DID of the member to add - Subject string `json:"subject" cborgen:"subject"` -} - -// TempServerAddMember calls the XRPC method "org.tangled.temp.server.addMember". -func TempServerAddMember(ctx context.Context, c util.LexClient, input *TempServerAddMember_Input) error { - if err := c.LexDo(ctx, util.Procedure, "application/json", "org.tangled.temp.server.addMember", nil, input, nil); err != nil { - return err - } - - return nil -} diff --git a/api/org_tangled/serverbanUser.go b/api/org_tangled/serverbanUser.go deleted file mode 100644 index c600e3fed..000000000 --- a/api/org_tangled/serverbanUser.go +++ /dev/null @@ -1,30 +0,0 @@ -// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. - -package org_tangled - -// schema: org.tangled.temp.server.banUser - -import ( - "context" - - "github.com/bluesky-social/indigo/lex/util" -) - -const ( - TempServerBanUserNSID = "org.tangled.temp.server.banUser" -) - -// TempServerBanUser_Input is the input argument to a org.tangled.temp.server.banUser call. -type TempServerBanUser_Input struct { - // subject: DID of the account to ban - Subject string `json:"subject" cborgen:"subject"` -} - -// TempServerBanUser calls the XRPC method "org.tangled.temp.server.banUser". -func TempServerBanUser(ctx context.Context, c util.LexClient, input *TempServerBanUser_Input) error { - if err := c.LexDo(ctx, util.Procedure, "application/json", "org.tangled.temp.server.banUser", nil, input, nil); err != nil { - return err - } - - return nil -} diff --git a/api/org_tangled/serverremoveMember.go b/api/org_tangled/serverremoveMember.go deleted file mode 100644 index c618215ef..000000000 --- a/api/org_tangled/serverremoveMember.go +++ /dev/null @@ -1,30 +0,0 @@ -// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. - -package org_tangled - -// schema: org.tangled.temp.server.removeMember - -import ( - "context" - - "github.com/bluesky-social/indigo/lex/util" -) - -const ( - TempServerRemoveMemberNSID = "org.tangled.temp.server.removeMember" -) - -// TempServerRemoveMember_Input is the input argument to a org.tangled.temp.server.removeMember call. -type TempServerRemoveMember_Input struct { - // subject: DID of the member to remove - Subject string `json:"subject" cborgen:"subject"` -} - -// TempServerRemoveMember calls the XRPC method "org.tangled.temp.server.removeMember". -func TempServerRemoveMember(ctx context.Context, c util.LexClient, input *TempServerRemoveMember_Input) error { - if err := c.LexDo(ctx, util.Procedure, "application/json", "org.tangled.temp.server.removeMember", nil, input, nil); err != nil { - return err - } - - return nil -} diff --git a/api/org_tangled/serverunbanUser.go b/api/org_tangled/serverunbanUser.go deleted file mode 100644 index cdf2084da..000000000 --- a/api/org_tangled/serverunbanUser.go +++ /dev/null @@ -1,30 +0,0 @@ -// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. - -package org_tangled - -// schema: org.tangled.temp.server.unbanUser - -import ( - "context" - - "github.com/bluesky-social/indigo/lex/util" -) - -const ( - TempServerUnbanUserNSID = "org.tangled.temp.server.unbanUser" -) - -// TempServerUnbanUser_Input is the input argument to a org.tangled.temp.server.unbanUser call. -type TempServerUnbanUser_Input struct { - // subject: DID of the account to unban - Subject string `json:"subject" cborgen:"subject"` -} - -// TempServerUnbanUser calls the XRPC method "org.tangled.temp.server.unbanUser". -func TempServerUnbanUser(ctx context.Context, c util.LexClient, input *TempServerUnbanUser_Input) error { - if err := c.LexDo(ctx, util.Procedure, "application/json", "org.tangled.temp.server.unbanUser", nil, input, nil); err != nil { - return err - } - - return nil -} diff --git a/appview/oauth/handler.go b/appview/oauth/handler.go index 496dcd1cc..0644a0995 100644 --- a/appview/oauth/handler.go +++ b/appview/oauth/handler.go @@ -286,6 +286,19 @@ func ensureAcceptanceRecord(ctx context.Context, client lexutil.LexClient, did s return written.Uri, nil } +const knotMemberCollection = "org.tangled.knot.member" + +type knotMemberRecord struct { + LexiconTypeID string `json:"$type" cborgen:"$type"` +} + +type repoCreateRecordInput struct { + Repo string `json:"repo" cborgen:"repo"` + Collection string `json:"collection" cborgen:"collection"` + Rkey string `json:"rkey" cborgen:"rkey"` + Record knotMemberRecord `json:"record" cborgen:"record"` +} + func (o *OAuth) addMemberViaKnotAdmin(ctx context.Context, knotHost string, subject syntax.DID) error { ctx, cancel := context.WithTimeout(ctx, knotAdminTimeout) defer cancel() @@ -294,9 +307,14 @@ func (o *OAuth) addMemberViaKnotAdmin(ctx context.Context, knotHost string, subj if o.Config.Core.Dev { scheme = "http://" } - endpoint := fmt.Sprintf("%s%s/admin/addMember", scheme, knotHost) + endpoint := fmt.Sprintf("%s%s/xrpc/com.atproto.repo.createRecord", scheme, knotHost) - body, err := json.Marshal(tangled.KnotAddMember_Input{Subject: subject.String()}) + body, err := json.Marshal(repoCreateRecordInput{ + Repo: serviceauth.DidWeb(knotHost).String(), + Collection: knotMemberCollection, + Rkey: subject.String(), + Record: knotMemberRecord{LexiconTypeID: knotMemberCollection}, + }) if err != nil { return err } diff --git a/docs/src/content/docs/reference/knot-config.md b/docs/src/content/docs/reference/knot-config.md index 176330f0f..ac3d38695 100644 --- a/docs/src/content/docs/reference/knot-config.md +++ b/docs/src/content/docs/reference/knot-config.md @@ -90,7 +90,7 @@ hosts. Git data stays readable. | Key | Env var | Default | Description | | ------------------------- | ------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `admission` | `KNOT_ADMISSION` | `closed` | `closed`: an admin must add each member before they can create repos. `open`: any account not on the blocklist can create repos. | -| `legacy_admin_secret_env` | `KNOT_LEGACY_ADMIN_SECRET_ENV` | unset | Name of an env var holding a shared secret. When set, the knot serves `/admin/addMember` and `org.tangled.temp.server.*` with Basic auth, which `knot-server admin` uses. Unset disables those routes. | +| `legacy_admin_secret_env` | `KNOT_LEGACY_ADMIN_SECRET_ENV` | unset | Name of an env var holding a shared secret. When set, the knot admits Basic Auth under the power of first DID in `server.admins` (like, according to the Knot it's that first admin performing a given action) on `com.atproto.repo.createRecord` / `deleteRecord` for `org.tangled.knot.member` and `org.tangled.knot.block`, and serves `org.tangled.temp.server.createInviteCode`. `knot-server admin` uses it. Unset disables the shared-secret path. | ## `[repo]` diff --git a/docs/src/content/docs/reference/records.md b/docs/src/content/docs/reference/records.md index 81c95a953..b924dc8e7 100644 --- a/docs/src/content/docs/reference/records.md +++ b/docs/src/content/docs/reference/records.md @@ -35,12 +35,12 @@ Location key: | -------------------------------- | ---------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `org.tangled.repo.manifest` | Repo account | `self` | Repository metadata (description, website, topics, spindle DID, fork or import source) and the `declaration {owner, name}` half of the ownership claim. Written by the knot. | | `org.tangled.repo.declaration` | User PDS (owner) | Repository slug | The owner's half of the claim: `{repo: repoDid}`. A repository is listed under `owner/slug` only while manifest and declaration agree. | -| `org.tangled.graph.collaborator` | Repo account | Invitee DID | Collaboration offer. Indexed by bobbin. Collaborator access itself is granted on the knot with `sh.tangled.repo.addCollaborator`. | +| `org.tangled.graph.collaborator` | Repo account | Invitee DID | Collaboration offer. Indexed by bobbin. The repo owner will create it by sending a request to `com.atproto.repo.createRecord` with this NSID, conversely removing it with `deleteRecord`; the knot grants collaborator access from the record's presence. | | `org.tangled.label.definition` | Repo account | any | A label in the repository's catalogue: `{name, color}`. | ## Knots -- **`org.tangled.knot.member`**: a membership offer record, under the holistic Knot-level "account", where the rkey is the member's DID. Member access itself is granted on the knot with `sh.tangled.knot.addMember`, subject to change when we figure out the best way to use the more atproto-native way of`createRecord` of a member record, with a knot-level session. +- **`org.tangled.knot.member`**: a membership offer record, under the holistic Knot-level "account", where the rkey is the member's DID. An admin will create it using our generic `com.atproto.repo.createRecord` XRPC API and conversely removing it with `deleteRecord`; the knot grants member access from the record's presence. ## Issues and pull requests diff --git a/docs/src/content/docs/reference/xrpc.md b/docs/src/content/docs/reference/xrpc.md index a300607ed..77203b12c 100644 --- a/docs/src/content/docs/reference/xrpc.md +++ b/docs/src/content/docs/reference/xrpc.md @@ -52,7 +52,8 @@ account. | `sh.tangled.knot.listMembers` | none | List members. | | `sh.tangled.knot.ban` / `unban` | service auth (admin) | Blocklist an account. | | `org.tangled.temp.server.redeemInviteCode` | service auth | Join a knot with an invite code. Unstable. | -| `org.tangled.temp.server.createInviteCode`, `addMember`, `removeMember`, `banUser`, `unbanUser` | Basic auth (shared secret) | Operator admin routes, served only when `acl.legacy_admin_secret_env` is set. Used by `knot-server admin`. Unstable. | +| `org.tangled.temp.server.createInviteCode` | Basic auth (shared secret) | Operator admin route, served only when `acl.legacy_admin_secret_env` is set. Used by `knot-server admin`. Unstable. | +| `com.atproto.repo.createRecord` / `deleteRecord` on `org.tangled.knot.member` / `org.tangled.knot.block` | service auth (admin) or Basic auth (shared secret) | Admit/remove a member, ban/unban an account. When using a shared secret, the actions are attributed to the first DID in `server.admins`. | ### Repositories diff --git a/knot2/crates/knot-config/src/lib.rs b/knot2/crates/knot-config/src/lib.rs index aaab5580f..8d808836e 100644 --- a/knot2/crates/knot-config/src/lib.rs +++ b/knot2/crates/knot-config/src/lib.rs @@ -1033,22 +1033,36 @@ impl fmt::Display for ConfigError { } } -pub struct Validated(KnotConfig); +pub struct Validated { + config: KnotConfig, + hostname: knot_types::KnotHostname, +} impl Validated { + fn new(config: KnotConfig) -> Result { + config.validate()?; + let hostname = knot_types::KnotHostname::new(config.server.hostname.clone()) + .expect("validate() is supposed to reject an unparsable server.hostname"); + Ok(Self { config, hostname }) + } + + pub fn knot_hostname(&self) -> &knot_types::KnotHostname { + &self.hostname + } + pub fn verify_environment(&self) -> Result<(), EnvError> { - verify_writable_dir("repo.scan_path", &self.0.repo.scan_path)?; - self.0 + verify_writable_dir("repo.scan_path", &self.config.repo.scan_path)?; + self.config .lfs .store_path .as_deref() .map_or(Ok(()), |path| verify_writable_dir("lfs.store_path", path))?; - verify_homepage(self.0.homepage.source())?; - verify_master_key(&self.0.secrets.master_key_env) + verify_homepage(self.config.homepage.source())?; + verify_master_key(&self.config.secrets.master_key_env) } pub fn into_inner(self) -> KnotConfig { - self.0 + self.config } } @@ -1056,7 +1070,7 @@ impl std::ops::Deref for Validated { type Target = KnotConfig; fn deref(&self) -> &Self::Target { - &self.0 + &self.config } } @@ -1205,8 +1219,7 @@ pub fn load(path: Option<&Path>) -> Result { builder = builder.file(path); } let config = builder.file("/etc/knot/config.toml").load()?; - config.validate()?; - Ok(Validated(config)) + Ok(Validated::new(config)?) } pub fn template() -> String { diff --git a/knot2/crates/knot-server/Cargo.toml b/knot2/crates/knot-server/Cargo.toml index b2f4101cf..8344dd1b4 100644 --- a/knot2/crates/knot-server/Cargo.toml +++ b/knot2/crates/knot-server/Cargo.toml @@ -7,6 +7,7 @@ license.workspace = true [dependencies] knot-config = { workspace = true } +knot-record = { workspace = true } knot-types = { workspace = true } knot-git = { workspace = true } knot-lfs = { workspace = true } diff --git a/knot2/crates/knot-server/src/admin.rs b/knot2/crates/knot-server/src/admin.rs index 23ca58f24..a4d766b6a 100644 --- a/knot2/crates/knot-server/src/admin.rs +++ b/knot2/crates/knot-server/src/admin.rs @@ -1,6 +1,9 @@ use anyhow::Context; use clap::Subcommand; +use knot_record::grant::GrantCollection; +use knot_types::AccountDid; use knot_xrpc::legacy_admin; +use knot_xrpc::RepoMethod; use serde_json::json; #[derive(Subcommand)] @@ -22,42 +25,66 @@ pub enum AdminCommand { #[derive(Subcommand)] pub enum MemberCommand { /// Add an account as a member. - Add { subject: String }, + Add { subject: AccountDid }, /// Remove a member. - Remove { subject: String }, + Remove { subject: AccountDid }, } #[derive(Subcommand)] pub enum UserCommand { /// Ban an account. - Ban { subject: String }, + Ban { subject: AccountDid }, /// Unban an account. - Unban { subject: String }, + Unban { subject: AccountDid }, } pub async fn run(config: &knot_config::Validated, command: AdminCommand) -> anyhow::Result<()> { - let (route, body) = match command { - AdminCommand::CreateInviteCode => (legacy_admin::CREATE_INVITE_CODE_ROUTE, None), + let response = match command { + AdminCommand::CreateInviteCode => { + post(config, legacy_admin::CREATE_INVITE_CODE_ROUTE, None).await? + } AdminCommand::Member { command } => { - let (route, subject) = match command { - MemberCommand::Add { subject } => (legacy_admin::TEMP_ADD_MEMBER_ROUTE, subject), - MemberCommand::Remove { subject } => (legacy_admin::REMOVE_MEMBER_ROUTE, subject), + let (method, subject) = match command { + MemberCommand::Add { subject } => (RepoMethod::Create, subject), + MemberCommand::Remove { subject } => (RepoMethod::Delete, subject), }; - (route, Some(json!({ "subject": subject }))) + grant(config, method, GrantCollection::Member, &subject).await? } AdminCommand::User { command } => { - let (route, subject) = match command { - UserCommand::Ban { subject } => (legacy_admin::BAN_USER_ROUTE, subject), - UserCommand::Unban { subject } => (legacy_admin::UNBAN_USER_ROUTE, subject), + let (method, subject) = match command { + UserCommand::Ban { subject } => (RepoMethod::Create, subject), + UserCommand::Unban { subject } => (RepoMethod::Delete, subject), }; - (route, Some(json!({ "subject": subject }))) + grant(config, method, GrantCollection::Block, &subject).await? } }; - let response = post(config, route, body).await?; println!("{}", serde_json::to_string_pretty(&response)?); Ok(()) } +async fn grant( + config: &knot_config::Validated, + method: RepoMethod, + collection: GrantCollection, + subject: &AccountDid, +) -> anyhow::Result { + let repo = config.knot_hostname().knot_did(); + let body = match method { + RepoMethod::Create => json!({ + "repo": repo.as_str(), + "collection": collection.nsid(), + "rkey": subject.as_str(), + "record": { "$type": collection.nsid() }, + }), + RepoMethod::Delete => json!({ + "repo": repo.as_str(), + "collection": collection.nsid(), + "rkey": subject.as_str(), + }), + }; + post(config, method.route(), Some(body)).await +} + async fn post( config: &knot_config::Validated, route: &str, diff --git a/knot2/crates/knot-server/src/main.rs b/knot2/crates/knot-server/src/main.rs index e931e8c5e..570e26326 100644 --- a/knot2/crates/knot-server/src/main.rs +++ b/knot2/crates/knot-server/src/main.rs @@ -28,7 +28,7 @@ use knot_config::HomepageSource; use knot_index::Index; use knot_runtime::{Clock, HttpTransport, OsEntropy, ReqwestHttp, SystemClock}; use knot_secrets::{MasterKey, SealedStore}; -use knot_types::{ActorId, AuthorName, BranchName, CiLogsAddr, Email, KnotHostname, ObjectCount}; +use knot_types::{ActorId, AuthorName, BranchName, CiLogsAddr, Email, ObjectCount}; use knot_xrpc::XrpcState; use tower_http::services::ServeFile; @@ -223,8 +223,7 @@ async fn run(config: knot_config::Validated) -> anyhow::Result<()> { // so that confique can layer env-over-file. // Here's where it becomes a real type, // and the DID + the service url stack on this. - let hostname = KnotHostname::new(config.server.hostname.clone()) - .context("server.hostname isn't a valid knot hostname")?; + let hostname = config.knot_hostname().clone(); let knot_did = hostname.knot_did(); let object_format = config.object_format().context("parse git.object_format")?; let default_branch = @@ -648,6 +647,7 @@ async fn run(config: knot_config::Validated) -> anyhow::Result<()> { git_http, pack_limits, service_owner, + legacy_admin_secret: legacy_admin.clone(), subscriber_gate, maintenance: maintenance_handle, appview: appview_endpoint, @@ -737,12 +737,12 @@ async fn run(config: knot_config::Validated) -> anyhow::Result<()> { archive_limit: byte_limits.archive, ..knot_pack::EdgeConfig::serving(layout, resolver, Arc::new(SystemClock)) }); - let legacy_admin_routes = legacy_admin.map(|secret| { + let legacy_admin_routes = legacy_admin.map(|_| { tracing::warn!( - route = knot_xrpc::legacy_admin::ADD_MEMBER_ROUTE, + route = knot_xrpc::legacy_admin::CREATE_INVITE_CODE_ROUTE, "serving the legacy basic-auth admin route" ); - knot_xrpc::legacy_admin::router(Arc::clone(&xrpc_state), secret) + knot_xrpc::legacy_admin::router(Arc::clone(&xrpc_state)) }); let base_router = write_routes .merge(knot_xrpc::router(Arc::clone(&xrpc_state))) diff --git a/knot2/crates/knot-sim/src/harness.rs b/knot2/crates/knot-sim/src/harness.rs index 88c95a54c..ac93c48c2 100644 --- a/knot2/crates/knot-sim/src/harness.rs +++ b/knot2/crates/knot-sim/src/harness.rs @@ -853,6 +853,7 @@ fn assemble_router(parts: StateParts) -> (Router, FakeState) { })), pack_limits: knot_pack::PackLimits::default(), service_owner, + legacy_admin_secret: None, firehose: Arc::new(EventLog::new( Arc::clone(&clock), knot_events::ReplayBounds::new( diff --git a/knot2/crates/knot-sim/tests/lfs_roundtrip.rs b/knot2/crates/knot-sim/tests/lfs_roundtrip.rs index 60610e987..e2f18c935 100644 --- a/knot2/crates/knot-sim/tests/lfs_roundtrip.rs +++ b/knot2/crates/knot-sim/tests/lfs_roundtrip.rs @@ -409,6 +409,7 @@ async fn spawn(published_line: String, with_h3: bool) -> World { })), pack_limits: knot_pack::PackLimits::default(), service_owner: owner(), + legacy_admin_secret: None, firehose: Arc::new(knot_events::EventLog::new( ManualClock::new(UnixMicros::new(1_000_000_000)), knot_events::ReplayBounds::new( diff --git a/knot2/crates/knot-ssh/tests/ssh_push.rs b/knot2/crates/knot-ssh/tests/ssh_push.rs index c19960dee..505ed777d 100644 --- a/knot2/crates/knot-ssh/tests/ssh_push.rs +++ b/knot2/crates/knot-ssh/tests/ssh_push.rs @@ -461,6 +461,7 @@ async fn spawn_server_core( )), pack_limits: knot_pack::PackLimits::default(), service_owner: owner(), + legacy_admin_secret: None, subscriber_gate: Arc::new(knot_events::SubscriberGate::new( knot_events::GlobalSubscriberLimit::new(16), knot_events::PerPeerSubscriberLimit::new(4), diff --git a/knot2/crates/knot-xrpc/src/legacy_admin.rs b/knot2/crates/knot-xrpc/src/legacy_admin.rs index bba0d8926..6f310dc9b 100644 --- a/knot2/crates/knot-xrpc/src/legacy_admin.rs +++ b/knot2/crates/knot-xrpc/src/legacy_admin.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use axum::body::Bytes; use axum::extract::{DefaultBodyLimit, State}; use axum::middleware::from_fn_with_state; use axum::response::{IntoResponse, Response}; @@ -11,26 +10,13 @@ use serde_json::json; use subtle::ConstantTimeEq; use zeroize::Zeroizing; -use knot_cobs::{Grant, MembersChange, Removal}; use knot_runtime::{Clock, HttpTransport}; -use crate::blocklist::{ban_account, unban_account}; -use crate::cob::Authorized; use crate::error::XrpcError; use crate::invite_codes::invite_url; -use crate::members::{commit_members, offer_membership}; -use crate::{XrpcState, basic_credentials, decode, enforce_pre_auth_limit}; +use crate::{XrpcState, basic_credentials, enforce_pre_auth_limit}; -#[derive(serde::Deserialize)] -struct SubjectInput { - subject: knot_types::AccountDid, -} -pub const ADD_MEMBER_ROUTE: &str = "/admin/addMember"; pub const CREATE_INVITE_CODE_ROUTE: &str = "/xrpc/org.tangled.temp.server.createInviteCode"; -pub const TEMP_ADD_MEMBER_ROUTE: &str = "/xrpc/org.tangled.temp.server.addMember"; -pub const REMOVE_MEMBER_ROUTE: &str = "/xrpc/org.tangled.temp.server.removeMember"; -pub const BAN_USER_ROUTE: &str = "/xrpc/org.tangled.temp.server.banUser"; -pub const UNBAN_USER_ROUTE: &str = "/xrpc/org.tangled.temp.server.unbanUser"; const BASIC_USER: &str = "admin"; @@ -38,8 +24,15 @@ const BASIC_USER: &str = "admin"; #[error("legacy admin secret mustn't be empty")] pub struct EmptySecret; +#[derive(Clone)] pub struct LegacyAdminSecret(Zeroizing); +pub(crate) enum LegacyAdminAttempt { + NotBasic, + Admitted, + Denied, +} + impl LegacyAdminSecret { pub fn new(value: &str) -> Result { let value = value.trim(); @@ -49,104 +42,49 @@ impl LegacyAdminSecret { } } - fn authorize(&self, headers: &HeaderMap) -> Result<(), XrpcError> { - let denied = || XrpcError::auth_required("invalid admin credentials"); - let credentials = headers + pub(crate) fn attempt(&self, headers: &HeaderMap) -> LegacyAdminAttempt { + let Some(value) = headers .get(http::header::AUTHORIZATION) .and_then(|value| value.to_str().ok()) - .and_then(basic_credentials) - .ok_or_else(denied)?; - let admitted = credentials.user.matches(BASIC_USER) - && bool::from(credentials.password.as_bytes().ct_eq(self.0.as_bytes())); - match admitted { - true => Ok(()), - false => Err(denied()), + else { + return LegacyAdminAttempt::NotBasic; + }; + let Some((scheme, _)) = value.split_once(' ') else { + return LegacyAdminAttempt::NotBasic; + }; + if !scheme.eq_ignore_ascii_case("Basic") { + return LegacyAdminAttempt::NotBasic; + } + if basic_credentials(value).is_some_and(|credentials| { + credentials.user.matches(BASIC_USER) + && bool::from(credentials.password.as_bytes().ct_eq(self.0.as_bytes())) + }) { + LegacyAdminAttempt::Admitted + } else { + LegacyAdminAttempt::Denied + } + } + + pub(crate) fn authorize(&self, headers: &HeaderMap) -> Result<(), XrpcError> { + match self.attempt(headers) { + LegacyAdminAttempt::Admitted => Ok(()), + _ => Err(XrpcError::auth_required("invalid admin creds")), } } } struct LegacyAdmin { state: Arc>, - secret: LegacyAdminSecret, } -pub fn router( - state: Arc>, - secret: LegacyAdminSecret, -) -> Router { +pub fn router(state: Arc>) -> Router { let limits = state.byte_limits.body.get(); let limiter = Arc::clone(&state); Router::new() - .route(ADD_MEMBER_ROUTE, post(add_member::)) - .route(TEMP_ADD_MEMBER_ROUTE, post(add_member::)) - .route(REMOVE_MEMBER_ROUTE, post(remove_member::)) - .route(BAN_USER_ROUTE, post(ban_user::)) - .route(UNBAN_USER_ROUTE, post(unban_user::)) .route(CREATE_INVITE_CODE_ROUTE, post(create_invite_code::)) .layer(DefaultBodyLimit::max(limits)) .layer(from_fn_with_state(limiter, enforce_pre_auth_limit::)) - .with_state(Arc::new(LegacyAdmin { state, secret })) -} - -async fn add_member( - State(admin): State>>, - headers: HeaderMap, - body: Bytes, -) -> Result { - admin.secret.authorize(&headers)?; - let SubjectInput { subject } = decode(&body)?; - tracing::warn!( - route = ADD_MEMBER_ROUTE, - %subject, - "legacy shared-secret admin path is still in use" - ); - offer_membership( - &admin.state, - Grant { - subject, - added_by: admin.state.service_owner.clone(), - created_at: admin.state.now(), - }, - ) - .await -} - -async fn remove_member( - State(admin): State>>, - headers: HeaderMap, - body: Bytes, -) -> Result { - admin.secret.authorize(&headers)?; - let SubjectInput { subject } = decode(&body)?; - tracing::info!(route = REMOVE_MEMBER_ROUTE, %subject, "removing a member"); - commit_members( - &admin.state, - Authorized::by_decree(MembersChange::Remove(Removal { subject })), - admin.state.now(), - ) - .await -} - -async fn ban_user( - State(admin): State>>, - headers: HeaderMap, - body: Bytes, -) -> Result { - admin.secret.authorize(&headers)?; - let SubjectInput { subject } = decode(&body)?; - tracing::info!(route = BAN_USER_ROUTE, %subject, "banning an account"); - ban_account(&admin.state, subject, admin.state.service_owner.clone()).await -} - -async fn unban_user( - State(admin): State>>, - headers: HeaderMap, - body: Bytes, -) -> Result { - admin.secret.authorize(&headers)?; - let SubjectInput { subject } = decode(&body)?; - tracing::info!(route = UNBAN_USER_ROUTE, %subject, "unbanning an account"); - unban_account(&admin.state, subject).await + .with_state(Arc::new(LegacyAdmin { state })) } fn rfc3339(at: knot_types::UnixSeconds) -> Result { @@ -159,7 +97,7 @@ async fn create_invite_code( State(admin): State>>, headers: HeaderMap, ) -> Result { - admin.secret.authorize(&headers)?; + admin.state.legacy_admin_authorize(&headers)?; let state = &admin.state; let (code, expires_at) = state.invite_codes.mint(&*state.entropy, state.now()); tracing::info!( @@ -226,4 +164,30 @@ mod tests { "authorize refuses an untrimmed, wrong, empty or malformed credential" ); } + + #[test] + fn attempt_can_tell_a_basic_header_from_bearer_or_missing() { + let secret = LegacyAdminSecret::new("nekomilk2").expect("secret is non-empty"); + + assert!(matches!( + secret.attempt(&basic("Basic", "admin", "nekomilk2")), + LegacyAdminAttempt::Admitted + )); + assert!(matches!( + secret.attempt(&basic("Basic", "admin", "nope")), + LegacyAdminAttempt::Denied + )); + assert!(matches!( + secret.attempt(&header("Basic !!!not-base64!!!")), + LegacyAdminAttempt::Denied + )); + assert!(matches!( + secret.attempt(&header("Bearer nekomilk2")), + LegacyAdminAttempt::NotBasic + )); + assert!(matches!( + secret.attempt(&HeaderMap::new()), + LegacyAdminAttempt::NotBasic + )); + } } diff --git a/knot2/crates/knot-xrpc/src/lib.rs b/knot2/crates/knot-xrpc/src/lib.rs index c695db47c..a8f615756 100644 --- a/knot2/crates/knot-xrpc/src/lib.rs +++ b/knot2/crates/knot-xrpc/src/lib.rs @@ -48,8 +48,7 @@ pub use merge::Committer; pub use receive::advertiser as receive_advertiser; pub use refrecords::RefProjection; pub use reservations::{GlobalQuota, PerActorQuota, ReservationTtl, Reservations}; -pub use social::RecordKeys; -pub use social::seal_pending_merges; +pub use social::{RecordKeys, RepoMethod, seal_pending_merges}; use std::collections::BTreeSet; use std::net::IpAddr; @@ -70,6 +69,8 @@ use serde::de::DeserializeOwned; use serde_json::json; use knot_atproto::{Atproto, AtprotoError, ServiceJwt}; + +use crate::legacy_admin::LegacyAdminSecret; use knot_events::{EventLog, SubscriberGate}; pub use knot_git::ArchiveLimit; use knot_git::Layout; @@ -206,6 +207,7 @@ pub struct XrpcState { pub git_http: Arc, pub pack_limits: knot_pack::PackLimits, pub service_owner: AccountDid, + pub legacy_admin_secret: Option, pub subscriber_gate: Arc, pub maintenance: MaintenanceHandle, pub appview: AppviewEndpoint, @@ -277,6 +279,30 @@ impl XrpcState { Ok(actor) } + pub(crate) fn legacy_admin_authorize(&self, headers: &HeaderMap) -> Result<(), XrpcError> { + let secret = self + .legacy_admin_secret + .as_ref() + .ok_or_else(|| XrpcError::auth_required("invalid admin credentials"))?; + secret.authorize(headers) + } + + pub(crate) fn legacy_admin_actor( + &self, + headers: &HeaderMap, + ) -> Result, XrpcError> { + let Some(secret) = self.legacy_admin_secret.as_ref() else { + return Ok(None); + }; + match secret.attempt(headers) { + legacy_admin::LegacyAdminAttempt::Admitted => Ok(Some(self.service_owner.clone())), + legacy_admin::LegacyAdminAttempt::Denied => { + Err(XrpcError::auth_required("invalid admin credentials")) + } + legacy_admin::LegacyAdminAttempt::NotBasic => Ok(None), + } + } + pub(crate) async fn audience_repo(&self, headers: &HeaderMap) -> Result { let token = bearer(headers)?; knot_atproto::token_audience(&token) diff --git a/knot2/crates/knot-xrpc/src/social.rs b/knot2/crates/knot-xrpc/src/social.rs index fc2bf98ef..07d42c0ed 100644 --- a/knot2/crates/knot-xrpc/src/social.rs +++ b/knot2/crates/knot-xrpc/src/social.rs @@ -51,9 +51,25 @@ use crate::error::XrpcError; use crate::query::ValidatedQuery; use crate::{XrpcState, admit_social, decode, project_collaborators, run_blocking}; -pub(crate) const CREATE_RECORD_ROUTE: &str = "/xrpc/com.atproto.repo.createRecord"; +pub const CREATE_RECORD_ROUTE: &str = "/xrpc/com.atproto.repo.createRecord"; pub(crate) const PUT_RECORD_ROUTE: &str = "/xrpc/com.atproto.repo.putRecord"; -pub(crate) const DELETE_RECORD_ROUTE: &str = "/xrpc/com.atproto.repo.deleteRecord"; +pub const DELETE_RECORD_ROUTE: &str = "/xrpc/com.atproto.repo.deleteRecord"; + +#[derive(Debug, Clone, Copy)] +pub enum RepoMethod { + Create, + Delete, +} + +impl RepoMethod { + pub fn route(self) -> &'static str { + match self { + Self::Create => CREATE_RECORD_ROUTE, + Self::Delete => DELETE_RECORD_ROUTE, + } + } +} + pub(crate) const LIST_RECORD_VERSIONS_ROUTE: &str = "/xrpc/org.tangled.repo.listRecordVersions"; const NOT_STATE_AUTHORITY: &str = "State changes belong to the ticket's author or a maintainer"; @@ -2004,6 +2020,14 @@ async fn grant_actor( .await } else { on_this_knot(repo, &state.knot_repo, "record")?; + if let Some(actor) = state.legacy_admin_actor(headers)? { + tracing::warn!( + collection = collection.as_str(), + actor = actor.as_str(), + "legacy shared-secret admin path is still in use" + ); + return Ok(actor); + } state.authenticate_write(headers, method).await } } diff --git a/knot2/crates/knot-xrpc/src/tests.rs b/knot2/crates/knot-xrpc/src/tests.rs index 23c0bc66c..6e136877f 100644 --- a/knot2/crates/knot-xrpc/src/tests.rs +++ b/knot2/crates/knot-xrpc/src/tests.rs @@ -31,7 +31,7 @@ use knot_types::{ RecordRkey, RepoDid, RepoName, RepoRkey, ServiceDid, UnixSeconds, }; -use crate::XrpcState; +use crate::{RepoMethod, XrpcState}; const KNOT_HOST: &str = "knot.nel.pet"; const ADMIN_HOST: &str = "admin.nel.pet"; @@ -377,6 +377,7 @@ fn state_from( git_http, pack_limits: knot_pack::PackLimits::default(), service_owner: account(ADMIN_HOST), + legacy_admin_secret: None, subscriber_gate: Arc::new(knot_events::SubscriberGate::new( knot_events::GlobalSubscriberLimit::new(16), knot_events::PerPeerSubscriberLimit::new(8), @@ -674,6 +675,14 @@ impl World { State(Arc::clone(&self.state)) } + fn with_legacy_admin_secret(mut self, secret: &str) -> Self { + Arc::get_mut(&mut self.state) + .expect("za warudo does the only state handling at constructoin") + .legacy_admin_secret = + Some(crate::legacy_admin::LegacyAdminSecret::new(secret).unwrap()); + self + } + fn publish_pds_record(&self, rkey: &RecordRkey) { self.respond_pds_record(rkey, StatusCode::OK); } @@ -5349,107 +5358,115 @@ mod fork_endpoints { mod legacy_admin_route { use super::*; - use crate::legacy_admin::{ADD_MEMBER_ROUTE, LegacyAdminSecret}; + use crate::legacy_admin::CREATE_INVITE_CODE_ROUTE; use tower::ServiceExt; const SECRET: &str = "nekomilk2"; - async fn call( - router: &axum::Router, - user: &str, - password: &str, - subject: &AccountDid, - ) -> StatusCode { - call_at(router, ADD_MEMBER_ROUTE, user, password, subject).await + fn basic(user: &str, password: &str) -> String { + format!( + "Basic {}", + base64::engine::general_purpose::STANDARD + .encode(format!("{user}:{password}").as_bytes()) + ) + } + + fn router(world: &World) -> axum::Router { + crate::router(Arc::clone(&world.state)) + .merge(crate::legacy_admin::router(Arc::clone(&world.state))) + } + + async fn invite(router: &axum::Router, user: &str, password: &str) -> StatusCode { + let request = http::Request::builder() + .method("POST") + .uri(CREATE_INVITE_CODE_ROUTE) + .header(AUTHORIZATION, basic(user, password)) + .body(axum::body::Body::empty()) + .unwrap(); + router.clone().oneshot(request).await.unwrap().status() } - async fn call_at( - router: &axum::Router, - route: &str, + async fn grant( + world: &World, + method: RepoMethod, + collection: &RecordCollection, user: &str, password: &str, subject: &AccountDid, ) -> StatusCode { - let encoded = base64::engine::general_purpose::STANDARD - .encode(format!("{user}:{password}").as_bytes()); + let body = match method { + RepoMethod::Create => grant_body(&world.state.knot_repo, collection, subject), + RepoMethod::Delete => json!({ + "repo": world.state.knot_repo, + "collection": collection, + "rkey": subject, + }), + }; let request = http::Request::builder() .method("POST") - .uri(route) - .header(AUTHORIZATION, format!("Basic {encoded}")) + .uri(method.route()) + .header(AUTHORIZATION, basic(user, password)) .header(http::header::CONTENT_TYPE, "application/json") - .body(axum::body::Body::from( - json!({ "subject": subject }).to_string(), - )) + .body(axum::body::Body::from(body.to_string())) .unwrap(); - router.clone().oneshot(request).await.unwrap().status() + router(world).oneshot(request).await.unwrap().status() } #[tokio::test] - async fn the_legacy_route_admits_a_member_only_with_the_configured_credentials() { - let world = World::new(); - let router = crate::router(Arc::clone(&world.state)).merge(crate::legacy_admin::router( - Arc::clone(&world.state), - LegacyAdminSecret::new(SECRET).unwrap(), - )); - let subject = account(MEMBER_HOST); + async fn the_invite_code_route_admits_only_the_configured_credentials() { + let world = World::new().with_legacy_admin_secret(SECRET); + let router = router(&world); - let version = http::Request::builder() - .method("GET") - .uri(crate::service::VERSION_ROUTE) - .body(axum::body::Body::empty()) - .unwrap(); + assert_eq!(invite(&router, "admin", SECRET).await, StatusCode::OK); assert_eq!( - router.clone().oneshot(version).await.unwrap().status(), - StatusCode::OK, - "merging the legacy route leaves the xrpc routes reachable" + invite(&router, "admin", "nope").await, + StatusCode::UNAUTHORIZED ); - - let refused = futures::future::join_all( - [("admin", "nope"), ("root", SECRET), ("admin", "")] - .map(|(user, password)| call(&router, user, password, &subject)), - ) - .await; - assert!( - refused - .iter() - .all(|status| *status == StatusCode::UNAUTHORIZED), - "the knot refuses a wrong user or secret, got {refused:?}" + assert_eq!( + invite(&router, "root", SECRET).await, + StatusCode::UNAUTHORIZED ); - let oversized = http::Request::builder() - .method("POST") - .uri(ADD_MEMBER_ROUTE) - .header( - AUTHORIZATION, - format!( - "Basic {}", - base64::engine::general_purpose::STANDARD - .encode(format!("admin:{SECRET}").as_bytes()) - ), - ) - .header(http::header::CONTENT_TYPE, "application/json") - .body(axum::body::Body::from( - json!({ "subject": "n".repeat(world.state.byte_limits.body.get() + 1) }) - .to_string(), - )) - .unwrap(); + } + + #[tokio::test] + async fn create_record_admits_the_shared_secret_for_members() { + let world = World::new().with_legacy_admin_secret(SECRET); + let subject = account(MEMBER_HOST); + assert_eq!( - router.clone().oneshot(oversized).await.unwrap().status(), - StatusCode::PAYLOAD_TOO_LARGE + grant( + &world, + RepoMethod::Create, + &member_collection(), + "admin", + "nope", + &subject + ) + .await, + StatusCode::UNAUTHORIZED, + "the knot 401s a wrong secret before the grant runs" ); assert_eq!( - world.state.index.effective_member(&account(MEMBER_HOST)), + world.state.index.effective_member(&subject), Resolved::Ready(false), "a refused call grants nothing" ); assert_eq!( - call(&router, "admin", SECRET, &subject).await, + grant( + &world, + RepoMethod::Create, + &member_collection(), + "admin", + SECRET, + &subject + ) + .await, StatusCode::OK ); assert_eq!( - world.state.index.effective_member(&account(MEMBER_HOST)), - Resolved::Ready(true), - "the legacy route adds a member outright, just like the xrpc route" + world.state.index.effective_member(&subject), + Resolved::Ready(true) ); let Resolved::Ready(members) = world.state.index.member_entries() else { panic!("The member grant list is warm in this test"); @@ -5457,58 +5474,74 @@ mod legacy_admin_route { assert_eq!( members .iter() - .find(|(subject, _)| *subject == account(MEMBER_HOST)) + .find(|(who, _)| *who == subject) .expect("Member is on the grant list") .1 .added_by, world.state.service_owner, - "the legacy route doesn't take an actor, so `added_by` is the service owner" + "the shared secret doesn't take an actor, so `added_by` is the service owner" ); assert_eq!( - call(&router, "admin", SECRET, &subject).await, - StatusCode::OK, - "the legacy route is idempotent, matching the Go knot" + grant( + &world, + RepoMethod::Delete, + &member_collection(), + "admin", + SECRET, + &subject + ) + .await, + StatusCode::OK + ); + assert_eq!( + world.state.index.effective_member(&subject), + Resolved::Ready(false) ); } #[tokio::test] - async fn the_temp_server_routes_ban_unban_and_remove_with_the_configured_credentials() { - use crate::legacy_admin::{ - BAN_USER_ROUTE, REMOVE_MEMBER_ROUTE, TEMP_ADD_MEMBER_ROUTE, UNBAN_USER_ROUTE, - }; - - let world = World::open(); - let router = crate::legacy_admin::router( - Arc::clone(&world.state), - LegacyAdminSecret::new(SECRET).unwrap(), - ); + async fn create_record_admits_the_shared_secret_for_blocks() { + let world = World::open().with_legacy_admin_secret(SECRET); let stranger = account(STRANGER_HOST); - let blocked = || world.state.index.is_blocked(&account(STRANGER_HOST)); assert_eq!( - call_at(&router, BAN_USER_ROUTE, "admin", "nope", &stranger).await, - StatusCode::UNAUTHORIZED + grant( + &world, + RepoMethod::Create, + &block_collection(), + "admin", + SECRET, + &stranger + ) + .await, + StatusCode::OK ); assert_eq!( - blocked(), - Resolved::Ready(false), - "a refused call bans nobody" + world.state.index.is_blocked(&stranger), + Resolved::Ready(true) ); assert_eq!( - call_at(&router, BAN_USER_ROUTE, "admin", SECRET, &stranger).await, + grant( + &world, + RepoMethod::Delete, + &block_collection(), + "admin", + SECRET, + &stranger + ) + .await, StatusCode::OK ); - assert_eq!(blocked(), Resolved::Ready(true)); assert_eq!( - call_at(&router, UNBAN_USER_ROUTE, "admin", SECRET, &stranger).await, - StatusCode::OK + world.state.index.is_blocked(&stranger), + Resolved::Ready(false) ); - assert_eq!(blocked(), Resolved::Ready(false)); assert_eq!( - call_at( - &router, - BAN_USER_ROUTE, + grant( + &world, + RepoMethod::Create, + &block_collection(), "admin", SECRET, &account(ADMIN_HOST) @@ -5517,24 +5550,6 @@ mod legacy_admin_route { StatusCode::FORBIDDEN, "the secret doesn't lift the admin carve-out" ); - - let member = account(MEMBER_HOST); - assert_eq!( - call_at(&router, TEMP_ADD_MEMBER_ROUTE, "admin", SECRET, &member).await, - StatusCode::OK - ); - assert_eq!( - world.state.index.effective_member(&account(MEMBER_HOST)), - Resolved::Ready(true) - ); - assert_eq!( - call_at(&router, REMOVE_MEMBER_ROUTE, "admin", SECRET, &member).await, - StatusCode::OK - ); - assert_eq!( - world.state.index.effective_member(&account(MEMBER_HOST)), - Resolved::Ready(false) - ); } #[tokio::test] @@ -5542,11 +5557,8 @@ mod legacy_admin_route { use axum::extract::ConnectInfo; use std::net::SocketAddr; - let world = World::new(); - let router = crate::legacy_admin::router( - Arc::clone(&world.state), - LegacyAdminSecret::new(SECRET).unwrap(), - ); + let world = World::new().with_legacy_admin_secret(SECRET); + let router = crate::legacy_admin::router(Arc::clone(&world.state)); let peer = SocketAddr::from(([203, 0, 113, 9], 5555)); let statuses: Vec = futures::stream::iter(0..22) @@ -5555,7 +5567,7 @@ mod legacy_admin_route { async move { let mut request = http::Request::builder() .method("POST") - .uri(ADD_MEMBER_ROUTE) + .uri(CREATE_INVITE_CODE_ROUTE) .body(axum::body::Body::empty()) .unwrap(); request.extensions_mut().insert(ConnectInfo(peer)); diff --git a/knot2/crates/knot-xrpc/tests/common/mod.rs b/knot2/crates/knot-xrpc/tests/common/mod.rs index 1d6ea5d58..dfcdb105f 100644 --- a/knot2/crates/knot-xrpc/tests/common/mod.rs +++ b/knot2/crates/knot-xrpc/tests/common/mod.rs @@ -679,6 +679,7 @@ impl World { })), pack_limits: knot_pack::PackLimits::default(), service_owner: AccountDid::new(OWNER).unwrap(), + legacy_admin_secret: None, subscriber_gate: Arc::new(knot_events::SubscriberGate::new( knot_events::GlobalSubscriberLimit::new(16), knot_events::PerPeerSubscriberLimit::new(4), diff --git a/lexicons/org/tangled/temp/server/addMember.json b/lexicons/org/tangled/temp/server/addMember.json deleted file mode 100644 index f72a4fa6a..000000000 --- a/lexicons/org/tangled/temp/server/addMember.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "lexicon": 1, - "id": "org.tangled.temp.server.addMember", - "defs": { - "main": { - "type": "procedure", - "description": "Add a member to this knot", - "input": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": [ - "subject" - ], - "properties": { - "subject": { - "type": "string", - "format": "did", - "description": "DID of the member to add" - } - } - } - } - } - } -} diff --git a/lexicons/org/tangled/temp/server/banUser.json b/lexicons/org/tangled/temp/server/banUser.json deleted file mode 100644 index 0c43eebee..000000000 --- a/lexicons/org/tangled/temp/server/banUser.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "lexicon": 1, - "id": "org.tangled.temp.server.banUser", - "defs": { - "main": { - "type": "procedure", - "description": "Ban an account on this knot", - "input": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": [ - "subject" - ], - "properties": { - "subject": { - "type": "string", - "format": "did", - "description": "DID of the account to ban" - } - } - } - } - } - } -} diff --git a/lexicons/org/tangled/temp/server/removeMember.json b/lexicons/org/tangled/temp/server/removeMember.json deleted file mode 100644 index 50678e6b7..000000000 --- a/lexicons/org/tangled/temp/server/removeMember.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "lexicon": 1, - "id": "org.tangled.temp.server.removeMember", - "defs": { - "main": { - "type": "procedure", - "description": "Remove a member from this knot", - "input": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": [ - "subject" - ], - "properties": { - "subject": { - "type": "string", - "format": "did", - "description": "DID of the member to remove" - } - } - } - } - } - } -} diff --git a/lexicons/org/tangled/temp/server/unbanUser.json b/lexicons/org/tangled/temp/server/unbanUser.json deleted file mode 100644 index 954a0e3cb..000000000 --- a/lexicons/org/tangled/temp/server/unbanUser.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "lexicon": 1, - "id": "org.tangled.temp.server.unbanUser", - "defs": { - "main": { - "type": "procedure", - "description": "Unban an account on this knot", - "input": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": [ - "subject" - ], - "properties": { - "subject": { - "type": "string", - "format": "did", - "description": "DID of the account to unban" - } - } - } - } - } - } -} diff --git a/web/src/lib/api/lexicons/index.ts b/web/src/lib/api/lexicons/index.ts index 77a914931..ee501994e 100644 --- a/web/src/lib/api/lexicons/index.ts +++ b/web/src/lib/api/lexicons/index.ts @@ -91,12 +91,8 @@ export * as OrgTangledTempRepoGetSiteConfig from "./types/org/tangled/temp/repo/ export * as OrgTangledTempRepoUpdateSiteConfig from "./types/org/tangled/temp/repo/updateSiteConfig.js"; export * as OrgTangledTempReviewListPatches from "./types/org/tangled/temp/review/listPatches.js"; export * as OrgTangledTempSearchSearchCode from "./types/org/tangled/temp/search/searchCode.js"; -export * as OrgTangledTempServerAddMember from "./types/org/tangled/temp/server/addMember.js"; -export * as OrgTangledTempServerBanUser from "./types/org/tangled/temp/server/banUser.js"; export * as OrgTangledTempServerCreateInviteCode from "./types/org/tangled/temp/server/createInviteCode.js"; export * as OrgTangledTempServerRedeemInviteCode from "./types/org/tangled/temp/server/redeemInviteCode.js"; -export * as OrgTangledTempServerRemoveMember from "./types/org/tangled/temp/server/removeMember.js"; -export * as OrgTangledTempServerUnbanUser from "./types/org/tangled/temp/server/unbanUser.js"; export * as OrgTangledTempSiteClaimDomain from "./types/org/tangled/temp/site/claimDomain.js"; export * as OrgTangledTempSiteGetDomainClaim from "./types/org/tangled/temp/site/getDomainClaim.js"; export * as OrgTangledTempSiteReleaseDomain from "./types/org/tangled/temp/site/releaseDomain.js"; diff --git a/web/src/lib/api/lexicons/types/org/tangled/temp/server/addMember.ts b/web/src/lib/api/lexicons/types/org/tangled/temp/server/addMember.ts deleted file mode 100644 index 549c8ed46..000000000 --- a/web/src/lib/api/lexicons/types/org/tangled/temp/server/addMember.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type {} from "@atcute/lexicons"; -import * as v from "@atcute/lexicons/validations"; -import type {} from "@atcute/lexicons/ambient"; - -const _mainSchema = /*#__PURE__*/ v.procedure( - "org.tangled.temp.server.addMember", - { - params: null, - input: { - type: "lex", - schema: /*#__PURE__*/ v.object({ - /** - * DID of the member to add - */ - subject: /*#__PURE__*/ v.didString(), - }), - }, - output: null, - }, -); - -type main$schematype = typeof _mainSchema; - -export interface mainSchema extends main$schematype {} - -export const mainSchema = _mainSchema as mainSchema; - -export interface $params {} -export interface $input extends v.InferXRPCBodyInput {} - -declare module "@atcute/lexicons/ambient" { - interface XRPCProcedures { - "org.tangled.temp.server.addMember": mainSchema; - } -} diff --git a/web/src/lib/api/lexicons/types/org/tangled/temp/server/banUser.ts b/web/src/lib/api/lexicons/types/org/tangled/temp/server/banUser.ts deleted file mode 100644 index 5129a97ab..000000000 --- a/web/src/lib/api/lexicons/types/org/tangled/temp/server/banUser.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type {} from "@atcute/lexicons"; -import * as v from "@atcute/lexicons/validations"; -import type {} from "@atcute/lexicons/ambient"; - -const _mainSchema = /*#__PURE__*/ v.procedure( - "org.tangled.temp.server.banUser", - { - params: null, - input: { - type: "lex", - schema: /*#__PURE__*/ v.object({ - /** - * DID of the account to ban - */ - subject: /*#__PURE__*/ v.didString(), - }), - }, - output: null, - }, -); - -type main$schematype = typeof _mainSchema; - -export interface mainSchema extends main$schematype {} - -export const mainSchema = _mainSchema as mainSchema; - -export interface $params {} -export interface $input extends v.InferXRPCBodyInput {} - -declare module "@atcute/lexicons/ambient" { - interface XRPCProcedures { - "org.tangled.temp.server.banUser": mainSchema; - } -} diff --git a/web/src/lib/api/lexicons/types/org/tangled/temp/server/removeMember.ts b/web/src/lib/api/lexicons/types/org/tangled/temp/server/removeMember.ts deleted file mode 100644 index ac92e9deb..000000000 --- a/web/src/lib/api/lexicons/types/org/tangled/temp/server/removeMember.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type {} from "@atcute/lexicons"; -import * as v from "@atcute/lexicons/validations"; -import type {} from "@atcute/lexicons/ambient"; - -const _mainSchema = /*#__PURE__*/ v.procedure( - "org.tangled.temp.server.removeMember", - { - params: null, - input: { - type: "lex", - schema: /*#__PURE__*/ v.object({ - /** - * DID of the member to remove - */ - subject: /*#__PURE__*/ v.didString(), - }), - }, - output: null, - }, -); - -type main$schematype = typeof _mainSchema; - -export interface mainSchema extends main$schematype {} - -export const mainSchema = _mainSchema as mainSchema; - -export interface $params {} -export interface $input extends v.InferXRPCBodyInput {} - -declare module "@atcute/lexicons/ambient" { - interface XRPCProcedures { - "org.tangled.temp.server.removeMember": mainSchema; - } -} diff --git a/web/src/lib/api/lexicons/types/org/tangled/temp/server/unbanUser.ts b/web/src/lib/api/lexicons/types/org/tangled/temp/server/unbanUser.ts deleted file mode 100644 index f47dd2879..000000000 --- a/web/src/lib/api/lexicons/types/org/tangled/temp/server/unbanUser.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type {} from "@atcute/lexicons"; -import * as v from "@atcute/lexicons/validations"; -import type {} from "@atcute/lexicons/ambient"; - -const _mainSchema = /*#__PURE__*/ v.procedure( - "org.tangled.temp.server.unbanUser", - { - params: null, - input: { - type: "lex", - schema: /*#__PURE__*/ v.object({ - /** - * DID of the account to unban - */ - subject: /*#__PURE__*/ v.didString(), - }), - }, - output: null, - }, -); - -type main$schematype = typeof _mainSchema; - -export interface mainSchema extends main$schematype {} - -export const mainSchema = _mainSchema as mainSchema; - -export interface $params {} -export interface $input extends v.InferXRPCBodyInput {} - -declare module "@atcute/lexicons/ambient" { - interface XRPCProcedures { - "org.tangled.temp.server.unbanUser": mainSchema; - } -} -- 2.54.0