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 { | |
| 59 | 59 | if err != nil { | |
| 60 | 60 | return false | |
| 61 | 61 | } | |
| 62 | - | return hc.Dbpool.HasFeatureByUser(user.ID, "auth") | |
| 62 | + | return db.HasFeature(hc.Dbpool, user.ID, "auth") | |
| 63 | 63 | } | |
| 64 | 64 | ||
| 65 | 65 | func (hc *ApiConfig) HasPlusOrSpace(user *db.User, space string) bool { |
+14,
-8
| ... | ... | @@ -2,6 +2,7 @@ package auth | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "bytes" | |
| 5 | + | "database/sql" | |
| 5 | 6 | "encoding/json" | |
| 6 | 7 | "errors" | |
| 7 | 8 | "fmt" |
| ... | ... | @@ -237,18 +238,19 @@ func (a *AuthDb) FindUserByToken(token string) (*db.User, error) { | |
| 237 | 238 | return &db.User{ID: testUserID, Name: testUsername}, nil | |
| 238 | 239 | } | |
| 239 | 240 | ||
| 240 | - | func (a *AuthDb) HasFeatureByUser(userID string, feature string) bool { | |
| 241 | - | return true | |
| 242 | - | } | |
| 243 | - | ||
| 244 | 241 | func (a *AuthDb) FindKeysByUser(user *db.User) ([]*db.PublicKey, error) { | |
| 245 | 242 | return []*db.PublicKey{{ID: "1", UserID: user.ID, Name: "my-key", Key: "nice-pubkey", CreatedAt: &time.Time{}}}, nil | |
| 246 | 243 | } | |
| 247 | 244 | ||
| 245 | + | // FindFeature gives pico+ a fixed expiry for the feed snapshots, and every | |
| 246 | + | // other feature an active flag. | |
| 248 | 247 | func (a *AuthDb) FindFeature(userID string, feature string) (*db.FeatureFlag, error) { | |
| 249 | 248 | 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 | |
| 252 | 254 | } | |
| 253 | 255 | ||
| 254 | 256 | func (a *AuthDb) InsertPost(post *db.Post) (*db.Post, error) { |
| ... | ... | @@ -323,8 +325,12 @@ type stubDBWithFeatures struct { | |
| 323 | 325 | features map[string]bool | |
| 324 | 326 | } | |
| 325 | 327 | ||
| 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 | |
| 328 | 334 | } | |
| 329 | 335 | ||
| 330 | 336 | func newStubDBWithFeatures(features ...string) *stubDBWithFeatures { |
+1,
-3
| ... | ... | @@ -10,11 +10,9 @@ import ( | |
| 10 | 10 | ||
| 11 | 11 | // DB is the storage feeds uses. | |
| 12 | 12 | type DB interface { | |
| 13 | + | db.AuthDB | |
| 13 | 14 | db.PostDB | |
| 14 | - | FindUser(userID string) (*db.User, error) | |
| 15 | - | FindPost(postID string) (*db.Post, error) | |
| 16 | 15 | FindUsersWithPost(space string) ([]*db.User, error) | |
| 17 | - | HasFeatureByUser(userID string, feature string) bool | |
| 18 | 16 | InsertFeedItems(postID string, items []*db.FeedItem) error | |
| 19 | 17 | FindFeedItemsByPostID(postID string) ([]*db.FeedItem, error) | |
| 20 | 18 | } |
+1,
-2
| ... | ... | @@ -9,10 +9,9 @@ import ( | |
| 9 | 9 | ||
| 10 | 10 | // DB is the storage pastes uses. | |
| 11 | 11 | type DB interface { | |
| 12 | + | db.AuthDB | |
| 12 | 13 | db.PostDB | |
| 13 | - | FindUserByName(name string) (*db.User, error) | |
| 14 | 14 | FindExpiredPosts(space string) ([]*db.Post, error) | |
| 15 | - | FindFeature(userID string, feature string) (*db.FeatureFlag, error) | |
| 16 | 15 | } | |
| 17 | 16 | ||
| 18 | 17 | // PsqlDB is pastes's postgres storage: the queries apps share, and the ones |
+1,
-6
| ... | ... | @@ -7,14 +7,9 @@ import ( | |
| 7 | 7 | ) | |
| 8 | 8 | ||
| 9 | 9 | 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 | |
| 13 | 11 | FindUsers() ([]*db.User, error) | |
| 14 | 12 | ||
| 15 | - | FindFeature(userID string, name string) (*db.FeatureFlag, error) | |
| 16 | - | InsertAccessLog(*db.AccessLog) error | |
| 17 | - | ||
| 18 | 13 | InsertProject(userID, name, projectDir string) (string, error) | |
| 19 | 14 | UpdateProject(userID, name string) error | |
| 20 | 15 | UpdateProjectAcl(userID, name string, acl db.ProjectAcl) error |
+1,
-1
| ... | ... | @@ -42,7 +42,7 @@ func Middleware(handler *CliHandler) pssh.SSHServerMiddleware { | |
| 42 | 42 | uuidStr := uuid.NewString() | |
| 43 | 43 | isAdmin := false | |
| 44 | 44 | if user != nil { | |
| 45 | - | isAdmin = handler.DBPool.HasFeatureByUser(user.ID, "admin") | |
| 45 | + | isAdmin = db.HasFeature(handler.DBPool, user.ID, "admin") | |
| 46 | 46 | if isAdmin && strings.HasPrefix(sesh.User(), "admin__") { | |
| 47 | 47 | uuidStr = fmt.Sprintf("admin-%s", uuidStr) | |
| 48 | 48 | } |
+1,
-2
| ... | ... | @@ -10,9 +10,8 @@ import ( | |
| 10 | 10 | ||
| 11 | 11 | // DB is the storage pipe uses. | |
| 12 | 12 | type DB interface { | |
| 13 | - | FindUserByName(name string) (*db.User, error) | |
| 13 | + | db.AuthDB | |
| 14 | 14 | FindUserByToken(token string) (*db.User, error) | |
| 15 | - | HasFeatureByUser(userID string, feature string) bool | |
| 16 | 15 | UpsertPipeMonitor(userID, topic string, dur time.Duration, winEnd *time.Time) error | |
| 17 | 16 | UpdatePipeMonitorLastPing(userID, topic string, lastPing *time.Time) error | |
| 18 | 17 | RemovePipeMonitor(userID, topic string) error |
+0,
-8
| ... | ... | @@ -92,14 +92,6 @@ func (t *TestDB) FindFeature(userID, name string) (*db.FeatureFlag, error) { | |
| 92 | 92 | return nil, fmt.Errorf("feature not found") | |
| 93 | 93 | } | |
| 94 | 94 | ||
| 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 | - | ||
| 103 | 95 | func (t *TestDB) InsertAccessLog(_ *db.AccessLog) error { | |
| 104 | 96 | return nil | |
| 105 | 97 | } |
+2,
-4
| ... | ... | @@ -14,13 +14,11 @@ import ( | |
| 14 | 14 | ||
| 15 | 15 | // DB is the storage prose uses. | |
| 16 | 16 | type DB interface { | |
| 17 | + | db.AuthDB | |
| 17 | 18 | db.PostDB | |
| 18 | - | ReplaceTagsByPost(tags []string, postID string) error | |
| 19 | - | ReplaceAliasesByPost(aliases []string, postID string) error | |
| 19 | + | db.TagDB | |
| 20 | 20 | FindUserPostsByTag(pager *db.Pager, tag, userID, space string) (*db.Paginate[*db.Post], error) | |
| 21 | 21 | FindPopularPosts() ([]*db.Post, error) | |
| 22 | - | FindUserByName(name string) (*db.User, error) | |
| 23 | - | FindFeature(userID string, feature string) (*db.FeatureFlag, error) | |
| 24 | 22 | } | |
| 25 | 23 | ||
| 26 | 24 | // PsqlDB is prose's postgres storage: the queries apps share, and the ones |
+20,
-4
| ... | ... | @@ -587,6 +587,15 @@ type UserDB interface { | |
| 587 | 587 | FindUserStats(userID string) (*UserStats, error) | |
| 588 | 588 | } | |
| 589 | 589 | ||
| 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 | + | ||
| 590 | 599 | type KeyDB interface { | |
| 591 | 600 | UpdatePublicKey(pubkeyID, name string) (*PublicKey, error) | |
| 592 | 601 | InsertPublicKey(userID, pubkey, name string) error |
| ... | ... | @@ -609,6 +618,7 @@ type PostDB interface { | |
| 609 | 618 | FindAllPostsByUser(userID string, space string) ([]*Post, error) | |
| 610 | 619 | FindPostWithFilename(filename string, userID string, space string) (*Post, error) | |
| 611 | 620 | FindPostWithSlug(slug string, userID string, space string) (*Post, error) | |
| 621 | + | FindPost(postID string) (*Post, error) | |
| 612 | 622 | InsertPost(post *Post) (*Post, error) | |
| 613 | 623 | UpdatePost(post *Post) (*Post, error) | |
| 614 | 624 | // RenamePost moves cur to next's filename, with the slug, title, |
| ... | ... | @@ -637,7 +647,6 @@ type FeatureDB interface { | |
| 637 | 647 | AddFeatureUser(username, name string) error | |
| 638 | 648 | FindFeature(userID string, feature string) (*FeatureFlag, error) | |
| 639 | 649 | FindFeaturesByUser(userID string) ([]*FeatureFlag, error) | |
| 640 | - | HasFeatureByUser(userID string, feature string) bool | |
| 641 | 650 | InsertFeature(userID, name string, expiresAt time.Time) (*FeatureFlag, error) | |
| 642 | 651 | RemoveFeature(userID, names string) error | |
| 643 | 652 | } |
| ... | ... | @@ -682,7 +691,6 @@ type DB interface { | |
| 682 | 691 | InviteDB | |
| 683 | 692 | ||
| 684 | 693 | FindPosts() ([]*Post, error) | |
| 685 | - | FindPost(postID string) (*Post, error) | |
| 686 | 694 | ||
| 687 | 695 | Close() error | |
| 688 | 696 | } |
| ... | ... | @@ -725,8 +733,16 @@ func FindAccess(dbpool interface { | |
| 725 | 733 | return nil, fmt.Errorf("ERROR: uploading to %s requires an invitation or pico+: https://blog.pico.sh/ann-038-pico-invite-system", service) | |
| 726 | 734 | } | |
| 727 | 735 | ||
| 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 | + | ||
| 728 | 744 | func HasPlusOrSpace(dbpool interface { | |
| 729 | - | HasFeatureByUser(userID string, feature string) bool | |
| 745 | + | FindFeature(userID string, feature string) (*FeatureFlag, error) | |
| 730 | 746 | }, userID, space string) bool { | |
| 731 | - | return dbpool.HasFeatureByUser(userID, "plus") || dbpool.HasFeatureByUser(userID, space) | |
| 747 | + | return HasFeature(dbpool, userID, "plus") || HasFeature(dbpool, userID, space) | |
| 732 | 748 | } |
+0,
-8
| ... | ... | @@ -31,14 +31,6 @@ func (me *PsqlDB) FindFeaturesByUser(userID string) ([]*db.FeatureFlag, error) { | |
| 31 | 31 | return features, nil | |
| 32 | 32 | } | |
| 33 | 33 | ||
| 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 | - | ||
| 42 | 34 | func (me *PsqlDB) InsertFeature(userID, name string, expiresAt time.Time) (*db.FeatureFlag, error) { | |
| 43 | 35 | var featureID string | |
| 44 | 36 | err := me.Db.QueryRow( |
+2,
-2
| ... | ... | @@ -48,7 +48,7 @@ func (me *PsqlDB) hasActiveInvite(userID string) (bool, error) { | |
| 48 | 48 | return false, err | |
| 49 | 49 | } | |
| 50 | 50 | for _, name := range inviteFeatures { | |
| 51 | - | if me.HasFeatureByUser(userID, name) { | |
| 51 | + | if db.HasFeature(me, userID, name) { | |
| 52 | 52 | return true, nil | |
| 53 | 53 | } | |
| 54 | 54 | } |
| ... | ... | @@ -56,7 +56,7 @@ func (me *PsqlDB) hasActiveInvite(userID string) (bool, error) { | |
| 56 | 56 | } | |
| 57 | 57 | ||
| 58 | 58 | func (me *PsqlDB) InviteUser(fromUserID string, toUserID string) error { | |
| 59 | - | canInvite := me.HasFeatureByUser(fromUserID, "plus") | |
| 59 | + | canInvite := db.HasFeature(me, fromUserID, "plus") | |
| 60 | 60 | if !canInvite { | |
| 61 | 61 | invited, err := me.hasActiveInvite(fromUserID) | |
| 62 | 62 | if err != nil { |
+8,
-8
| ... | ... | @@ -881,21 +881,21 @@ func TestFindFeaturesByUser(t *testing.T) { | |
| 881 | 881 | } | |
| 882 | 882 | } | |
| 883 | 883 | ||
| 884 | - | func TestHasFeatureByUser(t *testing.T) { | |
| 884 | + | func TestHasFeature(t *testing.T) { | |
| 885 | 885 | cleanupTestData(t) | |
| 886 | 886 | ||
| 887 | 887 | user, _ := testDB.RegisterUser("hasfeatureowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI hasfeatureowner", "comment", "") | |
| 888 | 888 | expiresAt := time.Now().Add(365 * 24 * time.Hour) | |
| 889 | 889 | _, _ = testDB.InsertFeature(user.ID, "plus", expiresAt) | |
| 890 | 890 | ||
| 891 | - | has := testDB.HasFeatureByUser(user.ID, "plus") | |
| 891 | + | has := db.HasFeature(testDB, user.ID, "plus") | |
| 892 | 892 | if !has { | |
| 893 | - | t.Error("expected HasFeatureByUser to return true") | |
| 893 | + | t.Error("expected HasFeature to return true") | |
| 894 | 894 | } | |
| 895 | 895 | ||
| 896 | - | hasNot := testDB.HasFeatureByUser(user.ID, "nonexistent") | |
| 896 | + | hasNot := db.HasFeature(testDB, user.ID, "nonexistent") | |
| 897 | 897 | if hasNot { | |
| 898 | - | t.Error("expected HasFeatureByUser to return false for nonexistent feature") | |
| 898 | + | t.Error("expected HasFeature to return false for nonexistent feature") | |
| 899 | 899 | } | |
| 900 | 900 | } | |
| 901 | 901 |
| ... | ... | @@ -1286,7 +1286,7 @@ func TestInviteUser_PlusMember(t *testing.T) { | |
| 1286 | 1286 | } | |
| 1287 | 1287 | ||
| 1288 | 1288 | for _, name := range []string{"prose", "pgs", "pastes"} { | |
| 1289 | - | if !testDB.HasFeatureByUser(to.ID, name) { | |
| 1289 | + | if !db.HasFeature(testDB, to.ID, name) { | |
| 1290 | 1290 | t.Errorf("expected invitee to have %s", name) | |
| 1291 | 1291 | } | |
| 1292 | 1292 | } |
| ... | ... | @@ -1314,7 +1314,7 @@ func TestInviteUser_InvitedUserCanInvite(t *testing.T) { | |
| 1314 | 1314 | if err := testDB.InviteUser(mid.ID, last.ID); err != nil { | |
| 1315 | 1315 | t.Fatalf("InviteUser from invited user failed: %v", err) | |
| 1316 | 1316 | } | |
| 1317 | - | if !testDB.HasFeatureByUser(last.ID, "pastes") { | |
| 1317 | + | if !db.HasFeature(testDB, last.ID, "pastes") { | |
| 1318 | 1318 | t.Error("expected second invitee to have pastes") | |
| 1319 | 1319 | } | |
| 1320 | 1320 | } |
| ... | ... | @@ -1328,7 +1328,7 @@ func TestInviteUser_Unauthorized(t *testing.T) { | |
| 1328 | 1328 | if err := testDB.InviteUser(from.ID, to.ID); err == nil { | |
| 1329 | 1329 | t.Fatal("expected error for user without pico+ or invite") | |
| 1330 | 1330 | } | |
| 1331 | - | if testDB.HasFeatureByUser(to.ID, "prose") { | |
| 1331 | + | if db.HasFeature(testDB, to.ID, "prose") { | |
| 1332 | 1332 | t.Error("expected invitee not to have prose") | |
| 1333 | 1333 | } | |
| 1334 | 1334 | } |
+0,
-4
| ... | ... | @@ -160,10 +160,6 @@ func (me *StubDB) FindFeaturesByUser(userID string) ([]*db.FeatureFlag, error) { | |
| 160 | 160 | return []*db.FeatureFlag{}, errNotImpl | |
| 161 | 161 | } | |
| 162 | 162 | ||
| 163 | - | func (me *StubDB) HasFeatureByUser(userID string, feature string) bool { | |
| 164 | - | return false | |
| 165 | - | } | |
| 166 | - | ||
| 167 | 163 | func (me *StubDB) InsertFeedItems(postID string, items []*db.FeedItem) error { | |
| 168 | 164 | return errNotImpl | |
| 169 | 165 | } |
| ... | ... | @@ -132,13 +132,13 @@ func CleanHost(raw string) (string, error) { | |
| 132 | 132 | var ErrAnalyticsDisabled = errors.New("owner does not have site analytics enabled") | |
| 133 | 133 | ||
| 134 | 134 | func AnalyticsVisitFromVisit(visit *db.AnalyticsVisits, dbpool interface { | |
| 135 | - | HasFeatureByUser(userID string, feature string) bool | |
| 135 | + | FindFeature(userID string, feature string) (*db.FeatureFlag, error) | |
| 136 | 136 | }, secret string) error { | |
| 137 | 137 | if visit.PostID != "" { | |
| 138 | - | if !dbpool.HasFeatureByUser(visit.UserID, "plus") && !dbpool.HasFeatureByUser(visit.UserID, "prose") { | |
| 138 | + | if !db.HasPlusOrSpace(dbpool, visit.UserID, "prose") { | |
| 139 | 139 | return ErrAnalyticsDisabled | |
| 140 | 140 | } | |
| 141 | - | } else if !dbpool.HasFeatureByUser(visit.UserID, "analytics") { | |
| 141 | + | } else if !db.HasFeature(dbpool, visit.UserID, "analytics") { | |
| 142 | 142 | return ErrAnalyticsDisabled | |
| 143 | 143 | } | |
| 144 | 144 |
| ... | ... | @@ -13,19 +13,12 @@ import ( | |
| 13 | 13 | const adminPrefix = "admin__" | |
| 14 | 14 | ||
| 15 | 15 | type SshAuthHandler struct { | |
| 16 | - | DB AuthFindUser | |
| 16 | + | DB db.AuthDB | |
| 17 | 17 | Logger *slog.Logger | |
| 18 | 18 | Principal string | |
| 19 | 19 | } | |
| 20 | 20 | ||
| 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 { | |
| 29 | 22 | return &SshAuthHandler{ | |
| 30 | 23 | DB: dbh, | |
| 31 | 24 | Logger: logger, |
+1,
-1
| ... | ... | @@ -307,7 +307,7 @@ func FindUser(shrd *SharedModel) (*db.User, error) { | |
| 307 | 307 | ||
| 308 | 308 | // impersonation | |
| 309 | 309 | if strings.HasPrefix(usr, adminPrefix) { | |
| 310 | - | hasFeature := shrd.Dbpool.HasFeatureByUser(user.ID, "admin") | |
| 310 | + | hasFeature := db.HasFeature(shrd.Dbpool, user.ID, "admin") | |
| 311 | 311 | if hasFeature { | |
| 312 | 312 | impersonate := strings.TrimPrefix(usr, adminPrefix) | |
| 313 | 313 | user, err = shrd.Dbpool.FindUserByName(impersonate) |