- 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
+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
+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+}
+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"
+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)
+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+}
+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
+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