pico

created pr with 57.1 on 2025-03-28T16:55:01Z · by c8ef7d19
added 57.2 on 2025-04-06T20:54:25Z · by c8ef7d19
1: b47b145 = 1: 461e887 feat(pgs): admins can impersonate
2: 7275c44 = 2: 91dabf9 chore: use `user_id` in log middleware to set the user ctx
cmds
checkout latest patchset:
ssh pr.pico.sh print 57 | git am -3
checkout any patchset in a patch request:
ssh pr.pico.sh print 57.[rev] | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 57

Patchset 57.2 on 2025-04-06T20:54:25Z · commit 461e887

feat(pgs): admins can impersonate
Eric Bower 2025-03-28T15:53:44Z
This change will let admins impersonate any user for the pgs cli
Semantic diff summary
6 added, 31 modified, 0 signature changed, 6 removed across 14 analyzed files
+3 -3 pkg/apps/auth/api.go #
......@@ -303,7 +303,7 @@ func userHandler(apiConfig *shared.ApiConfig) http.HandlerFunc {
303303 "publicKey", data.PublicKey,
304304 )
305305
306- user, err := apiConfig.Dbpool.FindUserForName(data.Username)
306+ user, err := apiConfig.Dbpool.FindUserByName(data.Username)
307307 if err != nil {
308308 apiConfig.Cfg.Logger.Error(err.Error())
309309 http.Error(w, err.Error(), http.StatusNotFound)
......@@ -461,7 +461,7 @@ func paymentWebhookHandler(apiConfig *shared.ApiConfig) http.HandlerFunc {
461461 status := event.Data.Attr.Status
462462 txID := fmt.Sprint(event.Data.Attr.OrderNumber)
463463
464- user, err := apiConfig.Dbpool.FindUserForName(username)
464+ user, err := apiConfig.Dbpool.FindUserByName(username)
465465 if err != nil {
466466 logger.Error("no user found with username", "username", username)
467467 w.WriteHeader(http.StatusOK)
......@@ -624,7 +624,7 @@ func deserializeCaddyAccessLog(dbpool db.DB, access *AccessLog) (*db.AnalyticsVi
624624 }
625625
626626 // get user ID
627- user, err := dbpool.FindUserForName(props.Username)
627+ user, err := dbpool.FindUserByName(props.Username)
628628 if err != nil {
629629 return nil, fmt.Errorf("could not find user for name %s: %w", props.Username, err)
630630 }
+2 -2 pkg/apps/auth/api_test.go #
......@@ -220,7 +220,7 @@ func (a *AuthDb) AddPicoPlusUser(username, email, from, txid string) error {
220220 return nil
221221 }
222222
223-func (a *AuthDb) FindUserForName(username string) (*db.User, error) {
223+func (a *AuthDb) FindUserByName(username string) (*db.User, error) {
224224 return &db.User{ID: testUserID, Name: username}, nil
225225 }
226226
......@@ -243,7 +243,7 @@ func (a *AuthDb) FindKeysForUser(user *db.User) ([]*db.PublicKey, error) {
243243 return []*db.PublicKey{{ID: "1", UserID: user.ID, Name: "my-key", Key: "nice-pubkey", CreatedAt: &time.Time{}}}, nil
244244 }
245245
246-func (a *AuthDb) FindFeatureForUser(userID string, feature string) (*db.FeatureFlag, error) {
246+func (a *AuthDb) FindFeature(userID string, feature string) (*db.FeatureFlag, error) {
247247 now := time.Date(2021, 8, 15, 14, 30, 45, 100, time.UTC)
248248 oneDayWarning := now.AddDate(0, 0, 1)
249249 return &db.FeatureFlag{ID: "2", UserID: userID, Name: "plus", ExpiresAt: &oneDayWarning, CreatedAt: &now}, nil
+3 -3 pkg/apps/pastes/api.go #
......@@ -78,7 +78,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
7878 logger := blogger.With("user", username)
7979 cfg := shared.GetCfg(r)
8080
81- user, err := dbpool.FindUserForName(username)
81+ user, err := dbpool.FindUserByName(username)
8282 if err != nil {
8383 logger.Info("user not found")
8484 http.Error(w, "user not found", http.StatusNotFound)
......@@ -170,7 +170,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
170170 blogger := shared.GetLogger(r)
171171 logger := blogger.With("slug", slug, "user", username)
172172
173- user, err := dbpool.FindUserForName(username)
173+ user, err := dbpool.FindUserByName(username)
174174 if err != nil {
175175 logger.Info("paste not found")
176176 http.Error(w, "paste not found", http.StatusNotFound)
......@@ -271,7 +271,7 @@ func postHandlerRaw(w http.ResponseWriter, r *http.Request) {
271271 blogger := shared.GetLogger(r)
272272 logger := blogger.With("user", username, "slug", slug)
273273
274- user, err := dbpool.FindUserForName(username)
274+ user, err := dbpool.FindUserByName(username)
275275 if err != nil {
276276 logger.Info("user not found")
277277 http.Error(w, "user not found", http.StatusNotFound)
+4 -16 pkg/apps/pgs/cli_middleware.go #
......@@ -10,26 +10,14 @@ import (
1010 "github.com/picosh/pico/pkg/db"
1111 "github.com/picosh/pico/pkg/pssh"
1212 sendutils "github.com/picosh/pico/pkg/send/utils"
13- "github.com/picosh/utils"
1413 )
1514
1615 func getUser(s *pssh.SSHServerConnSession, dbpool pgsdb.PgsDB) (*db.User, error) {
17- if s.PublicKey() == nil {
18- return nil, fmt.Errorf("key not found")
16+ userID, ok := s.Conn.Permissions.Extensions["user_id"]
17+ if !ok {
18+ return nil, fmt.Errorf("`user_id` extension not found")
1919 }
20-
21- key := utils.KeyForKeyText(s.PublicKey())
22-
23- user, err := dbpool.FindUserByPubkey(key)
24- if err != nil {
25- return nil, err
26- }
27-
28- if user.Name == "" {
29- return nil, fmt.Errorf("must have username set")
30- }
31-
32- return user, nil
20+ return dbpool.FindUser(userID)
3321 }
3422
3523 type arrayFlags []string
+2 -2 pkg/apps/pico/cli.go #
......@@ -117,10 +117,10 @@ func Middleware(handler *CliHandler) pssh.SSHServerMiddleware {
117117 return err
118118 }
119119
120- ff, err := dbpool.FindFeatureForUser(user.ID, "plus")
120+ ff, err := dbpool.FindFeature(user.ID, "plus")
121121 if err != nil {
122122 handler.Logger.Error("Unable to find plus feature flag", "err", err, "user", user, "command", args)
123- ff, err = dbpool.FindFeatureForUser(user.ID, "bouncer")
123+ ff, err = dbpool.FindFeature(user.ID, "bouncer")
124124 if err != nil {
125125 handler.Logger.Error("Unable to find bouncer feature flag", "err", err, "user", user, "command", args)
126126 sesh.Fatal(err)
+6 -6 pkg/apps/prose/api.go #
......@@ -125,7 +125,7 @@ func blogStyleHandler(w http.ResponseWriter, r *http.Request) {
125125 logger := shared.GetLogger(r)
126126 cfg := shared.GetCfg(r)
127127
128- user, err := dbpool.FindUserForName(username)
128+ user, err := dbpool.FindUserByName(username)
129129 if err != nil {
130130 logger.Info("blog not found", "user", username)
131131 http.Error(w, "blog not found", http.StatusNotFound)
......@@ -155,7 +155,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
155155 logger := shared.GetLogger(r)
156156 cfg := shared.GetCfg(r)
157157
158- user, err := dbpool.FindUserForName(username)
158+ user, err := dbpool.FindUserByName(username)
159159 if err != nil {
160160 logger.Info("blog not found", "user", username)
161161 http.Error(w, "blog not found", http.StatusNotFound)
......@@ -301,7 +301,7 @@ func postRawHandler(w http.ResponseWriter, r *http.Request) {
301301 logger := shared.GetLogger(r)
302302 logger = logger.With("slug", slug)
303303
304- user, err := dbpool.FindUserForName(username)
304+ user, err := dbpool.FindUserByName(username)
305305 if err != nil {
306306 logger.Info("blog not found", "user", username)
307307 http.Error(w, "blog not found", http.StatusNotFound)
......@@ -341,7 +341,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
341341 dbpool := shared.GetDB(r)
342342 logger := shared.GetLogger(r)
343343
344- user, err := dbpool.FindUserForName(username)
344+ user, err := dbpool.FindUserByName(username)
345345 if err != nil {
346346 logger.Info("blog not found", "user", username)
347347 http.Error(w, "blog not found", http.StatusNotFound)
......@@ -589,7 +589,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
589589 logger := shared.GetLogger(r)
590590 cfg := shared.GetCfg(r)
591591
592- user, err := dbpool.FindUserForName(username)
592+ user, err := dbpool.FindUserByName(username)
593593 if err != nil {
594594 logger.Info("rss feed not found", "user", username)
595595 http.Error(w, "rss feed not found", http.StatusNotFound)
......@@ -852,7 +852,7 @@ func imgRequest(w http.ResponseWriter, r *http.Request) {
852852 logger := shared.GetLogger(r)
853853 dbpool := shared.GetDB(r)
854854 username := shared.GetUsernameFromRequest(r)
855- user, err := dbpool.FindUserForName(username)
855+ user, err := dbpool.FindUserByName(username)
856856 if err != nil {
857857 logger.Error("could not find user", "username", username)
858858 http.Error(w, "could find user", http.StatusNotFound)
+3 -3 pkg/db/db.go #
......@@ -212,7 +212,7 @@ type Token struct {
212212 type FeatureFlag struct {
213213 ID string `json:"id" db:"id"`
214214 UserID string `json:"user_id" db:"user_id"`
215- PaymentHistoryID string `json:"payment_history_id" db:"payment_history_id"`
215+ PaymentHistoryID sql.NullString `json:"payment_history_id" db:"payment_history_id"`
216216 Name string `json:"name" db:"name"`
217217 CreatedAt *time.Time `json:"created_at" db:"created_at"`
218218 ExpiresAt *time.Time `json:"expires_at" db:"expires_at"`
......@@ -370,7 +370,7 @@ type DB interface {
370370 RemoveKeys(pubkeyIDs []string) error
371371
372372 FindUsers() ([]*User, error)
373- FindUserForName(name string) (*User, error)
373+ FindUserByName(name string) (*User, error)
374374 FindUserForNameAndKey(name string, pubkey string) (*User, error)
375375 FindUserForKey(name string, pubkey string) (*User, error)
376376 FindUserByPubkey(pubkey string) (*User, error)
......@@ -414,7 +414,7 @@ type DB interface {
414414 FindVisitSiteList(opts *SummaryOpts) ([]*VisitUrl, error)
415415
416416 AddPicoPlusUser(username, email, paymentType, txId string) error
417- FindFeatureForUser(userID string, feature string) (*FeatureFlag, error)
417+ FindFeature(userID string, feature string) (*FeatureFlag, error)
418418 FindFeaturesForUser(userID string) ([]*FeatureFlag, error)
419419 HasFeatureForUser(userID string, feature string) bool
420420 FindTotalSizeForUser(userID string) (int, error)
+9 -9 pkg/db/postgres/storage.go #
......@@ -617,14 +617,14 @@ func (me *PsqlDB) ValidateName(name string) (bool, error) {
617617 if !v {
618618 return false, fmt.Errorf("%s is invalid: %w", lower, db.ErrNameInvalid)
619619 }
620- user, _ := me.FindUserForName(lower)
620+ user, _ := me.FindUserByName(lower)
621621 if user == nil {
622622 return true, nil
623623 }
624624 return false, fmt.Errorf("%s already taken: %w", lower, db.ErrNameTaken)
625625 }
626626
627-func (me *PsqlDB) FindUserForName(name string) (*db.User, error) {
627+func (me *PsqlDB) FindUserByName(name string) (*db.User, error) {
628628 user := &db.User{}
629629 r := me.Db.QueryRow(sqlSelectUserForName, strings.ToLower(name))
630630 err := r.Scan(&user.ID, &user.Name, &user.CreatedAt)
......@@ -1457,7 +1457,7 @@ func (me *PsqlDB) FindTagsForPost(postID string) ([]string, error) {
14571457 return tags, nil
14581458 }
14591459
1460-func (me *PsqlDB) FindFeatureForUser(userID string, feature string) (*db.FeatureFlag, error) {
1460+func (me *PsqlDB) FindFeature(userID string, feature string) (*db.FeatureFlag, error) {
14611461 ff := &db.FeatureFlag{}
14621462 // payment history is allowed to be null
14631463 // https://devtidbits.com/2020/08/03/go-sql-error-converting-null-to-string-is-unsupported/
......@@ -1475,7 +1475,7 @@ func (me *PsqlDB) FindFeatureForUser(userID string, feature string) (*db.Feature
14751475 return nil, err
14761476 }
14771477
1478- ff.PaymentHistoryID = paymentHistoryID.String
1478+ ff.PaymentHistoryID = paymentHistoryID
14791479
14801480 return ff, nil
14811481 }
......@@ -1507,7 +1507,7 @@ func (me *PsqlDB) FindFeaturesForUser(userID string) ([]*db.FeatureFlag, error)
15071507 if err != nil {
15081508 return features, err
15091509 }
1510- ff.PaymentHistoryID = paymentHistoryID.String
1510+ ff.PaymentHistoryID = paymentHistoryID
15111511
15121512 features = append(features, ff)
15131513 }
......@@ -1518,7 +1518,7 @@ func (me *PsqlDB) FindFeaturesForUser(userID string) ([]*db.FeatureFlag, error)
15181518 }
15191519
15201520 func (me *PsqlDB) HasFeatureForUser(userID string, feature string) bool {
1521- ff, err := me.FindFeatureForUser(userID, feature)
1521+ ff, err := me.FindFeature(userID, feature)
15221522 if err != nil {
15231523 return false
15241524 }
......@@ -1695,7 +1695,7 @@ func (me *PsqlDB) InsertFeature(userID, name string, expiresAt time.Time) (*db.F
16951695 return nil, err
16961696 }
16971697
1698- feature, err := me.FindFeatureForUser(userID, name)
1698+ feature, err := me.FindFeature(userID, name)
16991699 if err != nil {
17001700 return nil, err
17011701 }
......@@ -1709,7 +1709,7 @@ func (me *PsqlDB) RemoveFeature(userID string, name string) error {
17091709 }
17101710
17111711 func (me *PsqlDB) createFeatureExpiresAt(userID, name string) time.Time {
1712- ff, _ := me.FindFeatureForUser(userID, name)
1712+ ff, _ := me.FindFeature(userID, name)
17131713 if ff == nil {
17141714 t := time.Now()
17151715 return t.AddDate(1, 0, 0)
......@@ -1718,7 +1718,7 @@ func (me *PsqlDB) createFeatureExpiresAt(userID, name string) time.Time {
17181718 }
17191719
17201720 func (me *PsqlDB) AddPicoPlusUser(username, email, paymentType, txId string) error {
1721- user, err := me.FindUserForName(username)
1721+ user, err := me.FindUserByName(username)
17221722 if err != nil {
17231723 return err
17241724 }
+2 -2 pkg/db/stub/stub.go #
......@@ -77,7 +77,7 @@ func (me *StubDB) ValidateName(name string) (bool, error) {
7777 return false, notImpl
7878 }
7979
80-func (me *StubDB) FindUserForName(name string) (*db.User, error) {
80+func (me *StubDB) FindUserByName(name string) (*db.User, error) {
8181 return nil, notImpl
8282 }
8383
......@@ -189,7 +189,7 @@ func (me *StubDB) FindTagsForPost(postID string) ([]string, error) {
189189 return []string{}, notImpl
190190 }
191191
192-func (me *StubDB) FindFeatureForUser(userID string, feature string) (*db.FeatureFlag, error) {
192+func (me *StubDB) FindFeature(userID string, feature string) (*db.FeatureFlag, error) {
193193 return nil, notImpl
194194 }
195195
+1 -1 pkg/shared/api.go #
......@@ -76,7 +76,7 @@ func CheckHandler(w http.ResponseWriter, r *http.Request) {
7676 if !strings.Contains(hostDomain, appDomain) {
7777 subdomain := GetCustomDomain(hostDomain, cfg.Space)
7878 if subdomain != "" {
79- u, err := dbpool.FindUserForName(subdomain)
79+ u, err := dbpool.FindUserByName(subdomain)
8080 if u != nil && err == nil {
8181 w.WriteHeader(http.StatusOK)
8282 return
+1 -1 pkg/shared/feed.go #
......@@ -90,7 +90,7 @@ func UserFeed(me db.DB, user *db.User, token string) (*feeds.Feed, error) {
9090 var feedItems []*feeds.Item
9191
9292 now := time.Now()
93- ff, err := me.FindFeatureForUser(user.ID, "plus")
93+ ff, err := me.FindFeature(user.ID, "plus")
9494 if err != nil {
9595 // still want to send an empty feed
9696 } else {
+25 -1 pkg/shared/ssh.go #
......@@ -3,6 +3,7 @@ package shared
33 import (
44 "fmt"
55 "log/slog"
6+ "strings"
67
78 "github.com/picosh/pico/pkg/db"
89 "github.com/picosh/utils"
......@@ -16,6 +17,8 @@ type SshAuthHandler struct {
1617
1718 type AuthFindUser interface {
1819 FindUserByPubkey(key string) (*db.User, error)
20+ FindUserByName(name string) (*db.User, error)
21+ FindFeature(userID, name string) (*db.FeatureFlag, error)
1922 }
2023
2124 func NewSshAuthHandler(dbh AuthFindUser, logger *slog.Logger) *SshAuthHandler {
......@@ -43,8 +46,29 @@ func (r *SshAuthHandler) PubkeyAuthHandler(conn ssh.ConnMetadata, key ssh.Public
4346 return nil, fmt.Errorf("username is not set")
4447 }
4548
49+ // impersonation
50+ impID := user.ID
51+ adminPrefix := "admin__"
52+ usr := conn.User()
53+ if strings.HasPrefix(usr, adminPrefix) {
54+ ff, err := r.DB.FindFeature(user.ID, "admin")
55+ if err != nil {
56+ return nil, fmt.Errorf("only admins can impersonate a user: %w", err)
57+ }
58+ if !ff.IsValid() {
59+ return nil, fmt.Errorf("expired admin feature flag, cannot impersonate a user")
60+ }
61+
62+ impersonate := strings.Replace(usr, adminPrefix, "", 1)
63+ user, err = r.DB.FindUserByName(impersonate)
64+ if err != nil {
65+ return nil, err
66+ }
67+ }
68+
4669 return &ssh.Permissions{
4770 Extensions: map[string]string{
71+ "imp_id": impID,
4872 "user_id": user.ID,
4973 "pubkey": pubkey,
5074 },
......@@ -52,7 +76,7 @@ func (r *SshAuthHandler) PubkeyAuthHandler(conn ssh.ConnMetadata, key ssh.Public
5276 }
5377
5478 func FindPlusFF(dbpool db.DB, cfg *ConfigSite, userID string) *db.FeatureFlag {
55- ff, _ := dbpool.FindFeatureForUser(userID, "plus")
79+ ff, _ := dbpool.FindFeature(userID, "plus")
5680 // we have free tiers so users might not have a feature flag
5781 // in which case we set sane defaults
5882 if ff == nil {
+1 -1 pkg/tui/tuns.go #
......@@ -230,7 +230,7 @@ func (m *TunsPage) HandleEvent(ev vaxis.Event, ph vxfw.EventPhase) (vxfw.Command
230230 switch msg := ev.(type) {
231231 case PageIn:
232232 m.loading = true
233- ff, _ := m.shared.Dbpool.FindFeatureForUser(m.shared.User.ID, "admin")
233+ ff, _ := m.shared.Dbpool.FindFeature(m.shared.User.ID, "admin")
234234 if ff != nil {
235235 m.isAdmin = true
236236 }
+2 -2 pkg/tui/ui.go #
......@@ -296,7 +296,7 @@ func FindUser(shrd *SharedModel) (*db.User, error) {
296296 return nil, fmt.Errorf("only admins can impersonate a user")
297297 }
298298 impersonate := strings.Replace(usr, adminPrefix, "", 1)
299- user, err = shrd.Dbpool.FindUserForName(impersonate)
299+ user, err = shrd.Dbpool.FindUserByName(impersonate)
300300 if err != nil {
301301 return nil, err
302302 }
......@@ -311,7 +311,7 @@ func FindFeatureFlag(shrd *SharedModel, name string) (*db.FeatureFlag, error) {
311311 return nil, nil
312312 }
313313
314- ff, err := shrd.Dbpool.FindFeatureForUser(shrd.User.ID, name)
314+ ff, err := shrd.Dbpool.FindFeature(shrd.User.ID, name)
315315 if err != nil {
316316 return nil, err
317317 }
Back to top