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

@@ -115,6 +115,7 @@ func main() {
// Bootstrap admin from env (K8s secret) — upserts on every restart
handlers.BootstrapAdmin(cfg, stores, uekCache)
handlers.BootstrapPAT(cfg, stores)
// Seed additional users from env (dev/test only, skipped in production)
handlers.SeedUsers(cfg, stores, uekCache)
@@ -184,6 +185,10 @@ func main() {
handlers.InstallBundledPackages(cfg.BundledPackagesDir, bundledPkgDir, cfg.BundledPackages, stores, starlarkRunner)
}
// ── Register extension user permissions ─────
// Scan active packages on boot and populate the dynamic permission registry.
handlers.RegisterAllExtensionUserPermissions(stores)
// ── Trigger Engine ─────────────────
triggerEngine := triggers.New(stores, starlarkRunner, bus)
if err := triggerEngine.Start(context.Background()); err != nil {
@@ -342,7 +347,7 @@ func main() {
})
// WebSocket endpoint
base.GET("/ws", middleware.WsAuth(cfg, stores.Users, userCache, ticketAdapter), hub.HandleWebSocket)
base.GET("/ws", middleware.WsAuth(cfg, stores.Users, userCache, ticketAdapter, stores.APITokens), hub.HandleWebSocket)
// ── Auth routes (rate limited) ──────────────
authMode, err := auth.ParseMode(cfg.AuthMode)
@@ -448,12 +453,15 @@ func main() {
// Client SDK calls this on user interaction (debounced, max 1/min)
// to update last_activity_at for idle-timeout tracking.
activityGroup := api.Group("/auth")
activityGroup.Use(middleware.Auth(cfg, stores.Users, userCache))
activityGroup.Use(middleware.Auth(cfg, stores.Users, userCache, stores.APITokens))
activityGroup.POST("/activity", authH.Activity)
// ── Shared handler instances ──────────────
tokenH := handlers.NewAPITokenHandler(stores)
// ── Protected routes ────────────────────
protected := api.Group("")
protected.Use(middleware.Auth(cfg, stores.Users, userCache))
protected.Use(middleware.Auth(cfg, stores.Users, userCache, stores.APITokens))
protected.Use(middleware.ValidatePathParams())
{
// ── WebSocket Ticket ───────────
@@ -559,6 +567,11 @@ func main() {
permH := handlers.NewProfilePermissionsHandler(stores)
protected.GET("/profile/permissions", permH.GetMyPermissions)
// API Tokens (PATs)
protected.POST("/auth/tokens", tokenH.CreateToken)
protected.GET("/auth/tokens", tokenH.ListTokens)
protected.DELETE("/auth/tokens/:id", tokenH.RevokeToken)
// Boot payload — single-call SDK bootstrap
bootH := handlers.NewProfileBootstrapHandler(stores)
protected.GET("/profile/bootstrap", bootH.GetBootstrap)
@@ -680,7 +693,7 @@ func main() {
// ── Admin routes ────────────────────────
admin := api.Group("/admin")
admin.Use(middleware.Auth(cfg, stores.Users, userCache))
admin.Use(middleware.Auth(cfg, stores.Users, userCache, stores.APITokens))
admin.Use(middleware.RequireAdmin(stores))
admin.Use(middleware.ValidatePathParams())
{
@@ -696,6 +709,9 @@ func main() {
admin.POST("/users/:id/vault/reset", adm.ResetVault)
admin.DELETE("/users/:id", adm.DeleteUser)
// Admin API Tokens (create tokens for any user)
admin.POST("/tokens", tokenH.AdminCreateToken)
// Global settings
admin.GET("/settings", adm.ListGlobalSettings)
admin.GET("/settings/:key", adm.GetGlobalSetting)
@@ -910,7 +926,7 @@ func main() {
{
extAPIH := handlers.NewExtAPIHandler(stores, starlarkRunner)
extAPI := base.Group("/s/:slug/api")
extAPI.Use(middleware.Auth(cfg, stores.Users, userCache))
extAPI.Use(middleware.Auth(cfg, stores.Users, userCache, stores.APITokens))
extAPI.Any("/*path", extAPIH.Handle)
}