Commit 61ecf7c

Antonio Mika  ·  2026-10-11 10:58:11 -0400 EDT
parent def8a10
refactor(db): share the auth lookups and feature check across apps
17 files changed,  +58, -74
+1, -1
......@@ -59,7 +59,7 @@ func (hc *ApiConfig) HasPrivilegedAccess(apiToken string) bool {
5959 if err != nil {
6060 return false
6161 }
62- return hc.Dbpool.HasFeatureByUser(user.ID, "auth")
62+ return db.HasFeature(hc.Dbpool, user.ID, "auth")
6363 }
6464
6565 func (hc *ApiConfig) HasPlusOrSpace(user *db.User, space string) bool {
+14, -8
......@@ -2,6 +2,7 @@ package auth
22
33 import (
44 "bytes"
5+ "database/sql"
56 "encoding/json"
67 "errors"
78 "fmt"
......@@ -237,18 +238,19 @@ func (a *AuthDb) FindUserByToken(token string) (*db.User, error) {
237238 return &db.User{ID: testUserID, Name: testUsername}, nil
238239 }
239240
240-func (a *AuthDb) HasFeatureByUser(userID string, feature string) bool {
241- return true
242-}
243-
244241 func (a *AuthDb) FindKeysByUser(user *db.User) ([]*db.PublicKey, error) {
245242 return []*db.PublicKey{{ID: "1", UserID: user.ID, Name: "my-key", Key: "nice-pubkey", CreatedAt: &time.Time{}}}, nil
246243 }
247244
245+// FindFeature gives pico+ a fixed expiry for the feed snapshots, and every
246+// other feature an active flag.
248247 func (a *AuthDb) FindFeature(userID string, feature string) (*db.FeatureFlag, error) {
249248 now := time.Date(2021, 8, 15, 14, 30, 45, 100, time.UTC)
250- oneDayWarning := now.AddDate(0, 0, 1)
251- return &db.FeatureFlag{ID: "2", UserID: userID, Name: "plus", ExpiresAt: &oneDayWarning, CreatedAt: &now}, nil
249+ expiresAt := now.AddDate(0, 0, 1)
250+ if feature != "plus" {
251+ expiresAt = time.Now().AddDate(0, 0, 1)
252+ }
253+ return &db.FeatureFlag{ID: "2", UserID: userID, Name: feature, ExpiresAt: &expiresAt, CreatedAt: &now}, nil
252254 }
253255
254256 func (a *AuthDb) InsertPost(post *db.Post) (*db.Post, error) {
......@@ -323,8 +325,12 @@ type stubDBWithFeatures struct {
323325 features map[string]bool
324326 }
325327
326-func (s *stubDBWithFeatures) HasFeatureByUser(userID string, feature string) bool {
327- return s.features[feature]
328+func (s *stubDBWithFeatures) FindFeature(userID string, feature string) (*db.FeatureFlag, error) {
329+ if !s.features[feature] {
330+ return nil, sql.ErrNoRows
331+ }
332+ expiresAt := time.Now().AddDate(0, 0, 1)
333+ return &db.FeatureFlag{UserID: userID, Name: feature, ExpiresAt: &expiresAt}, nil
328334 }
329335
330336 func newStubDBWithFeatures(features ...string) *stubDBWithFeatures {
+1, -3
......@@ -10,11 +10,9 @@ import (
1010
1111 // DB is the storage feeds uses.
1212 type DB interface {
13+ db.AuthDB
1314 db.PostDB
14- FindUser(userID string) (*db.User, error)
15- FindPost(postID string) (*db.Post, error)
1615 FindUsersWithPost(space string) ([]*db.User, error)
17- HasFeatureByUser(userID string, feature string) bool
1816 InsertFeedItems(postID string, items []*db.FeedItem) error
1917 FindFeedItemsByPostID(postID string) ([]*db.FeedItem, error)
2018 }
+1, -2
......@@ -9,10 +9,9 @@ import (
99
1010 // DB is the storage pastes uses.
1111 type DB interface {
12+ db.AuthDB
1213 db.PostDB
13- FindUserByName(name string) (*db.User, error)
1414 FindExpiredPosts(space string) ([]*db.Post, error)
15- FindFeature(userID string, feature string) (*db.FeatureFlag, error)
1615 }
1716
1817 // PsqlDB is pastes's postgres storage: the queries apps share, and the ones
+1, -6
......@@ -7,14 +7,9 @@ import (
77 )
88
99 type PgsDB interface {
10- FindUser(userID string) (*db.User, error)
11- FindUserByName(name string) (*db.User, error)
12- FindUserByPubkey(pubkey string) (*db.User, error)
10+ db.AuthDB
1311 FindUsers() ([]*db.User, error)
1412
15- FindFeature(userID string, name string) (*db.FeatureFlag, error)
16- InsertAccessLog(*db.AccessLog) error
17-
1813 InsertProject(userID, name, projectDir string) (string, error)
1914 UpdateProject(userID, name string) error
2015 UpdateProjectAcl(userID, name string, acl db.ProjectAcl) error
+1, -1
......@@ -42,7 +42,7 @@ func Middleware(handler *CliHandler) pssh.SSHServerMiddleware {
4242 uuidStr := uuid.NewString()
4343 isAdmin := false
4444 if user != nil {
45- isAdmin = handler.DBPool.HasFeatureByUser(user.ID, "admin")
45+ isAdmin = db.HasFeature(handler.DBPool, user.ID, "admin")
4646 if isAdmin && strings.HasPrefix(sesh.User(), "admin__") {
4747 uuidStr = fmt.Sprintf("admin-%s", uuidStr)
4848 }
+1, -2
......@@ -10,9 +10,8 @@ import (
1010
1111 // DB is the storage pipe uses.
1212 type DB interface {
13- FindUserByName(name string) (*db.User, error)
13+ db.AuthDB
1414 FindUserByToken(token string) (*db.User, error)
15- HasFeatureByUser(userID string, feature string) bool
1615 UpsertPipeMonitor(userID, topic string, dur time.Duration, winEnd *time.Time) error
1716 UpdatePipeMonitorLastPing(userID, topic string, lastPing *time.Time) error
1817 RemovePipeMonitor(userID, topic string) error
+0, -8
......@@ -92,14 +92,6 @@ func (t *TestDB) FindFeature(userID, name string) (*db.FeatureFlag, error) {
9292 return nil, fmt.Errorf("feature not found")
9393 }
9494
95-func (t *TestDB) HasFeatureByUser(userID string, feature string) bool {
96- ff, err := t.FindFeature(userID, feature)
97- if err != nil {
98- return false
99- }
100- return ff.IsValid()
101-}
102-
10395 func (t *TestDB) InsertAccessLog(_ *db.AccessLog) error {
10496 return nil
10597 }
+2, -4
......@@ -14,13 +14,11 @@ import (
1414
1515 // DB is the storage prose uses.
1616 type DB interface {
17+ db.AuthDB
1718 db.PostDB
18- ReplaceTagsByPost(tags []string, postID string) error
19- ReplaceAliasesByPost(aliases []string, postID string) error
19+ db.TagDB
2020 FindUserPostsByTag(pager *db.Pager, tag, userID, space string) (*db.Paginate[*db.Post], error)
2121 FindPopularPosts() ([]*db.Post, error)
22- FindUserByName(name string) (*db.User, error)
23- FindFeature(userID string, feature string) (*db.FeatureFlag, error)
2422 }
2523
2624 // PsqlDB is prose's postgres storage: the queries apps share, and the ones
+20, -4
......@@ -587,6 +587,15 @@ type UserDB interface {
587587 FindUserStats(userID string) (*UserStats, error)
588588 }
589589
590+// AuthDB is what an SSH server needs to authenticate and log its users.
591+type AuthDB interface {
592+ FindUser(userID string) (*User, error)
593+ FindUserByPubkey(pubkey string) (*User, error)
594+ FindUserByName(name string) (*User, error)
595+ FindFeature(userID string, feature string) (*FeatureFlag, error)
596+ InsertAccessLog(log *AccessLog) error
597+}
598+
590599 type KeyDB interface {
591600 UpdatePublicKey(pubkeyID, name string) (*PublicKey, error)
592601 InsertPublicKey(userID, pubkey, name string) error
......@@ -609,6 +618,7 @@ type PostDB interface {
609618 FindAllPostsByUser(userID string, space string) ([]*Post, error)
610619 FindPostWithFilename(filename string, userID string, space string) (*Post, error)
611620 FindPostWithSlug(slug string, userID string, space string) (*Post, error)
621+ FindPost(postID string) (*Post, error)
612622 InsertPost(post *Post) (*Post, error)
613623 UpdatePost(post *Post) (*Post, error)
614624 // RenamePost moves cur to next's filename, with the slug, title,
......@@ -637,7 +647,6 @@ type FeatureDB interface {
637647 AddFeatureUser(username, name string) error
638648 FindFeature(userID string, feature string) (*FeatureFlag, error)
639649 FindFeaturesByUser(userID string) ([]*FeatureFlag, error)
640- HasFeatureByUser(userID string, feature string) bool
641650 InsertFeature(userID, name string, expiresAt time.Time) (*FeatureFlag, error)
642651 RemoveFeature(userID, names string) error
643652 }
......@@ -682,7 +691,6 @@ type DB interface {
682691 InviteDB
683692
684693 FindPosts() ([]*Post, error)
685- FindPost(postID string) (*Post, error)
686694
687695 Close() error
688696 }
......@@ -725,8 +733,16 @@ func FindAccess(dbpool interface {
725733 return nil, fmt.Errorf("ERROR: uploading to %s requires an invitation or pico+: https://blog.pico.sh/ann-038-pico-invite-system", service)
726734 }
727735
736+// HasFeature reports whether a user has a feature that has not expired.
737+func HasFeature(dbpool interface {
738+ FindFeature(userID string, feature string) (*FeatureFlag, error)
739+}, userID, feature string) bool {
740+ ff, err := dbpool.FindFeature(userID, feature)
741+ return err == nil && ff.IsValid()
742+}
743+
728744 func HasPlusOrSpace(dbpool interface {
729- HasFeatureByUser(userID string, feature string) bool
745+ FindFeature(userID string, feature string) (*FeatureFlag, error)
730746 }, userID, space string) bool {
731- return dbpool.HasFeatureByUser(userID, "plus") || dbpool.HasFeatureByUser(userID, space)
747+ return HasFeature(dbpool, userID, "plus") || HasFeature(dbpool, userID, space)
732748 }
+0, -8
......@@ -31,14 +31,6 @@ func (me *PsqlDB) FindFeaturesByUser(userID string) ([]*db.FeatureFlag, error) {
3131 return features, nil
3232 }
3333
34-func (me *PsqlDB) HasFeatureByUser(userID string, feature string) bool {
35- ff, err := me.FindFeature(userID, feature)
36- if err != nil {
37- return false
38- }
39- return ff.IsValid()
40-}
41-
4234 func (me *PsqlDB) InsertFeature(userID, name string, expiresAt time.Time) (*db.FeatureFlag, error) {
4335 var featureID string
4436 err := me.Db.QueryRow(
+2, -2
......@@ -48,7 +48,7 @@ func (me *PsqlDB) hasActiveInvite(userID string) (bool, error) {
4848 return false, err
4949 }
5050 for _, name := range inviteFeatures {
51- if me.HasFeatureByUser(userID, name) {
51+ if db.HasFeature(me, userID, name) {
5252 return true, nil
5353 }
5454 }
......@@ -56,7 +56,7 @@ func (me *PsqlDB) hasActiveInvite(userID string) (bool, error) {
5656 }
5757
5858 func (me *PsqlDB) InviteUser(fromUserID string, toUserID string) error {
59- canInvite := me.HasFeatureByUser(fromUserID, "plus")
59+ canInvite := db.HasFeature(me, fromUserID, "plus")
6060 if !canInvite {
6161 invited, err := me.hasActiveInvite(fromUserID)
6262 if err != nil {
+8, -8
......@@ -881,21 +881,21 @@ func TestFindFeaturesByUser(t *testing.T) {
881881 }
882882 }
883883
884-func TestHasFeatureByUser(t *testing.T) {
884+func TestHasFeature(t *testing.T) {
885885 cleanupTestData(t)
886886
887887 user, _ := testDB.RegisterUser("hasfeatureowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI hasfeatureowner", "comment", "")
888888 expiresAt := time.Now().Add(365 * 24 * time.Hour)
889889 _, _ = testDB.InsertFeature(user.ID, "plus", expiresAt)
890890
891- has := testDB.HasFeatureByUser(user.ID, "plus")
891+ has := db.HasFeature(testDB, user.ID, "plus")
892892 if !has {
893- t.Error("expected HasFeatureByUser to return true")
893+ t.Error("expected HasFeature to return true")
894894 }
895895
896- hasNot := testDB.HasFeatureByUser(user.ID, "nonexistent")
896+ hasNot := db.HasFeature(testDB, user.ID, "nonexistent")
897897 if hasNot {
898- t.Error("expected HasFeatureByUser to return false for nonexistent feature")
898+ t.Error("expected HasFeature to return false for nonexistent feature")
899899 }
900900 }
901901
......@@ -1286,7 +1286,7 @@ func TestInviteUser_PlusMember(t *testing.T) {
12861286 }
12871287
12881288 for _, name := range []string{"prose", "pgs", "pastes"} {
1289- if !testDB.HasFeatureByUser(to.ID, name) {
1289+ if !db.HasFeature(testDB, to.ID, name) {
12901290 t.Errorf("expected invitee to have %s", name)
12911291 }
12921292 }
......@@ -1314,7 +1314,7 @@ func TestInviteUser_InvitedUserCanInvite(t *testing.T) {
13141314 if err := testDB.InviteUser(mid.ID, last.ID); err != nil {
13151315 t.Fatalf("InviteUser from invited user failed: %v", err)
13161316 }
1317- if !testDB.HasFeatureByUser(last.ID, "pastes") {
1317+ if !db.HasFeature(testDB, last.ID, "pastes") {
13181318 t.Error("expected second invitee to have pastes")
13191319 }
13201320 }
......@@ -1328,7 +1328,7 @@ func TestInviteUser_Unauthorized(t *testing.T) {
13281328 if err := testDB.InviteUser(from.ID, to.ID); err == nil {
13291329 t.Fatal("expected error for user without pico+ or invite")
13301330 }
1331- if testDB.HasFeatureByUser(to.ID, "prose") {
1331+ if db.HasFeature(testDB, to.ID, "prose") {
13321332 t.Error("expected invitee not to have prose")
13331333 }
13341334 }
+0, -4
......@@ -160,10 +160,6 @@ func (me *StubDB) FindFeaturesByUser(userID string) ([]*db.FeatureFlag, error) {
160160 return []*db.FeatureFlag{}, errNotImpl
161161 }
162162
163-func (me *StubDB) HasFeatureByUser(userID string, feature string) bool {
164- return false
165-}
166-
167163 func (me *StubDB) InsertFeedItems(postID string, items []*db.FeedItem) error {
168164 return errNotImpl
169165 }
+3, -3
......@@ -132,13 +132,13 @@ func CleanHost(raw string) (string, error) {
132132 var ErrAnalyticsDisabled = errors.New("owner does not have site analytics enabled")
133133
134134 func AnalyticsVisitFromVisit(visit *db.AnalyticsVisits, dbpool interface {
135- HasFeatureByUser(userID string, feature string) bool
135+ FindFeature(userID string, feature string) (*db.FeatureFlag, error)
136136 }, secret string) error {
137137 if visit.PostID != "" {
138- if !dbpool.HasFeatureByUser(visit.UserID, "plus") && !dbpool.HasFeatureByUser(visit.UserID, "prose") {
138+ if !db.HasPlusOrSpace(dbpool, visit.UserID, "prose") {
139139 return ErrAnalyticsDisabled
140140 }
141- } else if !dbpool.HasFeatureByUser(visit.UserID, "analytics") {
141+ } else if !db.HasFeature(dbpool, visit.UserID, "analytics") {
142142 return ErrAnalyticsDisabled
143143 }
144144
+2, -9
......@@ -13,19 +13,12 @@ import (
1313 const adminPrefix = "admin__"
1414
1515 type SshAuthHandler struct {
16- DB AuthFindUser
16+ DB db.AuthDB
1717 Logger *slog.Logger
1818 Principal string
1919 }
2020
21-type AuthFindUser interface {
22- FindUserByPubkey(key string) (*db.User, error)
23- FindUserByName(name string) (*db.User, error)
24- FindFeature(userID, name string) (*db.FeatureFlag, error)
25- InsertAccessLog(log *db.AccessLog) error
26-}
27-
28-func NewSshAuthHandler(dbh AuthFindUser, logger *slog.Logger, principal string) *SshAuthHandler {
21+func NewSshAuthHandler(dbh db.AuthDB, logger *slog.Logger, principal string) *SshAuthHandler {
2922 return &SshAuthHandler{
3023 DB: dbh,
3124 Logger: logger,
+1, -1
......@@ -307,7 +307,7 @@ func FindUser(shrd *SharedModel) (*db.User, error) {
307307
308308 // impersonation
309309 if strings.HasPrefix(usr, adminPrefix) {
310- hasFeature := shrd.Dbpool.HasFeatureByUser(user.ID, "admin")
310+ hasFeature := db.HasFeature(shrd.Dbpool, user.ID, "admin")
311311 if hasFeature {
312312 impersonate := strings.TrimPrefix(usr, adminPrefix)
313313 user, err = shrd.Dbpool.FindUserByName(impersonate)