tenseleyflow/shithub / 82fbbcf

Browse files

users/ssh-keys: add kind column (authentication|signing) + kind-filtered queries

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
82fbbcf063ccf0e4c533712e8d25cabc8051aa7d
Parents
080c421
Tree
94cbc43

6 changed files

StatusFile+-
A internal/migrationsfs/migrations/0062_user_ssh_keys_kind.sql 26 0
M internal/users/queries/user_ssh_keys.sql 27 5
M internal/users/sqlc/models.go 1 0
M internal/users/sqlc/querier.go 8 0
M internal/users/sqlc/user_ssh_keys.sql.go 115 5
M internal/web/handlers/auth/sshkeys.go 1 0
internal/migrationsfs/migrations/0062_user_ssh_keys_kind.sqladded
@@ -0,0 +1,26 @@
1
+-- SPDX-License-Identifier: AGPL-3.0-or-later
2
+--
3
+-- Add a `kind` discriminator to user_ssh_keys so callers can register
4
+-- a key for git authentication (authorized-keys lookup) or for git
5
+-- commit signing without conflating the two intents.
6
+--
7
+-- Pre-existing rows are git-auth keys (the only kind shithub recognised
8
+-- through S07-S16); we backfill `authentication` and let the check
9
+-- constraint enforce the closed set going forward.
10
+--
11
+-- The /api/v1/user/keys REST surface (S50 §1) returns rows filtered to
12
+-- `authentication` by default to mirror GitHub's /user/keys shape; the
13
+-- signing surface lands behind /user/ssh_signing_keys when a CLI sprint
14
+-- consumes it.
15
+
16
+-- +goose Up
17
+ALTER TABLE user_ssh_keys
18
+    ADD COLUMN kind text NOT NULL DEFAULT 'authentication'
19
+    CHECK (kind IN ('authentication', 'signing'));
20
+
21
+CREATE INDEX user_ssh_keys_user_id_kind_idx
22
+    ON user_ssh_keys (user_id, kind, created_at DESC);
23
+
24
+-- +goose Down
25
+DROP INDEX IF EXISTS user_ssh_keys_user_id_kind_idx;
26
+ALTER TABLE user_ssh_keys DROP COLUMN IF EXISTS kind;
internal/users/queries/user_ssh_keys.sqlmodified
@@ -1,18 +1,32 @@
11
 -- SPDX-License-Identifier: AGPL-3.0-or-later
22
 
33
 -- name: InsertUserSSHKey :one
4
-INSERT INTO user_ssh_keys (user_id, title, fingerprint_sha256, key_type, key_bits, public_key)
5
-VALUES ($1, $2, $3, $4, $5, $6)
4
+INSERT INTO user_ssh_keys (user_id, title, fingerprint_sha256, key_type, key_bits, public_key, kind)
5
+VALUES ($1, $2, $3, $4, $5, $6, $7)
66
 RETURNING id, user_id, title, fingerprint_sha256, key_type, key_bits, public_key,
7
-          last_used_at, last_used_ip, created_at;
7
+          last_used_at, last_used_ip, created_at, kind;
88
 
99
 -- name: ListUserSSHKeys :many
1010
 SELECT id, user_id, title, fingerprint_sha256, key_type, key_bits, public_key,
11
-       last_used_at, last_used_ip, created_at
11
+       last_used_at, last_used_ip, created_at, kind
1212
 FROM user_ssh_keys
1313
 WHERE user_id = $1
1414
 ORDER BY created_at DESC;
1515
 
16
+-- name: ListUserSSHKeysByKind :many
17
+-- Paginated kind-filtered list used by the REST surface. Order matches
18
+-- ListUserSSHKeys so callers can swap between them without observing a
19
+-- reshuffle.
20
+SELECT id, user_id, title, fingerprint_sha256, key_type, key_bits, public_key,
21
+       last_used_at, last_used_ip, created_at, kind
22
+FROM user_ssh_keys
23
+WHERE user_id = $1 AND kind = $2
24
+ORDER BY created_at DESC
25
+LIMIT $3 OFFSET $4;
26
+
27
+-- name: CountUserSSHKeysByKind :one
28
+SELECT count(*) FROM user_ssh_keys WHERE user_id = $1 AND kind = $2;
29
+
1630
 -- name: CountUserSSHKeys :one
1731
 SELECT count(*) FROM user_ssh_keys WHERE user_id = $1;
1832
 
