feat: plugin-declared member token scopes (pact.HasFrontendTokenScopes)
- CompileAPITokenScopes builds the catalog from the built-ins and the activated plugins' declarations; a bad name, a redeclared built-in, a duplicate, an empty label or a missing phrase key fails boot - POST /tokens, user:token:create and MintAPITokenFor check the application's own catalog (422 text unchanged); MintAPIToken keeps its signature with a process-wide fallback for declared scopes - GET /_user/api/v1/tokens/scopes lists the built-ins and the declared scopes with translated labels - built-in scope labels in en and pl; TestMemberTokenScopesSmoke
This commit is contained in:
@@ -3,7 +3,6 @@ package controllers
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -35,7 +34,8 @@ func APITokenStore(app *backpack.App) http.HandlerFunc {
|
||||
writeValidation(w, map[string][]string{"name": {"The name field is required and may not be longer than 255 characters."}})
|
||||
return
|
||||
}
|
||||
scopes, err := apiTokenScopes(fields["scopes"])
|
||||
catalog := classes.APITokenScopesFor(app)
|
||||
scopes, err := apiTokenScopes(catalog, fields["scopes"])
|
||||
if err != nil {
|
||||
writeValidation(w, map[string][]string{"scopes": {err.Error()}})
|
||||
return
|
||||
@@ -54,7 +54,7 @@ func APITokenStore(app *backpack.App) http.HandlerFunc {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
secret, token, err := classes.MintAPIToken(prefix, user.ID, name, scopes, expiresAt, nil)
|
||||
secret, token, err := classes.MintAPITokenFor(app, prefix, user.ID, name, scopes, expiresAt, nil)
|
||||
if err != nil || db.WithContext(r.Context()).Create(token).Error != nil {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
@@ -63,6 +63,37 @@ func APITokenStore(app *backpack.App) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// APITokenScopes serves GET /_user/api/v1/tokens/scopes behind jwt.auth:
|
||||
// the member token scopes the caller may mint, {"data":[{"name","label",
|
||||
// "description","built_in"}]}, the built-ins first, then the scopes the
|
||||
// application's plugins declared. Phrase-key labels and descriptions are
|
||||
// translated for the request locale.
|
||||
func APITokenScopes(app *backpack.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := bouncer.User(r.Context())
|
||||
if !ok || user == nil || user.ID == 0 {
|
||||
writeUnauthorized(w)
|
||||
return
|
||||
}
|
||||
tr := appTranslator(app)
|
||||
text := func(s string) string {
|
||||
if tr == nil || !strings.Contains(s, "::") {
|
||||
return s
|
||||
}
|
||||
return tr.Get(r.Context(), s, nil)
|
||||
}
|
||||
scopes := classes.APITokenScopesFor(app).Scopes()
|
||||
data := make([]map[string]any, 0, len(scopes))
|
||||
for _, scope := range scopes {
|
||||
data = append(data, map[string]any{
|
||||
"name": scope.Name, "label": text(scope.Label),
|
||||
"description": text(scope.Description), "built_in": scope.BuiltIn,
|
||||
})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"data": wire.Slice(data)})
|
||||
}
|
||||
}
|
||||
|
||||
// APITokenIndex lists the caller's live personal tokens without hashes.
|
||||
func APITokenIndex(app *backpack.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -131,7 +162,9 @@ func apiTokenDeps(w http.ResponseWriter, r *http.Request, app *backpack.App) (*g
|
||||
return db, user, true
|
||||
}
|
||||
|
||||
func apiTokenScopes(raw any) ([]string, error) {
|
||||
// apiTokenScopes reads the scopes field: nil, or an array of names the
|
||||
// catalog allows.
|
||||
func apiTokenScopes(catalog *classes.APITokenScopeCatalog, raw any) ([]string, error) {
|
||||
if raw == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -142,7 +175,7 @@ func apiTokenScopes(raw any) ([]string, error) {
|
||||
out := make([]string, 0, len(values))
|
||||
for _, rawScope := range values {
|
||||
scope, ok := rawScope.(string)
|
||||
if !ok || !slices.Contains(classes.AllowedAPITokenScopes, scope) {
|
||||
if !ok || !catalog.Allowed(scope) {
|
||||
return nil, errors.New("The selected scope is invalid.")
|
||||
}
|
||||
out = append(out, scope)
|
||||
|
||||
Reference in New Issue
Block a user