Add API endpoints for resource metadata management (ownership & permissions)

New types: UpdateResourceMetadataRequest and GetResourceMetadataResponse in types.go
    AuthService methods: StoreResourceMetadata and GetResourceMetadata in auth/auth.go
    Handlers: getResourceMetadataHandler and updateResourceMetadataHandler in server/handlers.go
    Routes: /kv/{path}/metadata (GET for read, PUT for update) with auth middleware in server/routes.go

Enables fine-grained control over KV path ownership, group assignments, and POSIX-inspired permissions.
This commit is contained in:
Kalzu Rekku
2025-09-29 19:04:28 +03:00
parent 2431d3cfb0
commit 32b347f1fd
4 changed files with 211 additions and 2 deletions

View File

@@ -1271,3 +1271,142 @@ func (s *Server) getRevisionHistory(key string) ([]map[string]interface{}, error
func (s *Server) getSpecificRevision(key string, revision int) (*types.StoredValue, error) {
return s.revisionService.GetSpecificRevision(key, revision)
}
// getResourceMetadataHandler retrieves metadata for a resource path
func (s *Server) getResourceMetadataHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
path := vars["path"]
authCtx := auth.GetAuthContext(r.Context())
if authCtx == nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Check read permission on the resource
if !s.authService.CheckResourcePermission(authCtx, path, "read") {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
metadata, err := s.authService.GetResourceMetadata(path)
if err == badger.ErrKeyNotFound {
// Return default metadata if not found
defaultMetadata := types.ResourceMetadata{
OwnerUUID: authCtx.UserUUID,
GroupUUID: "",
Permissions: types.DefaultPermissions,
CreatedAt: time.Now().Unix(),
UpdatedAt: time.Now().Unix(),
}
metadata = &defaultMetadata
} 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
}
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 resource path
func (s *Server) updateResourceMetadataHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
path := vars["path"]
authCtx := auth.GetAuthContext(r.Context())
if authCtx == nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Check write permission on the resource (owner write required for metadata changes)
if !s.authService.CheckResourcePermission(authCtx, path, "write") {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
var req types.UpdateResourceMetadataRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
// Get current metadata (or default if not exists)
currentMetadata, err := s.authService.GetResourceMetadata(path)
if err == badger.ErrKeyNotFound {
currentMetadata = &types.ResourceMetadata{
OwnerUUID: authCtx.UserUUID,
GroupUUID: "",
Permissions: types.DefaultPermissions,
CreatedAt: time.Now().Unix(),
UpdatedAt: time.Now().Unix(),
}
} else if err != nil {
s.logger.WithError(err).WithField("path", path).Error("Failed to get current resource metadata")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Apply updates only to provided fields
updated := false
if req.OwnerUUID != "" {
currentMetadata.OwnerUUID = req.OwnerUUID
updated = true
}
if req.GroupUUID != "" {
currentMetadata.GroupUUID = req.GroupUUID
updated = true
}
if req.Permissions != 0 {
currentMetadata.Permissions = req.Permissions
updated = true
}
if req.TTL != "" {
currentMetadata.TTL = req.TTL
updated = true
}
if !updated {
http.Error(w, "No fields provided for update", http.StatusBadRequest)
return
}
// Store updated metadata
if err := s.authService.StoreResourceMetadata(path, currentMetadata); err != nil {
s.logger.WithError(err).WithField("path", path).Error("Failed to store resource metadata")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
response := types.GetResourceMetadataResponse{
OwnerUUID: currentMetadata.OwnerUUID,
GroupUUID: currentMetadata.GroupUUID,
Permissions: currentMetadata.Permissions,
TTL: currentMetadata.TTL,
CreatedAt: currentMetadata.CreatedAt,
UpdatedAt: currentMetadata.UpdatedAt,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response)
s.logger.WithFields(logrus.Fields{
"path": path,
"user_uuid": authCtx.UserUUID,
"owner_uuid": currentMetadata.OwnerUUID,
"group_uuid": currentMetadata.GroupUUID,
"permissions": currentMetadata.Permissions,
}).Info("Resource metadata updated")
}

View File

@@ -39,6 +39,19 @@ func (s *Server) setupRoutes() *mux.Router {
router.HandleFunc("/kv/{path:.+}", s.deleteKVHandler).Methods("DELETE")
}
// Resource Metadata endpoints (available when auth is enabled)
if s.config.AuthEnabled {
// GET metadata - require read permission
router.Handle("/kv/{path:.+}/metadata", s.authService.Middleware(
[]string{"read"}, func(r *http.Request) string { return mux.Vars(r)["path"] }, "read",
)(s.getResourceMetadataHandler)).Methods("GET")
// PUT metadata - require write permission (owner write)
router.Handle("/kv/{path:.+}/metadata", s.authService.Middleware(
[]string{"write"}, func(r *http.Request) string { return mux.Vars(r)["path"] }, "write",
)(s.updateResourceMetadataHandler)).Methods("PUT")
}
// Member endpoints (available when clustering is enabled)
if s.config.ClusteringEnabled {
router.HandleFunc("/members/", s.getMembersHandler).Methods("GET")