coverage~bigbes/sr-ht-doltede3b0bbdb/keys.go

Coverage
85.1% 40/47 statements
Δ
Blob
375ba47
1 package db
2
3 import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "time"
9
10 "github.com/lib/pq"
11
12 "sourcecraft.dev/bigbes/sr-ht-dolt/core"
13 )
14
15 // DoltKey is a registered Ed25519 credential (dolt creds / dolt login). PubKey
16 // is the raw 32-byte public key; Comment and LastUsed are nullable in the
17 // schema and default to "" / nil.
18 type DoltKey struct {
19 ID int
20 UserID int
21 KID string
22 PubKey []byte
23 Comment string
24 Created time.Time
25 LastUsed *time.Time
26 }
27
28 // KeyAuth is everything the Bearer-JWT verifier needs to authenticate a dolt
29 // keypair request: the stored public key to verify the JWS signature, plus the
30 // owning user's identity to build a core.Caller. Suspended is derived by the
31 // caller from UserType == core.UserTypeSuspended.
32 type KeyAuth struct {
33 KeyID int
34 UserID int
35 Username string
36 UserType core.UserType
37 PubKey []byte
38 }
39
40 // InsertKey registers a dolt key for a user. kid is base32(SHA-512/224(pubkey))
41 // in dolt's alphabet; pubkey is the raw 32-byte Ed25519 public key. A duplicate
42 // kid (dolt_key.kid UNIQUE) is mapped to ErrKeyExists. Returns the created row.
43 2 func (s *Store) InsertKey(ctx context.Context, userID int, kid string, pubkey []byte, comment string) (*DoltKey, error) {
44 2 now := time.Now().UTC()
45 2 const q = `
46 2 INSERT INTO dolt_key (created, user_id, kid, pubkey, comment)
47 2 VALUES ($1, $2, $3, $4, $5)
48 2 RETURNING id`
49 2 var cmt any
50 2 if comment != "" {
51 2 cmt = comment
52 2 }
53 2 var id int
54 2 err := s.q.QueryRowContext(ctx, q, now, userID, kid, pubkey, cmt).Scan(&id)
55 2 if err != nil {
56 1 var pqErr *pq.Error
57 1 if errors.As(err, &pqErr) && pqErr.Code == "23505" {
58 1 return nil, ErrKeyExists
59 1 }
60 0 return nil, fmt.Errorf("insert dolt key: %w", err)
61 }
62 1 return &DoltKey{
63 1 ID: id,
64 1 UserID: userID,
65 1 KID: kid,
66 1 PubKey: pubkey,
67 1 Comment: comment,
68 1 Created: now,
69 1 }, nil
70 }
71
72 // KeyByKID resolves a key by its kid and returns the public key together with
73 // the owning user's identity, for authentication. Returns ErrNotFound if no key
74 // with that kid is registered.
75 2 func (s *Store) KeyByKID(ctx context.Context, kid string) (*KeyAuth, error) {
76 2 const q = `
77 2 SELECT k.id, k.pubkey, u.id, COALESCE(u.username, ''), u.user_type
78 2 FROM dolt_key k
79 2 JOIN "user" u ON u.id = k.user_id
80 2 WHERE k.kid = $1`
81 2 var (
82 2 ka KeyAuth
83 2 userType string
84 2 )
85 2 err := s.q.QueryRowContext(ctx, q, kid).Scan(
86 2 &ka.KeyID, &ka.PubKey, &ka.UserID, &ka.Username, &userType)
87 2 if errors.Is(err, sql.ErrNoRows) {
88 1 return nil, ErrNotFound
89 1 }
90 1 if err != nil {
91 0 return nil, fmt.Errorf("key by kid %s: %w", kid, err)
92 0 }
93 1 ka.UserType = core.UserType(userType)
94 1 return &ka, nil
95 }
96
97 // ListKeysByUser returns all of a user's registered dolt keys, newest first.
98 2 func (s *Store) ListKeysByUser(ctx context.Context, userID int) ([]*DoltKey, error) {
99 2 const q = `
100 2 SELECT id, user_id, kid, pubkey, COALESCE(comment, ''), created, last_used
101 2 FROM dolt_key
102 2 WHERE user_id = $1
103 2 ORDER BY created DESC, id DESC`
104 2 rows, err := s.q.QueryContext(ctx, q, userID)
105 2 if err != nil {
106 0 return nil, fmt.Errorf("list keys user=%d: %w", userID, err)
107 0 }
108 2 defer rows.Close()
109 2 var keys []*DoltKey
110 2 for rows.Next() {
111 1 var (
112 1 k DoltKey
113 1 lastUsed sql.NullTime
114 1 )
115 1 if err := rows.Scan(&k.ID, &k.UserID, &k.KID, &k.PubKey,
116 1 &k.Comment, &k.Created, &lastUsed); err != nil {
117 0 return nil, fmt.Errorf("scan key: %w", err)
118 0 }
119 1 if lastUsed.Valid {
120 1 t := lastUsed.Time
121 1 k.LastUsed = &t
122 1 }
123 1 keys = append(keys, &k)
124 }
125 2 if err := rows.Err(); err != nil {
126 0 return nil, fmt.Errorf("iterate keys: %w", err)
127 0 }
128 2 return keys, nil
129 }
130
131 // DeleteKey removes one of a user's keys. It is scoped by userID so a user can
132 // only delete keys they own; a mismatch (or missing id) yields ErrNotFound.
133 2 func (s *Store) DeleteKey(ctx context.Context, id, userID int) error {
134 2 res, err := s.q.ExecContext(ctx,
135 2 `DELETE FROM dolt_key WHERE id = $1 AND user_id = $2`, id, userID)
136 2 if err != nil {
137 0 return fmt.Errorf("delete key %d: %w", id, err)
138 0 }
139 2 return requireOne(res, "delete key")
140 }
141
142 // TouchKeyLastUsed stamps a key's last_used with the current time, called after
143 // a successful keypair authentication. Returns ErrNotFound if the kid vanished
144 // (e.g. the key was deleted concurrently).
145 2 func (s *Store) TouchKeyLastUsed(ctx context.Context, kid string) error {
146 2 res, err := s.q.ExecContext(ctx,
147 2 `UPDATE dolt_key SET last_used = $2 WHERE kid = $1`, kid, time.Now().UTC())
148 2 if err != nil {
149 0 return fmt.Errorf("touch key %s: %w", kid, err)
150 0 }
151 2 return requireOne(res, "touch key")
152 }