package handlers import ( "net/http" "github.com/gin-gonic/gin" "switchboard-core/models" "switchboard-core/store" ) // ExtPermHandler serves extension permission management endpoints. 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 }