[#726] Use client time on regular requests

Use `X-Amz-Date` header as `now` when
* compute expiration epoch
* set Timestamp for object and container
* forming locks
* send notifications

Signed-off-by: Denis Kirillov <denis@nspcc.ru>
This commit is contained in:
Denis Kirillov 2022-11-08 12:12:55 +03:00 committed by Alex Vanin
parent d3702f86d1
commit 094eb12578
19 changed files with 106 additions and 43 deletions

View file

@ -33,7 +33,13 @@ var postPolicyCredentialRegexp = regexp.MustCompile(`(?P<access_key_id>[^/]+)/(?
type ( type (
// Center is a user authentication interface. // Center is a user authentication interface.
Center interface { Center interface {
Authenticate(request *http.Request) (*accessbox.Box, error) Authenticate(request *http.Request) (*Box, error)
}
// Box contains access box and additional info.
Box struct {
AccessBox *accessbox.Box
ClientTime time.Time
} }
center struct { center struct {
@ -126,11 +132,12 @@ func (a *authHeader) getAddress() (oid.Address, error) {
return addr, nil return addr, nil
} }
func (c *center) Authenticate(r *http.Request) (*accessbox.Box, error) { func (c *center) Authenticate(r *http.Request) (*Box, error) {
var ( var (
err error err error
authHdr *authHeader authHdr *authHeader
signatureDateTimeStr string signatureDateTimeStr string
needClientTime bool
) )
queryValues := r.URL.Query() queryValues := r.URL.Query()
@ -166,6 +173,7 @@ func (c *center) Authenticate(r *http.Request) (*accessbox.Box, error) {
return nil, err return nil, err
} }
signatureDateTimeStr = r.Header.Get(AmzDate) signatureDateTimeStr = r.Header.Get(AmzDate)
needClientTime = true
} }
signatureDateTime, err := time.Parse("20060102T150405Z", signatureDateTimeStr) signatureDateTime, err := time.Parse("20060102T150405Z", signatureDateTimeStr)
@ -192,7 +200,12 @@ func (c *center) Authenticate(r *http.Request) (*accessbox.Box, error) {
return nil, err return nil, err
} }
return box, nil result := &Box{AccessBox: box}
if needClientTime {
result.ClientTime = signatureDateTime
}
return result, nil
} }
func (c center) checkAccessKeyID(accessKeyID string) error { func (c center) checkAccessKeyID(accessKeyID string) error {
@ -209,7 +222,7 @@ func (c center) checkAccessKeyID(accessKeyID string) error {
return apiErrors.GetAPIError(apiErrors.ErrAccessDenied) return apiErrors.GetAPIError(apiErrors.ErrAccessDenied)
} }
func (c *center) checkFormData(r *http.Request) (*accessbox.Box, error) { func (c *center) checkFormData(r *http.Request) (*Box, error) {
if err := r.ParseMultipartForm(maxFormSizeMemory); err != nil { if err := r.ParseMultipartForm(maxFormSizeMemory); err != nil {
return nil, apiErrors.GetAPIError(apiErrors.ErrInvalidArgument) return nil, apiErrors.GetAPIError(apiErrors.ErrInvalidArgument)
} }
@ -251,7 +264,7 @@ func (c *center) checkFormData(r *http.Request) (*accessbox.Box, error) {
return nil, apiErrors.GetAPIError(apiErrors.ErrSignatureDoesNotMatch) return nil, apiErrors.GetAPIError(apiErrors.ErrSignatureDoesNotMatch)
} }
return box, nil return &Box{AccessBox: box}, nil
} }
func cloneRequest(r *http.Request, authHeader *authHeader) *http.Request { func cloneRequest(r *http.Request, authHeader *authHeader) *http.Request {

View file

@ -2,6 +2,7 @@ package handler
import ( import (
"errors" "errors"
"time"
"github.com/nspcc-dev/neofs-s3-gw/api" "github.com/nspcc-dev/neofs-s3-gw/api"
"github.com/nspcc-dev/neofs-s3-gw/api/layer" "github.com/nspcc-dev/neofs-s3-gw/api/layer"
@ -19,7 +20,7 @@ type (
Notificator interface { Notificator interface {
SendNotifications(topics map[string]string, p *SendNotificationParams) error SendNotifications(topics map[string]string, p *SendNotificationParams) error
SendTestNotification(topic, bucketName, requestID, HostID string) error SendTestNotification(topic, bucketName, requestID, HostID string, now time.Time) error
} }
// Config contains data which handler needs to keep. // Config contains data which handler needs to keep.

View file

@ -184,7 +184,7 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
CopiesNuber: copiesNumber, CopiesNuber: copiesNumber,
} }
params.Lock, err = formObjectLock(dstBktInfo, settings.LockConfiguration, r.Header) params.Lock, err = formObjectLock(r.Context(), dstBktInfo, settings.LockConfiguration, r.Header)
if err != nil { if err != nil {
h.logAndSendError(w, "could not form object lock", reqInfo, err) h.logAndSendError(w, "could not form object lock", reqInfo, err)
return return

View file

@ -1,6 +1,7 @@
package handler package handler
import ( import (
"context"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"net/http" "net/http"
@ -208,7 +209,7 @@ func (h *handler) PutObjectRetentionHandler(w http.ResponseWriter, r *http.Reque
return return
} }
lock, err := formObjectLockFromRetention(retention, r.Header) lock, err := formObjectLockFromRetention(r.Context(), retention, r.Header)
if err != nil { if err != nil {
h.logAndSendError(w, "invalid retention configuration", reqInfo, err) h.logAndSendError(w, "invalid retention configuration", reqInfo, err)
return return
@ -300,7 +301,7 @@ func checkLockConfiguration(conf *data.ObjectLockConfiguration) error {
return nil return nil
} }
func formObjectLock(bktInfo *data.BucketInfo, defaultConfig *data.ObjectLockConfiguration, header http.Header) (*data.ObjectLock, error) { func formObjectLock(ctx context.Context, bktInfo *data.BucketInfo, defaultConfig *data.ObjectLockConfiguration, header http.Header) (*data.ObjectLock, error) {
if !bktInfo.ObjectLockEnabled { if !bktInfo.ObjectLockEnabled {
if existLockHeaders(header) { if existLockHeaders(header) {
return nil, apiErrors.GetAPIError(apiErrors.ErrObjectLockConfigurationNotFound) return nil, apiErrors.GetAPIError(apiErrors.ErrObjectLockConfigurationNotFound)
@ -318,7 +319,7 @@ func formObjectLock(bktInfo *data.BucketInfo, defaultConfig *data.ObjectLockConf
retention := &data.RetentionLock{} retention := &data.RetentionLock{}
defaultRetention := defaultConfig.Rule.DefaultRetention defaultRetention := defaultConfig.Rule.DefaultRetention
retention.IsCompliance = defaultRetention.Mode == complianceMode retention.IsCompliance = defaultRetention.Mode == complianceMode
now := time.Now() now := layer.TimeNow(ctx)
if defaultRetention.Days != 0 { if defaultRetention.Days != 0 {
retention.Until = now.Add(time.Duration(defaultRetention.Days) * dayDuration) retention.Until = now.Add(time.Duration(defaultRetention.Days) * dayDuration)
} else { } else {
@ -370,7 +371,7 @@ func formObjectLock(bktInfo *data.BucketInfo, defaultConfig *data.ObjectLockConf
objectLock.Retention.ByPassedGovernance = bypass objectLock.Retention.ByPassedGovernance = bypass
} }
if objectLock.Retention.Until.Before(time.Now()) { if objectLock.Retention.Until.Before(layer.TimeNow(ctx)) {
return nil, apiErrors.GetAPIError(apiErrors.ErrPastObjectLockRetainDate) return nil, apiErrors.GetAPIError(apiErrors.ErrPastObjectLockRetainDate)
} }
} }
@ -384,7 +385,7 @@ func existLockHeaders(header http.Header) bool {
header.Get(api.AmzObjectLockRetainUntilDate) != "" header.Get(api.AmzObjectLockRetainUntilDate) != ""
} }
func formObjectLockFromRetention(retention *data.Retention, header http.Header) (*data.ObjectLock, error) { func formObjectLockFromRetention(ctx context.Context, retention *data.Retention, header http.Header) (*data.ObjectLock, error) {
if retention.Mode != governanceMode && retention.Mode != complianceMode { if retention.Mode != governanceMode && retention.Mode != complianceMode {
return nil, apiErrors.GetAPIError(apiErrors.ErrMalformedXML) return nil, apiErrors.GetAPIError(apiErrors.ErrMalformedXML)
} }
@ -394,7 +395,7 @@ func formObjectLockFromRetention(retention *data.Retention, header http.Header)
return nil, apiErrors.GetAPIError(apiErrors.ErrMalformedXML) return nil, apiErrors.GetAPIError(apiErrors.ErrMalformedXML)
} }
if retentionDate.Before(time.Now()) { if retentionDate.Before(layer.TimeNow(ctx)) {
return nil, apiErrors.GetAPIError(apiErrors.ErrPastObjectLockRetainDate) return nil, apiErrors.GetAPIError(apiErrors.ErrPastObjectLockRetainDate)
} }

View file

@ -19,6 +19,8 @@ import (
const defaultURL = "http://localhost/" const defaultURL = "http://localhost/"
func TestFormObjectLock(t *testing.T) { func TestFormObjectLock(t *testing.T) {
ctx := context.Background()
for _, tc := range []struct { for _, tc := range []struct {
name string name string
bktInfo *data.BucketInfo bktInfo *data.BucketInfo
@ -73,7 +75,7 @@ func TestFormObjectLock(t *testing.T) {
}, },
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
actualObjLock, err := formObjectLock(tc.bktInfo, tc.config, tc.header) actualObjLock, err := formObjectLock(ctx, tc.bktInfo, tc.config, tc.header)
if tc.expectedError { if tc.expectedError {
require.Error(t, err) require.Error(t, err)
return return
@ -86,6 +88,8 @@ func TestFormObjectLock(t *testing.T) {
} }
func TestFormObjectLockFromRetention(t *testing.T) { func TestFormObjectLockFromRetention(t *testing.T) {
ctx := context.Background()
for _, tc := range []struct { for _, tc := range []struct {
name string name string
retention *data.Retention retention *data.Retention
@ -132,7 +136,7 @@ func TestFormObjectLockFromRetention(t *testing.T) {
}, },
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
actualObjLock, err := formObjectLockFromRetention(tc.retention, tc.header) actualObjLock, err := formObjectLockFromRetention(ctx, tc.retention, tc.header)
if tc.expectedError { if tc.expectedError {
require.Error(t, err) require.Error(t, err)
return return

View file

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/nspcc-dev/neofs-s3-gw/api" "github.com/nspcc-dev/neofs-s3-gw/api"
@ -22,6 +23,7 @@ type (
BktInfo *data.BucketInfo BktInfo *data.BucketInfo
ReqInfo *api.ReqInfo ReqInfo *api.ReqInfo
User string User string
Time time.Time
} }
NotificationConfiguration struct { NotificationConfiguration struct {
@ -107,7 +109,7 @@ func (h *handler) PutBucketNotificationHandler(w http.ResponseWriter, r *http.Re
return return
} }
if _, err = h.checkBucketConfiguration(conf, reqInfo); err != nil { if _, err = h.checkBucketConfiguration(r.Context(), conf, reqInfo); err != nil {
h.logAndSendError(w, "couldn't check bucket configuration", reqInfo, err) h.logAndSendError(w, "couldn't check bucket configuration", reqInfo, err)
return return
} }
@ -164,13 +166,15 @@ func (h *handler) sendNotifications(ctx context.Context, p *SendNotificationPara
p.User = bearer.ResolveIssuer(*box.Gate.BearerToken).EncodeToString() p.User = bearer.ResolveIssuer(*box.Gate.BearerToken).EncodeToString()
} }
p.Time = layer.TimeNow(ctx)
topics := filterSubjects(conf, p.Event, p.NotificationInfo.Name) topics := filterSubjects(conf, p.Event, p.NotificationInfo.Name)
return h.notificator.SendNotifications(topics, p) return h.notificator.SendNotifications(topics, p)
} }
// checkBucketConfiguration checks notification configuration and generates an ID for configurations with empty ids. // checkBucketConfiguration checks notification configuration and generates an ID for configurations with empty ids.
func (h *handler) checkBucketConfiguration(conf *data.NotificationConfiguration, r *api.ReqInfo) (completed bool, err error) { func (h *handler) checkBucketConfiguration(ctx context.Context, conf *data.NotificationConfiguration, r *api.ReqInfo) (completed bool, err error) {
if conf == nil { if conf == nil {
return return
} }
@ -189,7 +193,7 @@ func (h *handler) checkBucketConfiguration(conf *data.NotificationConfiguration,
} }
if h.cfg.NotificatorEnabled { if h.cfg.NotificatorEnabled {
if err = h.notificator.SendTestNotification(q.QueueArn, r.BucketName, r.RequestID, r.Host); err != nil { if err = h.notificator.SendTestNotification(q.QueueArn, r.BucketName, r.RequestID, r.Host, layer.TimeNow(ctx)); err != nil {
return return
} }
} else { } else {

View file

@ -240,7 +240,7 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
params.Lock, err = formObjectLock(bktInfo, settings.LockConfiguration, r.Header) params.Lock, err = formObjectLock(r.Context(), bktInfo, settings.LockConfiguration, r.Header)
if err != nil { if err != nil {
h.logAndSendError(w, "could not form object lock", reqInfo, err) h.logAndSendError(w, "could not form object lock", reqInfo, err)
return return

View file

@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"strconv" "strconv"
"time"
"github.com/nspcc-dev/neofs-s3-gw/api" "github.com/nspcc-dev/neofs-s3-gw/api"
"github.com/nspcc-dev/neofs-s3-gw/api/data" "github.com/nspcc-dev/neofs-s3-gw/api/data"
@ -116,7 +115,7 @@ func (n *layer) createContainer(ctx context.Context, p *CreateBucketParams) (*da
bktInfo := &data.BucketInfo{ bktInfo := &data.BucketInfo{
Name: p.Name, Name: p.Name,
Owner: ownerID, Owner: ownerID,
Created: time.Now(), // this can be a little incorrect since the real time is set later Created: TimeNow(ctx),
LocationConstraint: p.LocationConstraint, LocationConstraint: p.LocationConstraint,
ObjectLockEnabled: p.ObjectLockEnabled, ObjectLockEnabled: p.ObjectLockEnabled,
} }
@ -138,6 +137,7 @@ func (n *layer) createContainer(ctx context.Context, p *CreateBucketParams) (*da
Policy: p.Policy, Policy: p.Policy,
Name: p.Name, Name: p.Name,
SessionToken: p.SessionContainerCreation, SessionToken: p.SessionContainerCreation,
CreationTime: bktInfo.Created,
AdditionalAttributes: attributes, AdditionalAttributes: attributes,
}) })
if err != nil { if err != nil {

View file

@ -41,6 +41,7 @@ func (n *layer) PutBucketCORS(ctx context.Context, p *PutCORSParams) error {
Creator: p.BktInfo.Owner, Creator: p.BktInfo.Owner,
Payload: p.Reader, Payload: p.Reader,
Filepath: p.BktInfo.CORSObjectName(), Filepath: p.BktInfo.CORSObjectName(),
CreationTime: TimeNow(ctx),
CopiesNumber: p.CopiesNumber, CopiesNumber: p.CopiesNumber,
} }

View file

@ -306,6 +306,15 @@ func IsAuthenticatedRequest(ctx context.Context) bool {
return ok return ok
} }
// TimeNow returns client time from request or time.Now().
func TimeNow(ctx context.Context) time.Time {
if now, ok := ctx.Value(api.ClientTime).(time.Time); ok {
return now
}
return time.Now()
}
// Owner returns owner id from BearerToken (context) or from client owner. // Owner returns owner id from BearerToken (context) or from client owner.
func (n *layer) Owner(ctx context.Context) user.ID { func (n *layer) Owner(ctx context.Context) user.ID {
if bd, ok := ctx.Value(api.BoxData).(*accessbox.Box); ok && bd != nil && bd.Gate != nil && bd.Gate.BearerToken != nil { if bd, ok := ctx.Value(api.BoxData).(*accessbox.Box); ok && bd != nil && bd.Gate != nil && bd.Gate.BearerToken != nil {
@ -565,7 +574,7 @@ func (n *layer) deleteObject(ctx context.Context, bkt *data.BucketInfo, settings
FilePath: obj.Name, FilePath: obj.Name,
}, },
DeleteMarker: &data.DeleteMarkerInfo{ DeleteMarker: &data.DeleteMarkerInfo{
Created: time.Now(), Created: TimeNow(ctx),
Owner: n.Owner(ctx), Owner: n.Owner(ctx),
}, },
IsUnversioned: settings.VersioningSuspended(), IsUnversioned: settings.VersioningSuspended(),

View file

@ -143,7 +143,7 @@ func (n *layer) CreateMultipartUpload(ctx context.Context, p *CreateMultipartPar
Key: p.Info.Key, Key: p.Info.Key,
UploadID: p.Info.UploadID, UploadID: p.Info.UploadID,
Owner: n.Owner(ctx), Owner: n.Owner(ctx),
Created: time.Now(), Created: TimeNow(ctx),
Meta: make(map[string]string, metaSize), Meta: make(map[string]string, metaSize),
CopiesNumber: p.CopiesNumber, CopiesNumber: p.CopiesNumber,
} }
@ -205,6 +205,7 @@ func (n *layer) uploadPart(ctx context.Context, multipartInfo *data.MultipartInf
Creator: bktInfo.Owner, Creator: bktInfo.Owner,
Attributes: make([][2]string, 2), Attributes: make([][2]string, 2),
Payload: p.Reader, Payload: p.Reader,
CreationTime: TimeNow(ctx),
CopiesNumber: multipartInfo.CopiesNumber, CopiesNumber: multipartInfo.CopiesNumber,
} }
@ -234,7 +235,7 @@ func (n *layer) uploadPart(ctx context.Context, multipartInfo *data.MultipartInf
OID: id, OID: id,
Size: decSize, Size: decSize,
ETag: hex.EncodeToString(hash), ETag: hex.EncodeToString(hash),
Created: time.Now(), Created: prm.CreationTime,
} }
oldPartID, err := n.treeService.AddPart(ctx, bktInfo, multipartInfo.ID, partInfo) oldPartID, err := n.treeService.AddPart(ctx, bktInfo, multipartInfo.ID, partInfo)

View file

@ -30,6 +30,9 @@ type PrmContainerCreate struct {
// Name for the container. // Name for the container.
Name string Name string
// CreationTime value for Timestamp attribute
CreationTime time.Time
// Token of the container's creation session. Nil means session absence. // Token of the container's creation session. Nil means session absence.
SessionToken *session.Container SessionToken *session.Container
@ -94,6 +97,9 @@ type PrmObjectCreate struct {
// Key-value object attributes. // Key-value object attributes.
Attributes [][2]string Attributes [][2]string
// Value for Timestamp attribute (optional).
CreationTime time.Time
// List of ids to lock (optional). // List of ids to lock (optional).
Locks []oid.ID Locks []oid.ID
@ -204,11 +210,11 @@ type NeoFS interface {
// It returns any error encountered which prevented the removal request from being sent. // It returns any error encountered which prevented the removal request from being sent.
DeleteObject(context.Context, PrmObjectDelete) error DeleteObject(context.Context, PrmObjectDelete) error
// TimeToEpoch computes current epoch and the epoch that corresponds to the provided time. // TimeToEpoch computes current epoch and the epoch that corresponds to the provided now and future time.
// Note: // Note:
// * time must be in the future // * future time must be after the now
// * time will be ceil rounded to match epoch // * future time will be ceil rounded to match epoch
// //
// It returns any error encountered which prevented computing epochs. // It returns any error encountered which prevented computing epochs.
TimeToEpoch(context.Context, time.Time) (uint64, uint64, error) TimeToEpoch(ctx context.Context, now time.Time, future time.Time) (uint64, uint64, error)
} }

View file

@ -75,7 +75,12 @@ func (t *TestNeoFS) CreateContainer(_ context.Context, prm PrmContainerCreate) (
cnr.SetOwner(prm.Creator) cnr.SetOwner(prm.Creator)
cnr.SetPlacementPolicy(prm.Policy) cnr.SetPlacementPolicy(prm.Policy)
cnr.SetBasicACL(prm.BasicACL) cnr.SetBasicACL(prm.BasicACL)
container.SetCreationTime(&cnr, time.Now())
creationTime := prm.CreationTime
if creationTime.IsZero() {
creationTime = time.Now()
}
container.SetCreationTime(&cnr, creationTime)
if prm.Name != "" { if prm.Name != "" {
var d container.Domain var d container.Domain
@ -235,8 +240,8 @@ func (t *TestNeoFS) DeleteObject(ctx context.Context, prm PrmObjectDelete) error
return nil return nil
} }
func (t *TestNeoFS) TimeToEpoch(_ context.Context, futureTime time.Time) (uint64, uint64, error) { func (t *TestNeoFS) TimeToEpoch(_ context.Context, now, futureTime time.Time) (uint64, uint64, error) {
return t.currentEpoch, t.currentEpoch + uint64(futureTime.Second()), nil return t.currentEpoch, t.currentEpoch + uint64(futureTime.Sub(now).Seconds()), nil
} }
func (t *TestNeoFS) AllObjects(cnrID cid.ID) []oid.ID { func (t *TestNeoFS) AllObjects(cnrID cid.ID) []oid.ID {

View file

@ -30,6 +30,7 @@ func (n *layer) PutBucketNotificationConfiguration(ctx context.Context, p *PutBu
Creator: p.BktInfo.Owner, Creator: p.BktInfo.Owner,
Payload: bytes.NewReader(confXML), Payload: bytes.NewReader(confXML),
Filepath: p.BktInfo.NotificationConfigurationObjectName(), Filepath: p.BktInfo.NotificationConfigurationObjectName(),
CreationTime: TimeNow(ctx),
CopiesNumber: p.CopiesNumber, CopiesNumber: p.CopiesNumber,
} }

View file

@ -13,7 +13,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time"
"github.com/minio/sio" "github.com/minio/sio"
"github.com/nspcc-dev/neofs-s3-gw/api" "github.com/nspcc-dev/neofs-s3-gw/api"
@ -234,6 +233,7 @@ func (n *layer) PutObject(ctx context.Context, p *PutObjectParams) (*data.Extend
PayloadSize: uint64(p.Size), PayloadSize: uint64(p.Size),
Filepath: p.Object, Filepath: p.Object,
Payload: r, Payload: r,
CreationTime: TimeNow(ctx),
CopiesNumber: p.CopiesNumber, CopiesNumber: p.CopiesNumber,
} }
@ -281,7 +281,7 @@ func (n *layer) PutObject(ctx context.Context, p *PutObjectParams) (*data.Extend
Bucket: p.BktInfo.Name, Bucket: p.BktInfo.Name,
Name: p.Object, Name: p.Object,
Size: p.Size, Size: p.Size,
Created: time.Now(), Created: prm.CreationTime,
Headers: p.Header, Headers: p.Header,
ContentType: p.Header[api.ContentType], ContentType: p.Header[api.ContentType],
HashSum: newVersion.ETag, HashSum: newVersion.ETag,

View file

@ -116,6 +116,7 @@ func (n *layer) putLockObject(ctx context.Context, bktInfo *data.BucketInfo, obj
Container: bktInfo.CID, Container: bktInfo.CID,
Creator: bktInfo.Owner, Creator: bktInfo.Owner,
Locks: []oid.ID{objID}, Locks: []oid.ID{objID},
CreationTime: TimeNow(ctx),
CopiesNumber: copiesNumber, CopiesNumber: copiesNumber,
} }
@ -227,7 +228,7 @@ func (n *layer) attributesFromLock(ctx context.Context, lock *data.ObjectLock) (
) )
if lock.Retention != nil { if lock.Retention != nil {
if _, expEpoch, err = n.neoFS.TimeToEpoch(ctx, lock.Retention.Until); err != nil { if _, expEpoch, err = n.neoFS.TimeToEpoch(ctx, TimeNow(ctx), lock.Retention.Until); err != nil {
return nil, fmt.Errorf("fetch time to epoch: %w", err) return nil, fmt.Errorf("fetch time to epoch: %w", err)
} }

View file

@ -197,11 +197,11 @@ func (c *Controller) SendNotifications(topics map[string]string, p *handler.Send
return nil return nil
} }
func (c *Controller) SendTestNotification(topic, bucketName, requestID, HostID string) error { func (c *Controller) SendTestNotification(topic, bucketName, requestID, HostID string, now time.Time) error {
event := &TestEvent{ event := &TestEvent{
Service: "NeoFS S3", Service: "NeoFS S3",
Event: "s3:TestEvent", Event: "s3:TestEvent",
Time: time.Now(), Time: now,
Bucket: bucketName, Bucket: bucketName,
RequestID: requestID, RequestID: requestID,
HostID: HostID, HostID: HostID,
@ -222,7 +222,7 @@ func prepareEvent(p *handler.SendNotificationParams) *Event {
EventVersion: EventVersion21, EventVersion: EventVersion21,
EventSource: "neofs:s3", EventSource: "neofs:s3",
AWSRegion: "", AWSRegion: "",
EventTime: time.Now(), EventTime: p.Time,
EventName: p.Event, EventName: p.Event,
UserIdentity: UserIdentity{ UserIdentity: UserIdentity{
PrincipalID: p.User, PrincipalID: p.User,

View file

@ -16,6 +16,9 @@ type KeyWrapper string
// BoxData is an ID used to store accessbox.Box in a context. // BoxData is an ID used to store accessbox.Box in a context.
var BoxData = KeyWrapper("__context_box_key") var BoxData = KeyWrapper("__context_box_key")
// ClientTime is an ID used to store client time.Time in a context.
var ClientTime = KeyWrapper("__context_client_time")
// AttachUserAuth adds user authentication via center to router using log for logging. // AttachUserAuth adds user authentication via center to router using log for logging.
func AttachUserAuth(router *mux.Router, center auth.Center, log *zap.Logger) { func AttachUserAuth(router *mux.Router, center auth.Center, log *zap.Logger) {
router.Use(func(h http.Handler) http.Handler { router.Use(func(h http.Handler) http.Handler {
@ -35,7 +38,10 @@ func AttachUserAuth(router *mux.Router, center auth.Center, log *zap.Logger) {
return return
} }
} else { } else {
ctx = context.WithValue(r.Context(), BoxData, box) ctx = context.WithValue(r.Context(), BoxData, box.AccessBox)
if !box.ClientTime.IsZero() {
ctx = context.WithValue(ctx, ClientTime, box.ClientTime)
}
} }
h.ServeHTTP(w, r.WithContext(ctx)) h.ServeHTTP(w, r.WithContext(ctx))

View file

@ -52,8 +52,7 @@ func NewNeoFS(p *pool.Pool) *NeoFS {
} }
// TimeToEpoch implements neofs.NeoFS interface method. // TimeToEpoch implements neofs.NeoFS interface method.
func (x *NeoFS) TimeToEpoch(ctx context.Context, futureTime time.Time) (uint64, uint64, error) { func (x *NeoFS) TimeToEpoch(ctx context.Context, now, futureTime time.Time) (uint64, uint64, error) {
now := time.Now()
dur := futureTime.Sub(now) dur := futureTime.Sub(now)
if dur < 0 { if dur < 0 {
return 0, 0, fmt.Errorf("time '%s' must be in the future (after %s)", return 0, 0, fmt.Errorf("time '%s' must be in the future (after %s)",
@ -116,7 +115,12 @@ func (x *NeoFS) CreateContainer(ctx context.Context, prm layer.PrmContainerCreat
cnr.SetPlacementPolicy(prm.Policy) cnr.SetPlacementPolicy(prm.Policy)
cnr.SetOwner(prm.Creator) cnr.SetOwner(prm.Creator)
cnr.SetBasicACL(prm.BasicACL) cnr.SetBasicACL(prm.BasicACL)
container.SetCreationTime(&cnr, time.Now())
creationTime := prm.CreationTime
if creationTime.IsZero() {
creationTime = time.Now()
}
container.SetCreationTime(&cnr, creationTime)
if prm.Name != "" { if prm.Name != "" {
var d container.Domain var d container.Domain
@ -227,7 +231,13 @@ func (x *NeoFS) CreateObject(ctx context.Context, prm layer.PrmObjectCreate) (oi
a = object.NewAttribute() a = object.NewAttribute()
a.SetKey(object.AttributeTimestamp) a.SetKey(object.AttributeTimestamp)
a.SetValue(strconv.FormatInt(time.Now().Unix(), 10))
creationTime := prm.CreationTime
if creationTime.IsZero() {
creationTime = time.Now()
}
a.SetValue(strconv.FormatInt(creationTime.Unix(), 10))
attrs = append(attrs, *a) attrs = append(attrs, *a)
for i := range prm.Attributes { for i := range prm.Attributes {
@ -489,7 +499,7 @@ func (x *AuthmateNeoFS) ContainerExists(ctx context.Context, idCnr cid.ID) error
// TimeToEpoch implements authmate.NeoFS interface method. // TimeToEpoch implements authmate.NeoFS interface method.
func (x *AuthmateNeoFS) TimeToEpoch(ctx context.Context, futureTime time.Time) (uint64, uint64, error) { func (x *AuthmateNeoFS) TimeToEpoch(ctx context.Context, futureTime time.Time) (uint64, uint64, error) {
return x.neoFS.TimeToEpoch(ctx, futureTime) return x.neoFS.TimeToEpoch(ctx, time.Now(), futureTime)
} }
// CreateContainer implements authmate.NeoFS interface method. // CreateContainer implements authmate.NeoFS interface method.