diff --git a/api/handler/locking.go b/api/handler/locking.go index 92ecd1f5a..2c7aaf5d9 100644 --- a/api/handler/locking.go +++ b/api/handler/locking.go @@ -7,12 +7,11 @@ import ( "strconv" "time" - oid "github.com/nspcc-dev/neofs-sdk-go/object/id" - "github.com/nspcc-dev/neofs-s3-gw/api" "github.com/nspcc-dev/neofs-s3-gw/api/data" apiErrors "github.com/nspcc-dev/neofs-s3-gw/api/errors" "github.com/nspcc-dev/neofs-s3-gw/api/layer" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" ) const ( @@ -282,8 +281,6 @@ func (h *handler) PutObjectRetentionHandler(w http.ResponseWriter, r *http.Reque return } - //objectv2.ReadLock() - if err = checkLockInfo(lockInfo, r.Header); err != nil { h.logAndSendError(w, "couldn't change lock mode", reqInfo, err) return diff --git a/api/layer/layer.go b/api/layer/layer.go index 21abcf7d1..44c0602a3 100644 --- a/api/layer/layer.go +++ b/api/layer/layer.go @@ -290,7 +290,7 @@ func (n *layer) Initialize(ctx context.Context, c Notificator) error { return fmt.Errorf("already initialized") } - if err := c.Subscribe(ctx, "lock", MsgHandlerFunc(n.handleLockTick)); err != nil { + if err := c.Subscribe(ctx, LockTopic, MsgHandlerFunc(n.handleLockTick)); err != nil { return fmt.Errorf("couldn't initialize layer: %w", err) } diff --git a/api/layer/locking_test.go b/api/layer/locking_test.go new file mode 100644 index 000000000..57cdad227 --- /dev/null +++ b/api/layer/locking_test.go @@ -0,0 +1,47 @@ +package layer + +import ( + "testing" + "time" + + "github.com/nspcc-dev/neofs-s3-gw/api/data" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + "github.com/stretchr/testify/require" +) + +func TestObjectLockAttributes(t *testing.T) { + tc := prepareContext(t) + err := tc.layer.PutBucketSettings(tc.ctx, &PutSettingsParams{ + BktInfo: tc.bktInfo, + Settings: &data.BucketSettings{VersioningEnabled: true}, + }) + require.NoError(t, err) + + obj := tc.putObject([]byte("content obj1 v1")) + + _, err = tc.layer.PutSystemObject(tc.ctx, &PutSystemObjectParams{ + BktInfo: tc.bktInfo, + ObjName: obj.RetentionObject(), + Metadata: make(map[string]string), + Lock: &data.ObjectLock{ + Until: time.Now(), + Objects: []oid.ID{*obj.ID}, + }, + }) + require.NoError(t, err) + + lockObj := tc.getSystemObject(obj.RetentionObject()) + require.NotNil(t, lockObj) + + tickTopic, tickEpoch := false, false + for _, attr := range lockObj.Attributes() { + if attr.Key() == AttributeSysTickEpoch { + tickEpoch = true + } else if attr.Key() == AttributeSysTickTopic { + tickTopic = true + } + } + + require.Truef(t, tickTopic, "system header __NEOFS__TICK_TOPIC presence") + require.Truef(t, tickEpoch, "system header __NEOFS__TICK_EPOCH presence") +} diff --git a/api/layer/neofs/neofs.go b/api/layer/neofs/neofs.go index c1ffe8f49..6df95933d 100644 --- a/api/layer/neofs/neofs.go +++ b/api/layer/neofs/neofs.go @@ -223,4 +223,12 @@ type NeoFS interface { // // Returns any error encountered which prevented the removal request to be sent. DeleteObject(context.Context, PrmObjectDelete) error + + // TimeToEpoch compute current epoch and epoch that corresponds provided time. + // Note: + // * time must be in the future + // * time will be ceil rounded to match epoch + // + // Returns any error encountered which prevented computing epochs. + TimeToEpoch(context.Context, time.Time) (uint64, uint64, error) } diff --git a/api/layer/system_object.go b/api/layer/system_object.go index 315e9c4ee..64b9e3f3f 100644 --- a/api/layer/system_object.go +++ b/api/layer/system_object.go @@ -20,6 +20,9 @@ import ( const ( AttributeComplianceMode = ".s3-compliance-mode" AttributeRetainUntil = ".s3-retain-until" + AttributeSysTickEpoch = "__NEOFS__TICK_EPOCH" + AttributeSysTickTopic = "__NEOFS__TICK_TOPIC" + LockTopic = "lock" ) func (n *layer) PutSystemObject(ctx context.Context, p *PutSystemObjectParams) (*data.ObjectInfo, error) { @@ -102,7 +105,13 @@ func (n *layer) putSystemObjectIntoNeoFS(ctx context.Context, p *PutSystemObject if p.Lock != nil && len(p.Lock.Objects) > 0 { prm.Locks = p.Lock.Objects - prm.Attributes = append(prm.Attributes, attributesFromLock(p.Lock)...) + + attrs, err := n.attributesFromLock(ctx, p.Lock) + if err != nil { + return nil, err + } + + prm.Attributes = append(prm.Attributes, attrs...) } prm.Attributes = append(prm.Attributes, [2]string{k, v}) @@ -278,22 +287,28 @@ func (n *layer) PutBucketSettings(ctx context.Context, p *PutSettingsParams) err return nil } -func attributesFromLock(lock *data.ObjectLock) [][2]string { +func (n *layer) attributesFromLock(ctx context.Context, lock *data.ObjectLock) ([][2]string, error) { var result [][2]string if !lock.Until.IsZero() { - attrRetainUntil := [2]string{ - AttributeRetainUntil, - lock.Until.Format(time.RFC3339), + _, exp, err := n.neoFS.TimeToEpoch(ctx, lock.Until) + if err != nil { + return nil, err } - result = append(result, attrRetainUntil) + + attrs := [][2]string{ + {AttributeSysTickEpoch, strconv.FormatUint(exp, 10)}, + {AttributeSysTickTopic, LockTopic}, + {AttributeRetainUntil, lock.Until.Format(time.RFC3339)}, + } + + result = append(result, attrs...) if lock.IsCompliance { attrCompliance := [2]string{ - AttributeComplianceMode, - strconv.FormatBool(true), + AttributeComplianceMode, strconv.FormatBool(true), } result = append(result, attrCompliance) } } - return result + return result, nil } diff --git a/authmate/authmate.go b/authmate/authmate.go index dfb298983..d6e712192 100644 --- a/authmate/authmate.go +++ b/authmate/authmate.go @@ -7,7 +7,6 @@ import ( "encoding/json" "fmt" "io" - "math" "os" "time" @@ -68,12 +67,13 @@ type NeoFS interface { // prevented the container to be created. CreateContainer(context.Context, PrmContainerCreate) (*cid.ID, error) - // NetworkState returns current state of the NeoFS network. - // Returns any error encountered which prevented state to be read. + // TimeToEpoch compute current epoch and epoch that corresponds provided time. + // Note: + // * time must be in the future + // * time will be ceil rounded to match epoch // - // Returns exactly one non-nil value. Returns any error encountered which - // prevented the state to be read. - NetworkState(context.Context) (*NetworkState, error) + // Returns any error encountered which prevented computing epochs. + TimeToEpoch(context.Context, time.Time) (uint64, uint64, error) } // Agent contains client communicating with NeoFS and logger. @@ -213,22 +213,10 @@ func (a *Agent) IssueSecret(ctx context.Context, w io.Writer, options *IssueSecr return err } - netState, err := a.neoFS.NetworkState(ctx) + lifetime.Iat, lifetime.Exp, err = a.neoFS.TimeToEpoch(ctx, time.Now().Add(options.Lifetime)) if err != nil { return err } - lifetime.Iat = netState.Epoch - msPerEpoch := netState.EpochDuration * uint64(netState.BlockDuration) - epochLifetime := uint64(options.Lifetime.Milliseconds()) / msPerEpoch - if uint64(options.Lifetime.Milliseconds())%msPerEpoch != 0 { - epochLifetime++ - } - - if epochLifetime >= math.MaxUint64-lifetime.Iat { - lifetime.Exp = math.MaxUint64 - } else { - lifetime.Exp = lifetime.Iat + epochLifetime - } gatesData, err := createTokens(options, lifetime) if err != nil { diff --git a/internal/neofs/neofs.go b/internal/neofs/neofs.go index 868a424cf..98e5551f2 100644 --- a/internal/neofs/neofs.go +++ b/internal/neofs/neofs.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "math" "strconv" "strings" "time" @@ -44,16 +45,23 @@ func (x *NeoFS) SetConnectionPool(p *pool.Pool) { x.pool = p } -// NetworkState implements authmate.NeoFS interface method. -func (x *NeoFS) NetworkState(ctx context.Context) (*authmate.NetworkState, error) { +// TimeToEpoch implements authmate.NeoFS interface method. +func (x *NeoFS) TimeToEpoch(ctx context.Context, futureTime time.Time) (uint64, uint64, error) { + now := time.Now() + dur := futureTime.Sub(now) + if dur < 0 { + return 0, 0, fmt.Errorf("time '%s' must be in the future (after %s)", + futureTime.Format(time.RFC3339), now.Format(time.RFC3339)) + } + conn, _, err := x.pool.Connection() if err != nil { - return nil, fmt.Errorf("get connection from pool: %w", err) + return 0, 0, fmt.Errorf("get connection from pool: %w", err) } res, err := conn.NetworkInfo(ctx, client.PrmNetworkInfo{}) if err != nil { - return nil, fmt.Errorf("get network info via client: %w", err) + return 0, 0, fmt.Errorf("get network info via client: %w", err) } networkInfo := res.Info() @@ -74,14 +82,25 @@ func (x *NeoFS) NetworkState(ctx context.Context) (*authmate.NetworkState, error }) if durEpoch == 0 { - return nil, errors.New("epoch duration is missing or zero") + return 0, 0, errors.New("epoch duration is missing or zero") } - return &authmate.NetworkState{ - Epoch: networkInfo.CurrentEpoch(), - BlockDuration: networkInfo.MsPerBlock(), - EpochDuration: durEpoch, - }, nil + curr := networkInfo.CurrentEpoch() + msPerEpoch := durEpoch * uint64(networkInfo.MsPerBlock()) + + epochLifetime := uint64(dur.Milliseconds()) / msPerEpoch + if uint64(dur.Milliseconds())%msPerEpoch != 0 { + epochLifetime++ + } + + var epoch uint64 + if epochLifetime >= math.MaxUint64-curr { + epoch = math.MaxUint64 + } else { + epoch = curr + epochLifetime + } + + return curr, epoch, nil } // Container reads container by ID using connection pool. Returns exact one non-nil value. diff --git a/internal/neofstest/neofs_mock.go b/internal/neofstest/neofs_mock.go index f681a3aca..7e0431bb7 100644 --- a/internal/neofstest/neofs_mock.go +++ b/internal/neofstest/neofs_mock.go @@ -9,6 +9,7 @@ import ( "io" "strconv" "strings" + "time" objectv2 "github.com/nspcc-dev/neofs-api-go/v2/object" "github.com/nspcc-dev/neofs-s3-gw/api/layer/neofs" @@ -240,6 +241,10 @@ func (t *TestNeoFS) DeleteObject(_ context.Context, prm neofs.PrmObjectDelete) e return nil } +func (t *TestNeoFS) TimeToEpoch(ctx context.Context, futureTime time.Time) (uint64, uint64, error) { + return t.currentEpoch, t.currentEpoch + uint64(futureTime.Second()), nil +} + func isMatched(attributes []*object.Attribute, filter object.SearchFilter) bool { for _, attr := range attributes { if attr.Key() == filter.Header() && attr.Value() == filter.Value() {