@@ -21,11 +35,19 @@ SELECT count(*) FROM user_ssh_keys WHERE user_id = $1;
2135
 -- handler can never delete keys it doesn't own.
2236
 DELETE FROM user_ssh_keys WHERE id = $1 AND user_id = $2;
2337
 
38
+-- name: GetUserSSHKey :one
39
+-- Single-key lookup for the REST GET-by-id endpoint. user_id filter so
40
+-- one caller can't read another's key by ID.
41
+SELECT id, user_id, title, fingerprint_sha256, key_type, key_bits, public_key,
42
+       last_used_at, last_used_ip, created_at, kind
43
+FROM user_ssh_keys
44
+WHERE id = $1 AND user_id = $2;
45
+
2446
 -- name: GetUserSSHKeyByFingerprint :one
2547
 -- Hot path for sshd's AuthorizedKeysCommand. Index lookup via the UNIQUE
2648
 -- index on fingerprint_sha256.
2749
 SELECT id, user_id, title, fingerprint_sha256, key_type, key_bits, public_key,
28
-       last_used_at, last_used_ip, created_at
50
+       last_used_at, last_used_ip, created_at, kind
2951
 FROM user_ssh_keys
3052
 WHERE fingerprint_sha256 = $1;
3153
 
internal/users/sqlc/models.gomodified
@@ -2556,6 +2556,7 @@ type UserSshKey struct {
25562556
 	LastUsedAt        pgtype.Timestamptz
25572557
 	LastUsedIp        *netip.Addr
25582558
 	CreatedAt         pgtype.Timestamptz
2559
+	Kind              string
25592560
 }
25602561
 
