Changeset 0.29.0 (#195)
This commit is contained in:
213
server/handlers/extension_permissions.go
Normal file
213
server/handlers/extension_permissions.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
)
|
||||
|
||||
// ExtPermHandler serves extension permission management endpoints.
|
||||
// v0.29.0 CS2: Admin reviews and grants/revokes declared permissions.
|
||||
type ExtPermHandler struct {
|
||||
stores store.Stores
|
||||
}
|
||||
|
||||
func NewExtPermHandler(stores store.Stores) *ExtPermHandler {
|
||||
return &ExtPermHandler{stores: stores}
|
||||
}
|
||||
|
||||
// ListPackagePermissions returns all declared permissions for a package.
|
||||
// GET /api/v1/admin/extensions/:id/permissions
|
||||
func (h *ExtPermHandler) ListPackagePermissions(c *gin.Context) {
|
||||
pkgID := c.Param("id")
|
||||
|
||||
perms, err := h.stores.ExtPermissions.ListForPackage(c.Request.Context(), pkgID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list permissions"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": perms})
|
||||
}
|
||||
|
||||
// GrantPermission grants a single declared permission for a package.
|
||||
// POST /api/v1/admin/extensions/:id/permissions/:perm/grant
|
||||
func (h *ExtPermHandler) GrantPermission(c *gin.Context) {
|
||||
pkgID := c.Param("id")
|
||||
perm := c.Param("perm")
|
||||
userID := c.GetString("user_id")
|
||||
|
||||
if !models.ValidExtensionPermissions[perm] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "unrecognized permission: " + perm})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.stores.ExtPermissions.Grant(c.Request.Context(), pkgID, perm, userID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to grant permission"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if all permissions are now granted → activate package
|
||||
h.maybeActivate(c, pkgID)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "granted", "package_id": pkgID, "permission": perm})
|
||||
}
|
||||
|
||||
// RevokePermission revokes a single permission for a package.
|
||||
// POST /api/v1/admin/extensions/:id/permissions/:perm/revoke
|
||||
func (h *ExtPermHandler) RevokePermission(c *gin.Context) {
|
||||
pkgID := c.Param("id")
|
||||
perm := c.Param("perm")
|
||||
|
||||
if err := h.stores.ExtPermissions.Revoke(c.Request.Context(), pkgID, perm); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to revoke permission"})
|
||||
return
|
||||
}
|
||||
|
||||
// Suspend if a previously-active package loses a grant
|
||||
h.maybeSuspend(c, pkgID)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "revoked", "package_id": pkgID, "permission": perm})
|
||||
}
|
||||
|
||||
// GrantAllPermissions grants all declared permissions for a package.
|
||||
// POST /api/v1/admin/extensions/:id/permissions/grant-all
|
||||
func (h *ExtPermHandler) GrantAllPermissions(c *gin.Context) {
|
||||
pkgID := c.Param("id")
|
||||
userID := c.GetString("user_id")
|
||||
|
||||
if err := h.stores.ExtPermissions.GrantAll(c.Request.Context(), pkgID, userID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to grant all permissions"})
|
||||
return
|
||||
}
|
||||
|
||||
// Activate the package now that all permissions are granted
|
||||
h.maybeActivate(c, pkgID)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "granted_all", "package_id": pkgID})
|
||||
}
|
||||
|
||||
// ReviewPackage returns the package with its declared permissions.
|
||||
// GET /api/v1/admin/extensions/:id/review
|
||||
func (h *ExtPermHandler) ReviewPackage(c *gin.Context) {
|
||||
pkgID := c.Param("id")
|
||||
|
||||
pkg, err := h.stores.Packages.Get(c.Request.Context(), pkgID)
|
||||
if err != nil || pkg == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "package not found"})
|
||||
return
|
||||
}
|
||||
|
||||
perms, err := h.stores.ExtPermissions.ListForPackage(c.Request.Context(), pkgID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list permissions"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"package": pkg,
|
||||
"permissions": perms,
|
||||
})
|
||||
}
|
||||
|
||||
// ── Lifecycle helpers ────────────────────────
|
||||
|
||||
// maybeActivate transitions a pending_review package to active
|
||||
// if all declared permissions have been granted.
|
||||
func (h *ExtPermHandler) maybeActivate(c *gin.Context, pkgID string) {
|
||||
pkg, err := h.stores.Packages.Get(c.Request.Context(), pkgID)
|
||||
if err != nil || pkg == nil {
|
||||
return
|
||||
}
|
||||
if pkg.Status != models.PackageStatusPendingReview {
|
||||
return
|
||||
}
|
||||
|
||||
perms, err := h.stores.ExtPermissions.ListForPackage(c.Request.Context(), pkgID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
allGranted := true
|
||||
for _, p := range perms {
|
||||
if !p.Granted {
|
||||
allGranted = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allGranted {
|
||||
_ = h.stores.Packages.SetStatus(c.Request.Context(), pkgID, models.PackageStatusActive)
|
||||
}
|
||||
}
|
||||
|
||||
// maybeSuspend transitions an active package to suspended
|
||||
// if a required permission was revoked.
|
||||
func (h *ExtPermHandler) maybeSuspend(c *gin.Context, pkgID string) {
|
||||
pkg, err := h.stores.Packages.Get(c.Request.Context(), pkgID)
|
||||
if err != nil || pkg == nil {
|
||||
return
|
||||
}
|
||||
if pkg.Status != models.PackageStatusActive {
|
||||
return
|
||||
}
|
||||
|
||||
perms, err := h.stores.ExtPermissions.ListForPackage(c.Request.Context(), pkgID)
|
||||
if err != nil || len(perms) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, p := range perms {
|
||||
if !p.Granted {
|
||||
_ = h.stores.Packages.SetStatus(c.Request.Context(), pkgID, models.PackageStatusSuspended)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Manifest Permission Parsing ──────────────
|
||||
|
||||
// SyncManifestPermissions extracts the "permissions" array from a
|
||||
// package manifest and declares them in the store. Returns the list
|
||||
// of declared permissions. If the manifest declares permissions,
|
||||
// the package status is set to pending_review.
|
||||
//
|
||||
// Called by AdminInstallExtension and InstallPackage handlers.
|
||||
func SyncManifestPermissions(c *gin.Context, stores store.Stores, pkgID string, manifest map[string]any) []string {
|
||||
if stores.ExtPermissions == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
raw, ok := manifest["permissions"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
arr, ok := raw.([]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
var perms []string
|
||||
for _, v := range arr {
|
||||
if s, ok := v.(string); ok && models.ValidExtensionPermissions[s] {
|
||||
perms = append(perms, s)
|
||||
}
|
||||
}
|
||||
|
||||
if len(perms) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := stores.ExtPermissions.DeclareForPackage(c.Request.Context(), pkgID, perms); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Package needs review before activation
|
||||
_ = stores.Packages.SetStatus(c.Request.Context(), pkgID, models.PackageStatusPendingReview)
|
||||
|
||||
return perms
|
||||
}
|
||||
Reference in New Issue
Block a user