package api import ( "fmt" "net/http" "strconv" "strings" "github.com/ez-api/ez-api/internal/model" "github.com/gin-gonic/gin" "gorm.io/gorm" ) type AccessResponse struct { DefaultNamespace string `json:"default_namespace"` Namespaces []string `json:"namespaces"` } type UpdateAccessRequest struct { DefaultNamespace string `json:"default_namespace"` Namespaces []string `json:"namespaces"` PropagateToKeys bool `json:"propagate_to_keys,omitempty"` } // GetMasterAccess godoc // @Summary Get master access settings // @Description Returns master default_namespace and namespaces // @Tags admin // @Produce json // @Security AdminAuth // @Param id path int true "Master ID" // @Success 200 {object} ResponseEnvelope{data=AccessResponse} // @Failure 400 {object} ResponseEnvelope{data=MapData} // @Failure 404 {object} ResponseEnvelope{data=MapData} // @Failure 500 {object} ResponseEnvelope{data=MapData} // @Router /admin/masters/{id}/access [get] func (h *Handler) GetMasterAccess(c *gin.Context) { id, ok := parseUintParam(c, "id") if !ok { return } var m model.Master if err := h.db.First(&m, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "master not found"}) return } c.JSON(http.StatusOK, AccessResponse{ DefaultNamespace: normalizeDefaultNamespace(m.DefaultNamespace), Namespaces: normalizeNamespaces(m.Namespaces, m.DefaultNamespace), }) } // UpdateMasterAccess godoc // @Summary Update master access settings // @Description Updates master default_namespace and namespaces; optionally propagate to existing keys // @Tags admin // @Accept json // @Produce json // @Security AdminAuth // @Param id path int true "Master ID" // @Param request body UpdateAccessRequest true "Access settings" // @Success 200 {object} ResponseEnvelope{data=AccessResponse} // @Failure 400 {object} ResponseEnvelope{data=MapData} // @Failure 404 {object} ResponseEnvelope{data=MapData} // @Failure 500 {object} ResponseEnvelope{data=MapData} // @Router /admin/masters/{id}/access [put] func (h *Handler) UpdateMasterAccess(c *gin.Context) { id, ok := parseUintParam(c, "id") if !ok { return } var req UpdateAccessRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var m model.Master if err := h.db.First(&m, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "master not found"}) return } nextDefault := strings.TrimSpace(req.DefaultNamespace) if nextDefault == "" { nextDefault = strings.TrimSpace(m.DefaultNamespace) } nextDefault = normalizeDefaultNamespace(nextDefault) nextNamespaces := m.Namespaces if req.Namespaces != nil { nextNamespaces = strings.Join(req.Namespaces, ",") } nsList := normalizeNamespaces(nextNamespaces, nextDefault) nextNamespaces = strings.Join(nsList, ",") if err := ensureNamespacesExist(h.db, nsList); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.db.Model(&m).Updates(map[string]any{ "default_namespace": nextDefault, "namespaces": nextNamespaces, }).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update master access", "details": err.Error()}) return } if req.PropagateToKeys { if err := h.db.Model(&model.Key{}).Where("master_id = ?", m.ID).Updates(map[string]any{ "default_namespace": nextDefault, "namespaces": nextNamespaces, }).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to propagate access to keys", "details": err.Error()}) return } var keys []model.Key if err := h.db.Where("master_id = ?", m.ID).Find(&keys).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to reload keys for sync", "details": err.Error()}) return } for i := range keys { if err := h.sync.SyncKey(&keys[i]); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to sync key access", "details": err.Error()}) return } } } c.JSON(http.StatusOK, AccessResponse{ DefaultNamespace: nextDefault, Namespaces: nsList, }) } // GetKeyAccess godoc // @Summary Get key access settings // @Description Returns key default_namespace and namespaces // @Tags admin // @Produce json // @Security AdminAuth // @Param id path int true "Key ID" // @Success 200 {object} ResponseEnvelope{data=AccessResponse} // @Failure 400 {object} ResponseEnvelope{data=MapData} // @Failure 404 {object} ResponseEnvelope{data=MapData} // @Failure 500 {object} ResponseEnvelope{data=MapData} // @Router /admin/keys/{id}/access [get] func (h *Handler) GetKeyAccess(c *gin.Context) { id, ok := parseUintParam(c, "id") if !ok { return } var k model.Key if err := h.db.First(&k, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "key not found"}) return } c.JSON(http.StatusOK, AccessResponse{ DefaultNamespace: normalizeDefaultNamespace(k.DefaultNamespace), Namespaces: normalizeNamespaces(k.Namespaces, k.DefaultNamespace), }) } // UpdateKeyAccess godoc // @Summary Update key access settings // @Description Updates key default_namespace and namespaces and syncs to Redis // @Tags admin // @Accept json // @Produce json // @Security AdminAuth // @Param id path int true "Key ID" // @Param request body UpdateAccessRequest true "Access settings" // @Success 200 {object} ResponseEnvelope{data=AccessResponse} // @Failure 400 {object} ResponseEnvelope{data=MapData} // @Failure 404 {object} ResponseEnvelope{data=MapData} // @Failure 500 {object} ResponseEnvelope{data=MapData} // @Router /admin/keys/{id}/access [put] func (h *Handler) UpdateKeyAccess(c *gin.Context) { id, ok := parseUintParam(c, "id") if !ok { return } var req UpdateAccessRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var k model.Key if err := h.db.First(&k, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "key not found"}) return } nextDefault := strings.TrimSpace(req.DefaultNamespace) if nextDefault == "" { nextDefault = strings.TrimSpace(k.DefaultNamespace) } nextDefault = normalizeDefaultNamespace(nextDefault) nextNamespaces := k.Namespaces if req.Namespaces != nil { nextNamespaces = strings.Join(req.Namespaces, ",") } nsList := normalizeNamespaces(nextNamespaces, nextDefault) nextNamespaces = strings.Join(nsList, ",") if err := ensureNamespacesExist(h.db, nsList); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var master model.Master if err := h.db.First(&master, k.MasterID).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "master not found"}) return } masterNamespaces := normalizeNamespaces(master.Namespaces, master.DefaultNamespace) if !isSubset(nsList, masterNamespaces) { c.JSON(http.StatusBadRequest, gin.H{"error": "namespaces must be a subset of master namespaces"}) return } if err := h.db.Model(&k).Updates(map[string]any{ "default_namespace": nextDefault, "namespaces": nextNamespaces, }).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update key access", "details": err.Error()}) return } if err := h.db.First(&k, id).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to reload key", "details": err.Error()}) return } if err := h.sync.SyncKey(&k); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to sync key access", "details": err.Error()}) return } c.JSON(http.StatusOK, AccessResponse{ DefaultNamespace: nextDefault, Namespaces: nsList, }) } func normalizeDefaultNamespace(ns string) string { ns = strings.TrimSpace(ns) if ns == "" { return "default" } return ns } func normalizeNamespaces(raw string, defaultNamespace string) []string { defaultNamespace = normalizeDefaultNamespace(defaultNamespace) raw = strings.TrimSpace(raw) var parts []string if raw != "" { parts = strings.FieldsFunc(raw, func(r rune) bool { return r == ',' || r == ' ' || r == ';' || r == '\t' || r == '\n' }) } out := make([]string, 0, len(parts)+1) seen := make(map[string]struct{}, len(parts)+1) for _, p := range parts { p = strings.TrimSpace(p) if p == "" { continue } if _, ok := seen[p]; ok { continue } seen[p] = struct{}{} out = append(out, p) } if _, ok := seen[defaultNamespace]; !ok { out = append(out, defaultNamespace) } if len(out) == 0 { out = append(out, defaultNamespace) } return out } func ensureNamespacesExist(db *gorm.DB, namespaces []string) error { if db == nil { return fmt.Errorf("db required") } if len(namespaces) == 0 { return fmt.Errorf("namespaces required") } var rows []model.Namespace if err := db.Where("name IN ?", namespaces).Find(&rows).Error; err != nil { return fmt.Errorf("failed to load namespaces") } if len(rows) != len(namespaces) { return fmt.Errorf("namespace not found") } return nil } func isSubset(child, parent []string) bool { if len(child) == 0 { return true } parentSet := make(map[string]struct{}, len(parent)) for _, p := range parent { parentSet[strings.TrimSpace(p)] = struct{}{} } for _, c := range child { if _, ok := parentSet[strings.TrimSpace(c)]; !ok { return false } } return true } func parseUintParam(c *gin.Context, name string) (uint, bool) { idRaw := strings.TrimSpace(c.Param(name)) if idRaw == "" { c.JSON(http.StatusBadRequest, gin.H{"error": name + " required"}) return 0, false } idU64, err := strconv.ParseUint(idRaw, 10, 64) if err != nil || idU64 == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid " + name}) return 0, false } return uint(idU64), true }