Monorepo for Tangled tangled.org
8

Configure Feed

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

knot: add checkPushAllowed xrpc, normalize ingested pubkeys

Signed-off-by: dawn <dawn@tangled.org>

author
dawn
date (Jun 25, 2026, 8:34 PM +0300) commit 6c3753a7 parent 47a0e6cf change-id yulorrpl
+202 -6
+40
api/tangled/repocheckPushAllowed.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package tangled 4 + 5 + // schema: sh.tangled.repo.checkPushAllowed 6 + 7 + import ( 8 + "context" 9 + 10 + "github.com/bluesky-social/indigo/lex/util" 11 + ) 12 + 13 + const ( 14 + RepoCheckPushAllowedNSID = "sh.tangled.repo.checkPushAllowed" 15 + ) 16 + 17 + // RepoCheckPushAllowed_Output is the output of a sh.tangled.repo.checkPushAllowed call. 18 + type RepoCheckPushAllowed_Output struct { 19 + // allowed: Whether the key's owner may push to the repo. 20 + Allowed bool `json:"allowed" cborgen:"allowed"` 21 + // did: DID the key resolved to, if a match was found. 22 + Did *string `json:"did,omitempty" cborgen:"did,omitempty"` 23 + } 24 + 25 + // RepoCheckPushAllowed calls the XRPC method "sh.tangled.repo.checkPushAllowed". 26 + // 27 + // key: Public key in OpenSSH authorized_keys format. 28 + // repo: A repo DID. 29 + func RepoCheckPushAllowed(ctx context.Context, c util.LexClient, key string, repo string) (*RepoCheckPushAllowed_Output, error) { 30 + var out RepoCheckPushAllowed_Output 31 + 32 + params := map[string]interface{}{} 33 + params["key"] = key 34 + params["repo"] = repo 35 + if err := c.LexDo(ctx, util.Query, "", "sh.tangled.repo.checkPushAllowed", params, nil, &out); err != nil { 36 + return nil, err 37 + } 38 + 39 + return &out, nil 40 + }
+5 -1
flake.nix
··· 466 466 program = 467 467 (pkgs.writeShellApplication { 468 468 name = "lexgen"; 469 + runtimeInputs = [pkgs.stdenv.cc]; 469 470 text = '' 470 471 if ! command -v lexgen > /dev/null; then 471 472 echo "error: must be executed from devshell" 472 473 exit 1 473 474 fi 475 + 476 + # pin CC to a glibc gcc so a stray musl cc cant mess up CGO 477 + export CC=${pkgs.stdenv.cc}/bin/cc 474 478 475 479 rootDir=$(jj --ignore-working-copy root || git rev-parse --show-toplevel) || (echo "error: can't find repo root?"; exit 1) 476 480 cd "$rootDir" ··· 482 486 find api/tangled/*.go -not -name "cbor_gen.go" -exec \ 483 487 sed -i '/^func.*\(MarshalCBOR\|UnmarshalCBOR\)/,/^}/ s/^/\/\/ /' {} + 484 488 ${pkgs.gotools}/bin/goimports -w api/tangled/* 485 - CGO_ENABLED=0 go run ./cmd/cborgen/ 489 + CGO_ENABLED=1 go run ./cmd/cborgen/ 486 490 lexgen --build-file lexicon-build-config.json lexicons 487 491 rm api/tangled/*.bak 488 492 '';
+41
knotserver/db/pubkeys.go
··· 4 4 "database/sql" 5 5 "log/slog" 6 6 "strconv" 7 + "strings" 7 8 "time" 8 9 9 10 "github.com/bluesky-social/indigo/atproto/syntax" 11 + "golang.org/x/crypto/ssh" 10 12 "tangled.org/core/api/tangled" 11 13 ) 12 14 ··· 41 43 logger.Warn("skipping public key with empty key value", "did", pk.Did, "rkey", pk.Rkey) 42 44 return nil 43 45 } 46 + 47 + canonical, ok := normalizePublicKey(pk.Key) 48 + if !ok { 49 + logger.Warn("skipping malformed public key", "did", pk.Did, "rkey", pk.Rkey) 50 + return nil 51 + } 52 + pk.Key = canonical 44 53 45 54 if pk.CreatedAt == "" { 46 55 pk.CreatedAt = time.Now().Format(time.RFC3339) ··· 107 116 "key": pk.Key, 108 117 "createdAt": pk.CreatedAt, 109 118 } 119 + } 120 + 121 + func normalizePublicKey(key string) (string, bool) { 122 + parsed, comment, _, _, err := ssh.ParseAuthorizedKey([]byte(key)) 123 + if err != nil { 124 + return "", false 125 + } 126 + 127 + canonical := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(parsed))) 128 + if comment != "" { 129 + canonical += " " + comment 130 + } 131 + 132 + return canonical, true 133 + } 134 + 135 + func (d *DB) DidForPublicKey(offered ssh.PublicKey) (syntax.DID, bool, error) { 136 + prefix := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(offered))) 137 + 138 + var did syntax.DID 139 + err := d.db.QueryRow( 140 + `select did from public_keys where key = ? or key like ? limit 1`, 141 + prefix, prefix+" %", 142 + ).Scan(&did) 143 + if err == sql.ErrNoRows { 144 + return "", false, nil 145 + } 146 + if err != nil { 147 + return "", false, err 148 + } 149 + 150 + return did, true, nil 110 151 } 111 152 112 153 func (d *DB) GetAllPublicKeys() ([]PublicKey, error) {
+3 -3
knotserver/db/pubkeys_test.go
··· 10 10 const ( 11 11 didBoltless = "did:plc:boltless" 12 12 didAkshay = "did:plc:akshay" 13 - keyShared = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAISharedSharedSharedSharedSharedSharedShar01" 14 - keyRotated = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIRotatedRotatedRotatedRotatedRotatedRot02" 15 - keyOther = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOtherOtherOtherOtherOtherOtherOtherOth03" 13 + keyShared = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIwwmlNQEh5NdGL4ERWj3uXWXylXsB8fPnO5frkl2sps" 14 + keyRotated = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAj/UuveywM4LZdjbcsH5LVmXhu8VX5jdUR6UdEQFGBo" 15 + keyOther = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBqWLXSkuE6AUkAwXThOsudIGqMV/u4ZnE8yTd6DSpoR" 16 16 ) 17 17 18 18 func TestUpsertPublicKey_GlobalUniqueness(t *testing.T) {
+2 -2
knotserver/keys/keys_test.go
··· 19 19 20 20 const ( 21 21 didBoltless = "did:plc:boltless" 22 - keyAlpha = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAlphaAlphaAlphaAlphaAlphaAlphaAlphaAlpha01" 23 - keyBravo = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIABravoBravoBravoBravoBravoBravoBravoBravo02" 22 + keyAlpha = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICrWubjgc3IM/zqjpWQJSig6l6iFyaDx7HWTiWlasjcM" 23 + keyBravo = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMax5MnG4RGoxp0TlaEj8mcFhZZp13cdIIvO8s4a6KZ2" 24 24 ) 25 25 26 26 func TestFetchAndStore_EmptyResponseDoesNotWipe(t *testing.T) {
+57
knotserver/xrpc/check_push_allowed.go
··· 1 + package xrpc 2 + 3 + import ( 4 + "net/http" 5 + "strings" 6 + 7 + "golang.org/x/crypto/ssh" 8 + "tangled.org/core/api/tangled" 9 + "tangled.org/core/rbac" 10 + xrpcerr "tangled.org/core/xrpc/errors" 11 + ) 12 + 13 + func (x *Xrpc) CheckPushAllowed(w http.ResponseWriter, r *http.Request) { 14 + repo := strings.TrimSpace(r.URL.Query().Get("repo")) 15 + keyStr := r.URL.Query().Get("key") 16 + 17 + if !strings.HasPrefix(repo, "did:") || keyStr == "" { 18 + writeError(w, xrpcerr.NewXrpcError( 19 + xrpcerr.WithTag("InvalidRequest"), 20 + xrpcerr.WithMessage("repo (a repo DID) and key are required"), 21 + ), http.StatusBadRequest) 22 + return 23 + } 24 + 25 + offered, _, _, _, err := ssh.ParseAuthorizedKey([]byte(keyStr)) 26 + if err != nil { 27 + writeError(w, xrpcerr.NewXrpcError( 28 + xrpcerr.WithTag("InvalidRequest"), 29 + xrpcerr.WithMessage("malformed public key"), 30 + ), http.StatusBadRequest) 31 + return 32 + } 33 + 34 + did, ok, err := x.Db.DidForPublicKey(offered) 35 + if err != nil { 36 + x.Logger.Error("failed to look up public key", "error", err) 37 + x.writeJson(w, tangled.RepoCheckPushAllowed_Output{Allowed: false}) 38 + return 39 + } 40 + if !ok { 41 + // unknown key, not an error, just not allowed 42 + x.writeJson(w, tangled.RepoCheckPushAllowed_Output{Allowed: false}) 43 + return 44 + } 45 + 46 + didStr := did.String() 47 + 48 + allowed, err := x.Enforcer.IsPushAllowed(didStr, rbac.ThisServer, repo) 49 + if err != nil { 50 + x.Logger.Error("enforcer error", "did", didStr, "repo", repo, "error", err) 51 + allowed = false 52 + } 53 + 54 + out := tangled.RepoCheckPushAllowed_Output{Allowed: allowed} 55 + out.Did = &didStr 56 + x.writeJson(w, out) 57 + }
+1
knotserver/xrpc/xrpc.go
··· 84 84 r.Get("/"+tangled.RepoArchiveNSID, x.RepoArchive) 85 85 r.Get("/"+tangled.RepoLanguagesNSID, x.RepoLanguages) 86 86 r.Get("/"+tangled.RepoListCollaboratorsNSID, x.ListCollaborators) 87 + r.Get("/"+tangled.RepoCheckPushAllowedNSID, x.CheckPushAllowed) 87 88 88 89 // knot query endpoints (no auth required) 89 90 r.Get("/"+tangled.KnotListKeysNSID, x.ListKeys)
+53
lexicons/repo/checkPushAllowed.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.repo.checkPushAllowed", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Check whether the holder of a public key is allowed to push to a repo.", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["repo", "key"], 11 + "properties": { 12 + "repo": { 13 + "type": "string", 14 + "description": "A repo DID." 15 + }, 16 + "key": { 17 + "type": "string", 18 + "maxLength": 4096, 19 + "description": "Public key in OpenSSH authorized_keys format." 20 + } 21 + } 22 + }, 23 + "output": { 24 + "encoding": "application/json", 25 + "schema": { 26 + "type": "object", 27 + "required": ["allowed"], 28 + "properties": { 29 + "allowed": { 30 + "type": "boolean", 31 + "description": "Whether the key's owner may push to the repo." 32 + }, 33 + "did": { 34 + "type": "string", 35 + "format": "did", 36 + "description": "DID the key resolved to, if a match was found." 37 + } 38 + } 39 + } 40 + }, 41 + "errors": [ 42 + { 43 + "name": "RepoNotFound", 44 + "description": "The repo could not be resolved on this knot." 45 + }, 46 + { 47 + "name": "InvalidRequest", 48 + "description": "Missing or malformed parameters." 49 + } 50 + ] 51 + } 52 + } 53 + }