forked from TrueCloudLab/frostfs-rest-gw
Alex Vanin
fa2bcf198f
Previous implementation does not provide 'at least' lifetime guarantee. Signed-off-by: Alex Vanin <a.vanin@yadro.com>
221 lines
6.5 KiB
Go
221 lines
6.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"time"
|
|
|
|
objectv2 "github.com/nspcc-dev/neofs-api-go/v2/object"
|
|
sessionv2 "github.com/nspcc-dev/neofs-api-go/v2/session"
|
|
"github.com/nspcc-dev/neofs-rest-gw/gen/models"
|
|
"github.com/nspcc-dev/neofs-sdk-go/object"
|
|
"github.com/nspcc-dev/neofs-sdk-go/pool"
|
|
)
|
|
|
|
// PrmAttributes groups parameters to form attributes from request headers.
|
|
type PrmAttributes struct {
|
|
DefaultTimestamp bool
|
|
DefaultFileName string
|
|
}
|
|
|
|
type epochDurations struct {
|
|
currentEpoch uint64
|
|
msPerBlock int64
|
|
blockPerEpoch uint64
|
|
}
|
|
|
|
const (
|
|
SystemAttributePrefix = "__NEOFS__"
|
|
|
|
ExpirationDurationAttr = SystemAttributePrefix + "EXPIRATION_DURATION"
|
|
ExpirationTimestampAttr = SystemAttributePrefix + "EXPIRATION_TIMESTAMP"
|
|
ExpirationRFC3339Attr = SystemAttributePrefix + "EXPIRATION_RFC3339"
|
|
)
|
|
|
|
// GetObjectAttributes forms object attributes from request headers.
|
|
func GetObjectAttributes(ctx context.Context, pool *pool.Pool, attrs []*models.Attribute, prm PrmAttributes) ([]object.Attribute, error) {
|
|
headers := make(map[string]string, len(attrs))
|
|
|
|
for _, attr := range attrs {
|
|
headers[*attr.Key] = *attr.Value
|
|
}
|
|
delete(headers, object.AttributeFileName)
|
|
|
|
if needParseExpiration(headers) {
|
|
epochDuration, err := getEpochDurations(ctx, pool)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not get epoch durations from network info: %w", err)
|
|
}
|
|
if err = prepareExpirationHeader(headers, epochDuration); err != nil {
|
|
return nil, fmt.Errorf("could not prepare expiration header: %w", err)
|
|
}
|
|
}
|
|
|
|
attributes := make([]object.Attribute, 0, len(headers))
|
|
for key, val := range headers {
|
|
attribute := object.NewAttribute()
|
|
attribute.SetKey(key)
|
|
attribute.SetValue(val)
|
|
attributes = append(attributes, *attribute)
|
|
}
|
|
|
|
filename := object.NewAttribute()
|
|
filename.SetKey(object.AttributeFileName)
|
|
filename.SetValue(prm.DefaultFileName)
|
|
attributes = append(attributes, *filename)
|
|
|
|
if _, ok := headers[object.AttributeTimestamp]; !ok && prm.DefaultTimestamp {
|
|
timestamp := object.NewAttribute()
|
|
timestamp.SetKey(object.AttributeTimestamp)
|
|
timestamp.SetValue(strconv.FormatInt(time.Now().Unix(), 10))
|
|
attributes = append(attributes, *timestamp)
|
|
}
|
|
|
|
return attributes, nil
|
|
}
|
|
|
|
func getEpochDurations(ctx context.Context, p *pool.Pool) (*epochDurations, error) {
|
|
networkInfo, err := p.NetworkInfo(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res := &epochDurations{
|
|
currentEpoch: networkInfo.CurrentEpoch(),
|
|
msPerBlock: networkInfo.MsPerBlock(),
|
|
blockPerEpoch: networkInfo.EpochDuration(),
|
|
}
|
|
|
|
if res.blockPerEpoch == 0 {
|
|
return nil, fmt.Errorf("EpochDuration is zero")
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func needParseExpiration(headers map[string]string) bool {
|
|
_, ok1 := headers[ExpirationDurationAttr]
|
|
_, ok2 := headers[ExpirationRFC3339Attr]
|
|
_, ok3 := headers[ExpirationTimestampAttr]
|
|
return ok1 || ok2 || ok3
|
|
}
|
|
|
|
func prepareExpirationHeader(headers map[string]string, epochDurations *epochDurations) error {
|
|
expirationInEpoch := headers[objectv2.SysAttributeExpEpoch]
|
|
|
|
if timeRFC3339, ok := headers[ExpirationRFC3339Attr]; ok {
|
|
expTime, err := time.Parse(time.RFC3339, timeRFC3339)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't parse value %s of header %s", timeRFC3339, ExpirationRFC3339Attr)
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
if expTime.Before(now) {
|
|
return fmt.Errorf("value %s of header %s must be in the future", timeRFC3339, ExpirationRFC3339Attr)
|
|
}
|
|
updateExpirationHeader(headers, epochDurations, expTime.Sub(now))
|
|
delete(headers, ExpirationRFC3339Attr)
|
|
}
|
|
|
|
if timestamp, ok := headers[ExpirationTimestampAttr]; ok {
|
|
value, err := strconv.ParseInt(timestamp, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't parse value %s of header %s", timestamp, ExpirationTimestampAttr)
|
|
}
|
|
expTime := time.Unix(value, 0)
|
|
|
|
now := time.Now()
|
|
if expTime.Before(now) {
|
|
return fmt.Errorf("value %s of header %s must be in the future", timestamp, ExpirationTimestampAttr)
|
|
}
|
|
updateExpirationHeader(headers, epochDurations, expTime.Sub(now))
|
|
delete(headers, ExpirationTimestampAttr)
|
|
}
|
|
|
|
if duration, ok := headers[ExpirationDurationAttr]; ok {
|
|
expDuration, err := time.ParseDuration(duration)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't parse value %s of header %s", duration, ExpirationDurationAttr)
|
|
}
|
|
if expDuration <= 0 {
|
|
return fmt.Errorf("value %s of header %s must be positive", expDuration, ExpirationDurationAttr)
|
|
}
|
|
updateExpirationHeader(headers, epochDurations, expDuration)
|
|
delete(headers, ExpirationDurationAttr)
|
|
}
|
|
|
|
if expirationInEpoch != "" {
|
|
headers[objectv2.SysAttributeExpEpoch] = expirationInEpoch
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func updateExpirationHeader(headers map[string]string, durations *epochDurations, expDuration time.Duration) {
|
|
epochDuration := uint64(durations.msPerBlock) * durations.blockPerEpoch
|
|
currentEpoch := durations.currentEpoch
|
|
numEpoch := uint64(expDuration.Milliseconds()) / epochDuration
|
|
|
|
if uint64(expDuration.Milliseconds())%epochDuration != 0 {
|
|
numEpoch++
|
|
}
|
|
|
|
expirationEpoch := uint64(math.MaxUint64)
|
|
if numEpoch < math.MaxUint64-currentEpoch {
|
|
expirationEpoch = currentEpoch + numEpoch
|
|
}
|
|
|
|
headers[objectv2.SysAttributeExpEpoch] = strconv.FormatUint(expirationEpoch, 10)
|
|
}
|
|
|
|
// IsObjectToken check that provided token is for object.
|
|
func IsObjectToken(token *models.Bearer) (bool, error) {
|
|
isObject := len(token.Object) != 0
|
|
isContainer := token.Container != nil
|
|
|
|
if !isObject && !isContainer {
|
|
return false, fmt.Errorf("token '%s': rules must not be empty", token.Name)
|
|
}
|
|
|
|
if isObject && isContainer {
|
|
return false, fmt.Errorf("token '%s': only one type rules can be provided: object or container, not both", token.Name)
|
|
}
|
|
|
|
return isObject, nil
|
|
}
|
|
|
|
func prepareBearerTokenHeaders(signature, key *string, isWalletConnect, isFullToken bool) (*BearerTokenHeaders, error) {
|
|
bearerHeaders := &BearerTokenHeaders{
|
|
IsWalletConnect: isWalletConnect,
|
|
IsFullToken: isFullToken,
|
|
}
|
|
if isFullToken {
|
|
return bearerHeaders, nil
|
|
}
|
|
|
|
if signature == nil || key == nil {
|
|
return nil, errors.New("missed signature or key header")
|
|
}
|
|
|
|
bearerHeaders.Signature = *signature
|
|
bearerHeaders.Key = *key
|
|
|
|
return bearerHeaders, nil
|
|
}
|
|
|
|
func formSessionTokenFromHeaders(principal *models.Principal, signature, key *string, verb sessionv2.ContainerSessionVerb) (*SessionToken, error) {
|
|
if signature == nil || key == nil {
|
|
return nil, errors.New("missed signature or key header")
|
|
}
|
|
|
|
return &SessionToken{
|
|
BearerToken: BearerToken{
|
|
Token: string(*principal),
|
|
Signature: *signature,
|
|
Key: *key,
|
|
},
|
|
Verb: verb,
|
|
}, nil
|
|
}
|