Feat v0.7.7 API tokens + extension permissions (#61)

Personal access tokens (PATs) for programmatic API access with
SHA-256 hashing, permission scoping (git model), and Settings/Admin UI.
Extension-declared user permissions with dynamic registry, gate_permission
manifest field, permissions Starlark module, and grouped admin UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 18:20:16 +00:00
parent e02b13dc12
commit f32eefab14
34 changed files with 1769 additions and 58 deletions

View File

@@ -41,6 +41,7 @@ import (
"github.com/gin-gonic/gin"
"go.starlark.net/starlark"
"armature/auth"
"armature/models"
"armature/sandbox"
"armature/store"
@@ -125,8 +126,17 @@ func (h *ExtAPIHandler) Handle(c *gin.Context) {
return
}
// ── 4b. Gate permission check ──────────────
if gatePerm, _ := pkg.Manifest["gate_permission"].(string); gatePerm != "" {
userPerms, err := auth.ResolvePermissions(c.Request.Context(), h.stores, getUserID(c))
if err != nil || !userPerms[gatePerm] {
c.JSON(http.StatusForbidden, gin.H{"error": "permission required: " + gatePerm})
return
}
}
// ── 5. Build request dict ──────────────────
reqDict, err := buildRequestDict(c, rawPath)
reqDict, err := buildRequestDict(c, rawPath, h.stores)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read request: " + err.Error()})
return
@@ -228,7 +238,7 @@ func matchAPIRoute(manifest map[string]any, method, path string) bool {
// "body": "...",
// "user_id": "uuid",
// }
func buildRequestDict(c *gin.Context, path string) (*starlark.Dict, error) {
func buildRequestDict(c *gin.Context, path string, stores ...store.Stores) (*starlark.Dict, error) {
// Read body (capped at 1 MB for safety)
body, err := io.ReadAll(io.LimitReader(c.Request.Body, 1<<20))
if err != nil {
@@ -251,13 +261,24 @@ func buildRequestDict(c *gin.Context, path string) (*starlark.Dict, error) {
}
}
d := starlark.NewDict(6)
d := starlark.NewDict(7)
_ = d.SetKey(starlark.String("method"), starlark.String(c.Request.Method))
_ = d.SetKey(starlark.String("path"), starlark.String(path))
_ = d.SetKey(starlark.String("headers"), hdrs)
_ = d.SetKey(starlark.String("query"), query)
_ = d.SetKey(starlark.String("body"), starlark.String(string(body)))
_ = d.SetKey(starlark.String("user_id"), starlark.String(getUserID(c)))
// Resolve user permissions for extension inline checks
if len(stores) > 0 {
userPerms, _ := auth.ResolvePermissions(c.Request.Context(), stores[0], getUserID(c))
permList := make([]starlark.Value, 0, len(userPerms))
for p := range userPerms {
permList = append(permList, starlark.String(p))
}
_ = d.SetKey(starlark.String("permissions"), starlark.NewList(permList))
}
return d, nil
}