github.com/ethersphere/bee/v2@v2.2.0/pkg/accesscontrol/history.go (about)

     1  // Copyright 2024 The Swarm Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package accesscontrol
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"math"
    12  	"strconv"
    13  	"time"
    14  
    15  	"github.com/ethersphere/bee/v2/pkg/file"
    16  	"github.com/ethersphere/bee/v2/pkg/manifest"
    17  	"github.com/ethersphere/bee/v2/pkg/manifest/mantaray"
    18  	"github.com/ethersphere/bee/v2/pkg/swarm"
    19  )
    20  
    21  var (
    22  	// ErrEndIteration indicates that the iteration terminated.
    23  	ErrEndIteration = errors.New("end iteration")
    24  	// ErrUnexpectedType indicates that an error occurred during the mantary-manifest creation.
    25  	ErrUnexpectedType = errors.New("unexpected type")
    26  	// ErrInvalidTimestamp indicates that the timestamp given to Lookup is invalid.
    27  	ErrInvalidTimestamp = errors.New("invalid timestamp")
    28  	// ErrNotFound is returned when an Entry is not found in the history.
    29  	ErrNotFound = errors.New("access control: not found")
    30  )
    31  
    32  // History represents the interface for managing access control history.
    33  type History interface {
    34  	// Add adds a new entry to the access control history with the given timestamp and metadata.
    35  	Add(ctx context.Context, ref swarm.Address, timestamp *int64, metadata *map[string]string) error
    36  	// Lookup retrieves the entry from the history based on the given timestamp or returns error if not found.
    37  	Lookup(ctx context.Context, timestamp int64) (manifest.Entry, error)
    38  	// Store stores the history to the underlying storage and returns the reference.
    39  	Store(ctx context.Context) (swarm.Address, error)
    40  }
    41  
    42  var _ History = (*HistoryStruct)(nil)
    43  
    44  // manifestInterface extends the `manifest.Interface` interface and adds a `Root` method.
    45  type manifestInterface interface {
    46  	manifest.Interface
    47  	Root() *mantaray.Node
    48  }
    49  
    50  // HistoryStruct represents an access control history with a mantaray-based manifest.
    51  type HistoryStruct struct {
    52  	manifest manifestInterface
    53  	ls       file.LoadSaver
    54  }
    55  
    56  // NewHistory creates a new history with a mantaray-based manifest.
    57  func NewHistory(ls file.LoadSaver) (*HistoryStruct, error) {
    58  	m, err := manifest.NewMantarayManifest(ls, false)
    59  	if err != nil {
    60  		return nil, fmt.Errorf("failed to create mantaray manifest: %w", err)
    61  	}
    62  
    63  	mm, ok := m.(manifestInterface)
    64  	if !ok {
    65  		return nil, fmt.Errorf("%w: expected MantarayManifest, got %T", ErrUnexpectedType, m)
    66  	}
    67  
    68  	return &HistoryStruct{manifest: mm, ls: ls}, nil
    69  }
    70  
    71  // NewHistoryReference loads a history with a mantaray-based manifest.
    72  func NewHistoryReference(ls file.LoadSaver, ref swarm.Address) (*HistoryStruct, error) {
    73  	m, err := manifest.NewMantarayManifestReference(ref, ls)
    74  	if err != nil {
    75  		return nil, fmt.Errorf("failed to create mantaray manifest reference: %w", err)
    76  	}
    77  
    78  	mm, ok := m.(manifestInterface)
    79  	if !ok {
    80  		return nil, fmt.Errorf("%w: expected MantarayManifest, got %T", ErrUnexpectedType, m)
    81  	}
    82  
    83  	return &HistoryStruct{manifest: mm, ls: ls}, nil
    84  }
    85  
    86  // Add adds a new entry to the access control history with the given timestamp and metadata.
    87  func (h *HistoryStruct) Add(ctx context.Context, ref swarm.Address, timestamp *int64, metadata *map[string]string) error {
    88  	mtdt := map[string]string{}
    89  	if metadata != nil {
    90  		mtdt = *metadata
    91  	}
    92  	// add timestamps transformed so that the latests timestamp becomes the smallest key.
    93  	unixTime := time.Now().Unix()
    94  	if timestamp != nil {
    95  		unixTime = *timestamp
    96  	}
    97  
    98  	key := strconv.FormatInt(math.MaxInt64-unixTime, 10)
    99  	err := h.manifest.Add(ctx, key, manifest.NewEntry(ref, mtdt))
   100  	if err != nil {
   101  		return fmt.Errorf("failed to add to manifest: %w", err)
   102  	}
   103  
   104  	return nil
   105  }
   106  
   107  // Lookup retrieves the entry from the history based on the given timestamp or returns error if not found.
   108  func (h *HistoryStruct) Lookup(ctx context.Context, timestamp int64) (manifest.Entry, error) {
   109  	if timestamp <= 0 {
   110  		return manifest.NewEntry(swarm.ZeroAddress, map[string]string{}), ErrInvalidTimestamp
   111  	}
   112  
   113  	reversedTimestamp := math.MaxInt64 - timestamp
   114  	node, err := h.lookupNode(ctx, reversedTimestamp)
   115  	if err != nil {
   116  		switch {
   117  		case errors.Is(err, manifest.ErrNotFound):
   118  			return manifest.NewEntry(swarm.ZeroAddress, map[string]string{}), ErrNotFound
   119  		default:
   120  			return manifest.NewEntry(swarm.ZeroAddress, map[string]string{}), err
   121  		}
   122  	}
   123  
   124  	if node != nil {
   125  		return manifest.NewEntry(swarm.NewAddress(node.Entry()), node.Metadata()), nil
   126  	}
   127  
   128  	return manifest.NewEntry(swarm.ZeroAddress, map[string]string{}), ErrNotFound
   129  }
   130  
   131  func (h *HistoryStruct) lookupNode(ctx context.Context, searchedTimestamp int64) (*mantaray.Node, error) {
   132  	// before node's timestamp is the closest one that is less than or equal to the searched timestamp
   133  	// for instance: 2030, 2020, 1994 -> search for 2021 -> before is 2020
   134  	var beforeNode *mantaray.Node
   135  	// after node's timestamp is after the latest
   136  	// for instance: 2030, 2020, 1994 -> search for 1980 -> after is 1994
   137  	var afterNode *mantaray.Node
   138  
   139  	walker := func(pathTimestamp []byte, currNode *mantaray.Node, err error) error {
   140  		if err != nil {
   141  			return err
   142  		}
   143  
   144  		if currNode.IsValueType() && len(currNode.Entry()) > 0 {
   145  			afterNode = currNode
   146  
   147  			match, err := isBeforeMatch(pathTimestamp, searchedTimestamp)
   148  			if match {
   149  				beforeNode = currNode
   150  				// return error to stop the walk, this is how WalkNode works...
   151  				return ErrEndIteration
   152  			}
   153  
   154  			return err
   155  		}
   156  
   157  		return nil
   158  	}
   159  
   160  	rootNode := h.manifest.Root()
   161  	err := rootNode.WalkNode(ctx, []byte{}, h.ls, walker)
   162  
   163  	if err != nil && !errors.Is(err, ErrEndIteration) {
   164  		return nil, fmt.Errorf("history lookup node error: %w", err)
   165  	}
   166  
   167  	if beforeNode != nil {
   168  		return beforeNode, nil
   169  	}
   170  	if afterNode != nil {
   171  		return afterNode, nil
   172  	}
   173  	return nil, nil
   174  }
   175  
   176  // Store stores the history to the underlying storage and returns the reference.
   177  func (h *HistoryStruct) Store(ctx context.Context) (swarm.Address, error) {
   178  	return h.manifest.Store(ctx)
   179  }
   180  
   181  func bytesToInt64(b []byte) (int64, error) {
   182  	num, err := strconv.ParseInt(string(b), 10, 64)
   183  	if err != nil {
   184  		return -1, err
   185  	}
   186  
   187  	return num, nil
   188  }
   189  
   190  func isBeforeMatch(pathTimestamp []byte, searchedTimestamp int64) (bool, error) {
   191  	targetTimestamp, err := bytesToInt64(pathTimestamp)
   192  	if err != nil {
   193  		return false, err
   194  	}
   195  	if targetTimestamp == 0 {
   196  		return false, nil
   197  	}
   198  	return searchedTimestamp <= targetTimestamp, nil
   199  }