From 732bdc91fe6d613dcffdf0b7a94a54c130c89913 Mon Sep 17 00:00:00 2001 From: Lewis Date: Tue, 02 Jun 2026 11:09:18 +0000 Subject: [PATCH] knotserver/xrpc: admin-secret member endpoint Lewis: May this revision serve well! --- knotserver/config/config.go | 1 + knotserver/xrpc/admin.go | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ nix/modules/knot.nix | 14 ++++++++++++++ 3 file(s) changed, 86 insertion(s)(+), 0 deletion(s)(-) diff --git a/knotserver/config/config.go b/knotserver/config/config.go --- a/knotserver/config/config.go +++ b/knotserver/config/config.go @@ -24,6 +24,7 @@ Owner string `env:"OWNER, required"` LogDids bool `env:"LOG_DIDS, default=true"` MaxResponseKB int `env:"MAX_RESPONSE_KB, default=5120"` + AdminSecret string `env:"ADMIN_SECRET"` // This disables signature verification so use with caution. Dev bool `env:"DEV, default=false"` diff --git a/knotserver/xrpc/admin.go b/knotserver/xrpc/admin.go new file mode 100644 --- /dev/null +++ b/knotserver/xrpc/admin.go @@ -0,0 +1,71 @@ +package xrpc + +import ( + "crypto/subtle" + "encoding/json" + "errors" + "net/http" + + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/go-chi/chi/v5" + "tangled.org/core/api/tangled" + xrpcerr "tangled.org/core/xrpc/errors" +) + +const maxAdminBodyBytes = 4 << 10 + +func (x *Xrpc) AdminRouter() http.Handler { + r := chi.NewRouter() + r.Use(x.VerifyAdminSecret) + r.Post("/addMember", x.AddMemberAdmin) + return r +} + +func (x *Xrpc) VerifyAdminSecret(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + secret := x.Config.Server.AdminSecret + user, pass, ok := r.BasicAuth() + valid := secret != "" && + ok && + user == "admin" && + subtle.ConstantTimeCompare([]byte(pass), []byte(secret)) == 1 + if !valid { + writeError(w, xrpcerr.AuthError(errors.New("invalid admin credentials")), http.StatusUnauthorized) + return + } + next.ServeHTTP(w, r) + }) +} + +func (x *Xrpc) AddMemberAdmin(w http.ResponseWriter, r *http.Request) { + l := x.Logger.With("handler", "AddMemberAdmin") + fail := func(e xrpcerr.XrpcError, status int) { + l.Error("failed", "kind", e.Tag, "error", e.Message) + writeError(w, e, status) + } + + var data tangled.KnotAddMember_Input + if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxAdminBodyBytes)).Decode(&data); err != nil { + fail(xrpcerr.GenericError(err), http.StatusBadRequest) + return + } + + subject, err := syntax.ParseDID(data.Subject) + if err != nil { + fail(xrpcerr.GenericError(err), http.StatusBadRequest) + return + } + + owner, err := syntax.ParseDID(x.Config.Server.Owner) + if err != nil { + fail(xrpcerr.GenericError(err), http.StatusInternalServerError) + return + } + + status, xerr := x.addMemberToKnot(r.Context(), l, owner, subject) + if xerr != nil { + fail(*xerr, status) + return + } + w.WriteHeader(status) +} diff --git a/nix/modules/knot.nix b/nix/modules/knot.nix --- a/nix/modules/knot.nix +++ b/nix/modules/knot.nix @@ -190,6 +190,19 @@ description = "Maximum response size in kilobytes"; }; }; + + environmentFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/etc/knot.env"; + description = '' + Additional environment file as defined in {manpage}`systemd.exec(5)`. + + Sensitive secrets such as {env}`KNOT_SERVER_ADMIN_SECRET` may be + passed to the service without making them world readable in the nix + store. + ''; + }; }; }; @@ -336,6 +349,7 @@ else "false" }" ]; + EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; ExecStart = "${cfg.package}/bin/knot server"; Restart = "always"; } -- tangled.sh