feat(16.3-03): journal scopes and ResolveBackend token auth
- Register journal and journal:read via pact.HasTokenScopes - Switch API auth to cabana.ResolveBackend with golem15.journal.* cover - Map ErrTokenScope to PHP Insufficient permissions bodies - Add TestResolveErrorMapping and TestJournalTokenTracer Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -28,7 +27,7 @@ const (
|
||||
permAccessOtherPosts = "golem15.journal.access_other_posts"
|
||||
permAccessPublish = "golem15.journal.access_publish"
|
||||
|
||||
backendJWTBlacklistTable = "backend_jwt_blacklist"
|
||||
journalCover = "golem15.journal.*"
|
||||
)
|
||||
|
||||
func writeAPIError(w http.ResponseWriter, status int, message string) {
|
||||
@@ -55,55 +54,40 @@ func hasBearer(r *http.Request) bool {
|
||||
return ok && strings.TrimSpace(token) != ""
|
||||
}
|
||||
|
||||
func newBackendGuard(app *backpack.App, write func(http.ResponseWriter, error)) bouncer.Guard {
|
||||
secret := ""
|
||||
if app != nil && app.Config != nil {
|
||||
secret = strings.TrimSpace(app.Config.String("admin.jwt.secret"))
|
||||
// resolveError maps cabana.ResolveBackend errors to the PHP Journal API bodies.
|
||||
func resolveError(w http.ResponseWriter, err error) {
|
||||
if errors.Is(err, cabana.ErrTokenScope) {
|
||||
writeAPIError(w, http.StatusForbidden, errInsufficientPerms)
|
||||
return
|
||||
}
|
||||
var gdb *gorm.DB
|
||||
var sqlDB *sql.DB
|
||||
var reg *cabana.Registry
|
||||
if app != nil {
|
||||
gdb, _ = app.Lookup[*gorm.DB]()
|
||||
sqlDB, _ = app.Lookup[*sql.DB]()
|
||||
reg, _ = app.Lookup[*cabana.Registry]()
|
||||
if sqlDB == nil && gdb != nil {
|
||||
if db, err := gdb.DB(); err == nil {
|
||||
sqlDB = db
|
||||
}
|
||||
}
|
||||
}
|
||||
var bl bouncer.BlacklistStore
|
||||
if sqlDB != nil {
|
||||
bl = bouncer.NewPostgresBlacklist(sqlDB, backendJWTBlacklistTable)
|
||||
}
|
||||
return bouncer.NewBackendJWTGuard(secret, cabana.BackendUsers{DB: gdb, Registry: reg}, bl, write)
|
||||
writeAuthRequired(w, err)
|
||||
}
|
||||
|
||||
// optionalBackendPrincipal verifies a backend JWT only when Authorization
|
||||
// Bearer is present. Failure is anonymous: public GET must not 401.
|
||||
// optionalBackendPrincipal verifies a backend JWT or personal API token only
|
||||
// when Authorization Bearer is present. Failure is anonymous: public GET must
|
||||
// not 401. Uses cabana.ResolveBackend with cover golem15.journal.*.
|
||||
func optionalBackendPrincipal(app *backpack.App, r *http.Request) *http.Request {
|
||||
if r == nil || !hasBearer(r) {
|
||||
return r
|
||||
}
|
||||
pr, err := newBackendGuard(app, nil).Authenticate(r)
|
||||
if err != nil || pr == nil {
|
||||
out, _, err := cabana.ResolveBackend(app, r, journalCover)
|
||||
if err != nil || out == nil {
|
||||
return r
|
||||
}
|
||||
return r.WithContext(bouncer.WithUser(r.Context(), pr))
|
||||
return out
|
||||
}
|
||||
|
||||
// requireBackendPrincipal authenticates a backend-audience JWT and writes the
|
||||
// PHP {error:"Authentication required"} body when the token is missing or
|
||||
// invalid (D-15). It does not use cabana's admin unauthenticated envelope.
|
||||
// requireBackendPrincipal authenticates a backend JWT or personal API token
|
||||
// and writes the PHP {error:"Authentication required"} or
|
||||
// {error:"Insufficient permissions"} body (D-15). It does not use cabana's
|
||||
// admin unauthenticated envelope.
|
||||
func requireBackendPrincipal(app *backpack.App, w http.ResponseWriter, r *http.Request) (*http.Request, *bouncer.Principal, bool) {
|
||||
pr, err := newBackendGuard(app, writeAuthRequired).Authenticate(r)
|
||||
out, pr, err := cabana.ResolveBackend(app, r, journalCover)
|
||||
if err != nil || pr == nil {
|
||||
writeAuthRequired(w, err)
|
||||
resolveError(w, err)
|
||||
return r, nil, false
|
||||
}
|
||||
r = r.WithContext(bouncer.WithUser(r.Context(), pr))
|
||||
return r, pr, true
|
||||
return out, pr, true
|
||||
}
|
||||
|
||||
func isEditor(pr *bouncer.Principal) bool {
|
||||
|
||||
Reference in New Issue
Block a user