repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

commit
a83183c
parent
970de49
author
Eric Bower
date
2026-03-02 09:38:24 -0500 EST
feat(pgs): project names prefixed with `private-` will have new acl type "private"

For security reasons once a project is named `private-` it can never have its ACL type changed.

Right now we do not support making other projects private.
7 files changed,  +79, -2
M pkg/apps/pgs/access.go
+4, -0
 1@@ -21,6 +21,10 @@ func HasProjectAccess(project *db.Project, owner *db.User, requester *db.User, p
 2 		}
 3 	}
 4 
 5+	if aclType == "private" {
 6+		return false
 7+	}
 8+
 9 	if aclType == "pico" {
10 		if requester == nil {
11 			return false
A pkg/apps/pgs/access_test.go
+47, -0
 1@@ -0,0 +1,47 @@
 2+package pgs
 3+
 4+import (
 5+	"log/slog"
 6+	"net/http"
 7+	"net/http/httptest"
 8+	"strings"
 9+	"testing"
10+
11+	"github.com/picosh/pico/pkg/shared"
12+	"github.com/picosh/pico/pkg/shared/storage"
13+)
14+
15+func TestPrivateProjectDeniesWebAccess(t *testing.T) {
16+	logger := slog.Default()
17+	dbpool := NewPgsDb(logger)
18+	bucketName := shared.GetAssetBucketName(dbpool.Users[0].ID)
19+
20+	// Mark the test project as private
21+	project, err := dbpool.FindProjectByName(dbpool.Users[0].ID, "test")
22+	if err != nil {
23+		t.Fatalf("failed to get project: %v", err)
24+	}
25+	project.Acl.Type = "private"
26+	project.Acl.Data = []string{}
27+
28+	request := httptest.NewRequest("GET", "https://"+dbpool.Users[0].Name+"-test.pgs.test/", strings.NewReader(""))
29+	responseRecorder := httptest.NewRecorder()
30+
31+	st, _ := storage.NewStorageMemory(map[string]map[string]string{
32+		bucketName: {
33+			"/test/index.html": "hello world!",
34+		},
35+	})
36+	pubsub := NewPubsubChan()
37+	defer func() {
38+		_ = pubsub.Close()
39+	}()
40+	cfg := NewPgsConfig(logger, dbpool, st, pubsub)
41+	cfg.Domain = "pgs.test"
42+	router := NewWebRouter(cfg)
43+	router.ServeHTTP(responseRecorder, request)
44+
45+	if responseRecorder.Code != http.StatusUnauthorized {
46+		t.Errorf("want status %d, got %d", http.StatusUnauthorized, responseRecorder.Code)
47+	}
48+}
M pkg/apps/pgs/cli.go
+6, -0
 1@@ -147,6 +147,12 @@ You can also use unix pipes to directly upload files by providing the project na
 2 	# => https://erock-mysite.pgs.sh/index.html
 3 
 4 The leading "/" is important.
 5+
 6+You can also create private projects when you prefix the project name with 'private':
 7+
 8+	rsync -rv ./public/ pgs.sh:/private-site/
 9+
10+This means only you can access the site through a web tunnel or by downloading the files.
11 `
12 	helpStr += "\r\nCommands: [help, stats, ls, fzf, rm, link, unlink, prune, retain, depends, acl, cache]\r\n"
13 	helpStr += "For most of these commands you can provide a `-h` to learn about its usage.\r\n"
M pkg/apps/pgs/cli_middleware.go
+6, -0
 1@@ -236,6 +236,12 @@ func Middleware(handler *UploadAssetHandler) pssh.SSHServerMiddleware {
 2 					return err
 3 				}
 4 
 5+				if pgsdb.IsProjectPrivate(projectName) {
 6+					err = fmt.Errorf("projects prefixed with `private-` can *never* have their access changed; however you can symlink to it")
 7+					opts.bail(err)
 8+					return err
 9+				}
10+
11 				err := opts.acl(projectName, *aclType, acls)
12 				opts.notice()
13 				opts.bail(err)
M pkg/apps/pgs/db/db.go
+9, -1
 1@@ -1,6 +1,10 @@
 2 package pgsdb
 3 
 4-import "github.com/picosh/pico/pkg/db"
 5+import (
 6+	"strings"
 7+
 8+	"github.com/picosh/pico/pkg/db"
 9+)
10 
11 type PgsDB interface {
12 	FindUser(userID string) (*db.User, error)
13@@ -27,3 +31,7 @@ type PgsDB interface {
14 
15 	Close() error
16 }
17+
18+func IsProjectPrivate(projectName string) bool {
19+	return strings.HasPrefix(projectName, "private-")
20+}
M pkg/apps/pgs/db/postgres.go
+6, -0
 1@@ -154,6 +154,12 @@ func (me *PgsPsqlDB) UpsertProject(userID, projectName, projectDir string) (*db.
 2 		)
 3 		return nil, err
 4 	}
 5+	if IsProjectPrivate(projectName) {
 6+		err = me.UpdateProjectAcl(userID, projectName, db.ProjectAcl{Type: "private", Data: []string{}})
 7+		if err != nil {
 8+			return nil, err
 9+		}
10+	}
11 	return me.FindProjectByName(userID, projectName)
12 }
13 
M pkg/db/db.go
+1, -1
1@@ -82,7 +82,7 @@ type Project struct {
2 }
3 
4 type ProjectAcl struct {
5-	Type string   `json:"type" db:"type"`
6+	Type string   `json:"type" db:"type"` // public, pico, pubkeys, private
7 	Data []string `json:"data" db:"data"`
8 }
9