fix(uploads): allow signed audio previews and compact destination details
ci / module boundaries / boundaries (push) Successful in 7s
ci / db / db (push) Successful in 47s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m42s
ci / ts / ts (packages + web) (push) Failing after 5m24s
ci / api / api (push) Successful in 42s
ci / module boundaries / boundaries (push) Successful in 7s
ci / db / db (push) Successful in 47s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m42s
ci / ts / ts (packages + web) (push) Failing after 5m24s
ci / api / api (push) Successful in 42s
This commit is contained in:
1 parent
30c3c85f89
commit
37e33c656b
8 files changed
+137
-19
No files matched your search
@@ -8,6 +8,8 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"pipu/api/internal/platform/apperr"
|
||||
"pipu/api/internal/platform/httpx"
|
||||
)
|
||||
@@ -95,6 +97,7 @@ func Middleware(svc *Service, log *slog.Logger) func(http.Handler) http.Handler
|
||||
cover := (r.Method == http.MethodGet || r.Method == http.MethodHead) &&
|
||||
(strings.HasPrefix(r.URL.Path, "/api/v1/covers/") || strings.HasPrefix(r.URL.Path, "/api/v1/stream/") ||
|
||||
strings.HasPrefix(r.URL.Path, "/api/v1/app/android/download/") ||
|
||||
isUploadStreamPath(r.URL.Path) ||
|
||||
strings.HasPrefix(r.URL.Path, "/api/v1/app/download/"))
|
||||
if !strings.HasPrefix(r.URL.Path, "/api/v1/") || publicRoutes[route] || cover {
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
@@ -130,6 +133,21 @@ func Middleware(svc *Service, log *slog.Logger) func(http.Handler) http.Handler
|
||||
}
|
||||
}
|
||||
|
||||
// Only the signed audio endpoint bypasses Bearer authentication. Its handler
|
||||
// verifies the signature and expiry before accessing staging.
|
||||
func isUploadStreamPath(urlPath string) bool {
|
||||
id, ok := strings.CutPrefix(urlPath, "/api/v1/admin/uploads/files/")
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
id, ok = strings.CutSuffix(id, "/stream")
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
_, err := uuid.Parse(id)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func bearer(h string) (string, bool) {
|
||||
const prefix = "bearer "
|
||||
if len(h) <= len(prefix) || !strings.EqualFold(h[:len(prefix)], prefix) {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"pipu/api/internal/auth"
|
||||
)
|
||||
|
||||
func TestSignedUploadStreamReachesSignatureHandlerWithoutBearer(t *testing.T) {
|
||||
const file = "/api/v1/admin/uploads/files/01a18459-32bc-7589-a83d-635292a7a4e2"
|
||||
for _, tc := range []struct {
|
||||
method, path string
|
||||
want int
|
||||
}{
|
||||
{http.MethodGet, file + "/stream?exp=1&sig=invalid", http.StatusForbidden},
|
||||
{http.MethodHead, file + "/stream?exp=1&sig=invalid", http.StatusForbidden},
|
||||
{http.MethodGet, file, http.StatusUnauthorized},
|
||||
{http.MethodPatch, file + "/stream", http.StatusUnauthorized},
|
||||
{http.MethodGet, file + "/stream/other", http.StatusUnauthorized},
|
||||
{http.MethodGet, "/api/v1/admin/uploads", http.StatusUnauthorized},
|
||||
} {
|
||||
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
|
||||
h := auth.Middleware(nil, slog.New(slog.NewTextHandler(io.Discard, nil)))(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// A signed media route still delegates authorization to its signature handler.
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
}))
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(tc.method, tc.path, nil))
|
||||
require.Equal(t, tc.want, rec.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user