All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-runners (push) Has been skipped
CI/CD / test-go-pg (push) Successful in 2m48s
CI/CD / test-sqlite (push) Successful in 2m49s
CI/CD / build-and-deploy (push) Successful in 28s
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
264 lines
7.2 KiB
Go
264 lines
7.2 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"armature/auth"
|
|
"armature/models"
|
|
"armature/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
|
|
}
|
|
}
|
|
}
|
|
|
|
// RegisterAllExtensionUserPermissions scans all active packages and registers
|
|
// their user_permissions in the dynamic permission registry. Called at boot.
|
|
func RegisterAllExtensionUserPermissions(stores store.Stores) {
|
|
ctx := context.Background()
|
|
pkgs, err := stores.Packages.List(ctx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
count := 0
|
|
for _, pkg := range pkgs {
|
|
if pkg.Manifest != nil {
|
|
SyncUserPermissions(pkg.Manifest, pkg.ID)
|
|
if _, ok := pkg.Manifest["user_permissions"]; ok {
|
|
count++
|
|
}
|
|
}
|
|
}
|
|
if count > 0 {
|
|
log.Printf(" ✅ Registered user permissions from %d package(s)", count)
|
|
}
|
|
}
|
|
|
|
// ── 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)
|
|
|
|
// Register user-facing permissions in the dynamic permission registry
|
|
SyncUserPermissions(manifest, pkgID)
|
|
|
|
return perms
|
|
}
|
|
|
|
// SyncUserPermissions registers extension-declared user permissions in the
|
|
// kernel's dynamic permission registry. Called on install and at boot.
|
|
func SyncUserPermissions(manifest map[string]any, pkgID string) {
|
|
raw, ok := manifest["user_permissions"]
|
|
if !ok {
|
|
return
|
|
}
|
|
arr, ok := raw.([]any)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var userPerms []string
|
|
for _, v := range arr {
|
|
if s, ok := v.(string); ok && s != "" {
|
|
userPerms = append(userPerms, s)
|
|
}
|
|
}
|
|
if len(userPerms) > 0 {
|
|
auth.RegisterExtensionPermissions(pkgID, userPerms)
|
|
}
|
|
}
|