- 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>
115 lines
3.5 KiB
Go
115 lines
3.5 KiB
Go
package api
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/backpack"
|
|
"git.golem15.com/golem15/summercms/modules/bouncer"
|
|
"git.golem15.com/golem15/summercms/modules/cabana"
|
|
"git.golem15.com/golem15/summercms/modules/wire"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const (
|
|
errAuthenticationRequired = "Authentication required"
|
|
errInsufficientPerms = "Insufficient permissions"
|
|
errPublishPermission = "You do not have permission to publish posts"
|
|
errEditPermission = "You do not have permission to edit this post"
|
|
errPostNotFound = "Post not found"
|
|
errImageNotFound = "Image not found"
|
|
errValidationFailed = "Validation failed"
|
|
msgPostDeleted = "Post deleted"
|
|
msgImageRemoved = "Image removed"
|
|
|
|
permAccessPosts = "golem15.journal.access_posts"
|
|
permAccessOtherPosts = "golem15.journal.access_other_posts"
|
|
permAccessPublish = "golem15.journal.access_publish"
|
|
|
|
journalCover = "golem15.journal.*"
|
|
)
|
|
|
|
func writeAPIError(w http.ResponseWriter, status int, message string) {
|
|
wire.WriteJSON(w, status, map[string]any{"error": message})
|
|
}
|
|
|
|
func writeValidation(w http.ResponseWriter, errs map[string][]string) {
|
|
wire.WriteJSON(w, http.StatusUnprocessableEntity, map[string]any{
|
|
"error": errValidationFailed,
|
|
"errors": errs,
|
|
})
|
|
}
|
|
|
|
func writeAuthRequired(w http.ResponseWriter, _ error) {
|
|
writeAPIError(w, http.StatusUnauthorized, errAuthenticationRequired)
|
|
}
|
|
|
|
func hasBearer(r *http.Request) bool {
|
|
if r == nil {
|
|
return false
|
|
}
|
|
h := strings.TrimSpace(r.Header.Get("Authorization"))
|
|
token, ok := strings.CutPrefix(h, "Bearer ")
|
|
return ok && strings.TrimSpace(token) != ""
|
|
}
|
|
|
|
// 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
|
|
}
|
|
writeAuthRequired(w, err)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
out, _, err := cabana.ResolveBackend(app, r, journalCover)
|
|
if err != nil || out == nil {
|
|
return r
|
|
}
|
|
return out
|
|
}
|
|
|
|
// 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) {
|
|
out, pr, err := cabana.ResolveBackend(app, r, journalCover)
|
|
if err != nil || pr == nil {
|
|
resolveError(w, err)
|
|
return r, nil, false
|
|
}
|
|
return out, pr, true
|
|
}
|
|
|
|
func isEditor(pr *bouncer.Principal) bool {
|
|
return cabana.Allows(pr, []string{permAccessPosts})
|
|
}
|
|
|
|
func requireAccessPosts(w http.ResponseWriter, pr *bouncer.Principal) bool {
|
|
if !cabana.Allows(pr, []string{permAccessPosts}) {
|
|
writeAPIError(w, http.StatusForbidden, errInsufficientPerms)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func gdb(app *backpack.App) (*gorm.DB, error) {
|
|
if app == nil {
|
|
return nil, errors.New("journal api: application is not configured")
|
|
}
|
|
db, ok := app.Lookup[*gorm.DB]()
|
|
if !ok || db == nil {
|
|
return nil, errors.New("journal api: database is not configured")
|
|
}
|
|
return db, nil
|
|
}
|