Monorepo for Tangled tangled.org
2

Configure Feed

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

knotserver/xrpc: admin-secret member endpoint

Lewis: May this revision serve well! <lewis@tangled.org>

author
Lewis
committer
Tangled
date (Jun 16, 2026, 9:04 PM +0300) commit 732bdc91 parent 5863a312 change-id mlmmwnuu
+86
+1
knotserver/config/config.go
··· 24 24 Owner string `env:"OWNER, required"` 25 25 LogDids bool `env:"LOG_DIDS, default=true"` 26 26 MaxResponseKB int `env:"MAX_RESPONSE_KB, default=5120"` 27 + AdminSecret string `env:"ADMIN_SECRET"` 27 28 28 29 // This disables signature verification so use with caution. 29 30 Dev bool `env:"DEV, default=false"`
+71
knotserver/xrpc/admin.go
··· 1 + package xrpc 2 + 3 + import ( 4 + "crypto/subtle" 5 + "encoding/json" 6 + "errors" 7 + "net/http" 8 + 9 + "github.com/bluesky-social/indigo/atproto/syntax" 10 + "github.com/go-chi/chi/v5" 11 + "tangled.org/core/api/tangled" 12 + xrpcerr "tangled.org/core/xrpc/errors" 13 + ) 14 + 15 + const maxAdminBodyBytes = 4 << 10 16 + 17 + func (x *Xrpc) AdminRouter() http.Handler { 18 + r := chi.NewRouter() 19 + r.Use(x.VerifyAdminSecret) 20 + r.Post("/addMember", x.AddMemberAdmin) 21 + return r 22 + } 23 + 24 + func (x *Xrpc) VerifyAdminSecret(next http.Handler) http.Handler { 25 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 26 + secret := x.Config.Server.AdminSecret 27 + user, pass, ok := r.BasicAuth() 28 + valid := secret != "" && 29 + ok && 30 + user == "admin" && 31 + subtle.ConstantTimeCompare([]byte(pass), []byte(secret)) == 1 32 + if !valid { 33 + writeError(w, xrpcerr.AuthError(errors.New("invalid admin credentials")), http.StatusUnauthorized) 34 + return 35 + } 36 + next.ServeHTTP(w, r) 37 + }) 38 + } 39 + 40 + func (x *Xrpc) AddMemberAdmin(w http.ResponseWriter, r *http.Request) { 41 + l := x.Logger.With("handler", "AddMemberAdmin") 42 + fail := func(e xrpcerr.XrpcError, status int) { 43 + l.Error("failed", "kind", e.Tag, "error", e.Message) 44 + writeError(w, e, status) 45 + } 46 + 47 + var data tangled.KnotAddMember_Input 48 + if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxAdminBodyBytes)).Decode(&data); err != nil { 49 + fail(xrpcerr.GenericError(err), http.StatusBadRequest) 50 + return 51 + } 52 + 53 + subject, err := syntax.ParseDID(data.Subject) 54 + if err != nil { 55 + fail(xrpcerr.GenericError(err), http.StatusBadRequest) 56 + return 57 + } 58 + 59 + owner, err := syntax.ParseDID(x.Config.Server.Owner) 60 + if err != nil { 61 + fail(xrpcerr.GenericError(err), http.StatusInternalServerError) 62 + return 63 + } 64 + 65 + status, xerr := x.addMemberToKnot(r.Context(), l, owner, subject) 66 + if xerr != nil { 67 + fail(*xerr, status) 68 + return 69 + } 70 + w.WriteHeader(status) 71 + }
+14
nix/modules/knot.nix
··· 190 190 description = "Maximum response size in kilobytes"; 191 191 }; 192 192 }; 193 + 194 + environmentFile = mkOption { 195 + type = with types; nullOr path; 196 + default = null; 197 + example = "/etc/knot.env"; 198 + description = '' 199 + Additional environment file as defined in {manpage}`systemd.exec(5)`. 200 + 201 + Sensitive secrets such as {env}`KNOT_SERVER_ADMIN_SECRET` may be 202 + passed to the service without making them world readable in the nix 203 + store. 204 + ''; 205 + }; 193 206 }; 194 207 }; 195 208 ··· 336 349 else "false" 337 350 }" 338 351 ]; 352 + EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; 339 353 ExecStart = "${cfg.package}/bin/knot server"; 340 354 Restart = "always"; 341 355 }