feat: implement resource metadata management API (issue #12)
Add API endpoints to manage ResourceMetadata (ownership, groups, permissions) for KV resources. This enables administrators to configure granular access control for stored data. Changes: - Add GetResourceMetadataResponse and UpdateResourceMetadataRequest types - Add GetResourceMetadata and SetResourceMetadata methods to AuthService - Add GET /kv/{path}/metadata endpoint (requires admin:users:read) - Add PUT /kv/{path}/metadata endpoint (requires admin:users:update) - Both endpoints protected by JWT authentication - Metadata routes registered before general KV routes to prevent pattern conflicts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -213,6 +213,104 @@ func (s *Server) deleteKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||
s.logger.WithField("path", path).Info("Value deleted")
|
||||
}
|
||||
|
||||
// getResourceMetadataHandler retrieves metadata for a KV resource
|
||||
func (s *Server) getResourceMetadataHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
path := vars["path"]
|
||||
|
||||
// Get metadata from storage
|
||||
metadata, err := s.authService.GetResourceMetadata(path)
|
||||
if err == badger.ErrKeyNotFound {
|
||||
http.Error(w, "Not Found: No metadata exists for this resource", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
s.logger.WithError(err).WithField("path", path).Error("Failed to get resource metadata")
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := types.GetResourceMetadataResponse{
|
||||
OwnerUUID: metadata.OwnerUUID,
|
||||
GroupUUID: metadata.GroupUUID,
|
||||
Permissions: metadata.Permissions,
|
||||
TTL: metadata.TTL,
|
||||
CreatedAt: metadata.CreatedAt,
|
||||
UpdatedAt: metadata.UpdatedAt,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// updateResourceMetadataHandler updates metadata for a KV resource
|
||||
func (s *Server) updateResourceMetadataHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
path := vars["path"]
|
||||
|
||||
// Parse request body
|
||||
var req types.UpdateResourceMetadataRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Bad Request: Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get existing metadata or create new one
|
||||
metadata, err := s.authService.GetResourceMetadata(path)
|
||||
if err == badger.ErrKeyNotFound {
|
||||
// Create new metadata with defaults
|
||||
metadata = &types.ResourceMetadata{
|
||||
OwnerUUID: "",
|
||||
GroupUUID: "",
|
||||
Permissions: types.DefaultPermissions,
|
||||
TTL: "",
|
||||
CreatedAt: time.Now().Unix(),
|
||||
UpdatedAt: time.Now().Unix(),
|
||||
}
|
||||
} else if err != nil {
|
||||
s.logger.WithError(err).WithField("path", path).Error("Failed to get resource metadata")
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Update only provided fields
|
||||
if req.OwnerUUID != nil {
|
||||
metadata.OwnerUUID = *req.OwnerUUID
|
||||
}
|
||||
if req.GroupUUID != nil {
|
||||
metadata.GroupUUID = *req.GroupUUID
|
||||
}
|
||||
if req.Permissions != nil {
|
||||
metadata.Permissions = *req.Permissions
|
||||
}
|
||||
metadata.UpdatedAt = time.Now().Unix()
|
||||
|
||||
// Store updated metadata
|
||||
if err := s.authService.SetResourceMetadata(path, metadata); err != nil {
|
||||
s.logger.WithError(err).WithField("path", path).Error("Failed to update resource metadata")
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := types.GetResourceMetadataResponse{
|
||||
OwnerUUID: metadata.OwnerUUID,
|
||||
GroupUUID: metadata.GroupUUID,
|
||||
Permissions: metadata.Permissions,
|
||||
TTL: metadata.TTL,
|
||||
CreatedAt: metadata.CreatedAt,
|
||||
UpdatedAt: metadata.UpdatedAt,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
|
||||
s.logger.WithFields(logrus.Fields{
|
||||
"path": path,
|
||||
"owner_uuid": metadata.OwnerUUID,
|
||||
"group_uuid": metadata.GroupUUID,
|
||||
}).Info("Resource metadata updated")
|
||||
}
|
||||
|
||||
// isClusterMember checks if request is from a cluster member
|
||||
func (s *Server) isClusterMember(remoteAddr string) bool {
|
||||
host, _, err := net.SplitHostPort(remoteAddr)
|
||||
|
@@ -13,6 +13,18 @@ func (s *Server) setupRoutes() *mux.Router {
|
||||
// Health endpoint (always available)
|
||||
router.HandleFunc("/health", s.healthHandler).Methods("GET")
|
||||
|
||||
// Resource Metadata Management endpoints (Issue #12) - Must come BEFORE general KV routes
|
||||
// These need to be registered first to prevent /kv/{path:.+} from matching metadata paths
|
||||
if s.config.AuthEnabled {
|
||||
router.Handle("/kv/{path:.+}/metadata", s.authService.Middleware(
|
||||
[]string{"admin:users:read"}, nil, "",
|
||||
)(s.getResourceMetadataHandler)).Methods("GET")
|
||||
|
||||
router.Handle("/kv/{path:.+}/metadata", s.authService.Middleware(
|
||||
[]string{"admin:users:update"}, nil, "",
|
||||
)(s.updateResourceMetadataHandler)).Methods("PUT")
|
||||
}
|
||||
|
||||
// KV endpoints (with conditional authentication based on anonymous access settings)
|
||||
// GET endpoint - require auth if anonymous read is disabled
|
||||
if s.config.AuthEnabled && !s.config.AllowAnonymousRead {
|
||||
|
Reference in New Issue
Block a user