Files
sm-journal-plugin/controllers/api/auth.go
Jakub Zych 4ab94da66c feat: MCP tools for posts, categories and tags, the draft_post prompt
- journal.Plugin.MCP: list_posts, show_post, list_categories, list_tags,
  create_post, update_post, delete_post and the draft_post prompt, guard
  backend, listed with golem15.journal.access_posts; update fields are
  optional so only sent keys reach the route
- optionalBackendPrincipal honours the MCP dispatch marker (no bearer
  short-circuit; anonymous reads unchanged)
- require summercms v0.4.4
- TestJournalMCPSmoke, TestOptionalBackendPrincipalDispatch
2026-10-23 12:54:03 +02:00

118 lines
3.7 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 when
// one is presented, and honours the in-process dispatch marker the MCP
// endpoint sets (no Authorization header then, D-17). Failure is anonymous:
// public GET must not 401 (ResolveBackend answers ErrUnauthenticated for a
// request without a bearer). Uses cabana.ResolveBackend with cover
// golem15.journal.*.
func optionalBackendPrincipal(app *backpack.App, r *http.Request) *http.Request {
if r == nil {
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
}