From 32b347f1fd0e07364e295b7b198058ecb56c5272 Mon Sep 17 00:00:00 2001 From: Kalzu Rekku Date: Mon, 29 Sep 2025 19:04:28 +0300 Subject: [PATCH] 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. --- auth/auth.go | 42 +++++++++++++- server/handlers.go | 139 +++++++++++++++++++++++++++++++++++++++++++++ server/routes.go | 13 +++++ types/types.go | 19 ++++++- 4 files changed, 211 insertions(+), 2 deletions(-) diff --git a/auth/auth.go b/auth/auth.go index 00fa831..4fd7095 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -227,4 +227,44 @@ func (s *AuthService) HasUsers() (bool, error) { }) return hasUsers, err -} \ No newline at end of file +} + +// StoreResourceMetadata stores or updates resource metadata in BadgerDB +func (s *AuthService) StoreResourceMetadata(path string, metadata *types.ResourceMetadata) error { + now := time.Now().Unix() + if metadata.CreatedAt == 0 { + metadata.CreatedAt = now + } + metadata.UpdatedAt = now + + metadataData, err := json.Marshal(metadata) + if err != nil { + return err + } + + return s.db.Update(func(txn *badger.Txn) error { + return txn.Set([]byte(ResourceMetadataKey(path)), metadataData) + }) +} + +// GetResourceMetadata retrieves resource metadata from BadgerDB +func (s *AuthService) GetResourceMetadata(path string) (*types.ResourceMetadata, error) { + var metadata types.ResourceMetadata + + err := s.db.View(func(txn *badger.Txn) error { + item, err := txn.Get([]byte(ResourceMetadataKey(path))) + if err != nil { + return err + } + + return item.Value(func(val []byte) error { + return json.Unmarshal(val, &metadata) + }) + }) + + if err != nil { + return nil, err + } + + return &metadata, nil +} diff --git a/server/handlers.go b/server/handlers.go index 843f530..44bce89 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -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") +} diff --git a/server/routes.go b/server/routes.go index 814e5f7..14c1b35 100644 --- a/server/routes.go +++ b/server/routes.go @@ -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") diff --git a/types/types.go b/types/types.go index bcdf027..d781095 100644 --- a/types/types.go +++ b/types/types.go @@ -131,6 +131,23 @@ type CreateTokenResponse struct { ExpiresAt int64 `json:"expires_at"` } +// Resource Metadata Management API structures +type UpdateResourceMetadataRequest struct { + OwnerUUID string `json:"owner_uuid,omitempty"` + GroupUUID string `json:"group_uuid,omitempty"` + Permissions int `json:"permissions,omitempty"` + TTL string `json:"ttl,omitempty"` +} + +type GetResourceMetadataResponse struct { + OwnerUUID string `json:"owner_uuid"` + GroupUUID string `json:"group_uuid"` + Permissions int `json:"permissions"` + TTL string `json:"ttl"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + // Cluster and member management types type Member struct { ID string `json:"id"` @@ -277,4 +294,4 @@ type Config struct { // Anonymous access control (Issue #5) AllowAnonymousRead bool `yaml:"allow_anonymous_read"` // Allow unauthenticated read access to KV endpoints AllowAnonymousWrite bool `yaml:"allow_anonymous_write"` // Allow unauthenticated write access to KV endpoints -} \ No newline at end of file +}