File view with blame information shown in the left gutter beside each line.

barerepo / server / internal/store/accounts.go
492 lines · 15kb · master
log files threads runs releases config jump to file t
133728e barerepo 1mo
1
package store
133728e barerepo 1mo
2
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"context"
133728e barerepo 1mo
5
"database/sql"
133728e barerepo 1mo
6
"errors"
133728e barerepo 1mo
7
"fmt"
133728e barerepo 1mo
8
"strings"
133728e barerepo 1mo
9
"time"
133728e barerepo 1mo
10
133728e barerepo 1mo
11
"golang.org/x/crypto/ssh"
133728e barerepo 1mo
12
133728e barerepo 1mo
13
"github.com/barerepo/server/internal/gitx"
133728e barerepo 1mo
14
"github.com/barerepo/server/internal/repo"
133728e barerepo 1mo
15
)
133728e barerepo 1mo
16
133728e barerepo 1mo
17
var (
133728e barerepo 1mo
18
ErrNotFound = errors.New("not found")
133728e barerepo 1mo
19
ErrTaken = errors.New("already taken")
133728e barerepo 1mo
20
)
133728e barerepo 1mo
21
133728e barerepo 1mo
22
type Account struct {
133728e barerepo 1mo
23
Name string
133728e barerepo 1mo
24
Admin bool
133728e barerepo 1mo
25
Created time.Time
133728e barerepo 1mo
26
}
133728e barerepo 1mo
27
133728e barerepo 1mo
28
type PubKey struct {
133728e barerepo 1mo
29
ID int64
133728e barerepo 1mo
30
Account string
133728e barerepo 1mo
31
Fingerprint string
133728e barerepo 1mo
32
Algo string
133728e barerepo 1mo
33
Blob string // the full authorized_keys line body, without options
133728e barerepo 1mo
34
Comment string
133728e barerepo 1mo
35
Created time.Time
133728e barerepo 1mo
36
LastUsed time.Time
133728e barerepo 1mo
37
// Retired is when the key stopped granting access, and the zero time means it still does.
133728e barerepo 1mo
38
Retired time.Time
133728e barerepo 1mo
39
}
133728e barerepo 1mo
40
133728e barerepo 1mo
41
// ParsePubKey reads authorized_keys form and rejects a private key pasted by mistake. 45.4.
133728e barerepo 1mo
42
func ParsePubKey(raw string) (algo, blob, comment string, fingerprint string, err error) {
133728e barerepo 1mo
43
raw = strings.TrimSpace(raw)
133728e barerepo 1mo
44
if strings.Contains(raw, "PRIVATE KEY") {
133728e barerepo 1mo
45
return "", "", "", "", errors.New("that is a private key. paste the .pub file instead, and treat the key you pasted as compromised")
133728e barerepo 1mo
46
}
133728e barerepo 1mo
47
key, comment, _, _, err := ssh.ParseAuthorizedKey([]byte(raw))
133728e barerepo 1mo
48
if err != nil {
133728e barerepo 1mo
49
return "", "", "", "", errors.New("that does not parse as an ssh public key")
133728e barerepo 1mo
50
}
133728e barerepo 1mo
51
switch key.Type() {
133728e barerepo 1mo
52
case ssh.KeyAlgoED25519, ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521, ssh.KeyAlgoRSA:
133728e barerepo 1mo
53
default:
133728e barerepo 1mo
54
return "", "", "", "", fmt.Errorf("unsupported key type %s", key.Type())
133728e barerepo 1mo
55
}
133728e barerepo 1mo
56
line := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key)))
133728e barerepo 1mo
57
fields := strings.Fields(line)
133728e barerepo 1mo
58
return fields[0], fields[1], comment, ssh.FingerprintSHA256(key), nil
133728e barerepo 1mo
59
}
133728e barerepo 1mo
60
133728e barerepo 1mo
61
// CheckNewAccount refuses a taken name before the challenge, not after the person has signed.
133728e barerepo 1mo
62
func (db *DB) CheckNewAccount(ctx context.Context, name, rawKey string) error {
133728e barerepo 1mo
63
if !gitx.ValidName(name) {
133728e barerepo 1mo
64
if gitx.Reserved(name) {
133728e barerepo 1mo
65
// A name a page already uses is not available, and why is nobody else's business.
133728e barerepo 1mo
66
return fmt.Errorf("the name %q is %w", name, ErrTaken)
133728e barerepo 1mo
67
}
133728e barerepo 1mo
68
return fmt.Errorf("a name is 1 to 39 characters of a-z, 0-9 and -, and starts with a letter or digit")
133728e barerepo 1mo
69
}
133728e barerepo 1mo
70
if _, err := db.Account(ctx, name); err == nil {
133728e barerepo 1mo
71
return fmt.Errorf("the name %q is %w", name, ErrTaken)
133728e barerepo 1mo
72
}
133728e barerepo 1mo
73
_, _, _, fp, err := ParsePubKey(rawKey)
133728e barerepo 1mo
74
if err != nil {
133728e barerepo 1mo
75
return err
133728e barerepo 1mo
76
}
133728e barerepo 1mo
77
var n int
133728e barerepo 1mo
78
if err := db.QueryRowContext(ctx,
133728e barerepo 1mo
79
`SELECT count(*) FROM pubkeys WHERE fingerprint = ?`, fp).Scan(&n); err != nil {
133728e barerepo 1mo
80
return err
133728e barerepo 1mo
81
}
133728e barerepo 1mo
82
if n > 0 {
133728e barerepo 1mo
83
return fmt.Errorf("that key is %w by another account", ErrTaken)
133728e barerepo 1mo
84
}
133728e barerepo 1mo
85
return nil
133728e barerepo 1mo
86
}
133728e barerepo 1mo
87
133728e barerepo 1mo
88
// CreateAccount writes account and first key in one transaction, since neither works alone.
133728e barerepo 1mo
89
func (db *DB) CreateAccount(ctx context.Context, name, rawKey string, admin bool) (*Account, error) {
133728e barerepo 1mo
90
if !gitx.ValidName(name) {
133728e barerepo 1mo
91
if gitx.Reserved(name) {
133728e barerepo 1mo
92
// The same answer as a taken name, because they are the same answer to the visitor.
133728e barerepo 1mo
93
return nil, fmt.Errorf("the name %q is %w", name, ErrTaken)
133728e barerepo 1mo
94
}
133728e barerepo 1mo
95
return nil, fmt.Errorf("a name is 1 to 39 characters of a-z, 0-9 and -, and starts with a letter or digit")
133728e barerepo 1mo
96
}
133728e barerepo 1mo
97
algo, blob, comment, fp, err := ParsePubKey(rawKey)
133728e barerepo 1mo
98
if err != nil {
133728e barerepo 1mo
99
return nil, err
133728e barerepo 1mo
100
}
133728e barerepo 1mo
101
now := now()
133728e barerepo 1mo
102
tx, err := db.Begin(ctx)
133728e barerepo 1mo
103
if err != nil {
133728e barerepo 1mo
104
return nil, err
133728e barerepo 1mo
105
}
133728e barerepo 1mo
106
defer tx.Rollback()
133728e barerepo 1mo
107
133728e barerepo 1mo
108
_, err = tx.ExecContext(ctx, `INSERT INTO accounts (name, admin, created_at) VALUES (?, ?, ?)`,
133728e barerepo 1mo
109
name, boolInt(admin), now.Unix())
133728e barerepo 1mo
110
if err != nil {
133728e barerepo 1mo
111
if isUnique(err) {
133728e barerepo 1mo
112
return nil, fmt.Errorf("the name %q is %w", name, ErrTaken)
133728e barerepo 1mo
113
}
133728e barerepo 1mo
114
return nil, err
133728e barerepo 1mo
115
}
133728e barerepo 1mo
116
_, err = tx.ExecContext(ctx,
133728e barerepo 1mo
117
`INSERT INTO pubkeys (account, fingerprint, algo, blob, comment, created_at) VALUES (?, ?, ?, ?, ?, ?)`,
133728e barerepo 1mo
118
name, fp, algo, blob, comment, now.Unix())
133728e barerepo 1mo
119
if err != nil {
133728e barerepo 1mo
120
if isUnique(err) {
133728e barerepo 1mo
121
return nil, fmt.Errorf("that key is %w by another account", ErrTaken)
133728e barerepo 1mo
122
}
133728e barerepo 1mo
123
return nil, err
133728e barerepo 1mo
124
}
133728e barerepo 1mo
125
if err := tx.Commit(); err != nil {
133728e barerepo 1mo
126
return nil, err
133728e barerepo 1mo
127
}
133728e barerepo 1mo
128
// The file that grants ssh access is a copy of this table, so it is rewritten with it.
133728e barerepo 1mo
129
if err := db.SyncAuthorizedKeys(ctx); err != nil {
133728e barerepo 1mo
130
return nil, err
133728e barerepo 1mo
131
}
133728e barerepo 1mo
132
return &Account{Name: name, Admin: admin, Created: now}, nil
133728e barerepo 1mo
133
}
133728e barerepo 1mo
134
133728e barerepo 1mo
135
func (db *DB) Account(ctx context.Context, name string) (*Account, error) {
133728e barerepo 1mo
136
var a Account
133728e barerepo 1mo
137
var admin int
133728e barerepo 1mo
138
var created int64
133728e barerepo 1mo
139
err := db.QueryRowContext(ctx, `SELECT name, admin, created_at FROM accounts WHERE name = ?`, name).
133728e barerepo 1mo
140
Scan(&a.Name, &admin, &created)
133728e barerepo 1mo
141
if errors.Is(err, sql.ErrNoRows) {
133728e barerepo 1mo
142
return nil, ErrNotFound
133728e barerepo 1mo
143
}
133728e barerepo 1mo
144
if err != nil {
133728e barerepo 1mo
145
return nil, err
133728e barerepo 1mo
146
}
133728e barerepo 1mo
147
a.Admin = admin == 1
133728e barerepo 1mo
148
a.Created = time.Unix(created, 0)
133728e barerepo 1mo
149
return &a, nil
133728e barerepo 1mo
150
}
133728e barerepo 1mo
151
133728e barerepo 1mo
152
// AddKey stores another key, which chapter 10 urges, because losing every key loses the account.
133728e barerepo 1mo
153
func (db *DB) AddKey(ctx context.Context, account, rawKey string) (*PubKey, error) {
133728e barerepo 1mo
154
algo, blob, comment, fp, err := ParsePubKey(rawKey)
133728e barerepo 1mo
155
if err != nil {
133728e barerepo 1mo
156
return nil, err
133728e barerepo 1mo
157
}
133728e barerepo 1mo
158
now := now()
133728e barerepo 1mo
159
// RETURNING rather than LastInsertId, which PostgreSQL does not have.
133728e barerepo 1mo
160
var id int64
133728e barerepo 1mo
161
err = db.QueryRowContext(ctx,
133728e barerepo 1mo
162
`INSERT INTO pubkeys (account, fingerprint, algo, blob, comment, created_at)
133728e barerepo 1mo
163
VALUES (?, ?, ?, ?, ?, ?) RETURNING id`,
133728e barerepo 1mo
164
account, fp, algo, blob, comment, now.Unix()).Scan(&id)
133728e barerepo 1mo
165
if err != nil {
133728e barerepo 1mo
166
if isUnique(err) {
133728e barerepo 1mo
167
return nil, fmt.Errorf("that key is %w", ErrTaken)
133728e barerepo 1mo
168
}
133728e barerepo 1mo
169
return nil, err
133728e barerepo 1mo
170
}
133728e barerepo 1mo
171
if err := db.SyncAuthorizedKeys(ctx); err != nil {
133728e barerepo 1mo
172
return nil, err
133728e barerepo 1mo
173
}
133728e barerepo 1mo
174
return &PubKey{ID: id, Account: account, Fingerprint: fp, Algo: algo, Blob: blob, Comment: comment, Created: now}, nil
133728e barerepo 1mo
175
}
133728e barerepo 1mo
176
133728e barerepo 1mo
177
// Keys lists an account's keys, oldest first.
133728e barerepo 1mo
178
func (db *DB) Keys(ctx context.Context, account string) ([]PubKey, error) {
133728e barerepo 1mo
179
rows, err := db.QueryContext(ctx,
133728e barerepo 1mo
180
`SELECT id, account, fingerprint, algo, blob, comment, created_at, last_used
133728e barerepo 1mo
181
FROM pubkeys WHERE account = ? AND retired_at IS NULL ORDER BY created_at, id`, account)
133728e barerepo 1mo
182
if err != nil {
133728e barerepo 1mo
183
return nil, err
133728e barerepo 1mo
184
}
133728e barerepo 1mo
185
defer rows.Close()
133728e barerepo 1mo
186
var out []PubKey
133728e barerepo 1mo
187
for rows.Next() {
133728e barerepo 1mo
188
var k PubKey
133728e barerepo 1mo
189
var created int64
133728e barerepo 1mo
190
var used sql.NullInt64
133728e barerepo 1mo
191
if err := rows.Scan(&k.ID, &k.Account, &k.Fingerprint, &k.Algo, &k.Blob, &k.Comment, &created, &used); err != nil {
133728e barerepo 1mo
192
return nil, err
133728e barerepo 1mo
193
}
133728e barerepo 1mo
194
k.Created = time.Unix(created, 0)
133728e barerepo 1mo
195
if used.Valid {
133728e barerepo 1mo
196
k.LastUsed = time.Unix(used.Int64, 0)
133728e barerepo 1mo
197
}
133728e barerepo 1mo
198
out = append(out, k)
133728e barerepo 1mo
199
}
133728e barerepo 1mo
200
return out, rows.Err()
133728e barerepo 1mo
201
}
133728e barerepo 1mo
202
133728e barerepo 1mo
203
// SSHDir is where authorized_keys is written, and empty writes none. Set once at start.
133728e barerepo 1mo
204
var SSHDir string
133728e barerepo 1mo
205
133728e barerepo 1mo
206
// SSHBin is the program authorized_keys forces, which is this one.
133728e barerepo 1mo
207
var SSHBin string
133728e barerepo 1mo
208
133728e barerepo 1mo
209
// SignersPath is where the keys a commit signature is checked against are written, and empty writes none.
133728e barerepo 1mo
210
var SignersPath string
133728e barerepo 1mo
211
133728e barerepo 1mo
212
// SyncAuthorizedKeys rewrites the file from the database, which is the thing that grants access.
133728e barerepo 1mo
213
func (db *DB) SyncAuthorizedKeys(ctx context.Context) error {
133728e barerepo 1mo
214
if SSHDir == "" && SignersPath == "" {
133728e barerepo 1mo
215
return nil
133728e barerepo 1mo
216
}
216 of 492 lines · the whole file
history · rawbarerepo 0.1.0