25612562
 type UserToken struct {
internal/users/sqlc/querier.gomodified
@@ -36,6 +36,7 @@ type Querier interface {
3636
 	CountRecentUsernameChanges(ctx context.Context, db DBTX, arg CountRecentUsernameChangesParams) (int64, error)
3737
 	CountUnusedRecoveryCodes(ctx context.Context, db DBTX, userID int64) (int64, error)
3838
 	CountUserSSHKeys(ctx context.Context, db DBTX, userID int64) (int64, error)
39
+	CountUserSSHKeysByKind(ctx context.Context, db DBTX, arg CountUserSSHKeysByKindParams) (int64, error)
3940
 	CountUsers(ctx context.Context, db DBTX) (int64, error)
4041
 	CountVerifiedUserEmails(ctx context.Context, db DBTX, userID int64) (int64, error)
4142
 	// SPDX-License-Identifier: AGPL-3.0-or-later
@@ -67,6 +68,9 @@ type Querier interface {
6768
 	GetUserEmailByVerificationHash(ctx context.Context, db DBTX, verificationTokenHash []byte) (UserEmail, error)
6869
 	// Like GetUserByID but returns the row even when deleted_at IS NOT NULL.
6970
 	GetUserIncludingDeleted(ctx context.Context, db DBTX, id int64) (User, error)
71
+	// Single-key lookup for the REST GET-by-id endpoint. user_id filter so
72
+	// one caller can't read another's key by ID.
73
+	GetUserSSHKey(ctx context.Context, db DBTX, arg GetUserSSHKeyParams) (UserSshKey, error)
7074
 	// Hot path for sshd's AuthorizedKeysCommand. Index lookup via the UNIQUE
7175
 	// index on fingerprint_sha256.
7276
 	GetUserSSHKeyByFingerprint(ctx context.Context, db DBTX, fingerprintSha256 string) (UserSshKey, error)
@@ -96,6 +100,10 @@ type Querier interface {
96100
 	// SPDX-License-Identifier: AGPL-3.0-or-later
97101
 	ListUserNotificationPrefs(ctx context.Context, db DBTX, userID int64) ([]UserNotificationPref, error)
98102
 	ListUserSSHKeys(ctx context.Context, db DBTX, userID int64) ([]UserSshKey, error)
103
+	// Paginated kind-filtered list used by the REST surface. Order matches
104
+	// ListUserSSHKeys so callers can swap between them without observing a
105
+	// reshuffle.
106
+	ListUserSSHKeysByKind(ctx context.Context, db DBTX, arg ListUserSSHKeysByKindParams) ([]UserSshKey, error)
99107
 	ListUserTokens(ctx context.Context, db DBTX, userID int64) ([]UserToken, error)
100108
 	// SPDX-License-Identifier: AGPL-3.0-or-later
101109
 	// Resolve an old username to the current username via the user_id FK.
internal/users/sqlc/user_ssh_keys.sql.gomodified
@@ -21,6 +21,22 @@ func (q *Queries) CountUserSSHKeys(ctx context.Context, db DBTX, userID int64) (
2121
 	return count, err
2222
 }
2323
 
24
+const countUserSSHKeysByKind = `-- name: CountUserSSHKeysByKind :one
25
+SELECT count(*) FROM user_ssh_keys WHERE user_id = $1 AND kind = $2
26
+`
27
+
28
+type CountUserSSHKeysByKindParams struct {
29
+	UserID int64
30
+	Kind   string
31
+}
32
+
33
+func (q *Queries) CountUserSSHKeysByKind(ctx context.Context, db DBTX, arg CountUserSSHKeysByKindParams) (int64, error) {
34
+	row := db.QueryRow(ctx, countUserSSHKeysByKind, arg.UserID, arg.Kind)
35
+	var count int64
36
+	err := row.Scan(&count)
37
+	return count, err
38
+}
39
+
2440
 const deleteUserSSHKey = `-- name: DeleteUserSSHKey :execrows
2541
 DELETE FROM user_ssh_keys WHERE id = $1 AND user_id = $2
2642
 `
@@ -40,9 +56,42 @@ func (q *Queries) DeleteUserSSHKey(ctx context.Context, db DBTX, arg DeleteUserS
4056
 	return result.RowsAffected(), nil
4157
 }
4258
 
59
+const getUserSSHKey = `-- name: GetUserSSHKey :one
60
+SELECT id, user_id, title, fingerprint_sha256, key_type, key_bits, public_key,
61
+       last_used_at, last_used_ip, created_at, kind
62
+FROM user_ssh_keys
63
+WHERE id = $1 AND user_id = $2
64
+`
65
+
66
+type GetUserSSHKeyParams struct {
67
+	ID     int64
68
+	UserID int64
69
+}
70
+
71
+// Single-key lookup for the REST GET-by-id endpoint. user_id filter so
72
+// one caller can't read another's key by ID.
73
+func (q *Queries) GetUserSSHKey(ctx context.Context, db DBTX, arg GetUserSSHKeyParams) (UserSshKey, error) {
74
+	row := db.QueryRow(ctx, getUserSSHKey, arg.ID, arg.UserID)
75
+	var i UserSshKey
76
+	err := row.Scan(
77
+		&i.ID,
78
+		&i.UserID,
79
+		&i.Title,
80
+		&i.FingerprintSha256,
81
+		&i.KeyType,
82
+		&i.KeyBits,
83
+		&i.PublicKey,
84
+		&i.LastUsedAt,
85
+		&i.LastUsedIp,
86
+		&i.CreatedAt,
87
+		&i.Kind,
88
+	)
89
+	return i, err
90
+}
91
+
4392
 const getUserSSHKeyByFingerprint = `-- name: GetUserSSHKeyByFingerprint :one
4493
 SELECT id, user_id, title, fingerprint_sha256, key_type, key_bits, public_key,
45
-       last_used_at, last_used_ip, created_at
94
+       last_used_at, last_used_ip, created_at, kind
4695
 FROM user_ssh_keys
4796
 WHERE fingerprint_sha256 = $1
4897
 `
@@ -63,16 +112,17 @@ func (q *Queries) GetUserSSHKeyByFingerprint(ctx context.Context, db DBTX, finge
63112
 		&i.LastUsedAt,
64113
 		&i.LastUsedIp,
65114
 		&i.CreatedAt,
115
+		&i.Kind,
66116
 	)
67117
 	return i, err
68118
 }
69119
 
70120
 const insertUserSSHKey = `-- name: InsertUserSSHKey :one
71121
 
72
-INSERT INTO user_ssh_keys (user_id, title, fingerprint_sha256, key_type, key_bits, public_key)
73
-VALUES ($1, $2, $3, $4, $5, $6)
122
+INSERT INTO user_ssh_keys (user_id, title, fingerprint_sha256, key_type, key_bits, public_key, kind)
123
+VALUES ($1, $2, $3, $4, $5, $6, $7)
74124
 RETURNING id, user_id, title, fingerprint_sha256, key_type, key_bits, public_key,
75
-          last_used_at, last_used_ip, created_at
125
+          last_used_at, last_used_ip, created_at, kind
76126
 `
77127
 
78128
 type InsertUserSSHKeyParams struct {
@@ -82,6 +132,7 @@ type InsertUserSSHKeyParams struct {
82132
 	KeyType           string
83133
 	KeyBits           int32
84134
 	PublicKey         string
135
+	Kind              string
85136
 }
86137
 
87138
 // SPDX-License-Identifier: AGPL-3.0-or-later
@@ -93,6 +144,7 @@ func (q *Queries) InsertUserSSHKey(ctx context.Context, db DBTX, arg InsertUserS
93144
 		arg.KeyType,
94145
 		arg.KeyBits,
95146
 		arg.PublicKey,
147
+		arg.Kind,
96148
 	)
97149
 	var i UserSshKey
98150
 	err := row.Scan(
@@ -106,13 +158,14 @@ func (q *Queries) InsertUserSSHKey(ctx context.Context, db DBTX, arg InsertUserS
106158
 		&i.LastUsedAt,
107159
 		&i.LastUsedIp,
108160
 		&i.CreatedAt,
161
+		&i.Kind,
109162
 	)
110163
 	return i, err
111164
 }
112165
 
113166
 const listUserSSHKeys = `-- name: ListUserSSHKeys :many
114167
 SELECT id, user_id, title, fingerprint_sha256, key_type, key_bits, public_key,
115
-       last_used_at, last_used_ip, created_at
168
+       last_used_at, last_used_ip, created_at, kind
116169
 FROM user_ssh_keys
117170
 WHERE user_id = $1
118171
 ORDER BY created_at DESC
@@ -138,6 +191,63 @@ func (q *Queries) ListUserSSHKeys(ctx context.Context, db DBTX, userID int64) ([
138191
 			&i.LastUsedAt,
139192
 			&i.LastUsedIp,
140193
 			&i.CreatedAt,
194
+			&i.Kind,
195
+		); err != nil {
196
+			return nil, err
197
+		}
198
+		items = append(items, i)
199
+	}
200
+	if err := rows.Err(); err != nil {
201
+		return nil, err
202
+	}
203
+	return items, nil
204
+}
205
+
206
+const listUserSSHKeysByKind = `-- name: ListUserSSHKeysByKind :many
207
+SELECT id, user_id, title, fingerprint_sha256, key_type, key_bits, public_key,
208
+       last_used_at, last_used_ip, created_at, kind
209
+FROM user_ssh_keys
210
+WHERE user_id = $1 AND kind = $2
211
+ORDER BY created_at DESC
212
+LIMIT $3 OFFSET $4
213
+`
214
+
215
+type ListUserSSHKeysByKindParams struct {
216
+	UserID int64
217
+	Kind   string
218
+	Limit  int32
219
+	Offset int32
220
+}
221
+
222
+// Paginated kind-filtered list used by the REST surface. Order matches
223
+// ListUserSSHKeys so callers can swap between them without observing a
224
+// reshuffle.
225
+func (q *Queries) ListUserSSHKeysByKind(ctx context.Context, db DBTX, arg ListUserSSHKeysByKindParams) ([]UserSshKey, error) {
226
+	rows, err := db.Query(ctx, listUserSSHKeysByKind,
227
+		arg.UserID,
228
+		arg.Kind,
229
+		arg.Limit,
230
+		arg.Offset,
231
+	)
232
+	if err != nil {
233
+		return nil, err
234
+	}
235
+	defer rows.Close()
236
+	items := []UserSshKey{}
237
+	for rows.Next() {
238
+		var i UserSshKey
239
+		if err := rows.Scan(
240
+			&i.ID,
241
+			&i.UserID,
242
+			&i.Title,
243
+			&i.FingerprintSha256,
244
+			&i.KeyType,
245
+			&i.KeyBits,
246
+			&i.PublicKey,
247
+			&i.LastUsedAt,
248
+			&i.LastUsedIp,
249
+			&i.CreatedAt,
250
+			&i.Kind,
141251
 		); err != nil {
142252
 			return nil, err
143253
 		}
internal/web/handlers/auth/sshkeys.gomodified
@@ -81,6 +81,7 @@ func (h *Handlers) sshKeysAdd(w http.ResponseWriter, r *http.Request) {
8181
 		KeyType:           parsed.Type,
8282
 		KeyBits:           int32(parsed.Bits), //nolint:gosec // bits ≤ 8192 in practice; bounded.
8383
 		PublicKey:         parsed.PublicKey,
84
+		Kind:              "authentication",
8485
 	}); err != nil {
8586
 		if isPGUniqueViolation(err) {
8687
 			h.renderSSHKeysList(w, r,