forked from TrueCloudLab/frostfs-s3-gw
Compare commits
4 commits
master
...
feature/pa
Author | SHA1 | Date | |
---|---|---|---|
8c3679867f | |||
31b0455882 | |||
6daa8b4698 | |||
b2682e49ea |
101 changed files with 10420 additions and 2813 deletions
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1.21 AS builder
|
||||
FROM golang:1.21 as builder
|
||||
|
||||
ARG BUILD=now
|
||||
ARG REPO=git.frostfs.info/TrueCloudLab/frostfs-s3-gw
|
||||
|
|
|
@ -6,7 +6,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go_versions: [ '1.21', '1.22' ]
|
||||
go_versions: [ '1.20', '1.21' ]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.22'
|
||||
go-version: '1.21'
|
||||
|
||||
- name: Run commit format checker
|
||||
uses: https://git.frostfs.info/TrueCloudLab/dco-go@v3
|
||||
|
|
|
@ -10,7 +10,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.22'
|
||||
go-version: '1.21'
|
||||
cache: true
|
||||
|
||||
- name: Install linters
|
||||
|
@ -24,7 +24,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go_versions: [ '1.21', '1.22' ]
|
||||
go_versions: [ '1.20', '1.21' ]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.22'
|
||||
go-version: '1.21'
|
||||
|
||||
- name: Install govulncheck
|
||||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
|
|
44
CHANGELOG.md
44
CHANGELOG.md
|
@ -4,59 +4,25 @@ This document outlines major changes between releases.
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.30.0] - Kangshung -2024-07-19
|
||||
|
||||
### Fixed
|
||||
- Fix HTTP/2 requests (#341)
|
||||
- Fix Decoder.CharsetReader is nil (#379)
|
||||
- Fix flaky ACL encode test (#340)
|
||||
- Docs grammar (#432)
|
||||
|
||||
### Added
|
||||
- Add new `reconnect_interval` config param for server rebinding (#291)
|
||||
- Support `GetBucketPolicyStatus` (#301)
|
||||
- Support request IP filter with policy (#371, #377)
|
||||
- Support tag checks in policies (#357, #365, #392, #403, #411)
|
||||
- Support tag checks in policies (#357, #365, #392)
|
||||
- Support IAM-MFA checks (#367)
|
||||
- More docs (#334, #353)
|
||||
- Add `register-user` command to `authmate` (#414)
|
||||
- `User` field in request log (#396)
|
||||
- Erasure coding support in placement policy (#400)
|
||||
- Improved test coverage (#402)
|
||||
|
||||
### Changed
|
||||
- Update dependencies noted by govulncheck (#368)
|
||||
- Improve test coverage (#380, #387)
|
||||
- Improve test coverate (#380, #387)
|
||||
- Support updated naming in native policy JSON (#385)
|
||||
- Improve determining AccessBox latest version (#335)
|
||||
- Don't set full_control policy for bucket owner (#407)
|
||||
|
||||
### Removed
|
||||
- Remove control api (#406)
|
||||
- Remove notifications (#401)
|
||||
- Remove `layer.Client` interface (#410)
|
||||
- Remove extended ACL related code (#372)
|
||||
|
||||
## [0.29.3] - 2024-07-19
|
||||
|
||||
### Fixed
|
||||
- Support tree split environment when multiple nodes
|
||||
may be part of the same sub path (#430)
|
||||
- Collision of multipart name and system data in the tree (#430)
|
||||
- Workaround for removal of multiple null versions in unversioned bucket (#430)
|
||||
|
||||
## [0.29.2] - 2024-07-03
|
||||
|
||||
### Fixed
|
||||
- Parsing of put-bucket-setting retry configuration (#398)
|
||||
|
||||
## [0.29.1] - 2024-06-20
|
||||
|
||||
### Fixed
|
||||
- OPTIONS request processing for object operations (#399)
|
||||
|
||||
### Added
|
||||
- Retries of put-bucket-setting operation during container creation (#398)
|
||||
|
||||
## [0.29.0] - Zemu - 2024-05-27
|
||||
|
||||
|
@ -229,8 +195,4 @@ To see CHANGELOG for older versions, refer to https://github.com/nspcc-dev/neofs
|
|||
[0.28.1]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.28.0...v0.28.1
|
||||
[0.28.2]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.28.1...v0.28.2
|
||||
[0.29.0]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.28.2...v0.29.0
|
||||
[0.29.1]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.29.0...v0.29.1
|
||||
[0.29.2]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.29.1...v0.29.2
|
||||
[0.29.3]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.29.2...v0.29.3
|
||||
[0.30.0]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.29.3...v0.30.0
|
||||
[Unreleased]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.30.0...master
|
||||
[Unreleased]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.29.0...master
|
||||
|
|
2
Makefile
2
Makefile
|
@ -3,7 +3,7 @@
|
|||
# Common variables
|
||||
REPO ?= $(shell go list -m)
|
||||
VERSION ?= $(shell git describe --tags --dirty --match "v*" --always --abbrev=8 2>/dev/null || cat VERSION 2>/dev/null || echo "develop")
|
||||
GO_VERSION ?= 1.22
|
||||
GO_VERSION ?= 1.20
|
||||
LINT_VERSION ?= 1.56.1
|
||||
TRUECLOUDLAB_LINT_VERSION ?= 0.0.5
|
||||
BINDIR = bin
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
v0.30.0
|
||||
v0.29.0
|
||||
|
|
|
@ -270,9 +270,7 @@ func (c *Center) checkFormData(r *http.Request) (*middleware.Box, error) {
|
|||
return nil, fmt.Errorf("failed to parse x-amz-date field: %w", err)
|
||||
}
|
||||
|
||||
accessKeyID := submatches["access_key_id"]
|
||||
|
||||
addr, err := getAddress(accessKeyID)
|
||||
addr, err := getAddress(submatches["access_key_id"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -285,22 +283,14 @@ func (c *Center) checkFormData(r *http.Request) (*middleware.Box, error) {
|
|||
secret := box.Gate.SecretKey
|
||||
service, region := submatches["service"], submatches["region"]
|
||||
|
||||
signature := SignStr(secret, service, region, signatureDateTime, policy)
|
||||
signature := signStr(secret, service, region, signatureDateTime, policy)
|
||||
reqSignature := MultipartFormValue(r, "x-amz-signature")
|
||||
if signature != reqSignature {
|
||||
return nil, fmt.Errorf("%w: %s != %s", apiErrors.GetAPIError(apiErrors.ErrSignatureDoesNotMatch),
|
||||
reqSignature, signature)
|
||||
}
|
||||
|
||||
return &middleware.Box{
|
||||
AccessBox: box,
|
||||
AuthHeaders: &middleware.AuthHeader{
|
||||
AccessKeyID: accessKeyID,
|
||||
Region: region,
|
||||
SignatureV4: signature,
|
||||
},
|
||||
Attributes: attrs,
|
||||
}, nil
|
||||
return &middleware.Box{AccessBox: box, Attributes: attrs}, nil
|
||||
}
|
||||
|
||||
func cloneRequest(r *http.Request, authHeader *AuthHeader) *http.Request {
|
||||
|
@ -359,7 +349,7 @@ func (c *Center) checkSign(authHeader *AuthHeader, box *accessbox.Box, request *
|
|||
return nil
|
||||
}
|
||||
|
||||
func SignStr(secret, service, region string, t time.Time, strToSign string) string {
|
||||
func signStr(secret, service, region string, t time.Time, strToSign string) string {
|
||||
creds := deriveKey(secret, service, region, t)
|
||||
signature := hmacSHA256(creds, []byte(strToSign))
|
||||
return hex.EncodeToString(signature)
|
||||
|
|
|
@ -1,31 +1,12 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens"
|
||||
frostfsErrors "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/errors"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
func TestAuthHeaderParse(t *testing.T) {
|
||||
|
@ -115,7 +96,7 @@ func TestSignature(t *testing.T) {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
signature := SignStr(secret, "s3", "us-east-1", signTime, strToSign)
|
||||
signature := signStr(secret, "s3", "us-east-1", signTime, strToSign)
|
||||
require.Equal(t, "dfbe886241d9e369cf4b329ca0f15eb27306c97aa1022cc0bb5a914c4ef87634", signature)
|
||||
}
|
||||
|
||||
|
@ -142,11 +123,6 @@ func TestCheckFormatContentSHA256(t *testing.T) {
|
|||
hash: "ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f7s",
|
||||
error: defaultErr,
|
||||
},
|
||||
{
|
||||
name: "invalid hash format: hash size",
|
||||
hash: "5aadb45520dcd8726b2822a7a78bb53d794f557199d5d4abdedd2c55a4bd6ca73607605c558de3db80c8e86c3196484566163ed1327e82e8b6757d1932113cb8",
|
||||
error: defaultErr,
|
||||
},
|
||||
{
|
||||
name: "unsigned payload",
|
||||
hash: "UNSIGNED-PAYLOAD",
|
||||
|
@ -169,467 +145,3 @@ func TestCheckFormatContentSHA256(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
type frostFSMock struct {
|
||||
objects map[oid.Address]*object.Object
|
||||
}
|
||||
|
||||
func newFrostFSMock() *frostFSMock {
|
||||
return &frostFSMock{
|
||||
objects: map[oid.Address]*object.Object{},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frostFSMock) GetCredsObject(_ context.Context, address oid.Address) (*object.Object, error) {
|
||||
obj, ok := f.objects[address]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("not found")
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func (f *frostFSMock) CreateObject(context.Context, tokens.PrmObjectCreate) (oid.ID, error) {
|
||||
return oid.ID{}, fmt.Errorf("the mock method is not implemented")
|
||||
}
|
||||
|
||||
func TestAuthenticate(t *testing.T) {
|
||||
key, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := &cache.Config{
|
||||
Size: 10,
|
||||
Lifetime: 24 * time.Hour,
|
||||
Logger: zaptest.NewLogger(t),
|
||||
}
|
||||
|
||||
gateData := []*accessbox.GateData{{
|
||||
BearerToken: &bearer.Token{},
|
||||
GateKey: key.PublicKey(),
|
||||
}}
|
||||
|
||||
accessBox, secret, err := accessbox.PackTokens(gateData, []byte("secret"))
|
||||
require.NoError(t, err)
|
||||
data, err := accessBox.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
var obj object.Object
|
||||
obj.SetPayload(data)
|
||||
addr := oidtest.Address()
|
||||
obj.SetContainerID(addr.Container())
|
||||
obj.SetID(addr.Object())
|
||||
|
||||
frostfs := newFrostFSMock()
|
||||
frostfs.objects[addr] = &obj
|
||||
|
||||
accessKeyID := addr.Container().String() + "0" + addr.Object().String()
|
||||
|
||||
awsCreds := credentials.NewStaticCredentials(accessKeyID, secret.SecretKey, "")
|
||||
defaultSigner := v4.NewSigner(awsCreds)
|
||||
|
||||
service, region := "s3", "default"
|
||||
invalidValue := "invalid-value"
|
||||
|
||||
bigConfig := tokens.Config{
|
||||
FrostFS: frostfs,
|
||||
Key: key,
|
||||
CacheConfig: cfg,
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
prefixes []string
|
||||
request *http.Request
|
||||
err bool
|
||||
errCode errors.ErrorCode
|
||||
}{
|
||||
{
|
||||
name: "valid sign",
|
||||
prefixes: []string{addr.Container().String()},
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
_, err = defaultSigner.Sign(r, nil, service, region, time.Now())
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "no authorization header",
|
||||
request: func() *http.Request {
|
||||
return httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
}(),
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "invalid authorization header",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
r.Header.Set(AuthorizationHdr, invalidValue)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
errCode: errors.ErrAuthorizationHeaderMalformed,
|
||||
},
|
||||
{
|
||||
name: "invalid access key id format",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
signer := v4.NewSigner(credentials.NewStaticCredentials(addr.Object().String(), secret.SecretKey, ""))
|
||||
_, err = signer.Sign(r, nil, service, region, time.Now())
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
errCode: errors.ErrInvalidAccessKeyID,
|
||||
},
|
||||
{
|
||||
name: "not allowed access key id",
|
||||
prefixes: []string{addr.Object().String()},
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
_, err = defaultSigner.Sign(r, nil, service, region, time.Now())
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
errCode: errors.ErrAccessDenied,
|
||||
},
|
||||
{
|
||||
name: "invalid access key id value",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
signer := v4.NewSigner(credentials.NewStaticCredentials(accessKeyID[:len(accessKeyID)-4], secret.SecretKey, ""))
|
||||
_, err = signer.Sign(r, nil, service, region, time.Now())
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
errCode: errors.ErrInvalidAccessKeyID,
|
||||
},
|
||||
{
|
||||
name: "unknown access key id",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
signer := v4.NewSigner(credentials.NewStaticCredentials(addr.Object().String()+"0"+addr.Container().String(), secret.SecretKey, ""))
|
||||
_, err = signer.Sign(r, nil, service, region, time.Now())
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "invalid signature",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
signer := v4.NewSigner(credentials.NewStaticCredentials(accessKeyID, "secret", ""))
|
||||
_, err = signer.Sign(r, nil, service, region, time.Now())
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
errCode: errors.ErrSignatureDoesNotMatch,
|
||||
},
|
||||
{
|
||||
name: "invalid signature - AmzDate",
|
||||
prefixes: []string{addr.Container().String()},
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
_, err = defaultSigner.Sign(r, nil, service, region, time.Now())
|
||||
r.Header.Set(AmzDate, invalidValue)
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "invalid AmzContentSHA256",
|
||||
prefixes: []string{addr.Container().String()},
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
_, err = defaultSigner.Sign(r, nil, service, region, time.Now())
|
||||
r.Header.Set(AmzContentSHA256, invalidValue)
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "valid presign",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
_, err = defaultSigner.Presign(r, nil, service, region, time.Minute, time.Now())
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "presign, bad X-Amz-Credential",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
query := url.Values{
|
||||
AmzAlgorithm: []string{"AWS4-HMAC-SHA256"},
|
||||
AmzCredential: []string{invalidValue},
|
||||
}
|
||||
r.URL.RawQuery = query.Encode()
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "presign, bad X-Amz-Expires",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
_, err = defaultSigner.Presign(r, nil, service, region, time.Minute, time.Now())
|
||||
queryParams := r.URL.Query()
|
||||
queryParams.Set("X-Amz-Expires", invalidValue)
|
||||
r.URL.RawQuery = queryParams.Encode()
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "presign, expired",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
_, err = defaultSigner.Presign(r, nil, service, region, time.Minute, time.Now().Add(-time.Minute))
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
errCode: errors.ErrExpiredPresignRequest,
|
||||
},
|
||||
{
|
||||
name: "presign, signature from future",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
_, err = defaultSigner.Presign(r, nil, service, region, time.Minute, time.Now().Add(time.Minute))
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
errCode: errors.ErrBadRequest,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
creds := tokens.New(bigConfig)
|
||||
cntr := New(creds, tc.prefixes)
|
||||
box, err := cntr.Authenticate(tc.request)
|
||||
|
||||
if tc.err {
|
||||
require.Error(t, err)
|
||||
if tc.errCode > 0 {
|
||||
err = frostfsErrors.UnwrapErr(err)
|
||||
require.Equal(t, errors.GetAPIError(tc.errCode), err)
|
||||
}
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, accessKeyID, box.AuthHeaders.AccessKeyID)
|
||||
require.Equal(t, region, box.AuthHeaders.Region)
|
||||
require.Equal(t, secret.SecretKey, box.AccessBox.Gate.SecretKey)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPPostAuthenticate(t *testing.T) {
|
||||
const (
|
||||
policyBase64 = "eyJleHBpcmF0aW9uIjogIjIwMjUtMTItMDFUMTI6MDA6MDAuMDAwWiIsImNvbmRpdGlvbnMiOiBbCiBbInN0YXJ0cy13aXRoIiwgIiR4LWFtei1jcmVkZW50aWFsIiwgIiJdLAogWyJzdGFydHMtd2l0aCIsICIkeC1hbXotZGF0ZSIsICIiXQpdfQ=="
|
||||
invalidValue = "invalid-value"
|
||||
defaultFieldName = "file"
|
||||
service = "s3"
|
||||
region = "default"
|
||||
)
|
||||
|
||||
key, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := &cache.Config{
|
||||
Size: 10,
|
||||
Lifetime: 24 * time.Hour,
|
||||
Logger: zaptest.NewLogger(t),
|
||||
}
|
||||
|
||||
gateData := []*accessbox.GateData{{
|
||||
BearerToken: &bearer.Token{},
|
||||
GateKey: key.PublicKey(),
|
||||
}}
|
||||
|
||||
accessBox, secret, err := accessbox.PackTokens(gateData, []byte("secret"))
|
||||
require.NoError(t, err)
|
||||
data, err := accessBox.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
var obj object.Object
|
||||
obj.SetPayload(data)
|
||||
addr := oidtest.Address()
|
||||
obj.SetContainerID(addr.Container())
|
||||
obj.SetID(addr.Object())
|
||||
|
||||
frostfs := newFrostFSMock()
|
||||
frostfs.objects[addr] = &obj
|
||||
|
||||
accessKeyID := addr.Container().String() + "0" + addr.Object().String()
|
||||
invalidAccessKeyID := oidtest.Address().String() + "0" + oidtest.Address().Object().String()
|
||||
|
||||
timeToSign := time.Now()
|
||||
timeToSignStr := timeToSign.Format("20060102T150405Z")
|
||||
|
||||
bigConfig := tokens.Config{
|
||||
FrostFS: frostfs,
|
||||
Key: key,
|
||||
CacheConfig: cfg,
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
prefixes []string
|
||||
request *http.Request
|
||||
err bool
|
||||
errCode errors.ErrorCode
|
||||
}{
|
||||
{
|
||||
name: "HTTP POST valid",
|
||||
request: func() *http.Request {
|
||||
creds := getCredsStr(accessKeyID, timeToSignStr, region, service)
|
||||
sign := SignStr(secret.SecretKey, service, region, timeToSign, policyBase64)
|
||||
|
||||
return getRequestWithMultipartForm(t, policyBase64, creds, timeToSignStr, sign, defaultFieldName)
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "HTTP POST valid with custom field name",
|
||||
request: func() *http.Request {
|
||||
creds := getCredsStr(accessKeyID, timeToSignStr, region, service)
|
||||
sign := SignStr(secret.SecretKey, service, region, timeToSign, policyBase64)
|
||||
|
||||
return getRequestWithMultipartForm(t, policyBase64, creds, timeToSignStr, sign, "files")
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "HTTP POST valid with field name with a capital letter",
|
||||
request: func() *http.Request {
|
||||
creds := getCredsStr(accessKeyID, timeToSignStr, region, service)
|
||||
sign := SignStr(secret.SecretKey, service, region, timeToSign, policyBase64)
|
||||
|
||||
return getRequestWithMultipartForm(t, policyBase64, creds, timeToSignStr, sign, "File")
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "HTTP POST invalid multipart form",
|
||||
request: func() *http.Request {
|
||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
req.Header.Set(ContentTypeHdr, "multipart/form-data")
|
||||
|
||||
return req
|
||||
}(),
|
||||
err: true,
|
||||
errCode: errors.ErrInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "HTTP POST invalid signature date time",
|
||||
request: func() *http.Request {
|
||||
creds := getCredsStr(accessKeyID, timeToSignStr, region, service)
|
||||
sign := SignStr(secret.SecretKey, service, region, timeToSign, policyBase64)
|
||||
|
||||
return getRequestWithMultipartForm(t, policyBase64, creds, invalidValue, sign, defaultFieldName)
|
||||
}(),
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "HTTP POST invalid creds",
|
||||
request: func() *http.Request {
|
||||
sign := SignStr(secret.SecretKey, service, region, timeToSign, policyBase64)
|
||||
|
||||
return getRequestWithMultipartForm(t, policyBase64, invalidValue, timeToSignStr, sign, defaultFieldName)
|
||||
}(),
|
||||
err: true,
|
||||
errCode: errors.ErrAuthorizationHeaderMalformed,
|
||||
},
|
||||
{
|
||||
name: "HTTP POST missing policy",
|
||||
request: func() *http.Request {
|
||||
creds := getCredsStr(accessKeyID, timeToSignStr, region, service)
|
||||
sign := SignStr(secret.SecretKey, service, region, timeToSign, policyBase64)
|
||||
|
||||
return getRequestWithMultipartForm(t, "", creds, timeToSignStr, sign, defaultFieldName)
|
||||
}(),
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "HTTP POST invalid accessKeyId",
|
||||
request: func() *http.Request {
|
||||
creds := getCredsStr(invalidValue, timeToSignStr, region, service)
|
||||
sign := SignStr(secret.SecretKey, service, region, timeToSign, policyBase64)
|
||||
|
||||
return getRequestWithMultipartForm(t, policyBase64, creds, timeToSignStr, sign, defaultFieldName)
|
||||
}(),
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "HTTP POST invalid accessKeyId - a non-existent box",
|
||||
request: func() *http.Request {
|
||||
creds := getCredsStr(invalidAccessKeyID, timeToSignStr, region, service)
|
||||
sign := SignStr(secret.SecretKey, service, region, timeToSign, policyBase64)
|
||||
|
||||
return getRequestWithMultipartForm(t, policyBase64, creds, timeToSignStr, sign, defaultFieldName)
|
||||
}(),
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "HTTP POST invalid signature",
|
||||
request: func() *http.Request {
|
||||
creds := getCredsStr(accessKeyID, timeToSignStr, region, service)
|
||||
sign := SignStr(secret.SecretKey, service, region, timeToSign, invalidValue)
|
||||
|
||||
return getRequestWithMultipartForm(t, policyBase64, creds, timeToSignStr, sign, defaultFieldName)
|
||||
}(),
|
||||
err: true,
|
||||
errCode: errors.ErrSignatureDoesNotMatch,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
creds := tokens.New(bigConfig)
|
||||
cntr := New(creds, tc.prefixes)
|
||||
box, err := cntr.Authenticate(tc.request)
|
||||
|
||||
if tc.err {
|
||||
require.Error(t, err)
|
||||
if tc.errCode > 0 {
|
||||
err = frostfsErrors.UnwrapErr(err)
|
||||
require.Equal(t, errors.GetAPIError(tc.errCode), err)
|
||||
}
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, secret.SecretKey, box.AccessBox.Gate.SecretKey)
|
||||
require.Equal(t, accessKeyID, box.AuthHeaders.AccessKeyID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getCredsStr(accessKeyID, timeToSign, region, service string) string {
|
||||
return accessKeyID + "/" + timeToSign + "/" + region + "/" + service + "/aws4_request"
|
||||
}
|
||||
|
||||
func getRequestWithMultipartForm(t *testing.T, policy, creds, date, sign, fieldName string) *http.Request {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
defer writer.Close()
|
||||
|
||||
err := writer.WriteField("policy", policy)
|
||||
require.NoError(t, err)
|
||||
err = writer.WriteField(AmzCredential, creds)
|
||||
require.NoError(t, err)
|
||||
err = writer.WriteField(AmzDate, date)
|
||||
require.NoError(t, err)
|
||||
err = writer.WriteField(AmzSignature, sign)
|
||||
require.NoError(t, err)
|
||||
_, err = writer.CreateFormFile(fieldName, "test.txt")
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/", body)
|
||||
req.Header.Set(ContentTypeHdr, writer.FormDataContentType())
|
||||
|
||||
return req
|
||||
}
|
||||
|
|
18
api/cache/cache_test.go
vendored
18
api/cache/cache_test.go
vendored
|
@ -182,6 +182,24 @@ func TestSettingsCacheType(t *testing.T) {
|
|||
assertInvalidCacheEntry(t, cache.GetSettings(key), observedLog)
|
||||
}
|
||||
|
||||
func TestNotificationConfigurationCacheType(t *testing.T) {
|
||||
logger, observedLog := getObservedLogger()
|
||||
cache := NewSystemCache(DefaultSystemConfig(logger))
|
||||
|
||||
key := "key"
|
||||
notificationConfig := &data.NotificationConfiguration{}
|
||||
|
||||
err := cache.PutNotificationConfiguration(key, notificationConfig)
|
||||
require.NoError(t, err)
|
||||
val := cache.GetNotificationConfiguration(key)
|
||||
require.Equal(t, notificationConfig, val)
|
||||
require.Equal(t, 0, observedLog.Len())
|
||||
|
||||
err = cache.cache.Set(key, "tmp")
|
||||
require.NoError(t, err)
|
||||
assertInvalidCacheEntry(t, cache.GetNotificationConfiguration(key), observedLog)
|
||||
}
|
||||
|
||||
func TestFrostFSIDSubjectCacheType(t *testing.T) {
|
||||
logger, observedLog := getObservedLogger()
|
||||
cache := NewFrostfsIDCache(DefaultFrostfsIDConfig(logger))
|
||||
|
|
20
api/cache/system.go
vendored
20
api/cache/system.go
vendored
|
@ -104,6 +104,22 @@ func (o *SystemCache) GetSettings(key string) *data.BucketSettings {
|
|||
return result
|
||||
}
|
||||
|
||||
func (o *SystemCache) GetNotificationConfiguration(key string) *data.NotificationConfiguration {
|
||||
entry, err := o.cache.Get(key)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, ok := entry.(*data.NotificationConfiguration)
|
||||
if !ok {
|
||||
o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
|
||||
zap.String("expected", fmt.Sprintf("%T", result)))
|
||||
return nil
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetTagging returns tags of a bucket or an object.
|
||||
func (o *SystemCache) GetTagging(key string) map[string]string {
|
||||
entry, err := o.cache.Get(key)
|
||||
|
@ -137,6 +153,10 @@ func (o *SystemCache) PutSettings(key string, settings *data.BucketSettings) err
|
|||
return o.cache.Set(key, settings)
|
||||
}
|
||||
|
||||
func (o *SystemCache) PutNotificationConfiguration(key string, obj *data.NotificationConfiguration) error {
|
||||
return o.cache.Set(key, obj)
|
||||
}
|
||||
|
||||
// PutTagging puts tags of a bucket or an object.
|
||||
func (o *SystemCache) PutTagging(key string, tagSet map[string]string) error {
|
||||
return o.cache.Set(key, tagSet)
|
||||
|
|
|
@ -12,8 +12,9 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
bktSettingsObject = ".s3-settings"
|
||||
bktCORSConfigurationObject = ".s3-cors"
|
||||
bktSettingsObject = ".s3-settings"
|
||||
bktCORSConfigurationObject = ".s3-cors"
|
||||
bktNotificationConfigurationObject = ".s3-notifications"
|
||||
|
||||
VersioningUnversioned = "Unversioned"
|
||||
VersioningEnabled = "Enabled"
|
||||
|
@ -31,6 +32,7 @@ type (
|
|||
LocationConstraint string
|
||||
ObjectLockEnabled bool
|
||||
HomomorphicHashDisabled bool
|
||||
APEEnabled bool
|
||||
}
|
||||
|
||||
// ObjectInfo holds S3 object data.
|
||||
|
@ -50,6 +52,14 @@ type (
|
|||
Headers map[string]string
|
||||
}
|
||||
|
||||
// NotificationInfo store info to send s3 notification.
|
||||
NotificationInfo struct {
|
||||
Name string
|
||||
Version string
|
||||
Size uint64
|
||||
HashSum string
|
||||
}
|
||||
|
||||
// BucketSettings stores settings such as versioning.
|
||||
BucketSettings struct {
|
||||
Versioning string
|
||||
|
@ -83,12 +93,24 @@ type (
|
|||
}
|
||||
)
|
||||
|
||||
// NotificationInfoFromObject creates new NotificationInfo from ObjectInfo.
|
||||
func NotificationInfoFromObject(objInfo *ObjectInfo, md5Enabled bool) *NotificationInfo {
|
||||
return &NotificationInfo{
|
||||
Name: objInfo.Name,
|
||||
Version: objInfo.VersionID(),
|
||||
Size: objInfo.Size,
|
||||
HashSum: Quote(objInfo.ETag(md5Enabled)),
|
||||
}
|
||||
}
|
||||
|
||||
// SettingsObjectName is a system name for a bucket settings file.
|
||||
func (b *BucketInfo) SettingsObjectName() string { return bktSettingsObject }
|
||||
|
||||
// CORSObjectName returns a system name for a bucket CORS configuration file.
|
||||
func (b *BucketInfo) CORSObjectName() string {
|
||||
return b.CID.EncodeToString() + bktCORSConfigurationObject
|
||||
func (b *BucketInfo) CORSObjectName() string { return bktCORSConfigurationObject }
|
||||
|
||||
func (b *BucketInfo) NotificationConfigurationObjectName() string {
|
||||
return bktNotificationConfigurationObject
|
||||
}
|
||||
|
||||
// VersionID returns object version from ObjectInfo.
|
||||
|
|
42
api/data/notifications.go
Normal file
42
api/data/notifications.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package data
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
type (
|
||||
NotificationConfiguration struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ NotificationConfiguration" json:"-"`
|
||||
QueueConfigurations []QueueConfiguration `xml:"QueueConfiguration" json:"QueueConfigurations"`
|
||||
// Not supported topics
|
||||
TopicConfigurations []TopicConfiguration `xml:"TopicConfiguration" json:"TopicConfigurations"`
|
||||
LambdaFunctionConfigurations []LambdaFunctionConfiguration `xml:"CloudFunctionConfiguration" json:"CloudFunctionConfigurations"`
|
||||
}
|
||||
|
||||
QueueConfiguration struct {
|
||||
ID string `xml:"Id" json:"Id"`
|
||||
QueueArn string `xml:"Queue" json:"Queue"`
|
||||
Events []string `xml:"Event" json:"Events"`
|
||||
Filter Filter `xml:"Filter" json:"Filter"`
|
||||
}
|
||||
|
||||
Filter struct {
|
||||
Key Key `xml:"S3Key" json:"S3Key"`
|
||||
}
|
||||
|
||||
Key struct {
|
||||
FilterRules []FilterRule `xml:"FilterRule" json:"FilterRules"`
|
||||
}
|
||||
|
||||
FilterRule struct {
|
||||
Name string `xml:"Name" json:"Name"`
|
||||
Value string `xml:"Value" json:"Value"`
|
||||
}
|
||||
|
||||
// TopicConfiguration and LambdaFunctionConfiguration -- we don't support these configurations,
|
||||
// but we need them to detect in notification configurations in requests.
|
||||
TopicConfiguration struct{}
|
||||
LambdaFunctionConfiguration struct{}
|
||||
)
|
||||
|
||||
func (n NotificationConfiguration) IsEmpty() bool {
|
||||
return len(n.QueueConfigurations) == 0 && len(n.TopicConfigurations) == 0 && len(n.LambdaFunctionConfigurations) == 0
|
||||
}
|
1420
api/handler/acl.go
1420
api/handler/acl.go
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -11,6 +11,7 @@ import (
|
|||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||
|
@ -19,11 +20,17 @@ import (
|
|||
|
||||
type (
|
||||
handler struct {
|
||||
log *zap.Logger
|
||||
obj *layer.Layer
|
||||
cfg Config
|
||||
ape APE
|
||||
frostfsid FrostFSID
|
||||
log *zap.Logger
|
||||
obj layer.Client
|
||||
notificator Notificator
|
||||
cfg Config
|
||||
ape APE
|
||||
frostfsid FrostFSID
|
||||
}
|
||||
|
||||
Notificator interface {
|
||||
SendNotifications(topics map[string]string, p *SendNotificationParams) error
|
||||
SendTestNotification(topic, bucketName, requestID, HostID string, now time.Time) error
|
||||
}
|
||||
|
||||
// Config contains data which handler needs to keep.
|
||||
|
@ -34,14 +41,15 @@ type (
|
|||
DefaultCopiesNumbers(namespace string) []uint32
|
||||
NewXMLDecoder(io.Reader) *xml.Decoder
|
||||
DefaultMaxAge() int
|
||||
NotificatorEnabled() bool
|
||||
ResolveZoneList() []string
|
||||
IsResolveListAllow() bool
|
||||
BypassContentEncodingInChunks() bool
|
||||
MD5Enabled() bool
|
||||
ACLEnabled() bool
|
||||
RetryMaxAttempts() int
|
||||
RetryMaxBackoff() time.Duration
|
||||
RetryStrategy() RetryStrategy
|
||||
Domains() []string
|
||||
}
|
||||
|
||||
FrostFSID interface {
|
||||
|
@ -68,7 +76,7 @@ const (
|
|||
var _ api.Handler = (*handler)(nil)
|
||||
|
||||
// New creates new api.Handler using given logger and client.
|
||||
func New(log *zap.Logger, obj *layer.Layer, cfg Config, storage APE, ffsid FrostFSID) (api.Handler, error) {
|
||||
func New(log *zap.Logger, obj layer.Client, notificator Notificator, cfg Config, storage APE, ffsid FrostFSID) (api.Handler, error) {
|
||||
switch {
|
||||
case obj == nil:
|
||||
return nil, errors.New("empty FrostFS Object Layer")
|
||||
|
@ -80,12 +88,19 @@ func New(log *zap.Logger, obj *layer.Layer, cfg Config, storage APE, ffsid Frost
|
|||
return nil, errors.New("empty frostfsid")
|
||||
}
|
||||
|
||||
if !cfg.NotificatorEnabled() {
|
||||
log.Warn(logs.NotificatorIsDisabledS3WontProduceNotificationEvents)
|
||||
} else if notificator == nil {
|
||||
return nil, errors.New("empty notificator")
|
||||
}
|
||||
|
||||
return &handler{
|
||||
log: log,
|
||||
obj: obj,
|
||||
cfg: cfg,
|
||||
ape: storage,
|
||||
frostfsid: ffsid,
|
||||
log: log,
|
||||
obj: obj,
|
||||
cfg: cfg,
|
||||
ape: storage,
|
||||
notificator: notificator,
|
||||
frostfsid: ffsid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
|
@ -41,10 +42,11 @@ func path2BucketObject(path string) (string, string, error) {
|
|||
|
||||
func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
err error
|
||||
versionID string
|
||||
metadata map[string]string
|
||||
tagSet map[string]string
|
||||
err error
|
||||
versionID string
|
||||
metadata map[string]string
|
||||
tagSet map[string]string
|
||||
sessionTokenEACL *session.Container
|
||||
|
||||
ctx = r.Context()
|
||||
reqInfo = middleware.GetReqInfo(ctx)
|
||||
|
@ -91,11 +93,20 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if cannedACLStatus == aclStatusYes {
|
||||
apeEnabled := dstBktInfo.APEEnabled || settings.CannedACL != ""
|
||||
if apeEnabled && cannedACLStatus == aclStatusYes {
|
||||
h.logAndSendError(w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported))
|
||||
return
|
||||
}
|
||||
|
||||
needUpdateEACLTable := !(apeEnabled || cannedACLStatus == aclStatusNo)
|
||||
if needUpdateEACLTable {
|
||||
if sessionTokenEACL, err = getSessionTokenSetEACL(ctx); err != nil {
|
||||
h.logAndSendError(w, "could not get eacl session token from a box", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
extendedSrcObjInfo, err := h.obj.GetExtendedObjectInfo(ctx, srcObjPrm)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not find object", reqInfo, err)
|
||||
|
@ -228,6 +239,25 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if needUpdateEACLTable {
|
||||
newEaclTable, err := h.getNewEAclTable(r, dstBktInfo, dstObjInfo)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get new eacl table", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
p := &layer.PutBucketACLParams{
|
||||
BktInfo: dstBktInfo,
|
||||
EACL: newEaclTable,
|
||||
SessionToken: sessionTokenEACL,
|
||||
}
|
||||
|
||||
if err = h.obj.PutBucketACL(ctx, p); err != nil {
|
||||
h.logAndSendError(w, "could not put bucket acl", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if tagSet != nil {
|
||||
tagPrm := &data.PutObjectTaggingParams{
|
||||
ObjectVersion: &data.ObjectVersion{
|
||||
|
@ -238,7 +268,7 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
TagSet: tagSet,
|
||||
NodeVersion: extendedDstObjInfo.NodeVersion,
|
||||
}
|
||||
if err = h.obj.PutObjectTagging(ctx, tagPrm); err != nil {
|
||||
if _, err = h.obj.PutObjectTagging(ctx, tagPrm); err != nil {
|
||||
h.logAndSendError(w, "could not upload object tagging", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
@ -246,6 +276,16 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
h.reqLogger(ctx).Info(logs.ObjectIsCopied, zap.Stringer("object_id", dstObjInfo.ID))
|
||||
|
||||
s := &SendNotificationParams{
|
||||
Event: EventObjectCreatedCopy,
|
||||
NotificationInfo: data.NotificationInfoFromObject(dstObjInfo, h.cfg.MD5Enabled()),
|
||||
BktInfo: dstBktInfo,
|
||||
ReqInfo: reqInfo,
|
||||
}
|
||||
if err = h.sendNotifications(ctx, s); err != nil {
|
||||
h.reqLogger(ctx).Error(logs.CouldntSendNotification, zap.Error(err))
|
||||
}
|
||||
|
||||
if dstEncryptionParams.Enabled() {
|
||||
addSSECHeaders(w.Header(), r.Header)
|
||||
}
|
||||
|
|
|
@ -8,12 +8,16 @@ import (
|
|||
"strings"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// limitation of AWS https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html
|
||||
|
@ -97,6 +101,41 @@ func (h *handler) DeleteObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
var m *SendNotificationParams
|
||||
|
||||
if bktSettings.VersioningEnabled() && len(versionID) == 0 {
|
||||
m = &SendNotificationParams{
|
||||
Event: EventObjectRemovedDeleteMarkerCreated,
|
||||
NotificationInfo: &data.NotificationInfo{
|
||||
Name: reqInfo.ObjectName,
|
||||
HashSum: deletedObject.DeleteMarkerEtag,
|
||||
},
|
||||
BktInfo: bktInfo,
|
||||
ReqInfo: reqInfo,
|
||||
}
|
||||
} else {
|
||||
var objID oid.ID
|
||||
if len(versionID) != 0 {
|
||||
if err = objID.DecodeString(versionID); err != nil {
|
||||
h.reqLogger(ctx).Error(logs.CouldntSendNotification, zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
m = &SendNotificationParams{
|
||||
Event: EventObjectRemovedDelete,
|
||||
NotificationInfo: &data.NotificationInfo{
|
||||
Name: reqInfo.ObjectName,
|
||||
Version: objID.EncodeToString(),
|
||||
},
|
||||
BktInfo: bktInfo,
|
||||
ReqInfo: reqInfo,
|
||||
}
|
||||
}
|
||||
|
||||
if err = h.sendNotifications(ctx, m); err != nil {
|
||||
h.reqLogger(ctx).Error(logs.CouldntSendNotification, zap.Error(err))
|
||||
}
|
||||
|
||||
if deletedObject.VersionID != "" {
|
||||
w.Header().Set(api.AmzVersionID, deletedObject.VersionID)
|
||||
}
|
||||
|
@ -150,18 +189,15 @@ func (h *handler) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Re
|
|||
return
|
||||
}
|
||||
|
||||
unique := make(map[string]struct{})
|
||||
removed := make(map[string]*layer.VersionedObject)
|
||||
toRemove := make([]*layer.VersionedObject, 0, len(requested.Objects))
|
||||
for _, obj := range requested.Objects {
|
||||
versionedObj := &layer.VersionedObject{
|
||||
Name: obj.ObjectName,
|
||||
VersionID: obj.VersionID,
|
||||
}
|
||||
key := versionedObj.String()
|
||||
if _, ok := unique[key]; !ok {
|
||||
toRemove = append(toRemove, versionedObj)
|
||||
unique[key] = struct{}{}
|
||||
}
|
||||
toRemove = append(toRemove, versionedObj)
|
||||
removed[versionedObj.String()] = versionedObj
|
||||
}
|
||||
|
||||
response := &DeleteObjectsResponse{
|
||||
|
@ -237,18 +273,9 @@ func (h *handler) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
|
|||
sessionToken = boxData.Gate.SessionTokenForDelete()
|
||||
}
|
||||
|
||||
skipObjCheck := false
|
||||
if value, ok := r.Header[api.AmzForceBucketDelete]; ok {
|
||||
s := value[0]
|
||||
if s == "true" {
|
||||
skipObjCheck = true
|
||||
}
|
||||
}
|
||||
|
||||
if err = h.obj.DeleteBucket(r.Context(), &layer.DeleteBucketParams{
|
||||
BktInfo: bktInfo,
|
||||
SessionToken: sessionToken,
|
||||
SkipCheck: skipObjCheck,
|
||||
}); err != nil {
|
||||
h.logAndSendError(w, "couldn't delete bucket", reqInfo, err)
|
||||
return
|
||||
|
|
|
@ -85,37 +85,6 @@ func TestDeleteBucketOnNotFoundError(t *testing.T) {
|
|||
deleteBucket(t, hc, bktName, http.StatusNoContent)
|
||||
}
|
||||
|
||||
func TestForceDeleteBucket(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
|
||||
bktName, objName := "bucket-for-removal", "object-to-delete"
|
||||
bktInfo := createTestBucket(hc, bktName)
|
||||
|
||||
putObject(hc, bktName, objName)
|
||||
|
||||
nodeVersion, err := hc.tree.GetUnversioned(hc.context, bktInfo, objName)
|
||||
require.NoError(t, err)
|
||||
var addr oid.Address
|
||||
addr.SetContainer(bktInfo.CID)
|
||||
addr.SetObject(nodeVersion.OID)
|
||||
|
||||
deleteBucketForce(t, hc, bktName, http.StatusConflict, "false")
|
||||
deleteBucketForce(t, hc, bktName, http.StatusNoContent, "true")
|
||||
}
|
||||
|
||||
func TestDeleteMultipleObjectCheckUniqueness(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
|
||||
bktName, objName := "bucket", "object"
|
||||
createTestBucket(hc, bktName)
|
||||
|
||||
putObject(hc, bktName, objName)
|
||||
|
||||
resp := deleteObjects(t, hc, bktName, [][2]string{{objName, emptyVersion}, {objName, emptyVersion}})
|
||||
require.Empty(t, resp.Errors)
|
||||
require.Len(t, resp.DeletedObjects, 1)
|
||||
}
|
||||
|
||||
func TestDeleteObjectsError(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
|
||||
|
@ -489,16 +458,6 @@ func putBucketVersioning(t *testing.T, tc *handlerContext, bktName string, enabl
|
|||
assertStatus(t, w, http.StatusOK)
|
||||
}
|
||||
|
||||
func getBucketVersioning(hc *handlerContext, bktName string) *VersioningConfiguration {
|
||||
w, r := prepareTestRequest(hc, bktName, "", nil)
|
||||
hc.Handler().GetBucketVersioningHandler(w, r)
|
||||
assertStatus(hc.t, w, http.StatusOK)
|
||||
|
||||
res := &VersioningConfiguration{}
|
||||
parseTestResponse(hc.t, w, res)
|
||||
return res
|
||||
}
|
||||
|
||||
func deleteObject(t *testing.T, tc *handlerContext, bktName, objName, version string) (string, bool) {
|
||||
query := make(url.Values)
|
||||
query.Add(api.QueryVersionID, version)
|
||||
|
@ -535,13 +494,6 @@ func deleteObjectsBase(hc *handlerContext, bktName string, objVersions [][2]stri
|
|||
return w
|
||||
}
|
||||
|
||||
func deleteBucketForce(t *testing.T, tc *handlerContext, bktName string, code int, value string) {
|
||||
w, r := prepareTestRequest(tc, bktName, "", nil)
|
||||
r.Header.Set(api.AmzForceBucketDelete, value)
|
||||
tc.Handler().DeleteBucketHandler(w, r)
|
||||
assertStatus(t, w, code)
|
||||
}
|
||||
|
||||
func deleteBucket(t *testing.T, tc *handlerContext, bktName string, code int) {
|
||||
w, r := prepareTestRequest(tc, bktName, "", nil)
|
||||
tc.Handler().DeleteBucketHandler(w, r)
|
||||
|
|
|
@ -37,7 +37,7 @@ func TestSimpleGetEncrypted(t *testing.T) {
|
|||
|
||||
objInfo, err := tc.Layer().GetObjectInfo(tc.Context(), &layer.HeadObjectParams{BktInfo: bktInfo, Object: objName})
|
||||
require.NoError(t, err)
|
||||
obj, err := tc.MockedPool().GetObject(tc.Context(), layer.PrmObjectGet{Container: bktInfo.CID, Object: objInfo.ID})
|
||||
obj, err := tc.MockedPool().ReadObject(tc.Context(), layer.PrmObjectRead{Container: bktInfo.CID, Object: objInfo.ID})
|
||||
require.NoError(t, err)
|
||||
encryptedContent, err := io.ReadAll(obj.Payload)
|
||||
require.NoError(t, err)
|
||||
|
|
|
@ -58,7 +58,7 @@ func (hc *handlerContext) MockedPool() *layer.TestFrostFS {
|
|||
return hc.tp
|
||||
}
|
||||
|
||||
func (hc *handlerContext) Layer() *layer.Layer {
|
||||
func (hc *handlerContext) Layer() layer.Client {
|
||||
return hc.h.obj
|
||||
}
|
||||
|
||||
|
@ -72,7 +72,7 @@ type configMock struct {
|
|||
defaultCopiesNumbers []uint32
|
||||
bypassContentEncodingInChunks bool
|
||||
md5Enabled bool
|
||||
domains []string
|
||||
aclEnabled bool
|
||||
}
|
||||
|
||||
func (c *configMock) DefaultPlacementPolicy(_ string) netmap.PlacementPolicy {
|
||||
|
@ -104,6 +104,10 @@ func (c *configMock) DefaultMaxAge() int {
|
|||
return 0
|
||||
}
|
||||
|
||||
func (c *configMock) NotificatorEnabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *configMock) ResolveZoneList() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
@ -120,6 +124,10 @@ func (c *configMock) MD5Enabled() bool {
|
|||
return c.md5Enabled
|
||||
}
|
||||
|
||||
func (c *configMock) ACLEnabled() bool {
|
||||
return c.aclEnabled
|
||||
}
|
||||
|
||||
func (c *configMock) ResolveNamespaceAlias(ns string) string {
|
||||
return ns
|
||||
}
|
||||
|
@ -136,10 +144,6 @@ func (c *configMock) RetryStrategy() RetryStrategy {
|
|||
return RetryStrategyConstant
|
||||
}
|
||||
|
||||
func (c *configMock) Domains() []string {
|
||||
return c.domains
|
||||
}
|
||||
|
||||
func prepareHandlerContext(t *testing.T) *handlerContext {
|
||||
return prepareHandlerContextBase(t, layer.DefaultCachesConfigs(zap.NewExample()))
|
||||
}
|
||||
|
|
|
@ -7,9 +7,12 @@ import (
|
|||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
||||
s3errors "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -81,6 +84,31 @@ func headObject(t *testing.T, tc *handlerContext, bktName, objName string, heade
|
|||
assertStatus(t, w, status)
|
||||
}
|
||||
|
||||
func TestInvalidAccessThroughCache(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
|
||||
bktName, objName := "bucket-for-cache", "obj-for-cache"
|
||||
bktInfo, _ := createBucketAndObject(hc, bktName, objName)
|
||||
setContainerEACL(hc, bktInfo.CID)
|
||||
|
||||
headObject(t, hc, bktName, objName, nil, http.StatusOK)
|
||||
|
||||
w, r := prepareTestRequest(hc, bktName, objName, nil)
|
||||
hc.Handler().HeadObjectHandler(w, r.WithContext(middleware.SetBox(r.Context(), &middleware.Box{AccessBox: newTestAccessBox(t, nil)})))
|
||||
assertStatus(t, w, http.StatusForbidden)
|
||||
}
|
||||
|
||||
func setContainerEACL(hc *handlerContext, cnrID cid.ID) {
|
||||
table := eacl.NewTable()
|
||||
table.SetCID(cnrID)
|
||||
for _, op := range fullOps {
|
||||
table.AddRecord(getOthersRecord(op, eacl.ActionDeny))
|
||||
}
|
||||
|
||||
err := hc.MockedPool().SetContainerEACL(hc.Context(), *table, nil)
|
||||
require.NoError(hc.t, err)
|
||||
}
|
||||
|
||||
func TestHeadObject(t *testing.T) {
|
||||
hc := prepareHandlerContextWithMinCache(t)
|
||||
bktName, objName := "bucket", "obj"
|
||||
|
@ -127,7 +155,7 @@ func newTestAccessBox(t *testing.T, key *keys.PrivateKey) *accessbox.Box {
|
|||
}
|
||||
|
||||
var btoken bearer.Token
|
||||
btoken.SetImpersonate(true)
|
||||
btoken.SetEACLTable(*eacl.NewTable())
|
||||
err = btoken.Sign(key.PrivateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
|
@ -5,9 +5,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
||||
|
@ -15,6 +13,7 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
@ -28,11 +27,10 @@ type (
|
|||
}
|
||||
|
||||
CompleteMultipartUploadResponse struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CompleteMultipartUploadResult" json:"-"`
|
||||
Bucket string `xml:"Bucket"`
|
||||
Key string `xml:"Key"`
|
||||
ETag string `xml:"ETag"`
|
||||
Location string `xml:"Location"`
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CompleteMultipartUploadResult" json:"-"`
|
||||
Bucket string `xml:"Bucket"`
|
||||
Key string `xml:"Key"`
|
||||
ETag string `xml:"ETag"`
|
||||
}
|
||||
|
||||
ListMultipartUploadsResponse struct {
|
||||
|
@ -57,11 +55,11 @@ type (
|
|||
Initiator Initiator `xml:"Initiator"`
|
||||
IsTruncated bool `xml:"IsTruncated"`
|
||||
Key string `xml:"Key"`
|
||||
MaxParts int `xml:"MaxParts"`
|
||||
NextPartNumberMarker int `xml:"NextPartNumberMarker"`
|
||||
MaxParts int `xml:"MaxParts,omitempty"`
|
||||
NextPartNumberMarker int `xml:"NextPartNumberMarker,omitempty"`
|
||||
Owner Owner `xml:"Owner"`
|
||||
Parts []*layer.Part `xml:"Part"`
|
||||
PartNumberMarker int `xml:"PartNumberMarker"`
|
||||
PartNumberMarker int `xml:"PartNumberMarker,omitempty"`
|
||||
StorageClass string `xml:"StorageClass"`
|
||||
UploadID string `xml:"UploadId"`
|
||||
}
|
||||
|
@ -115,7 +113,14 @@ func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Re
|
|||
return
|
||||
}
|
||||
|
||||
if cannedACLStatus == aclStatusYes {
|
||||
settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "couldn't get bucket settings", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
apeEnabled := bktInfo.APEEnabled || settings.CannedACL != ""
|
||||
if apeEnabled && cannedACLStatus == aclStatusYes {
|
||||
h.logAndSendError(w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported))
|
||||
return
|
||||
}
|
||||
|
@ -129,6 +134,20 @@ func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Re
|
|||
Data: &layer.UploadData{},
|
||||
}
|
||||
|
||||
needUpdateEACLTable := !(apeEnabled || cannedACLStatus == aclStatusNo)
|
||||
if needUpdateEACLTable {
|
||||
key, err := h.bearerTokenIssuerKey(r.Context())
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "couldn't get gate key", reqInfo, err, additional...)
|
||||
return
|
||||
}
|
||||
if _, err = parseACLHeaders(r.Header, key); err != nil {
|
||||
h.logAndSendError(w, "could not parse acl", reqInfo, err, additional...)
|
||||
return
|
||||
}
|
||||
p.Data.ACLHeaders = formACLHeadersForMultipart(r.Header)
|
||||
}
|
||||
|
||||
if len(r.Header.Get(api.AmzTagging)) > 0 {
|
||||
p.Data.TagSet, err = parseTaggingHeader(r.Header)
|
||||
if err != nil {
|
||||
|
@ -178,6 +197,25 @@ func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Re
|
|||
}
|
||||
}
|
||||
|
||||
func formACLHeadersForMultipart(header http.Header) map[string]string {
|
||||
result := make(map[string]string)
|
||||
|
||||
if value := header.Get(api.AmzACL); value != "" {
|
||||
result[api.AmzACL] = value
|
||||
}
|
||||
if value := header.Get(api.AmzGrantRead); value != "" {
|
||||
result[api.AmzGrantRead] = value
|
||||
}
|
||||
if value := header.Get(api.AmzGrantFullControl); value != "" {
|
||||
result[api.AmzGrantFullControl] = value
|
||||
}
|
||||
if value := header.Get(api.AmzGrantWrite); value != "" {
|
||||
result[api.AmzGrantWrite] = value
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (h *handler) UploadPartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
reqInfo := middleware.GetReqInfo(r.Context())
|
||||
|
||||
|
@ -418,7 +456,7 @@ func (h *handler) CompleteMultipartUploadHandler(w http.ResponseWriter, r *http.
|
|||
|
||||
// Start complete multipart upload which may take some time to fetch object
|
||||
// and re-upload it part by part.
|
||||
objInfo, err := h.completeMultipartUpload(r, c, bktInfo)
|
||||
objInfo, err := h.completeMultipartUpload(r, c, bktInfo, reqInfo)
|
||||
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "complete multipart error", reqInfo, err, additional...)
|
||||
|
@ -426,10 +464,9 @@ func (h *handler) CompleteMultipartUploadHandler(w http.ResponseWriter, r *http.
|
|||
}
|
||||
|
||||
response := CompleteMultipartUploadResponse{
|
||||
Bucket: objInfo.Bucket,
|
||||
Key: objInfo.Name,
|
||||
ETag: data.Quote(objInfo.ETag(h.cfg.MD5Enabled())),
|
||||
Location: getObjectLocation(r, h.cfg.Domains(), reqInfo.BucketName, reqInfo.ObjectName),
|
||||
Bucket: objInfo.Bucket,
|
||||
Key: objInfo.Name,
|
||||
ETag: data.Quote(objInfo.ETag(h.cfg.MD5Enabled())),
|
||||
}
|
||||
|
||||
if settings.VersioningEnabled() {
|
||||
|
@ -441,36 +478,7 @@ func (h *handler) CompleteMultipartUploadHandler(w http.ResponseWriter, r *http.
|
|||
}
|
||||
}
|
||||
|
||||
// returns "https" if the tls boolean is true, "http" otherwise.
|
||||
func getURLScheme(r *http.Request) string {
|
||||
if r.TLS != nil {
|
||||
return "https"
|
||||
}
|
||||
return "http"
|
||||
}
|
||||
|
||||
// getObjectLocation gets the fully qualified URL of an object.
|
||||
func getObjectLocation(r *http.Request, domains []string, bucket, object string) string {
|
||||
proto := middleware.GetSourceScheme(r)
|
||||
if proto == "" {
|
||||
proto = getURLScheme(r)
|
||||
}
|
||||
u := &url.URL{
|
||||
Host: r.Host,
|
||||
Path: path.Join("/", bucket, object),
|
||||
Scheme: proto,
|
||||
}
|
||||
// If domain is set then we need to use bucket DNS style.
|
||||
for _, domain := range domains {
|
||||
if strings.HasPrefix(r.Host, bucket+"."+domain) {
|
||||
u.Path = path.Join("/", object)
|
||||
break
|
||||
}
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func (h *handler) completeMultipartUpload(r *http.Request, c *layer.CompleteMultipartParams, bktInfo *data.BucketInfo) (*data.ObjectInfo, error) {
|
||||
func (h *handler) completeMultipartUpload(r *http.Request, c *layer.CompleteMultipartParams, bktInfo *data.BucketInfo, reqInfo *middleware.ReqInfo) (*data.ObjectInfo, error) {
|
||||
ctx := r.Context()
|
||||
uploadData, extendedObjInfo, err := h.obj.CompleteMultipartUpload(ctx, c)
|
||||
if err != nil {
|
||||
|
@ -488,11 +496,48 @@ func (h *handler) completeMultipartUpload(r *http.Request, c *layer.CompleteMult
|
|||
TagSet: uploadData.TagSet,
|
||||
NodeVersion: extendedObjInfo.NodeVersion,
|
||||
}
|
||||
if err = h.obj.PutObjectTagging(ctx, tagPrm); err != nil {
|
||||
if _, err = h.obj.PutObjectTagging(ctx, tagPrm); err != nil {
|
||||
return nil, fmt.Errorf("could not put tagging file of completed multipart upload: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(uploadData.ACLHeaders) != 0 {
|
||||
sessionTokenSetEACL, err := getSessionTokenSetEACL(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't get eacl token: %w", err)
|
||||
}
|
||||
key, err := h.bearerTokenIssuerKey(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't get gate key: %w", err)
|
||||
}
|
||||
acl, err := parseACLHeaders(r.Header, key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse acl: %w", err)
|
||||
}
|
||||
|
||||
resInfo := &resourceInfo{
|
||||
Bucket: objInfo.Bucket,
|
||||
Object: objInfo.Name,
|
||||
}
|
||||
astObject, err := aclToAst(acl, resInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not translate acl of completed multipart upload to ast: %w", err)
|
||||
}
|
||||
if _, err = h.updateBucketACL(r, astObject, bktInfo, sessionTokenSetEACL); err != nil {
|
||||
return nil, fmt.Errorf("could not update bucket acl while completing multipart upload: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
s := &SendNotificationParams{
|
||||
Event: EventObjectCreatedCompleteMultipartUpload,
|
||||
NotificationInfo: data.NotificationInfoFromObject(objInfo, h.cfg.MD5Enabled()),
|
||||
BktInfo: bktInfo,
|
||||
ReqInfo: reqInfo,
|
||||
}
|
||||
if err = h.sendNotifications(ctx, s); err != nil {
|
||||
h.reqLogger(ctx).Error(logs.CouldntSendNotification, zap.Error(err))
|
||||
}
|
||||
|
||||
return objInfo, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -68,19 +68,6 @@ func TestDeleteMultipartAllParts(t *testing.T) {
|
|||
require.Empty(t, hc.tp.Objects())
|
||||
}
|
||||
|
||||
func TestSpecialMultipartName(t *testing.T) {
|
||||
hc := prepareHandlerContextWithMinCache(t)
|
||||
|
||||
bktName, objName := "bucket", "bucket-settings"
|
||||
|
||||
createTestBucket(hc, bktName)
|
||||
putBucketVersioning(t, hc, bktName, true)
|
||||
|
||||
createMultipartUpload(hc, bktName, objName, nil)
|
||||
res := getBucketVersioning(hc, bktName)
|
||||
require.Equal(t, enabledValue, res.Status)
|
||||
}
|
||||
|
||||
func TestMultipartReUploadPart(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
|
||||
|
@ -292,19 +279,13 @@ func TestListParts(t *testing.T) {
|
|||
require.Len(t, list.Parts, 2)
|
||||
require.Equal(t, etag1, list.Parts[0].ETag)
|
||||
require.Equal(t, etag2, list.Parts[1].ETag)
|
||||
require.Zero(t, list.PartNumberMarker)
|
||||
require.Equal(t, 2, list.NextPartNumberMarker)
|
||||
|
||||
list = listParts(hc, bktName, objName, uploadInfo.UploadID, "1", http.StatusOK)
|
||||
require.Len(t, list.Parts, 1)
|
||||
require.Equal(t, etag2, list.Parts[0].ETag)
|
||||
require.Equal(t, 1, list.PartNumberMarker)
|
||||
require.Equal(t, 2, list.NextPartNumberMarker)
|
||||
|
||||
list = listParts(hc, bktName, objName, uploadInfo.UploadID, "2", http.StatusOK)
|
||||
require.Len(t, list.Parts, 0)
|
||||
require.Equal(t, 2, list.PartNumberMarker)
|
||||
require.Equal(t, 0, list.NextPartNumberMarker)
|
||||
|
||||
list = listParts(hc, bktName, objName, uploadInfo.UploadID, "7", http.StatusOK)
|
||||
require.Len(t, list.Parts, 0)
|
||||
|
@ -441,80 +422,6 @@ func TestUploadPartCheckContentSHA256(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMultipartObjectLocation(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
req *http.Request
|
||||
bucket string
|
||||
object string
|
||||
domains []string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
req: &http.Request{
|
||||
Host: "127.0.0.1:8084",
|
||||
Header: map[string][]string{"X-Forwarded-Scheme": {"http"}},
|
||||
},
|
||||
bucket: "testbucket1",
|
||||
object: "test/1.txt",
|
||||
expected: "http://127.0.0.1:8084/testbucket1/test/1.txt",
|
||||
},
|
||||
{
|
||||
req: &http.Request{
|
||||
Host: "localhost:8084",
|
||||
Header: map[string][]string{"X-Forwarded-Scheme": {"https"}},
|
||||
},
|
||||
bucket: "testbucket1",
|
||||
object: "test/1.txt",
|
||||
expected: "https://localhost:8084/testbucket1/test/1.txt",
|
||||
},
|
||||
{
|
||||
req: &http.Request{
|
||||
Host: "s3.mybucket.org",
|
||||
Header: map[string][]string{"X-Forwarded-Scheme": {"http"}},
|
||||
},
|
||||
bucket: "mybucket",
|
||||
object: "test/1.txt",
|
||||
expected: "http://s3.mybucket.org/mybucket/test/1.txt",
|
||||
},
|
||||
{
|
||||
req: &http.Request{Host: "mys3.mybucket.org"},
|
||||
bucket: "mybucket",
|
||||
object: "test/1.txt",
|
||||
expected: "http://mys3.mybucket.org/mybucket/test/1.txt",
|
||||
},
|
||||
{
|
||||
req: &http.Request{Host: "s3.bucket.org", TLS: &tls.ConnectionState{}},
|
||||
bucket: "bucket",
|
||||
object: "obj",
|
||||
expected: "https://s3.bucket.org/bucket/obj",
|
||||
},
|
||||
{
|
||||
req: &http.Request{
|
||||
Host: "mybucket.s3dev.frostfs.devenv",
|
||||
},
|
||||
domains: []string{"s3dev.frostfs.devenv"},
|
||||
bucket: "mybucket",
|
||||
object: "test/1.txt",
|
||||
expected: "http://mybucket.s3dev.frostfs.devenv/test/1.txt",
|
||||
},
|
||||
{
|
||||
req: &http.Request{
|
||||
Host: "mybucket.s3dev.frostfs.devenv",
|
||||
Header: map[string][]string{"X-Forwarded-Scheme": {"https"}},
|
||||
},
|
||||
domains: []string{"s3dev.frostfs.devenv"},
|
||||
bucket: "mybucket",
|
||||
object: "test/1.txt",
|
||||
expected: "https://mybucket.s3dev.frostfs.devenv/test/1.txt",
|
||||
},
|
||||
} {
|
||||
t.Run("", func(t *testing.T) {
|
||||
location := getObjectLocation(tc.req, tc.domains, tc.bucket, tc.object)
|
||||
require.Equal(t, tc.expected, location)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func uploadPartCopy(hc *handlerContext, bktName, objName, uploadID string, num int, srcObj string, start, end int) *UploadPartCopyResponse {
|
||||
return uploadPartCopyBase(hc, bktName, objName, false, uploadID, num, srcObj, start, end)
|
||||
}
|
||||
|
|
274
api/handler/notifications.go
Normal file
274
api/handler/notifications.go
Normal file
|
@ -0,0 +1,274 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type (
|
||||
SendNotificationParams struct {
|
||||
Event string
|
||||
NotificationInfo *data.NotificationInfo
|
||||
BktInfo *data.BucketInfo
|
||||
ReqInfo *middleware.ReqInfo
|
||||
User string
|
||||
Time time.Time
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
filterRuleSuffixName = "suffix"
|
||||
filterRulePrefixName = "prefix"
|
||||
|
||||
EventObjectCreated = "s3:ObjectCreated:*"
|
||||
EventObjectCreatedPut = "s3:ObjectCreated:Put"
|
||||
EventObjectCreatedPost = "s3:ObjectCreated:Post"
|
||||
EventObjectCreatedCopy = "s3:ObjectCreated:Copy"
|
||||
EventReducedRedundancyLostObject = "s3:ReducedRedundancyLostObject"
|
||||
EventObjectCreatedCompleteMultipartUpload = "s3:ObjectCreated:CompleteMultipartUpload"
|
||||
EventObjectRemoved = "s3:ObjectRemoved:*"
|
||||
EventObjectRemovedDelete = "s3:ObjectRemoved:Delete"
|
||||
EventObjectRemovedDeleteMarkerCreated = "s3:ObjectRemoved:DeleteMarkerCreated"
|
||||
EventObjectRestore = "s3:ObjectRestore:*"
|
||||
EventObjectRestorePost = "s3:ObjectRestore:Post"
|
||||
EventObjectRestoreCompleted = "s3:ObjectRestore:Completed"
|
||||
EventReplication = "s3:Replication:*"
|
||||
EventReplicationOperationFailedReplication = "s3:Replication:OperationFailedReplication"
|
||||
EventReplicationOperationNotTracked = "s3:Replication:OperationNotTracked"
|
||||
EventReplicationOperationMissedThreshold = "s3:Replication:OperationMissedThreshold"
|
||||
EventReplicationOperationReplicatedAfterThreshold = "s3:Replication:OperationReplicatedAfterThreshold"
|
||||
EventObjectRestoreDelete = "s3:ObjectRestore:Delete"
|
||||
EventLifecycleTransition = "s3:LifecycleTransition"
|
||||
EventIntelligentTiering = "s3:IntelligentTiering"
|
||||
EventObjectACLPut = "s3:ObjectAcl:Put"
|
||||
EventLifecycleExpiration = "s3:LifecycleExpiration:*"
|
||||
EventLifecycleExpirationDelete = "s3:LifecycleExpiration:Delete"
|
||||
EventLifecycleExpirationDeleteMarkerCreated = "s3:LifecycleExpiration:DeleteMarkerCreated"
|
||||
EventObjectTagging = "s3:ObjectTagging:*"
|
||||
EventObjectTaggingPut = "s3:ObjectTagging:Put"
|
||||
EventObjectTaggingDelete = "s3:ObjectTagging:Delete"
|
||||
)
|
||||
|
||||
var validEvents = map[string]struct{}{
|
||||
EventReducedRedundancyLostObject: {},
|
||||
EventObjectCreated: {},
|
||||
EventObjectCreatedPut: {},
|
||||
EventObjectCreatedPost: {},
|
||||
EventObjectCreatedCopy: {},
|
||||
EventObjectCreatedCompleteMultipartUpload: {},
|
||||
EventObjectRemoved: {},
|
||||
EventObjectRemovedDelete: {},
|
||||
EventObjectRemovedDeleteMarkerCreated: {},
|
||||
EventObjectRestore: {},
|
||||
EventObjectRestorePost: {},
|
||||
EventObjectRestoreCompleted: {},
|
||||
EventReplication: {},
|
||||
EventReplicationOperationFailedReplication: {},
|
||||
EventReplicationOperationNotTracked: {},
|
||||
EventReplicationOperationMissedThreshold: {},
|
||||
EventReplicationOperationReplicatedAfterThreshold: {},
|
||||
EventObjectRestoreDelete: {},
|
||||
EventLifecycleTransition: {},
|
||||
EventIntelligentTiering: {},
|
||||
EventObjectACLPut: {},
|
||||
EventLifecycleExpiration: {},
|
||||
EventLifecycleExpirationDelete: {},
|
||||
EventLifecycleExpirationDeleteMarkerCreated: {},
|
||||
EventObjectTagging: {},
|
||||
EventObjectTaggingPut: {},
|
||||
EventObjectTaggingDelete: {},
|
||||
}
|
||||
|
||||
func (h *handler) PutBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
reqInfo := middleware.GetReqInfo(r.Context())
|
||||
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
conf := &data.NotificationConfiguration{}
|
||||
if err = h.cfg.NewXMLDecoder(r.Body).Decode(conf); err != nil {
|
||||
h.logAndSendError(w, "couldn't decode notification configuration", reqInfo, errors.GetAPIError(errors.ErrMalformedXML))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = h.checkBucketConfiguration(r.Context(), conf, reqInfo); err != nil {
|
||||
h.logAndSendError(w, "couldn't check bucket configuration", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
p := &layer.PutBucketNotificationConfigurationParams{
|
||||
RequestInfo: reqInfo,
|
||||
BktInfo: bktInfo,
|
||||
Configuration: conf,
|
||||
}
|
||||
|
||||
p.CopiesNumbers, err = h.pickCopiesNumbers(parseMetadata(r), reqInfo.Namespace, bktInfo.LocationConstraint)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "invalid copies number", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = h.obj.PutBucketNotificationConfiguration(r.Context(), p); err != nil {
|
||||
h.logAndSendError(w, "couldn't put bucket configuration", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) GetBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
reqInfo := middleware.GetReqInfo(r.Context())
|
||||
|
||||
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
conf, err := h.obj.GetBucketNotificationConfiguration(r.Context(), bktInfo)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket notification configuration", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = middleware.EncodeToResponse(w, conf); err != nil {
|
||||
h.logAndSendError(w, "could not encode bucket notification configuration to response", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) sendNotifications(ctx context.Context, p *SendNotificationParams) error {
|
||||
if !h.cfg.NotificatorEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
conf, err := h.obj.GetBucketNotificationConfiguration(ctx, p.BktInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get notification configuration: %w", err)
|
||||
}
|
||||
if conf.IsEmpty() {
|
||||
return nil
|
||||
}
|
||||
|
||||
box, err := middleware.GetBoxData(ctx)
|
||||
if err == nil && box.Gate.BearerToken != nil {
|
||||
p.User = bearer.ResolveIssuer(*box.Gate.BearerToken).EncodeToString()
|
||||
}
|
||||
|
||||
p.Time = layer.TimeNow(ctx)
|
||||
|
||||
topics := filterSubjects(conf, p.Event, p.NotificationInfo.Name)
|
||||
|
||||
return h.notificator.SendNotifications(topics, p)
|
||||
}
|
||||
|
||||
// checkBucketConfiguration checks notification configuration and generates an ID for configurations with empty ids.
|
||||
func (h *handler) checkBucketConfiguration(ctx context.Context, conf *data.NotificationConfiguration, r *middleware.ReqInfo) (completed bool, err error) {
|
||||
if conf == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if conf.TopicConfigurations != nil || conf.LambdaFunctionConfigurations != nil {
|
||||
return completed, errors.GetAPIError(errors.ErrNotificationTopicNotSupported)
|
||||
}
|
||||
|
||||
for i, q := range conf.QueueConfigurations {
|
||||
if err = checkEvents(q.Events); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = checkRules(q.Filter.Key.FilterRules); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if h.cfg.NotificatorEnabled() {
|
||||
if err = h.notificator.SendTestNotification(q.QueueArn, r.BucketName, r.RequestID, r.Host, layer.TimeNow(ctx)); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
h.reqLogger(ctx).Warn(logs.FailedToSendTestEventBecauseNotificationsIsDisabled)
|
||||
}
|
||||
|
||||
if q.ID == "" {
|
||||
completed = true
|
||||
conf.QueueConfigurations[i].ID = uuid.NewString()
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func checkRules(rules []data.FilterRule) error {
|
||||
names := make(map[string]struct{})
|
||||
|
||||
for _, r := range rules {
|
||||
if r.Name != filterRuleSuffixName && r.Name != filterRulePrefixName {
|
||||
return errors.GetAPIError(errors.ErrFilterNameInvalid)
|
||||
}
|
||||
if _, ok := names[r.Name]; ok {
|
||||
if r.Name == filterRuleSuffixName {
|
||||
return errors.GetAPIError(errors.ErrFilterNameSuffix)
|
||||
}
|
||||
return errors.GetAPIError(errors.ErrFilterNamePrefix)
|
||||
}
|
||||
|
||||
names[r.Name] = struct{}{}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkEvents(events []string) error {
|
||||
for _, e := range events {
|
||||
if _, ok := validEvents[e]; !ok {
|
||||
return errors.GetAPIError(errors.ErrEventNotification)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func filterSubjects(conf *data.NotificationConfiguration, eventType, objName string) map[string]string {
|
||||
topics := make(map[string]string)
|
||||
|
||||
for _, t := range conf.QueueConfigurations {
|
||||
event := false
|
||||
for _, e := range t.Events {
|
||||
// the second condition is comparison with the events ending with *:
|
||||
// s3:ObjectCreated:*, s3:ObjectRemoved:* etc without the last char
|
||||
if eventType == e || strings.HasPrefix(eventType, e[:len(e)-1]) {
|
||||
event = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !event {
|
||||
continue
|
||||
}
|
||||
|
||||
filter := true
|
||||
for _, f := range t.Filter.Key.FilterRules {
|
||||
if f.Name == filterRulePrefixName && !strings.HasPrefix(objName, f.Value) ||
|
||||
f.Name == filterRuleSuffixName && !strings.HasSuffix(objName, f.Value) {
|
||||
filter = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if filter {
|
||||
topics[t.ID] = t.QueueArn
|
||||
}
|
||||
}
|
||||
|
||||
return topics
|
||||
}
|
115
api/handler/notifications_test.go
Normal file
115
api/handler/notifications_test.go
Normal file
|
@ -0,0 +1,115 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFilterSubjects(t *testing.T) {
|
||||
config := &data.NotificationConfiguration{
|
||||
QueueConfigurations: []data.QueueConfiguration{
|
||||
{
|
||||
ID: "test1",
|
||||
QueueArn: "test1",
|
||||
Events: []string{EventObjectCreated, EventObjectRemovedDelete},
|
||||
},
|
||||
{
|
||||
ID: "test2",
|
||||
QueueArn: "test2",
|
||||
Events: []string{EventObjectTagging},
|
||||
Filter: data.Filter{Key: data.Key{FilterRules: []data.FilterRule{
|
||||
{Name: "prefix", Value: "dir/"},
|
||||
{Name: "suffix", Value: ".png"},
|
||||
}}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("no topics because suitable events not found", func(t *testing.T) {
|
||||
topics := filterSubjects(config, EventObjectACLPut, "dir/a.png")
|
||||
require.Empty(t, topics)
|
||||
})
|
||||
|
||||
t.Run("no topics because of not suitable prefix", func(t *testing.T) {
|
||||
topics := filterSubjects(config, EventObjectTaggingPut, "dirw/cat.png")
|
||||
require.Empty(t, topics)
|
||||
})
|
||||
|
||||
t.Run("no topics because of not suitable suffix", func(t *testing.T) {
|
||||
topics := filterSubjects(config, EventObjectTaggingPut, "a.jpg")
|
||||
require.Empty(t, topics)
|
||||
})
|
||||
|
||||
t.Run("filter topics from queue configs without prefix suffix filter and exact event", func(t *testing.T) {
|
||||
topics := filterSubjects(config, EventObjectCreatedPut, "dir/a.png")
|
||||
require.Contains(t, topics, "test1")
|
||||
require.Len(t, topics, 1)
|
||||
require.Equal(t, topics["test1"], "test1")
|
||||
})
|
||||
|
||||
t.Run("filter topics from queue configs with prefix suffix filter and '*' ending event", func(t *testing.T) {
|
||||
topics := filterSubjects(config, EventObjectTaggingPut, "dir/a.png")
|
||||
require.Contains(t, topics, "test2")
|
||||
require.Len(t, topics, 1)
|
||||
require.Equal(t, topics["test2"], "test2")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckRules(t *testing.T) {
|
||||
t.Run("correct rules with prefix and suffix", func(t *testing.T) {
|
||||
rules := []data.FilterRule{
|
||||
{Name: "prefix", Value: "asd"},
|
||||
{Name: "suffix", Value: "asd"},
|
||||
}
|
||||
err := checkRules(rules)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("correct rules with prefix", func(t *testing.T) {
|
||||
rules := []data.FilterRule{
|
||||
{Name: "prefix", Value: "asd"},
|
||||
}
|
||||
err := checkRules(rules)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("correct rules with suffix", func(t *testing.T) {
|
||||
rules := []data.FilterRule{
|
||||
{Name: "suffix", Value: "asd"},
|
||||
}
|
||||
err := checkRules(rules)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("incorrect rules with wrong name", func(t *testing.T) {
|
||||
rules := []data.FilterRule{
|
||||
{Name: "prefix", Value: "sdf"},
|
||||
{Name: "sfx", Value: "asd"},
|
||||
}
|
||||
err := checkRules(rules)
|
||||
require.ErrorIs(t, err, errors.GetAPIError(errors.ErrFilterNameInvalid))
|
||||
})
|
||||
|
||||
t.Run("incorrect rules with repeating suffix", func(t *testing.T) {
|
||||
rules := []data.FilterRule{
|
||||
{Name: "suffix", Value: "asd"},
|
||||
{Name: "suffix", Value: "asdf"},
|
||||
{Name: "prefix", Value: "jk"},
|
||||
}
|
||||
err := checkRules(rules)
|
||||
require.ErrorIs(t, err, errors.GetAPIError(errors.ErrFilterNameSuffix))
|
||||
})
|
||||
|
||||
t.Run("incorrect rules with repeating prefix", func(t *testing.T) {
|
||||
rules := []data.FilterRule{
|
||||
{Name: "suffix", Value: "ds"},
|
||||
{Name: "prefix", Value: "asd"},
|
||||
{Name: "prefix", Value: "asdf"},
|
||||
}
|
||||
err := checkRules(rules)
|
||||
require.ErrorIs(t, err, errors.GetAPIError(errors.ErrFilterNamePrefix))
|
||||
})
|
||||
}
|
|
@ -232,7 +232,7 @@ func (h *handler) ListBucketObjectVersionsHandler(w http.ResponseWriter, r *http
|
|||
return
|
||||
}
|
||||
|
||||
response := encodeListObjectVersionsToResponse(p, info, p.BktInfo.Name, h.cfg.MD5Enabled())
|
||||
response := encodeListObjectVersionsToResponse(info, p.BktInfo.Name, h.cfg.MD5Enabled())
|
||||
if err = middleware.EncodeToResponse(w, response); err != nil {
|
||||
h.logAndSendError(w, "something went wrong", reqInfo, err)
|
||||
}
|
||||
|
@ -264,28 +264,24 @@ func parseListObjectVersionsRequest(reqInfo *middleware.ReqInfo) (*layer.ListObj
|
|||
return &res, nil
|
||||
}
|
||||
|
||||
func encodeListObjectVersionsToResponse(p *layer.ListObjectVersionsParams, info *layer.ListObjectVersionsInfo, bucketName string, md5Enabled bool) *ListObjectsVersionsResponse {
|
||||
func encodeListObjectVersionsToResponse(info *layer.ListObjectVersionsInfo, bucketName string, md5Enabled bool) *ListObjectsVersionsResponse {
|
||||
res := ListObjectsVersionsResponse{
|
||||
Name: bucketName,
|
||||
IsTruncated: info.IsTruncated,
|
||||
KeyMarker: s3PathEncode(info.KeyMarker, p.Encode),
|
||||
NextKeyMarker: s3PathEncode(info.NextKeyMarker, p.Encode),
|
||||
KeyMarker: info.KeyMarker,
|
||||
NextKeyMarker: info.NextKeyMarker,
|
||||
NextVersionIDMarker: info.NextVersionIDMarker,
|
||||
VersionIDMarker: info.VersionIDMarker,
|
||||
Prefix: s3PathEncode(p.Prefix, p.Encode),
|
||||
Delimiter: s3PathEncode(p.Delimiter, p.Encode),
|
||||
EncodingType: p.Encode,
|
||||
MaxKeys: p.MaxKeys,
|
||||
}
|
||||
|
||||
for _, prefix := range info.CommonPrefixes {
|
||||
res.CommonPrefixes = append(res.CommonPrefixes, CommonPrefix{Prefix: s3PathEncode(prefix, p.Encode)})
|
||||
res.CommonPrefixes = append(res.CommonPrefixes, CommonPrefix{Prefix: prefix})
|
||||
}
|
||||
|
||||
for _, ver := range info.Version {
|
||||
res.Version = append(res.Version, ObjectVersionResponse{
|
||||
IsLatest: ver.IsLatest,
|
||||
Key: s3PathEncode(ver.NodeVersion.FilePath, p.Encode),
|
||||
Key: ver.NodeVersion.FilePath,
|
||||
LastModified: ver.NodeVersion.Created.UTC().Format(time.RFC3339),
|
||||
Owner: Owner{
|
||||
ID: ver.NodeVersion.Owner.String(),
|
||||
|
@ -301,7 +297,7 @@ func encodeListObjectVersionsToResponse(p *layer.ListObjectVersionsParams, info
|
|||
for _, del := range info.DeleteMarker {
|
||||
res.DeleteMarker = append(res.DeleteMarker, DeleteMarkerEntry{
|
||||
IsLatest: del.IsLatest,
|
||||
Key: s3PathEncode(del.NodeVersion.FilePath, p.Encode),
|
||||
Key: del.NodeVersion.FilePath,
|
||||
LastModified: del.NodeVersion.Created.UTC().Format(time.RFC3339),
|
||||
Owner: Owner{
|
||||
ID: del.NodeVersion.Owner.String(),
|
||||
|
|
|
@ -94,11 +94,11 @@ func TestListObjectsWithOldTreeNodes(t *testing.T) {
|
|||
}
|
||||
|
||||
func makeAllTreeObjectsOld(hc *handlerContext, bktInfo *data.BucketInfo) {
|
||||
nodes, err := hc.treeMock.GetSubTree(hc.Context(), bktInfo, "version", []uint64{0}, 0)
|
||||
nodes, err := hc.treeMock.GetSubTree(hc.Context(), bktInfo, "version", 0, 0)
|
||||
require.NoError(hc.t, err)
|
||||
|
||||
for _, node := range nodes {
|
||||
if node.GetNodeID()[0] == 0 {
|
||||
if node.GetNodeID() == 0 {
|
||||
continue
|
||||
}
|
||||
meta := make(map[string]string, len(node.GetMeta()))
|
||||
|
@ -108,7 +108,7 @@ func makeAllTreeObjectsOld(hc *handlerContext, bktInfo *data.BucketInfo) {
|
|||
}
|
||||
}
|
||||
|
||||
err = hc.treeMock.MoveNode(hc.Context(), bktInfo, "version", node.GetNodeID()[0], node.GetParentID()[0], meta)
|
||||
err = hc.treeMock.MoveNode(hc.Context(), bktInfo, "version", node.GetNodeID(), node.GetParentID(), meta)
|
||||
require.NoError(hc.t, err)
|
||||
}
|
||||
}
|
||||
|
@ -675,49 +675,6 @@ func TestMintVersioningListObjectVersionsVersionIDContinuation(t *testing.T) {
|
|||
require.Equal(t, page1.NextVersionIDMarker, page2.VersionIDMarker)
|
||||
}
|
||||
|
||||
func TestListObjectVersionsEncoding(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
|
||||
bktName := "bucket-for-listing-versions-encoding"
|
||||
bktInfo := createTestBucket(hc, bktName)
|
||||
putBucketVersioning(t, hc, bktName, true)
|
||||
|
||||
objects := []string{"foo()/bar", "foo()/bar/xyzzy", "auux ab/thud", "asdf+b"}
|
||||
for _, objName := range objects {
|
||||
createTestObject(hc, bktInfo, objName, encryption.Params{})
|
||||
}
|
||||
deleteObject(t, hc, bktName, "auux ab/thud", "")
|
||||
|
||||
listResponse := listObjectsVersionsURL(hc, bktName, "foo(", ")", "", "", -1)
|
||||
|
||||
require.Len(t, listResponse.CommonPrefixes, 1)
|
||||
require.Equal(t, "foo%28%29", listResponse.CommonPrefixes[0].Prefix)
|
||||
require.Len(t, listResponse.Version, 0)
|
||||
require.Len(t, listResponse.DeleteMarker, 0)
|
||||
require.Equal(t, "foo%28", listResponse.Prefix)
|
||||
require.Equal(t, "%29", listResponse.Delimiter)
|
||||
require.Equal(t, "url", listResponse.EncodingType)
|
||||
require.Equal(t, maxObjectList, listResponse.MaxKeys)
|
||||
|
||||
listResponse = listObjectsVersions(hc, bktName, "", "", "", "", 1)
|
||||
require.Empty(t, listResponse.EncodingType)
|
||||
|
||||
listResponse = listObjectsVersionsURL(hc, bktName, "", "", listResponse.NextKeyMarker, listResponse.NextVersionIDMarker, 3)
|
||||
|
||||
require.Len(t, listResponse.CommonPrefixes, 0)
|
||||
require.Len(t, listResponse.Version, 2)
|
||||
require.Equal(t, "auux%20ab/thud", listResponse.Version[0].Key)
|
||||
require.False(t, listResponse.Version[0].IsLatest)
|
||||
require.Equal(t, "foo%28%29/bar", listResponse.Version[1].Key)
|
||||
require.Len(t, listResponse.DeleteMarker, 1)
|
||||
require.Equal(t, "auux%20ab/thud", listResponse.DeleteMarker[0].Key)
|
||||
require.True(t, listResponse.DeleteMarker[0].IsLatest)
|
||||
require.Equal(t, "asdf%2Bb", listResponse.KeyMarker)
|
||||
require.Equal(t, "foo%28%29/bar", listResponse.NextKeyMarker)
|
||||
require.Equal(t, "url", listResponse.EncodingType)
|
||||
require.Equal(t, 3, listResponse.MaxKeys)
|
||||
}
|
||||
|
||||
func checkVersionsNames(t *testing.T, versions *ListObjectsVersionsResponse, names []string) {
|
||||
for i, v := range versions.Version {
|
||||
require.Equal(t, names[i], v.Key)
|
||||
|
@ -820,14 +777,6 @@ func listObjectsV1(hc *handlerContext, bktName, prefix, delimiter, marker string
|
|||
}
|
||||
|
||||
func listObjectsVersions(hc *handlerContext, bktName, prefix, delimiter, keyMarker, versionIDMarker string, maxKeys int) *ListObjectsVersionsResponse {
|
||||
return listObjectsVersionsBase(hc, bktName, prefix, delimiter, keyMarker, versionIDMarker, maxKeys, false)
|
||||
}
|
||||
|
||||
func listObjectsVersionsURL(hc *handlerContext, bktName, prefix, delimiter, keyMarker, versionIDMarker string, maxKeys int) *ListObjectsVersionsResponse {
|
||||
return listObjectsVersionsBase(hc, bktName, prefix, delimiter, keyMarker, versionIDMarker, maxKeys, true)
|
||||
}
|
||||
|
||||
func listObjectsVersionsBase(hc *handlerContext, bktName, prefix, delimiter, keyMarker, versionIDMarker string, maxKeys int, encode bool) *ListObjectsVersionsResponse {
|
||||
query := prepareCommonListObjectsQuery(prefix, delimiter, maxKeys)
|
||||
if len(keyMarker) != 0 {
|
||||
query.Add("key-marker", keyMarker)
|
||||
|
@ -835,9 +784,6 @@ func listObjectsVersionsBase(hc *handlerContext, bktName, prefix, delimiter, key
|
|||
if len(versionIDMarker) != 0 {
|
||||
query.Add("version-id-marker", versionIDMarker)
|
||||
}
|
||||
if encode {
|
||||
query.Add("encoding-type", "url")
|
||||
}
|
||||
|
||||
w, r := prepareTestFullRequest(hc, bktName, "", query, nil)
|
||||
hc.Handler().ListBucketObjectVersionsHandler(w, r)
|
||||
|
|
164
api/handler/patch.go
Normal file
164
api/handler/patch.go
Normal file
|
@ -0,0 +1,164 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (h *handler) PatchHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
reqInfo = middleware.GetReqInfo(ctx)
|
||||
)
|
||||
|
||||
byteRange, err := parseByteRange(r.Header.Get(api.ContentRange))
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not parse byte range", reqInfo, errors.GetAPIError(errors.ErrInvalidRange), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
if uint64(r.ContentLength) != (byteRange.End - byteRange.Start + 1) {
|
||||
h.logAndSendError(w, "content-length must be equal to byte range length", reqInfo, errors.GetAPIError(errors.ErrBadRequest))
|
||||
return
|
||||
}
|
||||
|
||||
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := h.obj.GetBucketSettings(ctx, bktInfo)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket settings", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !settings.VersioningEnabled() {
|
||||
h.logAndSendError(w, "could not patch object in unversioned bucket", reqInfo, errors.GetAPIError(errors.ErrBadRequest))
|
||||
return
|
||||
}
|
||||
|
||||
srcObjPrm := &layer.HeadObjectParams{
|
||||
Object: reqInfo.ObjectName,
|
||||
VersionID: reqInfo.URL.Query().Get(api.QueryVersionID),
|
||||
BktInfo: bktInfo,
|
||||
}
|
||||
|
||||
extendedSrcObjInfo, err := h.obj.GetExtendedObjectInfo(ctx, srcObjPrm)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not find object", reqInfo, err)
|
||||
return
|
||||
}
|
||||
srcObjInfo := extendedSrcObjInfo.ObjectInfo
|
||||
|
||||
srcSize, err := layer.GetObjectSize(srcObjInfo)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "failed to get source object size", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
if byteRange.Start > srcSize {
|
||||
h.logAndSendError(w, "start byte is greater than object size", reqInfo, errors.GetAPIError(errors.ErrBadRequest))
|
||||
return
|
||||
}
|
||||
|
||||
if len(srcObjInfo.ContentType) > 0 {
|
||||
srcObjInfo.Headers[api.ContentType] = srcObjInfo.ContentType
|
||||
}
|
||||
metadata := makeCopyMap(srcObjInfo.Headers)
|
||||
filterMetadataMap(metadata)
|
||||
|
||||
var size uint64
|
||||
if r.ContentLength > 0 {
|
||||
size = uint64(r.ContentLength)
|
||||
}
|
||||
|
||||
params := &layer.PatchObjectParams{
|
||||
Object: srcObjInfo,
|
||||
BktInfo: bktInfo,
|
||||
SrcSize: srcSize,
|
||||
Header: metadata,
|
||||
NewBytes: r.Body,
|
||||
NewBytesSize: size,
|
||||
Range: byteRange,
|
||||
}
|
||||
|
||||
params.CopiesNumbers, err = h.pickCopiesNumbers(metadata, reqInfo.Namespace, bktInfo.LocationConstraint)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "invalid copies number", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
extendedObjInfo, err := h.obj.PatchObject(ctx, params)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "couldn't patch object", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set(api.AmzVersionID, extendedObjInfo.ObjectInfo.VersionID())
|
||||
w.Header().Set(api.ETag, data.Quote(extendedObjInfo.ObjectInfo.ETag(h.cfg.MD5Enabled())))
|
||||
|
||||
resp := PatchObjectResult{
|
||||
Object: PatchObject{
|
||||
LastModified: extendedObjInfo.ObjectInfo.Created.UTC().Format(time.RFC3339),
|
||||
ETag: data.Quote(extendedObjInfo.ObjectInfo.ETag(h.cfg.MD5Enabled())),
|
||||
},
|
||||
}
|
||||
|
||||
if err = middleware.EncodeToResponse(w, resp); err != nil {
|
||||
h.logAndSendError(w, "could not encode PatchObjectResult to response", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func parseByteRange(rangeStr string) (*layer.RangeParams, error) {
|
||||
const (
|
||||
prefix = "bytes "
|
||||
suffix = "/*"
|
||||
)
|
||||
|
||||
if rangeStr == "" {
|
||||
return nil, fmt.Errorf("empty range")
|
||||
}
|
||||
if !strings.HasPrefix(rangeStr, prefix) {
|
||||
return nil, fmt.Errorf("unknown unit in range header")
|
||||
}
|
||||
if !strings.HasSuffix(rangeStr, suffix) {
|
||||
return nil, fmt.Errorf("invalid size in range header")
|
||||
}
|
||||
|
||||
parts := strings.Split(strings.TrimSuffix(strings.TrimPrefix(rangeStr, prefix), suffix), "-")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("invalid range: %s", rangeStr)
|
||||
}
|
||||
|
||||
start, err := strconv.ParseUint(parts[0], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid start byte: %s", parts[0])
|
||||
}
|
||||
|
||||
end, err := strconv.ParseUint(parts[1], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid end byte: %s", parts[1])
|
||||
}
|
||||
|
||||
if start > end {
|
||||
return nil, fmt.Errorf("start byte is greater than end byte")
|
||||
}
|
||||
|
||||
return &layer.RangeParams{
|
||||
Start: start,
|
||||
End: end,
|
||||
}, nil
|
||||
}
|
|
@ -4,12 +4,12 @@ import (
|
|||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
@ -29,6 +29,8 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/retryer"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/tree"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
|
||||
|
@ -172,9 +174,10 @@ func (p *policyCondition) UnmarshalJSON(data []byte) error {
|
|||
|
||||
// keywords of predefined basic ACL values.
|
||||
const (
|
||||
basicACLPrivate = "private"
|
||||
basicACLReadOnly = "public-read"
|
||||
basicACLPublic = "public-read-write"
|
||||
basicACLPrivate = "private"
|
||||
basicACLReadOnly = "public-read"
|
||||
basicACLPublic = "public-read-write"
|
||||
cannedACLAuthRead = "authenticated-read"
|
||||
)
|
||||
|
||||
type createBucketParams struct {
|
||||
|
@ -184,10 +187,12 @@ type createBucketParams struct {
|
|||
|
||||
func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
err error
|
||||
cannedACLStatus = aclHeadersStatus(r)
|
||||
ctx = r.Context()
|
||||
reqInfo = middleware.GetReqInfo(ctx)
|
||||
err error
|
||||
newEaclTable *eacl.Table
|
||||
sessionTokenEACL *session.Container
|
||||
cannedACLStatus = aclHeadersStatus(r)
|
||||
ctx = r.Context()
|
||||
reqInfo = middleware.GetReqInfo(ctx)
|
||||
)
|
||||
|
||||
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
||||
|
@ -202,11 +207,20 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if cannedACLStatus == aclStatusYes {
|
||||
apeEnabled := bktInfo.APEEnabled || settings.CannedACL != ""
|
||||
if apeEnabled && cannedACLStatus == aclStatusYes {
|
||||
h.logAndSendError(w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported))
|
||||
return
|
||||
}
|
||||
|
||||
needUpdateEACLTable := !(apeEnabled || cannedACLStatus == aclStatusNo)
|
||||
if needUpdateEACLTable {
|
||||
if sessionTokenEACL, err = getSessionTokenSetEACL(r.Context()); err != nil {
|
||||
h.logAndSendError(w, "could not get eacl session token from a box", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tagSet, err := parseTaggingHeader(r.Header)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not parse tagging header", reqInfo, err)
|
||||
|
@ -279,6 +293,23 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
objInfo := extendedObjInfo.ObjectInfo
|
||||
|
||||
s := &SendNotificationParams{
|
||||
Event: EventObjectCreatedPut,
|
||||
NotificationInfo: data.NotificationInfoFromObject(objInfo, h.cfg.MD5Enabled()),
|
||||
BktInfo: bktInfo,
|
||||
ReqInfo: reqInfo,
|
||||
}
|
||||
if err = h.sendNotifications(ctx, s); err != nil {
|
||||
h.reqLogger(ctx).Error(logs.CouldntSendNotification, zap.Error(err))
|
||||
}
|
||||
|
||||
if needUpdateEACLTable {
|
||||
if newEaclTable, err = h.getNewEAclTable(r, bktInfo, objInfo); err != nil {
|
||||
h.logAndSendError(w, "could not get new eacl table", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if tagSet != nil {
|
||||
tagPrm := &data.PutObjectTaggingParams{
|
||||
ObjectVersion: &data.ObjectVersion{
|
||||
|
@ -289,12 +320,25 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
TagSet: tagSet,
|
||||
NodeVersion: extendedObjInfo.NodeVersion,
|
||||
}
|
||||
if err = h.obj.PutObjectTagging(r.Context(), tagPrm); err != nil {
|
||||
if _, err = h.obj.PutObjectTagging(r.Context(), tagPrm); err != nil {
|
||||
h.logAndSendError(w, "could not upload object tagging", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if newEaclTable != nil {
|
||||
p := &layer.PutBucketACLParams{
|
||||
BktInfo: bktInfo,
|
||||
EACL: newEaclTable,
|
||||
SessionToken: sessionTokenEACL,
|
||||
}
|
||||
|
||||
if err = h.obj.PutBucketACL(r.Context(), p); err != nil {
|
||||
h.logAndSendError(w, "could not put bucket acl", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if settings.VersioningEnabled() {
|
||||
w.Header().Set(api.AmzVersionID, objInfo.VersionID())
|
||||
}
|
||||
|
@ -426,10 +470,13 @@ func formEncryptionParamsBase(r *http.Request, isCopySource bool) (enc encryptio
|
|||
|
||||
func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
tagSet map[string]string
|
||||
ctx = r.Context()
|
||||
reqInfo = middleware.GetReqInfo(ctx)
|
||||
metadata = make(map[string]string)
|
||||
newEaclTable *eacl.Table
|
||||
tagSet map[string]string
|
||||
sessionTokenEACL *session.Container
|
||||
ctx = r.Context()
|
||||
reqInfo = middleware.GetReqInfo(ctx)
|
||||
metadata = make(map[string]string)
|
||||
cannedACLStatus = aclHeadersStatus(r)
|
||||
)
|
||||
|
||||
policy, err := checkPostPolicy(r, reqInfo, metadata)
|
||||
|
@ -465,52 +512,35 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if acl := auth.MultipartFormValue(r, "acl"); acl != "" && acl != basicACLPrivate {
|
||||
apeEnabled := bktInfo.APEEnabled || settings.CannedACL != ""
|
||||
if apeEnabled && cannedACLStatus == aclStatusYes {
|
||||
h.logAndSendError(w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported))
|
||||
return
|
||||
}
|
||||
|
||||
reqInfo.ObjectName = auth.MultipartFormValue(r, "key")
|
||||
needUpdateEACLTable := !(apeEnabled || cannedACLStatus == aclStatusNo)
|
||||
if needUpdateEACLTable {
|
||||
if sessionTokenEACL, err = getSessionTokenSetEACL(ctx); err != nil {
|
||||
h.logAndSendError(w, "could not get eacl session token from a box", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var contentReader io.Reader
|
||||
var size uint64
|
||||
var filename string
|
||||
|
||||
if content, ok := r.MultipartForm.Value["file"]; ok {
|
||||
fullContent := strings.Join(content, "")
|
||||
contentReader = bytes.NewBufferString(fullContent)
|
||||
size = uint64(len(fullContent))
|
||||
|
||||
if reqInfo.ObjectName == "" || strings.Contains(reqInfo.ObjectName, "${filename}") {
|
||||
_, head, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not parse file field", reqInfo, err)
|
||||
return
|
||||
}
|
||||
filename = head.Filename
|
||||
}
|
||||
contentReader = bytes.NewBufferString(content[0])
|
||||
size = uint64(len(content[0]))
|
||||
} else {
|
||||
var head *multipart.FileHeader
|
||||
contentReader, head, err = r.FormFile("file")
|
||||
file, head, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not parse file field", reqInfo, err)
|
||||
h.logAndSendError(w, "could get uploading file", reqInfo, err)
|
||||
return
|
||||
}
|
||||
contentReader = file
|
||||
size = uint64(head.Size)
|
||||
filename = head.Filename
|
||||
reqInfo.ObjectName = strings.ReplaceAll(reqInfo.ObjectName, "${filename}", head.Filename)
|
||||
}
|
||||
|
||||
if reqInfo.ObjectName == "" {
|
||||
reqInfo.ObjectName = filename
|
||||
} else {
|
||||
reqInfo.ObjectName = strings.ReplaceAll(reqInfo.ObjectName, "${filename}", filename)
|
||||
}
|
||||
|
||||
if reqInfo.ObjectName == "" {
|
||||
h.logAndSendError(w, "missing object name", reqInfo, errors.GetAPIError(errors.ErrInvalidArgument))
|
||||
return
|
||||
}
|
||||
|
||||
if !policy.CheckContentLength(size) {
|
||||
h.logAndSendError(w, "invalid content-length", reqInfo, errors.GetAPIError(errors.ErrInvalidArgument))
|
||||
return
|
||||
|
@ -531,6 +561,28 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
objInfo := extendedObjInfo.ObjectInfo
|
||||
|
||||
s := &SendNotificationParams{
|
||||
Event: EventObjectCreatedPost,
|
||||
NotificationInfo: data.NotificationInfoFromObject(objInfo, h.cfg.MD5Enabled()),
|
||||
BktInfo: bktInfo,
|
||||
ReqInfo: reqInfo,
|
||||
}
|
||||
if err = h.sendNotifications(ctx, s); err != nil {
|
||||
h.reqLogger(ctx).Error(logs.CouldntSendNotification, zap.Error(err))
|
||||
}
|
||||
|
||||
if acl := auth.MultipartFormValue(r, "acl"); acl != "" {
|
||||
r.Header.Set(api.AmzACL, acl)
|
||||
r.Header.Set(api.AmzGrantFullControl, "")
|
||||
r.Header.Set(api.AmzGrantWrite, "")
|
||||
r.Header.Set(api.AmzGrantRead, "")
|
||||
|
||||
if newEaclTable, err = h.getNewEAclTable(r, bktInfo, objInfo); err != nil {
|
||||
h.logAndSendError(w, "could not get new eacl table", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if tagSet != nil {
|
||||
tagPrm := &data.PutObjectTaggingParams{
|
||||
ObjectVersion: &data.ObjectVersion{
|
||||
|
@ -541,12 +593,25 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) {
|
|||
NodeVersion: extendedObjInfo.NodeVersion,
|
||||
}
|
||||
|
||||
if err = h.obj.PutObjectTagging(ctx, tagPrm); err != nil {
|
||||
if _, err = h.obj.PutObjectTagging(ctx, tagPrm); err != nil {
|
||||
h.logAndSendError(w, "could not upload object tagging", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if newEaclTable != nil {
|
||||
p := &layer.PutBucketACLParams{
|
||||
BktInfo: bktInfo,
|
||||
EACL: newEaclTable,
|
||||
SessionToken: sessionTokenEACL,
|
||||
}
|
||||
|
||||
if err = h.obj.PutBucketACL(ctx, p); err != nil {
|
||||
h.logAndSendError(w, "could not put bucket acl", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if settings.VersioningEnabled() {
|
||||
w.Header().Set(api.AmzVersionID, objInfo.VersionID())
|
||||
}
|
||||
|
@ -626,6 +691,10 @@ func checkPostPolicy(r *http.Request, reqInfo *middleware.ReqInfo, metadata map[
|
|||
if key == "content-type" {
|
||||
metadata[api.ContentType] = value
|
||||
}
|
||||
|
||||
if key == "key" {
|
||||
reqInfo.ObjectName = value
|
||||
}
|
||||
}
|
||||
|
||||
for _, cond := range policy.Conditions {
|
||||
|
@ -668,6 +737,56 @@ func aclHeadersStatus(r *http.Request) aclStatus {
|
|||
return aclStatusNo
|
||||
}
|
||||
|
||||
func (h *handler) getNewEAclTable(r *http.Request, bktInfo *data.BucketInfo, objInfo *data.ObjectInfo) (*eacl.Table, error) {
|
||||
var newEaclTable *eacl.Table
|
||||
key, err := h.bearerTokenIssuerKey(r.Context())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get bearer token issuer: %w", err)
|
||||
}
|
||||
objectACL, err := parseACLHeaders(r.Header, key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse object acl: %w", err)
|
||||
}
|
||||
|
||||
resInfo := &resourceInfo{
|
||||
Bucket: objInfo.Bucket,
|
||||
Object: objInfo.Name,
|
||||
Version: objInfo.VersionID(),
|
||||
}
|
||||
|
||||
bktPolicy, err := aclToPolicy(objectACL, resInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not translate object acl to bucket policy: %w", err)
|
||||
}
|
||||
|
||||
astChild, err := policyToAst(bktPolicy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not translate policy to ast: %w", err)
|
||||
}
|
||||
|
||||
bacl, err := h.obj.GetBucketACL(r.Context(), bktInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get bucket eacl: %w", err)
|
||||
}
|
||||
|
||||
parentAst := tableToAst(bacl.EACL, objInfo.Bucket)
|
||||
strCID := bacl.Info.CID.EncodeToString()
|
||||
|
||||
for _, resource := range parentAst.Resources {
|
||||
if resource.Bucket == strCID {
|
||||
resource.Bucket = objInfo.Bucket
|
||||
}
|
||||
}
|
||||
|
||||
if resAst, updated := mergeAst(parentAst, astChild); updated {
|
||||
if newEaclTable, err = astToTable(resAst); err != nil {
|
||||
return nil, fmt.Errorf("could not translate ast to table: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return newEaclTable, nil
|
||||
}
|
||||
|
||||
func parseTaggingHeader(header http.Header) (map[string]string, error) {
|
||||
var tagSet map[string]string
|
||||
if tagging := header.Get(api.AmzTagging); len(tagging) > 0 {
|
||||
|
@ -707,7 +826,8 @@ func parseCannedACL(header http.Header) (string, error) {
|
|||
return basicACLPrivate, nil
|
||||
}
|
||||
|
||||
if acl == basicACLPrivate || acl == basicACLPublic || acl == basicACLReadOnly {
|
||||
if acl == basicACLPrivate || acl == basicACLPublic ||
|
||||
acl == cannedACLAuthRead || acl == basicACLReadOnly {
|
||||
return acl, nil
|
||||
}
|
||||
|
||||
|
@ -715,6 +835,11 @@ func parseCannedACL(header http.Header) (string, error) {
|
|||
}
|
||||
|
||||
func (h *handler) CreateBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if h.cfg.ACLEnabled() {
|
||||
h.createBucketHandlerACL(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
h.createBucketHandlerPolicy(w, r)
|
||||
}
|
||||
|
||||
|
@ -774,6 +899,7 @@ func (h *handler) createBucketHandlerPolicy(w http.ResponseWriter, r *http.Reque
|
|||
return
|
||||
}
|
||||
|
||||
p.APEEnabled = true
|
||||
bktInfo, err := h.obj.CreateBucket(ctx, p)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not create bucket", reqInfo, err)
|
||||
|
@ -781,7 +907,7 @@ func (h *handler) createBucketHandlerPolicy(w http.ResponseWriter, r *http.Reque
|
|||
}
|
||||
h.reqLogger(ctx).Info(logs.BucketIsCreated, zap.Stringer("container_id", bktInfo.CID))
|
||||
|
||||
chains := bucketCannedACLToAPERules(cannedACL, reqInfo, bktInfo.CID)
|
||||
chains := bucketCannedACLToAPERules(cannedACL, reqInfo, key, bktInfo.CID)
|
||||
if err = h.ape.SaveACLChains(bktInfo.CID.EncodeToString(), chains); err != nil {
|
||||
h.logAndSendError(w, "failed to add morph rule chain", reqInfo, err)
|
||||
return
|
||||
|
@ -819,7 +945,7 @@ func (h *handler) putBucketSettingsRetryer() aws.RetryerV2 {
|
|||
return retry.NewStandard(func(options *retry.StandardOptions) {
|
||||
options.MaxAttempts = h.cfg.RetryMaxAttempts()
|
||||
options.MaxBackoff = h.cfg.RetryMaxBackoff()
|
||||
if h.cfg.RetryStrategy() == RetryStrategyExponential {
|
||||
if h.cfg.RetryStrategy() == RetryStrategyConstant {
|
||||
options.Backoff = retry.NewExponentialJitterBackoff(options.MaxBackoff)
|
||||
} else {
|
||||
options.Backoff = retry.BackoffDelayerFunc(func(int, error) (time.Duration, error) {
|
||||
|
@ -836,6 +962,78 @@ func (h *handler) putBucketSettingsRetryer() aws.RetryerV2 {
|
|||
})
|
||||
}
|
||||
|
||||
func (h *handler) createBucketHandlerACL(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
reqInfo := middleware.GetReqInfo(ctx)
|
||||
|
||||
boxData, err := middleware.GetBoxData(ctx)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "get access box from request", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
key, p, err := h.parseCommonCreateBucketParams(reqInfo, boxData, r)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "parse create bucket params", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
aclPrm := &layer.PutBucketACLParams{SessionToken: boxData.Gate.SessionTokenForSetEACL()}
|
||||
if aclPrm.SessionToken == nil {
|
||||
h.logAndSendError(w, "couldn't find session token for setEACL", reqInfo, errors.GetAPIError(errors.ErrAccessDenied))
|
||||
return
|
||||
}
|
||||
|
||||
bktACL, err := parseACLHeaders(r.Header, key)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not parse bucket acl", reqInfo, err)
|
||||
return
|
||||
}
|
||||
resInfo := &resourceInfo{Bucket: reqInfo.BucketName}
|
||||
|
||||
aclPrm.EACL, err = bucketACLToTable(bktACL, resInfo)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could translate bucket acl to eacl", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
bktInfo, err := h.obj.CreateBucket(ctx, p)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not create bucket", reqInfo, err)
|
||||
return
|
||||
}
|
||||
h.reqLogger(ctx).Info(logs.BucketIsCreated, zap.Stringer("container_id", bktInfo.CID))
|
||||
|
||||
aclPrm.BktInfo = bktInfo
|
||||
if err = h.obj.PutBucketACL(r.Context(), aclPrm); err != nil {
|
||||
h.logAndSendError(w, "could not put bucket e/ACL", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
sp := &layer.PutSettingsParams{
|
||||
BktInfo: bktInfo,
|
||||
Settings: &data.BucketSettings{
|
||||
OwnerKey: key,
|
||||
Versioning: data.VersioningUnversioned,
|
||||
},
|
||||
}
|
||||
|
||||
if p.ObjectLockEnabled {
|
||||
sp.Settings.Versioning = data.VersioningEnabled
|
||||
}
|
||||
|
||||
if err = h.obj.PutBucketSettings(ctx, sp); err != nil {
|
||||
h.logAndSendError(w, "couldn't save bucket settings", reqInfo, err,
|
||||
zap.String("container_id", bktInfo.CID.EncodeToString()))
|
||||
return
|
||||
}
|
||||
|
||||
if err = middleware.WriteSuccessResponseHeadersOnly(w); err != nil {
|
||||
h.logAndSendError(w, "write response", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const s3ActionPrefix = "s3:"
|
||||
|
||||
var (
|
||||
|
@ -874,22 +1072,49 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
func bucketCannedACLToAPERules(cannedACL string, reqInfo *middleware.ReqInfo, cnrID cid.ID) []*chain.Chain {
|
||||
func bucketCannedACLToAPERules(cannedACL string, reqInfo *middleware.ReqInfo, key *keys.PublicKey, cnrID cid.ID) []*chain.Chain {
|
||||
cnrIDStr := cnrID.EncodeToString()
|
||||
|
||||
chains := []*chain.Chain{
|
||||
{
|
||||
ID: getBucketCannedChainID(chain.S3, cnrID),
|
||||
Rules: []chain.Rule{},
|
||||
},
|
||||
ID: getBucketCannedChainID(chain.S3, cnrID),
|
||||
Rules: []chain.Rule{{
|
||||
Status: chain.Allow,
|
||||
Actions: chain.Actions{Names: []string{"s3:*"}},
|
||||
Resources: chain.Resources{Names: []string{
|
||||
fmt.Sprintf(s3.ResourceFormatS3Bucket, reqInfo.BucketName),
|
||||
fmt.Sprintf(s3.ResourceFormatS3BucketObjects, reqInfo.BucketName),
|
||||
}},
|
||||
Condition: []chain.Condition{{
|
||||
Op: chain.CondStringEquals,
|
||||
Kind: chain.KindRequest,
|
||||
Key: s3.PropertyKeyOwner,
|
||||
Value: key.Address(),
|
||||
}},
|
||||
}}},
|
||||
{
|
||||
ID: getBucketCannedChainID(chain.Ingress, cnrID),
|
||||
Rules: []chain.Rule{},
|
||||
ID: getBucketCannedChainID(chain.Ingress, cnrID),
|
||||
Rules: []chain.Rule{{
|
||||
Status: chain.Allow,
|
||||
Actions: chain.Actions{Names: []string{"*"}},
|
||||
Resources: chain.Resources{Names: []string{
|
||||
fmt.Sprintf(native.ResourceFormatNamespaceContainer, reqInfo.Namespace, cnrIDStr),
|
||||
fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, reqInfo.Namespace, cnrIDStr),
|
||||
}},
|
||||
Condition: []chain.Condition{{
|
||||
Op: chain.CondStringEquals,
|
||||
Kind: chain.KindRequest,
|
||||
Key: native.PropertyKeyActorPublicKey,
|
||||
Value: hex.EncodeToString(key.Bytes()),
|
||||
}},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
switch cannedACL {
|
||||
case basicACLPrivate:
|
||||
case cannedACLAuthRead:
|
||||
fallthrough
|
||||
case basicACLReadOnly:
|
||||
chains[0].Rules = append(chains[0].Rules, chain.Rule{
|
||||
Status: chain.Allow,
|
||||
|
|
|
@ -17,7 +17,6 @@ import (
|
|||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth"
|
||||
v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
||||
s3errors "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||
|
@ -123,92 +122,6 @@ func TestEmptyPostPolicy(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// if content length is greater than this value
|
||||
// data will be writen to file location.
|
||||
const maxContentSizeForFormData = 10
|
||||
|
||||
func TestPostObject(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
|
||||
ns, bktName := "", "bucket"
|
||||
createTestBucket(hc, bktName)
|
||||
|
||||
for _, tc := range []struct {
|
||||
key string
|
||||
filename string
|
||||
content string
|
||||
objName string
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
key: "user/user1/${filename}",
|
||||
filename: "object",
|
||||
content: "content",
|
||||
objName: "user/user1/object",
|
||||
},
|
||||
{
|
||||
key: "user/user1/${filename}",
|
||||
filename: "object",
|
||||
content: "maxContentSizeForFormData",
|
||||
objName: "user/user1/object",
|
||||
},
|
||||
{
|
||||
key: "user/user1/key-object",
|
||||
filename: "object",
|
||||
content: "",
|
||||
objName: "user/user1/key-object",
|
||||
},
|
||||
{
|
||||
key: "user/user1/key-object",
|
||||
filename: "object",
|
||||
content: "maxContentSizeForFormData",
|
||||
objName: "user/user1/key-object",
|
||||
},
|
||||
{
|
||||
key: "",
|
||||
filename: "object",
|
||||
content: "",
|
||||
objName: "object",
|
||||
},
|
||||
{
|
||||
key: "",
|
||||
filename: "object",
|
||||
content: "maxContentSizeForFormData",
|
||||
objName: "object",
|
||||
},
|
||||
{
|
||||
// RFC 7578, Section 4.2 requires that if a filename is provided, the
|
||||
// directory path information must not be used.
|
||||
key: "",
|
||||
filename: "dir/object",
|
||||
content: "content",
|
||||
objName: "object",
|
||||
},
|
||||
{
|
||||
key: "object",
|
||||
filename: "",
|
||||
content: "content",
|
||||
objName: "object",
|
||||
},
|
||||
{
|
||||
key: "",
|
||||
filename: "",
|
||||
err: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.key+";"+tc.filename, func(t *testing.T) {
|
||||
w := postObjectBase(hc, ns, bktName, tc.key, tc.filename, tc.content)
|
||||
if tc.err {
|
||||
assertS3Error(hc.t, w, s3errors.GetAPIError(s3errors.ErrInternalError))
|
||||
return
|
||||
}
|
||||
assertStatus(hc.t, w, http.StatusNoContent)
|
||||
content, _ := getObject(hc, bktName, tc.objName)
|
||||
require.Equal(t, tc.content, string(content))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPutObjectOverrideCopiesNumber(t *testing.T) {
|
||||
tc := prepareHandlerContext(t)
|
||||
|
||||
|
@ -468,6 +381,21 @@ func TestCreateBucket(t *testing.T) {
|
|||
createBucketAssertS3Error(hc, bktName, box2, s3errors.ErrBucketAlreadyExists)
|
||||
}
|
||||
|
||||
func TestCreateOldBucketPutVersioning(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
hc.config.aclEnabled = true
|
||||
bktName := "bkt-name"
|
||||
|
||||
info := createBucket(hc, bktName)
|
||||
settings, err := hc.tree.GetSettingsNode(hc.Context(), info.BktInfo)
|
||||
require.NoError(t, err)
|
||||
settings.OwnerKey = nil
|
||||
err = hc.tree.PutSettingsNode(hc.Context(), info.BktInfo, settings)
|
||||
require.NoError(t, err)
|
||||
|
||||
putBucketVersioning(t, hc, bktName, true)
|
||||
}
|
||||
|
||||
func TestCreateNamespacedBucket(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
bktName := "bkt-name"
|
||||
|
@ -536,85 +464,3 @@ func TestPutObjectWithContentLanguage(t *testing.T) {
|
|||
tc.Handler().HeadObjectHandler(w, r)
|
||||
require.Equal(t, expectedContentLanguage, w.Header().Get(api.ContentLanguage))
|
||||
}
|
||||
|
||||
func postObjectBase(hc *handlerContext, ns, bktName, key, filename, content string) *httptest.ResponseRecorder {
|
||||
policy := "eyJleHBpcmF0aW9uIjogIjIwMjUtMTItMDFUMTI6MDA6MDAuMDAwWiIsImNvbmRpdGlvbnMiOiBbCiBbInN0YXJ0cy13aXRoIiwgIiR4LWFtei1jcmVkZW50aWFsIiwgIiJdLAogWyJzdGFydHMtd2l0aCIsICIkeC1hbXotZGF0ZSIsICIiXSwKIFsic3RhcnRzLXdpdGgiLCAiJGtleSIsICIiXQpdfQ=="
|
||||
|
||||
timeToSign := time.Now()
|
||||
timeToSignStr := timeToSign.Format("20060102T150405Z")
|
||||
region := "default"
|
||||
service := "s3"
|
||||
|
||||
accessKeyID := "5jizSbYu8hX345aqCKDgRWKCJYHxnzxRS8e6SUYHZ8Fw0HiRkf3KbJAWBn5mRzmiyHQ3UHADGyzVXLusn1BrmAfLn"
|
||||
secretKey := "abf066d77c6744cd956a123a0b9612df587f5c14d3350ecb01b363f182dd7279"
|
||||
|
||||
creds := getCredsStr(accessKeyID, timeToSignStr, region, service)
|
||||
sign := auth.SignStr(secretKey, service, region, timeToSign, policy)
|
||||
|
||||
body, contentType, err := getMultipartFormBody(policy, creds, timeToSignStr, sign, key, filename, content)
|
||||
require.NoError(hc.t, err)
|
||||
|
||||
w, r := prepareTestPostRequest(hc, bktName, body)
|
||||
r.Header.Set(auth.ContentTypeHdr, contentType)
|
||||
r.Header.Set("X-Frostfs-Namespace", ns)
|
||||
|
||||
err = r.ParseMultipartForm(50 * 1024 * 1024)
|
||||
require.NoError(hc.t, err)
|
||||
|
||||
hc.Handler().PostObject(w, r)
|
||||
return w
|
||||
}
|
||||
|
||||
func getCredsStr(accessKeyID, timeToSign, region, service string) string {
|
||||
return accessKeyID + "/" + timeToSign + "/" + region + "/" + service + "/aws4_request"
|
||||
}
|
||||
|
||||
func getMultipartFormBody(policy, creds, date, sign, key, filename, content string) (io.Reader, string, error) {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
defer writer.Close()
|
||||
|
||||
if err := writer.WriteField("policy", policy); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if err := writer.WriteField("key", key); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if err := writer.WriteField(strings.ToLower(auth.AmzCredential), creds); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if err := writer.WriteField(strings.ToLower(auth.AmzDate), date); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if err := writer.WriteField(strings.ToLower(auth.AmzSignature), sign); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
file, err := writer.CreateFormFile("file", filename)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if len(content) < maxContentSizeForFormData {
|
||||
if err = writer.WriteField("file", content); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
} else {
|
||||
if _, err = file.Write([]byte(content)); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
return body, writer.FormDataContentType(), nil
|
||||
}
|
||||
|
||||
func prepareTestPostRequest(hc *handlerContext, bktName string, payload io.Reader) (*httptest.ResponseRecorder, *http.Request) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodPost, defaultURL+bktName, payload)
|
||||
|
||||
reqInfo := middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: bktName}, "")
|
||||
r = r.WithContext(middleware.SetReqInfo(hc.Context(), reqInfo))
|
||||
|
||||
return w, r
|
||||
}
|
||||
|
|
|
@ -176,9 +176,6 @@ type ListObjectsVersionsResponse struct {
|
|||
DeleteMarker []DeleteMarkerEntry `xml:"DeleteMarker"`
|
||||
Version []ObjectVersionResponse `xml:"Version"`
|
||||
CommonPrefixes []CommonPrefix `xml:"CommonPrefixes"`
|
||||
Prefix string `xml:"Prefix"`
|
||||
Delimiter string `xml:"Delimiter,omitempty"`
|
||||
MaxKeys int `xml:"MaxKeys"`
|
||||
}
|
||||
|
||||
// VersioningConfiguration contains VersioningConfiguration XML representation.
|
||||
|
@ -195,6 +192,15 @@ type PostResponse struct {
|
|||
ETag string `xml:"Etag"`
|
||||
}
|
||||
|
||||
type PatchObjectResult struct {
|
||||
Object PatchObject `xml:"Object"`
|
||||
}
|
||||
|
||||
type PatchObject struct {
|
||||
LastModified string `xml:"LastModified"`
|
||||
ETag string `xml:"ETag"`
|
||||
}
|
||||
|
||||
// MarshalXML -- StringMap marshals into XML.
|
||||
func (s StringMap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
tokens := []xml.Token{start}
|
||||
|
|
|
@ -10,6 +10,8 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -44,12 +46,27 @@ func (h *handler) PutObjectTaggingHandler(w http.ResponseWriter, r *http.Request
|
|||
},
|
||||
TagSet: tagSet,
|
||||
}
|
||||
|
||||
if err = h.obj.PutObjectTagging(ctx, tagPrm); err != nil {
|
||||
nodeVersion, err := h.obj.PutObjectTagging(ctx, tagPrm)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not put object tagging", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
s := &SendNotificationParams{
|
||||
Event: EventObjectTaggingPut,
|
||||
NotificationInfo: &data.NotificationInfo{
|
||||
Name: nodeVersion.FilePath,
|
||||
Size: nodeVersion.Size,
|
||||
Version: nodeVersion.OID.EncodeToString(),
|
||||
HashSum: nodeVersion.ETag,
|
||||
},
|
||||
BktInfo: bktInfo,
|
||||
ReqInfo: reqInfo,
|
||||
}
|
||||
if err = h.sendNotifications(ctx, s); err != nil {
|
||||
h.reqLogger(ctx).Error(logs.CouldntSendNotification, zap.Error(err))
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
|
@ -106,11 +123,27 @@ func (h *handler) DeleteObjectTaggingHandler(w http.ResponseWriter, r *http.Requ
|
|||
VersionID: reqInfo.URL.Query().Get(api.QueryVersionID),
|
||||
}
|
||||
|
||||
if err = h.obj.DeleteObjectTagging(ctx, p); err != nil {
|
||||
nodeVersion, err := h.obj.DeleteObjectTagging(ctx, p)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not delete object tagging", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
s := &SendNotificationParams{
|
||||
Event: EventObjectTaggingDelete,
|
||||
NotificationInfo: &data.NotificationInfo{
|
||||
Name: nodeVersion.FilePath,
|
||||
Size: nodeVersion.Size,
|
||||
Version: nodeVersion.OID.EncodeToString(),
|
||||
HashSum: nodeVersion.ETag,
|
||||
},
|
||||
BktInfo: bktInfo,
|
||||
ReqInfo: reqInfo,
|
||||
}
|
||||
if err = h.sendNotifications(ctx, s); err != nil {
|
||||
h.reqLogger(ctx).Error(logs.CouldntSendNotification, zap.Error(err))
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
|
|
|
@ -58,11 +58,3 @@ func (h *handler) PutBucketLifecycleHandler(w http.ResponseWriter, r *http.Reque
|
|||
func (h *handler) PutBucketEncryptionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
h.logAndSendError(w, "not implemented", middleware.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented))
|
||||
}
|
||||
|
||||
func (h *handler) PutBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
h.logAndSendError(w, "not implemented", middleware.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented))
|
||||
}
|
||||
|
||||
func (h *handler) GetBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
h.logAndSendError(w, "not implemented", middleware.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented))
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
frosterrors "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/errors"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
@ -141,3 +142,16 @@ func parseRange(s string) (*layer.RangeParams, error) {
|
|||
End: values[1],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getSessionTokenSetEACL(ctx context.Context) (*session.Container, error) {
|
||||
boxData, err := middleware.GetBoxData(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sessionToken := boxData.Gate.SessionTokenForSetEACL()
|
||||
if sessionToken == nil {
|
||||
return nil, s3errors.GetAPIError(s3errors.ErrAccessDenied)
|
||||
}
|
||||
|
||||
return sessionToken, nil
|
||||
}
|
||||
|
|
|
@ -62,7 +62,6 @@ const (
|
|||
AmzMaxParts = "X-Amz-Max-Parts"
|
||||
AmzPartNumberMarker = "X-Amz-Part-Number-Marker"
|
||||
AmzStorageClass = "X-Amz-Storage-Class"
|
||||
AmzForceBucketDelete = "X-Amz-Force-Delete-Bucket"
|
||||
|
||||
AmzServerSideEncryptionCustomerAlgorithm = "x-amz-server-side-encryption-customer-algorithm"
|
||||
AmzServerSideEncryptionCustomerKey = "x-amz-server-side-encryption-customer-key"
|
||||
|
|
|
@ -233,7 +233,7 @@ func (c *Cache) PutSettings(owner user.ID, bktInfo *data.BucketInfo, settings *d
|
|||
}
|
||||
|
||||
func (c *Cache) GetCORS(owner user.ID, bkt *data.BucketInfo) *data.CORSConfiguration {
|
||||
key := bkt.CORSObjectName()
|
||||
key := bkt.Name + bkt.CORSObjectName()
|
||||
|
||||
if !c.accessCache.Get(owner, key) {
|
||||
return nil
|
||||
|
@ -243,7 +243,7 @@ func (c *Cache) GetCORS(owner user.ID, bkt *data.BucketInfo) *data.CORSConfigura
|
|||
}
|
||||
|
||||
func (c *Cache) PutCORS(owner user.ID, bkt *data.BucketInfo, cors *data.CORSConfiguration) {
|
||||
key := bkt.CORSObjectName()
|
||||
key := bkt.Name + bkt.CORSObjectName()
|
||||
|
||||
if err := c.systemCache.PutCORS(key, cors); err != nil {
|
||||
c.logger.Warn(logs.CouldntCacheCors, zap.String("bucket", bkt.Name), zap.Error(err))
|
||||
|
@ -255,5 +255,26 @@ func (c *Cache) PutCORS(owner user.ID, bkt *data.BucketInfo, cors *data.CORSConf
|
|||
}
|
||||
|
||||
func (c *Cache) DeleteCORS(bktInfo *data.BucketInfo) {
|
||||
c.systemCache.Delete(bktInfo.CORSObjectName())
|
||||
c.systemCache.Delete(bktInfo.Name + bktInfo.CORSObjectName())
|
||||
}
|
||||
|
||||
func (c *Cache) GetNotificationConfiguration(owner user.ID, bktInfo *data.BucketInfo) *data.NotificationConfiguration {
|
||||
key := bktInfo.Name + bktInfo.NotificationConfigurationObjectName()
|
||||
|
||||
if !c.accessCache.Get(owner, key) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.systemCache.GetNotificationConfiguration(key)
|
||||
}
|
||||
|
||||
func (c *Cache) PutNotificationConfiguration(owner user.ID, bktInfo *data.BucketInfo, configuration *data.NotificationConfiguration) {
|
||||
key := bktInfo.Name + bktInfo.NotificationConfigurationObjectName()
|
||||
if err := c.systemCache.PutNotificationConfiguration(key, configuration); err != nil {
|
||||
c.logger.Warn(logs.CouldntCacheNotificationConfiguration, zap.String("bucket", bktInfo.Name), zap.Error(err))
|
||||
}
|
||||
|
||||
if err := c.accessCache.Put(owner, key); err != nil {
|
||||
c.logger.Warn(logs.CouldntCacheAccessControlOperation, zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
s3errors "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||
)
|
||||
|
||||
func (n *Layer) GetObjectTaggingAndLock(ctx context.Context, objVersion *data.ObjectVersion, nodeVersion *data.NodeVersion) (map[string]string, data.LockInfo, error) {
|
||||
func (n *layer) GetObjectTaggingAndLock(ctx context.Context, objVersion *data.ObjectVersion, nodeVersion *data.NodeVersion) (map[string]string, data.LockInfo, error) {
|
||||
var err error
|
||||
owner := n.BearerOwner(ctx)
|
||||
|
||||
|
|
|
@ -12,15 +12,27 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
// BucketACL extends BucketInfo by eacl.Table.
|
||||
BucketACL struct {
|
||||
Info *data.BucketInfo
|
||||
EACL *eacl.Table
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
attributeLocationConstraint = ".s3-location-constraint"
|
||||
AttributeLockEnabled = "LockEnabled"
|
||||
)
|
||||
|
||||
func (n *Layer) containerInfo(ctx context.Context, prm PrmContainer) (*data.BucketInfo, error) {
|
||||
func (n *layer) containerInfo(ctx context.Context, prm PrmContainer) (*data.BucketInfo, error) {
|
||||
var (
|
||||
err error
|
||||
res *container.Container
|
||||
|
@ -52,6 +64,7 @@ func (n *Layer) containerInfo(ctx context.Context, prm PrmContainer) (*data.Buck
|
|||
info.Created = container.CreatedAt(cnr)
|
||||
info.LocationConstraint = cnr.Attribute(attributeLocationConstraint)
|
||||
info.HomomorphicHashDisabled = container.IsHomomorphicHashingDisabled(cnr)
|
||||
info.APEEnabled = cnr.BasicACL().Bits() == 0
|
||||
|
||||
attrLockEnabled := cnr.Attribute(AttributeLockEnabled)
|
||||
if len(attrLockEnabled) > 0 {
|
||||
|
@ -74,7 +87,7 @@ func (n *Layer) containerInfo(ctx context.Context, prm PrmContainer) (*data.Buck
|
|||
return info, nil
|
||||
}
|
||||
|
||||
func (n *Layer) containerList(ctx context.Context) ([]*data.BucketInfo, error) {
|
||||
func (n *layer) containerList(ctx context.Context) ([]*data.BucketInfo, error) {
|
||||
stoken := n.SessionTokenForRead(ctx)
|
||||
|
||||
prm := PrmUserContainers{
|
||||
|
@ -106,7 +119,7 @@ func (n *Layer) containerList(ctx context.Context) ([]*data.BucketInfo, error) {
|
|||
return list, nil
|
||||
}
|
||||
|
||||
func (n *Layer) createContainer(ctx context.Context, p *CreateBucketParams) (*data.BucketInfo, error) {
|
||||
func (n *layer) createContainer(ctx context.Context, p *CreateBucketParams) (*data.BucketInfo, error) {
|
||||
if p.LocationConstraint == "" {
|
||||
p.LocationConstraint = api.DefaultLocationConstraint // s3tests_boto3.functional.test_s3:test_bucket_get_location
|
||||
}
|
||||
|
@ -120,6 +133,7 @@ func (n *Layer) createContainer(ctx context.Context, p *CreateBucketParams) (*da
|
|||
Created: TimeNow(ctx),
|
||||
LocationConstraint: p.LocationConstraint,
|
||||
ObjectLockEnabled: p.ObjectLockEnabled,
|
||||
APEEnabled: p.APEEnabled,
|
||||
}
|
||||
|
||||
attributes := [][2]string{
|
||||
|
@ -132,6 +146,11 @@ func (n *Layer) createContainer(ctx context.Context, p *CreateBucketParams) (*da
|
|||
})
|
||||
}
|
||||
|
||||
basicACL := acl.PublicRWExtended
|
||||
if p.APEEnabled {
|
||||
basicACL = 0
|
||||
}
|
||||
|
||||
res, err := n.frostFS.CreateContainer(ctx, PrmContainerCreate{
|
||||
Creator: bktInfo.Owner,
|
||||
Policy: p.Policy,
|
||||
|
@ -140,7 +159,7 @@ func (n *Layer) createContainer(ctx context.Context, p *CreateBucketParams) (*da
|
|||
SessionToken: p.SessionContainerCreation,
|
||||
CreationTime: bktInfo.Created,
|
||||
AdditionalAttributes: attributes,
|
||||
BasicACL: 0, // means APE
|
||||
BasicACL: basicACL,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create container: %w", err)
|
||||
|
@ -153,3 +172,17 @@ func (n *Layer) createContainer(ctx context.Context, p *CreateBucketParams) (*da
|
|||
|
||||
return bktInfo, nil
|
||||
}
|
||||
|
||||
func (n *layer) setContainerEACLTable(ctx context.Context, idCnr cid.ID, table *eacl.Table, sessionToken *session.Container) error {
|
||||
table.SetCID(idCnr)
|
||||
|
||||
return n.frostFS.SetContainerEACL(ctx, *table, sessionToken)
|
||||
}
|
||||
|
||||
func (n *layer) GetContainerEACL(ctx context.Context, cnrID cid.ID) (*eacl.Table, error) {
|
||||
prm := PrmContainerEACL{
|
||||
ContainerID: cnrID,
|
||||
SessionToken: n.SessionTokenForRead(ctx),
|
||||
}
|
||||
return n.frostFS.ContainerEACL(ctx, prm)
|
||||
}
|
||||
|
|
|
@ -10,8 +10,6 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
|
@ -19,7 +17,7 @@ const wildcard = "*"
|
|||
|
||||
var supportedMethods = map[string]struct{}{"GET": {}, "HEAD": {}, "POST": {}, "PUT": {}, "DELETE": {}}
|
||||
|
||||
func (n *Layer) PutBucketCORS(ctx context.Context, p *PutCORSParams) error {
|
||||
func (n *layer) PutBucketCORS(ctx context.Context, p *PutCORSParams) error {
|
||||
var (
|
||||
buf bytes.Buffer
|
||||
tee = io.TeeReader(p.Reader, &buf)
|
||||
|
@ -39,36 +37,29 @@ func (n *Layer) PutBucketCORS(ctx context.Context, p *PutCORSParams) error {
|
|||
}
|
||||
|
||||
prm := PrmObjectCreate{
|
||||
Container: p.BktInfo.CID,
|
||||
Payload: &buf,
|
||||
Filepath: p.BktInfo.CORSObjectName(),
|
||||
CreationTime: TimeNow(ctx),
|
||||
CopiesNumber: p.CopiesNumbers,
|
||||
}
|
||||
|
||||
var corsBkt *data.BucketInfo
|
||||
if n.corsCnrInfo == nil {
|
||||
corsBkt = p.BktInfo
|
||||
prm.CopiesNumber = p.CopiesNumbers
|
||||
} else {
|
||||
corsBkt = n.corsCnrInfo
|
||||
prm.PrmAuth.PrivateKey = &n.gateKey.PrivateKey
|
||||
}
|
||||
|
||||
prm.Container = corsBkt.CID
|
||||
|
||||
_, objID, _, _, err := n.objectPutAndHash(ctx, prm, corsBkt)
|
||||
_, objID, _, _, err := n.objectPutAndHash(ctx, prm, p.BktInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("put cors object: %w", err)
|
||||
return fmt.Errorf("put system object: %w", err)
|
||||
}
|
||||
|
||||
objsToDelete, err := n.treeService.PutBucketCORS(ctx, p.BktInfo, newAddress(corsBkt.CID, objID))
|
||||
objToDeleteNotFound := errorsStd.Is(err, ErrNoNodeToRemove)
|
||||
if err != nil && !objToDeleteNotFound {
|
||||
objIDToDelete, err := n.treeService.PutBucketCORS(ctx, p.BktInfo, objID)
|
||||
objIDToDeleteNotFound := errorsStd.Is(err, ErrNoNodeToRemove)
|
||||
if err != nil && !objIDToDeleteNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
if !objToDeleteNotFound {
|
||||
for _, addr := range objsToDelete {
|
||||
n.deleteCORSObject(ctx, p.BktInfo, addr)
|
||||
if !objIDToDeleteNotFound {
|
||||
if err = n.objectDelete(ctx, p.BktInfo, objIDToDelete); err != nil {
|
||||
n.reqLogger(ctx).Error(logs.CouldntDeleteCorsObject, zap.Error(err),
|
||||
zap.String("cnrID", p.BktInfo.CID.EncodeToString()),
|
||||
zap.String("objID", objIDToDelete.EncodeToString()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,41 +68,27 @@ func (n *Layer) PutBucketCORS(ctx context.Context, p *PutCORSParams) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// deleteCORSObject removes object and logs in case of error.
|
||||
func (n *Layer) deleteCORSObject(ctx context.Context, bktInfo *data.BucketInfo, addr oid.Address) {
|
||||
var prmAuth PrmAuth
|
||||
corsBkt := bktInfo
|
||||
if !addr.Container().Equals(bktInfo.CID) && !addr.Container().Equals(cid.ID{}) {
|
||||
corsBkt = &data.BucketInfo{CID: addr.Container()}
|
||||
prmAuth.PrivateKey = &n.gateKey.PrivateKey
|
||||
}
|
||||
|
||||
if err := n.objectDeleteWithAuth(ctx, corsBkt, addr.Object(), prmAuth); err != nil {
|
||||
n.reqLogger(ctx).Error(logs.CouldntDeleteCorsObject, zap.Error(err),
|
||||
zap.String("cnrID", corsBkt.CID.EncodeToString()),
|
||||
zap.String("objID", addr.Object().EncodeToString()))
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Layer) GetBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) (*data.CORSConfiguration, error) {
|
||||
func (n *layer) GetBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) (*data.CORSConfiguration, error) {
|
||||
cors, err := n.getCORS(ctx, bktInfo)
|
||||
if err != nil {
|
||||
if errorsStd.Is(err, ErrNodeNotFound) {
|
||||
return nil, fmt.Errorf("%w: %s", errors.GetAPIError(errors.ErrNoSuchCORSConfiguration), err.Error())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cors, nil
|
||||
}
|
||||
|
||||
func (n *Layer) DeleteBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) error {
|
||||
objs, err := n.treeService.DeleteBucketCORS(ctx, bktInfo)
|
||||
objNotFound := errorsStd.Is(err, ErrNoNodeToRemove)
|
||||
if err != nil && !objNotFound {
|
||||
func (n *layer) DeleteBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) error {
|
||||
objID, err := n.treeService.DeleteBucketCORS(ctx, bktInfo)
|
||||
objIDNotFound := errorsStd.Is(err, ErrNoNodeToRemove)
|
||||
if err != nil && !objIDNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
if !objNotFound {
|
||||
for _, addr := range objs {
|
||||
n.deleteCORSObject(ctx, bktInfo, addr)
|
||||
if !objIDNotFound {
|
||||
if err = n.objectDelete(ctx, bktInfo, objID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
|
@ -63,6 +64,15 @@ type PrmUserContainers struct {
|
|||
SessionToken *session.Container
|
||||
}
|
||||
|
||||
// PrmContainerEACL groups parameters of FrostFS.ContainerEACL operation.
|
||||
type PrmContainerEACL struct {
|
||||
// Container identifier.
|
||||
ContainerID cid.ID
|
||||
|
||||
// Token of the container's creation session. Nil means session absence.
|
||||
SessionToken *session.Container
|
||||
}
|
||||
|
||||
// ContainerCreateResult is a result parameter of FrostFS.CreateContainer operation.
|
||||
type ContainerCreateResult struct {
|
||||
ContainerID cid.ID
|
||||
|
@ -78,8 +88,8 @@ type PrmAuth struct {
|
|||
PrivateKey *ecdsa.PrivateKey
|
||||
}
|
||||
|
||||
// PrmObjectHead groups parameters of FrostFS.HeadObject operation.
|
||||
type PrmObjectHead struct {
|
||||
// PrmObjectRead groups parameters of FrostFS.ReadObject operation.
|
||||
type PrmObjectRead struct {
|
||||
// Authentication parameters.
|
||||
PrmAuth
|
||||
|
||||
|
@ -88,39 +98,21 @@ type PrmObjectHead struct {
|
|||
|
||||
// ID of the object for which to read the header.
|
||||
Object oid.ID
|
||||
}
|
||||
|
||||
// PrmObjectGet groups parameters of FrostFS.GetObject operation.
|
||||
type PrmObjectGet struct {
|
||||
// Authentication parameters.
|
||||
PrmAuth
|
||||
// Flag to read object header.
|
||||
WithHeader bool
|
||||
|
||||
// Container to read the object header from.
|
||||
Container cid.ID
|
||||
|
||||
// ID of the object for which to read the header.
|
||||
Object oid.ID
|
||||
}
|
||||
|
||||
// PrmObjectRange groups parameters of FrostFS.RangeObject operation.
|
||||
type PrmObjectRange struct {
|
||||
// Authentication parameters.
|
||||
PrmAuth
|
||||
|
||||
// Container to read the object header from.
|
||||
Container cid.ID
|
||||
|
||||
// ID of the object for which to read the header.
|
||||
Object oid.ID
|
||||
// Flag to read object payload. False overlaps payload range.
|
||||
WithPayload bool
|
||||
|
||||
// Offset-length range of the object payload to be read.
|
||||
PayloadRange [2]uint64
|
||||
}
|
||||
|
||||
// Object represents full read FrostFS object.
|
||||
type Object struct {
|
||||
// Object header (doesn't contain payload).
|
||||
Header object.Object
|
||||
// ObjectPart represents partially read FrostFS object.
|
||||
type ObjectPart struct {
|
||||
// Object header with optional in-memory payload part.
|
||||
Head *object.Object
|
||||
|
||||
// Object payload part encapsulated in io.Reader primitive.
|
||||
// Returns ErrAccessDenied on read access violation.
|
||||
|
@ -224,6 +216,18 @@ type FrostFS interface {
|
|||
// prevented the containers from being listed.
|
||||
UserContainers(context.Context, PrmUserContainers) ([]cid.ID, error)
|
||||
|
||||
// SetContainerEACL saves the eACL table of the container in FrostFS. The
|
||||
// extended ACL is modified within session if session token is not nil.
|
||||
//
|
||||
// It returns any error encountered which prevented the eACL from being saved.
|
||||
SetContainerEACL(context.Context, eacl.Table, *session.Container) error
|
||||
|
||||
// ContainerEACL reads the container eACL from FrostFS by the container ID.
|
||||
//
|
||||
// It returns exactly one non-nil value. It returns any error encountered which
|
||||
// prevented the eACL from being read.
|
||||
ContainerEACL(context.Context, PrmContainerEACL) (*eacl.Table, error)
|
||||
|
||||
// DeleteContainer marks the container to be removed from FrostFS by ID.
|
||||
// Request is sent within session if the session token is specified.
|
||||
// Successful return does not guarantee actual removal.
|
||||
|
@ -231,15 +235,13 @@ type FrostFS interface {
|
|||
// It returns any error encountered which prevented the removal request from being sent.
|
||||
DeleteContainer(context.Context, cid.ID, *session.Container) error
|
||||
|
||||
// HeadObject reads an info of the object from the FrostFS container by identifier.
|
||||
// ReadObject reads a part of the object from the FrostFS container by identifier.
|
||||
// Exact part is returned according to the parameters:
|
||||
// * with header only: empty payload (both in-mem and reader parts are nil);
|
||||
// * with payload only: header is nil (zero range means full payload);
|
||||
// * with header and payload: full in-mem object, payload reader is nil.
|
||||
//
|
||||
// It returns ErrAccessDenied on read access violation.
|
||||
//
|
||||
// It returns exactly one non-nil value. It returns any error encountered which
|
||||
// prevented the object header from being read.
|
||||
HeadObject(ctx context.Context, prm PrmObjectHead) (*object.Object, error)
|
||||
|
||||
// GetObject reads an object from the FrostFS container by identifier.
|
||||
// WithHeader or WithPayload is true. Range length is positive if offset is positive.
|
||||
//
|
||||
// Payload reader should be closed if it is no longer needed.
|
||||
//
|
||||
|
@ -247,17 +249,7 @@ type FrostFS interface {
|
|||
//
|
||||
// It returns exactly one non-nil value. It returns any error encountered which
|
||||
// prevented the object header from being read.
|
||||
GetObject(ctx context.Context, prm PrmObjectGet) (*Object, error)
|
||||
|
||||
// RangeObject reads a part of object from the FrostFS container by identifier.
|
||||
//
|
||||
// Payload reader should be closed if it is no longer needed.
|
||||
//
|
||||
// It returns ErrAccessDenied on read access violation.
|
||||
//
|
||||
// It returns exactly one non-nil value. It returns any error encountered which
|
||||
// prevented the object header from being read.
|
||||
RangeObject(ctx context.Context, prm PrmObjectRange) (io.ReadCloser, error)
|
||||
ReadObject(context.Context, PrmObjectRead) (*ObjectPart, error)
|
||||
|
||||
// CreateObject creates and saves a parameterized object in the FrostFS container.
|
||||
// It sets 'Timestamp' attribute to the current time.
|
||||
|
|
|
@ -5,11 +5,13 @@ import (
|
|||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
|
||||
v2container "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
||||
objectv2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
||||
|
@ -18,6 +20,7 @@ import (
|
|||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
||||
|
@ -65,6 +68,7 @@ type TestFrostFS struct {
|
|||
objectErrors map[string]error
|
||||
objectPutErrors map[string]error
|
||||
containers map[string]*container.Container
|
||||
eaclTables map[string]*eacl.Table
|
||||
currentEpoch uint64
|
||||
key *keys.PrivateKey
|
||||
}
|
||||
|
@ -75,6 +79,7 @@ func NewTestFrostFS(key *keys.PrivateKey) *TestFrostFS {
|
|||
objectErrors: make(map[string]error),
|
||||
objectPutErrors: make(map[string]error),
|
||||
containers: make(map[string]*container.Container),
|
||||
eaclTables: make(map[string]*eacl.Table),
|
||||
key: key,
|
||||
}
|
||||
}
|
||||
|
@ -204,10 +209,10 @@ func (t *TestFrostFS) UserContainers(context.Context, PrmUserContainers) ([]cid.
|
|||
return res, nil
|
||||
}
|
||||
|
||||
func (t *TestFrostFS) retrieveObject(ctx context.Context, cnrID cid.ID, objID oid.ID) (*object.Object, error) {
|
||||
func (t *TestFrostFS) ReadObject(ctx context.Context, prm PrmObjectRead) (*ObjectPart, error) {
|
||||
var addr oid.Address
|
||||
addr.SetContainer(cnrID)
|
||||
addr.SetObject(objID)
|
||||
addr.SetContainer(prm.Container)
|
||||
addr.SetObject(prm.Object)
|
||||
|
||||
sAddr := addr.EncodeToString()
|
||||
|
||||
|
@ -217,44 +222,26 @@ func (t *TestFrostFS) retrieveObject(ctx context.Context, cnrID cid.ID, objID oi
|
|||
|
||||
if obj, ok := t.objects[sAddr]; ok {
|
||||
owner := getBearerOwner(ctx)
|
||||
if !t.checkAccess(cnrID, owner) {
|
||||
if !t.checkAccess(prm.Container, owner, eacl.OperationGet, obj) {
|
||||
return nil, ErrAccessDenied
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
payload := obj.Payload()
|
||||
|
||||
if prm.PayloadRange[0]+prm.PayloadRange[1] > 0 {
|
||||
off := prm.PayloadRange[0]
|
||||
payload = payload[off : off+prm.PayloadRange[1]]
|
||||
}
|
||||
|
||||
return &ObjectPart{
|
||||
Head: obj,
|
||||
Payload: io.NopCloser(bytes.NewReader(payload)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: %s", &apistatus.ObjectNotFound{}, addr)
|
||||
}
|
||||
|
||||
func (t *TestFrostFS) HeadObject(ctx context.Context, prm PrmObjectHead) (*object.Object, error) {
|
||||
return t.retrieveObject(ctx, prm.Container, prm.Object)
|
||||
}
|
||||
|
||||
func (t *TestFrostFS) GetObject(ctx context.Context, prm PrmObjectGet) (*Object, error) {
|
||||
obj, err := t.retrieveObject(ctx, prm.Container, prm.Object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Object{
|
||||
Header: *obj,
|
||||
Payload: io.NopCloser(bytes.NewReader(obj.Payload())),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *TestFrostFS) RangeObject(ctx context.Context, prm PrmObjectRange) (io.ReadCloser, error) {
|
||||
obj, err := t.retrieveObject(ctx, prm.Container, prm.Object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
off := prm.PayloadRange[0]
|
||||
payload := obj.Payload()[off : off+prm.PayloadRange[1]]
|
||||
|
||||
return io.NopCloser(bytes.NewReader(payload)), nil
|
||||
}
|
||||
|
||||
func (t *TestFrostFS) CreateObject(_ context.Context, prm PrmObjectCreate) (oid.ID, error) {
|
||||
b := make([]byte, 32)
|
||||
if _, err := io.ReadFull(rand.Reader, b); err != nil {
|
||||
|
@ -337,9 +324,9 @@ func (t *TestFrostFS) DeleteObject(ctx context.Context, prm PrmObjectDelete) err
|
|||
return err
|
||||
}
|
||||
|
||||
if _, ok := t.objects[addr.EncodeToString()]; ok {
|
||||
if obj, ok := t.objects[addr.EncodeToString()]; ok {
|
||||
owner := getBearerOwner(ctx)
|
||||
if !t.checkAccess(prm.Container, owner) {
|
||||
if !t.checkAccess(prm.Container, owner, eacl.OperationDelete, obj) {
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
|
@ -367,6 +354,30 @@ func (t *TestFrostFS) AllObjects(cnrID cid.ID) []oid.ID {
|
|||
return result
|
||||
}
|
||||
|
||||
func (t *TestFrostFS) SetContainerEACL(_ context.Context, table eacl.Table, _ *session.Container) error {
|
||||
cnrID, ok := table.CID()
|
||||
if !ok {
|
||||
return errors.New("invalid cid")
|
||||
}
|
||||
|
||||
if _, ok = t.containers[cnrID.EncodeToString()]; !ok {
|
||||
return errors.New("not found")
|
||||
}
|
||||
|
||||
t.eaclTables[cnrID.EncodeToString()] = &table
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TestFrostFS) ContainerEACL(_ context.Context, prm PrmContainerEACL) (*eacl.Table, error) {
|
||||
table, ok := t.eaclTables[prm.ContainerID.EncodeToString()]
|
||||
if !ok {
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
|
||||
return table, nil
|
||||
}
|
||||
|
||||
func (t *TestFrostFS) SearchObjects(_ context.Context, prm PrmObjectSearch) ([]oid.ID, error) {
|
||||
filters := object.NewSearchFilters()
|
||||
filters.AddRootFilter()
|
||||
|
@ -404,7 +415,7 @@ func (t *TestFrostFS) SearchObjects(_ context.Context, prm PrmObjectSearch) ([]o
|
|||
return res, nil
|
||||
}
|
||||
|
||||
func (t *TestFrostFS) checkAccess(cnrID cid.ID, owner user.ID) bool {
|
||||
func (t *TestFrostFS) checkAccess(cnrID cid.ID, owner user.ID, op eacl.Operation, obj *object.Object) bool {
|
||||
cnr, ok := t.containers[cnrID.EncodeToString()]
|
||||
if !ok {
|
||||
return false
|
||||
|
@ -414,6 +425,57 @@ func (t *TestFrostFS) checkAccess(cnrID cid.ID, owner user.ID) bool {
|
|||
return cnr.Owner().Equals(owner)
|
||||
}
|
||||
|
||||
table, ok := t.eaclTables[cnrID.EncodeToString()]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, rec := range table.Records() {
|
||||
if rec.Operation() != op {
|
||||
continue
|
||||
}
|
||||
|
||||
if !matchTarget(rec, owner) {
|
||||
continue
|
||||
}
|
||||
|
||||
if matchFilter(rec.Filters(), obj) {
|
||||
return rec.Action() == eacl.ActionAllow
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func matchTarget(rec eacl.Record, owner user.ID) bool {
|
||||
for _, trgt := range rec.Targets() {
|
||||
if trgt.Role() == eacl.RoleOthers {
|
||||
return true
|
||||
}
|
||||
var targetOwner user.ID
|
||||
for _, pk := range eacl.TargetECDSAKeys(&trgt) {
|
||||
user.IDFromKey(&targetOwner, *pk)
|
||||
if targetOwner.Equals(owner) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func matchFilter(filters []eacl.Filter, obj *object.Object) bool {
|
||||
objID, _ := obj.ID()
|
||||
for _, f := range filters {
|
||||
fv2 := f.ToV2()
|
||||
if fv2.GetMatchType() != acl.MatchTypeStringEqual ||
|
||||
fv2.GetHeaderType() != acl.HeaderTypeObject ||
|
||||
fv2.GetKey() != acl.FilterObjectID ||
|
||||
fv2.GetValue() != objID.EncodeToString() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -18,22 +17,34 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||
s3errors "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/encryption"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
EventListener interface {
|
||||
Subscribe(context.Context, string, MsgHandler) error
|
||||
Listen(context.Context)
|
||||
}
|
||||
|
||||
MsgHandler interface {
|
||||
HandleMessage(context.Context, *nats.Msg) error
|
||||
}
|
||||
|
||||
MsgHandlerFunc func(context.Context, *nats.Msg) error
|
||||
|
||||
BucketResolver interface {
|
||||
Resolve(ctx context.Context, name string) (cid.ID, error)
|
||||
}
|
||||
|
@ -45,17 +56,16 @@ type (
|
|||
FormContainerZone(ns string) (zone string, isDefault bool)
|
||||
}
|
||||
|
||||
Layer struct {
|
||||
layer struct {
|
||||
frostFS FrostFS
|
||||
gateOwner user.ID
|
||||
log *zap.Logger
|
||||
anonKey AnonymousKey
|
||||
resolver BucketResolver
|
||||
ncontroller EventListener
|
||||
cache *Cache
|
||||
treeService TreeService
|
||||
features FeatureSettings
|
||||
gateKey *keys.PrivateKey
|
||||
corsCnrInfo *data.BucketInfo
|
||||
}
|
||||
|
||||
Config struct {
|
||||
|
@ -66,8 +76,6 @@ type (
|
|||
Resolver BucketResolver
|
||||
TreeService TreeService
|
||||
Features FeatureSettings
|
||||
GateKey *keys.PrivateKey
|
||||
CORSCnrInfo *data.BucketInfo
|
||||
}
|
||||
|
||||
// AnonymousKey contains data for anonymous requests.
|
||||
|
@ -157,6 +165,19 @@ type (
|
|||
DstEncryption encryption.Params
|
||||
CopiesNumbers []uint32
|
||||
}
|
||||
|
||||
PatchObjectParams struct {
|
||||
Object *data.ObjectInfo
|
||||
BktInfo *data.BucketInfo
|
||||
SrcSize uint64
|
||||
Header map[string]string
|
||||
NewBytes io.Reader
|
||||
NewBytesSize uint64
|
||||
Range *RangeParams
|
||||
Encryption encryption.Params
|
||||
CopiesNumbers []uint32
|
||||
}
|
||||
|
||||
// CreateBucketParams stores bucket create request parameters.
|
||||
CreateBucketParams struct {
|
||||
Name string
|
||||
|
@ -165,12 +186,18 @@ type (
|
|||
SessionContainerCreation *session.Container
|
||||
LocationConstraint string
|
||||
ObjectLockEnabled bool
|
||||
APEEnabled bool
|
||||
}
|
||||
// PutBucketACLParams stores put bucket acl request parameters.
|
||||
PutBucketACLParams struct {
|
||||
BktInfo *data.BucketInfo
|
||||
EACL *eacl.Table
|
||||
SessionToken *session.Container
|
||||
}
|
||||
// DeleteBucketParams stores delete bucket request parameters.
|
||||
DeleteBucketParams struct {
|
||||
BktInfo *data.BucketInfo
|
||||
SessionToken *session.Container
|
||||
SkipCheck bool
|
||||
}
|
||||
|
||||
// ListObjectVersionsParams stores list objects versions parameters.
|
||||
|
@ -199,6 +226,70 @@ type (
|
|||
encrypted bool
|
||||
decryptedLen uint64
|
||||
}
|
||||
|
||||
// Client provides S3 API client interface.
|
||||
Client interface {
|
||||
Initialize(ctx context.Context, c EventListener) error
|
||||
EphemeralKey() *keys.PublicKey
|
||||
|
||||
GetBucketSettings(ctx context.Context, bktInfo *data.BucketInfo) (*data.BucketSettings, error)
|
||||
PutBucketSettings(ctx context.Context, p *PutSettingsParams) error
|
||||
|
||||
PutBucketCORS(ctx context.Context, p *PutCORSParams) error
|
||||
GetBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) (*data.CORSConfiguration, error)
|
||||
DeleteBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) error
|
||||
|
||||
ListBuckets(ctx context.Context) ([]*data.BucketInfo, error)
|
||||
GetBucketInfo(ctx context.Context, name string) (*data.BucketInfo, error)
|
||||
ResolveCID(ctx context.Context, name string) (cid.ID, error)
|
||||
GetBucketACL(ctx context.Context, bktInfo *data.BucketInfo) (*BucketACL, error)
|
||||
PutBucketACL(ctx context.Context, p *PutBucketACLParams) error
|
||||
CreateBucket(ctx context.Context, p *CreateBucketParams) (*data.BucketInfo, error)
|
||||
DeleteBucket(ctx context.Context, p *DeleteBucketParams) error
|
||||
|
||||
GetObject(ctx context.Context, p *GetObjectParams) (*ObjectPayload, error)
|
||||
GetObjectInfo(ctx context.Context, p *HeadObjectParams) (*data.ObjectInfo, error)
|
||||
GetExtendedObjectInfo(ctx context.Context, p *HeadObjectParams) (*data.ExtendedObjectInfo, error)
|
||||
|
||||
GetLockInfo(ctx context.Context, obj *data.ObjectVersion) (*data.LockInfo, error)
|
||||
PutLockInfo(ctx context.Context, p *PutLockInfoParams) error
|
||||
|
||||
GetBucketTagging(ctx context.Context, bktInfo *data.BucketInfo) (map[string]string, error)
|
||||
PutBucketTagging(ctx context.Context, bktInfo *data.BucketInfo, tagSet map[string]string) error
|
||||
DeleteBucketTagging(ctx context.Context, bktInfo *data.BucketInfo) error
|
||||
|
||||
GetObjectTagging(ctx context.Context, p *data.GetObjectTaggingParams) (string, map[string]string, error)
|
||||
PutObjectTagging(ctx context.Context, p *data.PutObjectTaggingParams) (*data.NodeVersion, error)
|
||||
DeleteObjectTagging(ctx context.Context, p *data.ObjectVersion) (*data.NodeVersion, error)
|
||||
|
||||
PutObject(ctx context.Context, p *PutObjectParams) (*data.ExtendedObjectInfo, error)
|
||||
|
||||
CopyObject(ctx context.Context, p *CopyObjectParams) (*data.ExtendedObjectInfo, error)
|
||||
|
||||
ListObjectsV1(ctx context.Context, p *ListObjectsParamsV1) (*ListObjectsInfoV1, error)
|
||||
ListObjectsV2(ctx context.Context, p *ListObjectsParamsV2) (*ListObjectsInfoV2, error)
|
||||
ListObjectVersions(ctx context.Context, p *ListObjectVersionsParams) (*ListObjectVersionsInfo, error)
|
||||
|
||||
DeleteObjects(ctx context.Context, p *DeleteObjectParams) []*VersionedObject
|
||||
|
||||
CreateMultipartUpload(ctx context.Context, p *CreateMultipartParams) error
|
||||
CompleteMultipartUpload(ctx context.Context, p *CompleteMultipartParams) (*UploadData, *data.ExtendedObjectInfo, error)
|
||||
UploadPart(ctx context.Context, p *UploadPartParams) (string, error)
|
||||
UploadPartCopy(ctx context.Context, p *UploadCopyParams) (*data.ObjectInfo, error)
|
||||
ListMultipartUploads(ctx context.Context, p *ListMultipartUploadsParams) (*ListMultipartUploadsInfo, error)
|
||||
AbortMultipartUpload(ctx context.Context, p *UploadInfoParams) error
|
||||
ListParts(ctx context.Context, p *ListPartsParams) (*ListPartsInfo, error)
|
||||
|
||||
PutBucketNotificationConfiguration(ctx context.Context, p *PutBucketNotificationConfigurationParams) error
|
||||
GetBucketNotificationConfiguration(ctx context.Context, bktInfo *data.BucketInfo) (*data.NotificationConfiguration, error)
|
||||
|
||||
PatchObject(ctx context.Context, p *PatchObjectParams) (*data.ExtendedObjectInfo, error)
|
||||
|
||||
// Compound methods for optimizations
|
||||
|
||||
// GetObjectTaggingAndLock unifies GetObjectTagging and GetLock methods in single tree service invocation.
|
||||
GetObjectTaggingAndLock(ctx context.Context, p *data.ObjectVersion, nodeVersion *data.NodeVersion) (map[string]string, data.LockInfo, error)
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -225,14 +316,18 @@ func (t *VersionedObject) String() string {
|
|||
return t.Name + ":" + t.VersionID
|
||||
}
|
||||
|
||||
func (f MsgHandlerFunc) HandleMessage(ctx context.Context, msg *nats.Msg) error {
|
||||
return f(ctx, msg)
|
||||
}
|
||||
|
||||
func (p HeadObjectParams) Versioned() bool {
|
||||
return len(p.VersionID) > 0
|
||||
}
|
||||
|
||||
// NewLayer creates an instance of a Layer. It checks credentials
|
||||
// NewLayer creates an instance of a layer. It checks credentials
|
||||
// and establishes gRPC connection with the node.
|
||||
func NewLayer(log *zap.Logger, frostFS FrostFS, config *Config) *Layer {
|
||||
return &Layer{
|
||||
func NewLayer(log *zap.Logger, frostFS FrostFS, config *Config) Client {
|
||||
return &layer{
|
||||
frostFS: frostFS,
|
||||
log: log,
|
||||
gateOwner: config.GateOwner,
|
||||
|
@ -241,15 +336,30 @@ func NewLayer(log *zap.Logger, frostFS FrostFS, config *Config) *Layer {
|
|||
cache: config.Cache,
|
||||
treeService: config.TreeService,
|
||||
features: config.Features,
|
||||
gateKey: config.GateKey,
|
||||
corsCnrInfo: config.CORSCnrInfo,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Layer) EphemeralKey() *keys.PublicKey {
|
||||
func (n *layer) EphemeralKey() *keys.PublicKey {
|
||||
return n.anonKey.Key.PublicKey()
|
||||
}
|
||||
|
||||
func (n *layer) Initialize(ctx context.Context, c EventListener) error {
|
||||
if n.IsNotificationEnabled() {
|
||||
return fmt.Errorf("already initialized")
|
||||
}
|
||||
|
||||
// todo add notification handlers (e.g. for lifecycles)
|
||||
|
||||
c.Listen(ctx)
|
||||
|
||||
n.ncontroller = c
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *layer) IsNotificationEnabled() bool {
|
||||
return n.ncontroller != nil
|
||||
}
|
||||
|
||||
// IsAuthenticatedRequest checks if access box exists in the current request.
|
||||
func IsAuthenticatedRequest(ctx context.Context) bool {
|
||||
_, err := middleware.GetBoxData(ctx)
|
||||
|
@ -266,7 +376,7 @@ func TimeNow(ctx context.Context) time.Time {
|
|||
}
|
||||
|
||||
// BearerOwner returns owner id from BearerToken (context) or from client owner.
|
||||
func (n *Layer) BearerOwner(ctx context.Context) user.ID {
|
||||
func (n *layer) BearerOwner(ctx context.Context) user.ID {
|
||||
if bd, err := middleware.GetBoxData(ctx); err == nil && bd.Gate.BearerToken != nil {
|
||||
return bearer.ResolveIssuer(*bd.Gate.BearerToken)
|
||||
}
|
||||
|
@ -278,7 +388,7 @@ func (n *Layer) BearerOwner(ctx context.Context) user.ID {
|
|||
}
|
||||
|
||||
// SessionTokenForRead returns session container token.
|
||||
func (n *Layer) SessionTokenForRead(ctx context.Context) *session.Container {
|
||||
func (n *layer) SessionTokenForRead(ctx context.Context) *session.Container {
|
||||
if bd, err := middleware.GetBoxData(ctx); err == nil && bd.Gate != nil {
|
||||
return bd.Gate.SessionToken()
|
||||
}
|
||||
|
@ -286,7 +396,7 @@ func (n *Layer) SessionTokenForRead(ctx context.Context) *session.Container {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (n *Layer) reqLogger(ctx context.Context) *zap.Logger {
|
||||
func (n *layer) reqLogger(ctx context.Context) *zap.Logger {
|
||||
reqLogger := middleware.GetReqLog(ctx)
|
||||
if reqLogger != nil {
|
||||
return reqLogger
|
||||
|
@ -294,11 +404,7 @@ func (n *Layer) reqLogger(ctx context.Context) *zap.Logger {
|
|||
return n.log
|
||||
}
|
||||
|
||||
func (n *Layer) prepareAuthParameters(ctx context.Context, prm *PrmAuth, bktOwner user.ID) {
|
||||
if prm.BearerToken != nil || prm.PrivateKey != nil {
|
||||
return
|
||||
}
|
||||
|
||||
func (n *layer) prepareAuthParameters(ctx context.Context, prm *PrmAuth, bktOwner user.ID) {
|
||||
if bd, err := middleware.GetBoxData(ctx); err == nil && bd.Gate.BearerToken != nil {
|
||||
if bd.Gate.BearerToken.Impersonate() || bktOwner.Equals(bearer.ResolveIssuer(*bd.Gate.BearerToken)) {
|
||||
prm.BearerToken = bd.Gate.BearerToken
|
||||
|
@ -310,7 +416,7 @@ func (n *Layer) prepareAuthParameters(ctx context.Context, prm *PrmAuth, bktOwne
|
|||
}
|
||||
|
||||
// GetBucketInfo returns bucket info by name.
|
||||
func (n *Layer) GetBucketInfo(ctx context.Context, name string) (*data.BucketInfo, error) {
|
||||
func (n *layer) GetBucketInfo(ctx context.Context, name string) (*data.BucketInfo, error) {
|
||||
name, err := url.QueryUnescape(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unescape bucket name: %w", err)
|
||||
|
@ -340,7 +446,7 @@ func (n *Layer) GetBucketInfo(ctx context.Context, name string) (*data.BucketInf
|
|||
}
|
||||
|
||||
// ResolveCID returns container id by name.
|
||||
func (n *Layer) ResolveCID(ctx context.Context, name string) (cid.ID, error) {
|
||||
func (n *layer) ResolveCID(ctx context.Context, name string) (cid.ID, error) {
|
||||
name, err := url.QueryUnescape(name)
|
||||
if err != nil {
|
||||
return cid.ID{}, fmt.Errorf("unescape bucket name: %w", err)
|
||||
|
@ -356,14 +462,32 @@ func (n *Layer) ResolveCID(ctx context.Context, name string) (cid.ID, error) {
|
|||
return n.ResolveBucket(ctx, name)
|
||||
}
|
||||
|
||||
// GetBucketACL returns bucket acl info by name.
|
||||
func (n *layer) GetBucketACL(ctx context.Context, bktInfo *data.BucketInfo) (*BucketACL, error) {
|
||||
eACL, err := n.GetContainerEACL(ctx, bktInfo.CID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get container eacl: %w", err)
|
||||
}
|
||||
|
||||
return &BucketACL{
|
||||
Info: bktInfo,
|
||||
EACL: eACL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PutBucketACL puts bucket acl by name.
|
||||
func (n *layer) PutBucketACL(ctx context.Context, param *PutBucketACLParams) error {
|
||||
return n.setContainerEACLTable(ctx, param.BktInfo.CID, param.EACL, param.SessionToken)
|
||||
}
|
||||
|
||||
// ListBuckets returns all user containers. The name of the bucket is a container
|
||||
// id. Timestamp is omitted since it is not saved in frostfs container.
|
||||
func (n *Layer) ListBuckets(ctx context.Context) ([]*data.BucketInfo, error) {
|
||||
func (n *layer) ListBuckets(ctx context.Context) ([]*data.BucketInfo, error) {
|
||||
return n.containerList(ctx)
|
||||
}
|
||||
|
||||
// GetObject from storage.
|
||||
func (n *Layer) GetObject(ctx context.Context, p *GetObjectParams) (*ObjectPayload, error) {
|
||||
func (n *layer) GetObject(ctx context.Context, p *GetObjectParams) (*ObjectPayload, error) {
|
||||
var params getParams
|
||||
|
||||
params.objInfo = p.ObjectInfo
|
||||
|
@ -477,7 +601,7 @@ func getDecrypter(p *GetObjectParams) (*encryption.Decrypter, error) {
|
|||
}
|
||||
|
||||
// GetObjectInfo returns meta information about the object.
|
||||
func (n *Layer) GetObjectInfo(ctx context.Context, p *HeadObjectParams) (*data.ObjectInfo, error) {
|
||||
func (n *layer) GetObjectInfo(ctx context.Context, p *HeadObjectParams) (*data.ObjectInfo, error) {
|
||||
extendedObjectInfo, err := n.GetExtendedObjectInfo(ctx, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -487,7 +611,7 @@ func (n *Layer) GetObjectInfo(ctx context.Context, p *HeadObjectParams) (*data.O
|
|||
}
|
||||
|
||||
// GetExtendedObjectInfo returns meta information and corresponding info from the tree service about the object.
|
||||
func (n *Layer) GetExtendedObjectInfo(ctx context.Context, p *HeadObjectParams) (*data.ExtendedObjectInfo, error) {
|
||||
func (n *layer) GetExtendedObjectInfo(ctx context.Context, p *HeadObjectParams) (*data.ExtendedObjectInfo, error) {
|
||||
var objInfo *data.ExtendedObjectInfo
|
||||
var err error
|
||||
|
||||
|
@ -508,7 +632,7 @@ func (n *Layer) GetExtendedObjectInfo(ctx context.Context, p *HeadObjectParams)
|
|||
}
|
||||
|
||||
// CopyObject from one bucket into another bucket.
|
||||
func (n *Layer) CopyObject(ctx context.Context, p *CopyObjectParams) (*data.ExtendedObjectInfo, error) {
|
||||
func (n *layer) CopyObject(ctx context.Context, p *CopyObjectParams) (*data.ExtendedObjectInfo, error) {
|
||||
objPayload, err := n.GetObject(ctx, &GetObjectParams{
|
||||
ObjectInfo: p.SrcObject,
|
||||
Versioned: p.SrcVersioned,
|
||||
|
@ -531,6 +655,61 @@ func (n *Layer) CopyObject(ctx context.Context, p *CopyObjectParams) (*data.Exte
|
|||
})
|
||||
}
|
||||
|
||||
func (n *layer) PatchObject(ctx context.Context, p *PatchObjectParams) (*data.ExtendedObjectInfo, error) {
|
||||
objPayload, err := n.GetObject(ctx, &GetObjectParams{
|
||||
ObjectInfo: p.Object,
|
||||
Versioned: true,
|
||||
BucketInfo: p.BktInfo,
|
||||
Encryption: p.Encryption,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get object to patch: %w", err)
|
||||
}
|
||||
|
||||
if p.Range.Start == p.SrcSize {
|
||||
return n.PutObject(ctx, &PutObjectParams{
|
||||
BktInfo: p.BktInfo,
|
||||
Object: p.Object.Name,
|
||||
Size: p.SrcSize + p.NewBytesSize,
|
||||
Reader: io.MultiReader(objPayload, p.NewBytes),
|
||||
Header: p.Header,
|
||||
Encryption: p.Encryption,
|
||||
CopiesNumbers: p.CopiesNumbers,
|
||||
})
|
||||
}
|
||||
|
||||
var size uint64
|
||||
if p.Range.Start == 0 {
|
||||
if p.Range.End >= p.SrcSize-1 {
|
||||
return n.PutObject(ctx, &PutObjectParams{
|
||||
BktInfo: p.BktInfo,
|
||||
Object: p.Object.Name,
|
||||
Size: p.NewBytesSize,
|
||||
Reader: p.NewBytes,
|
||||
Header: p.Header,
|
||||
Encryption: p.Encryption,
|
||||
CopiesNumbers: p.CopiesNumbers,
|
||||
})
|
||||
}
|
||||
|
||||
size = p.SrcSize - 1 - p.Range.End + p.NewBytesSize
|
||||
} else if p.Range.End >= p.SrcSize-1 {
|
||||
size = p.Range.Start + p.NewBytesSize
|
||||
} else {
|
||||
size = p.SrcSize
|
||||
}
|
||||
|
||||
return n.PutObject(ctx, &PutObjectParams{
|
||||
BktInfo: p.BktInfo,
|
||||
Object: p.Object.Name,
|
||||
Size: size,
|
||||
Reader: wrapPatchReader(objPayload, p.NewBytes, p.Range, 64*1024),
|
||||
Header: p.Header,
|
||||
Encryption: p.Encryption,
|
||||
CopiesNumbers: p.CopiesNumbers,
|
||||
})
|
||||
}
|
||||
|
||||
func getRandomOID() (oid.ID, error) {
|
||||
b := [32]byte{}
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
|
@ -542,28 +721,19 @@ func getRandomOID() (oid.ID, error) {
|
|||
return objID, nil
|
||||
}
|
||||
|
||||
func (n *Layer) deleteObject(ctx context.Context, bkt *data.BucketInfo, settings *data.BucketSettings, obj *VersionedObject) *VersionedObject {
|
||||
func (n *layer) deleteObject(ctx context.Context, bkt *data.BucketInfo, settings *data.BucketSettings, obj *VersionedObject) *VersionedObject {
|
||||
if len(obj.VersionID) != 0 || settings.Unversioned() {
|
||||
var nodeVersions []*data.NodeVersion
|
||||
if nodeVersions, obj.Error = n.getNodeVersionsToDelete(ctx, bkt, obj); obj.Error != nil {
|
||||
var nodeVersion *data.NodeVersion
|
||||
if nodeVersion, obj.Error = n.getNodeVersionToDelete(ctx, bkt, obj); obj.Error != nil {
|
||||
return n.handleNotFoundError(bkt, obj)
|
||||
}
|
||||
|
||||
for _, nodeVersion := range nodeVersions {
|
||||
if obj.DeleteMarkVersion, obj.Error = n.removeOldVersion(ctx, bkt, nodeVersion, obj); obj.Error != nil {
|
||||
if !client.IsErrObjectAlreadyRemoved(obj.Error) && !client.IsErrObjectNotFound(obj.Error) {
|
||||
return obj
|
||||
}
|
||||
n.reqLogger(ctx).Debug(logs.CouldntDeleteObjectFromStorageContinueDeleting,
|
||||
zap.Stringer("cid", bkt.CID), zap.String("oid", obj.VersionID), zap.Error(obj.Error))
|
||||
}
|
||||
|
||||
if obj.Error = n.treeService.RemoveVersion(ctx, bkt, nodeVersion.ID); obj.Error != nil {
|
||||
return obj
|
||||
}
|
||||
if obj.DeleteMarkVersion, obj.Error = n.removeOldVersion(ctx, bkt, nodeVersion, obj); obj.Error != nil {
|
||||
return n.handleObjectDeleteErrors(ctx, bkt, obj, nodeVersion.ID)
|
||||
}
|
||||
|
||||
n.cache.DeleteObjectName(bkt.CID, bkt.Name, obj.Name)
|
||||
obj.Error = n.treeService.RemoveVersion(ctx, bkt, nodeVersion.ID)
|
||||
n.cache.CleanListCacheEntriesContainingObject(obj.Name, bkt.CID)
|
||||
return obj
|
||||
}
|
||||
|
||||
|
@ -576,30 +746,20 @@ func (n *Layer) deleteObject(ctx context.Context, bkt *data.BucketInfo, settings
|
|||
if settings.VersioningSuspended() {
|
||||
obj.VersionID = data.UnversionedObjectVersionID
|
||||
|
||||
var nodeVersions []*data.NodeVersion
|
||||
if nodeVersions, obj.Error = n.getNodeVersionsToDelete(ctx, bkt, obj); obj.Error != nil {
|
||||
var nullVersionToDelete *data.NodeVersion
|
||||
if lastVersion.IsUnversioned {
|
||||
if !lastVersion.IsDeleteMarker {
|
||||
nullVersionToDelete = lastVersion
|
||||
}
|
||||
} else if nullVersionToDelete, obj.Error = n.getNodeVersionToDelete(ctx, bkt, obj); obj.Error != nil {
|
||||
if !isNotFoundError(obj.Error) {
|
||||
return obj
|
||||
}
|
||||
}
|
||||
|
||||
for _, nodeVersion := range nodeVersions {
|
||||
if nodeVersion.ID == lastVersion.ID && nodeVersion.IsDeleteMarker {
|
||||
continue
|
||||
}
|
||||
|
||||
if !nodeVersion.IsDeleteMarker {
|
||||
if obj.DeleteMarkVersion, obj.Error = n.removeOldVersion(ctx, bkt, nodeVersion, obj); obj.Error != nil {
|
||||
if !client.IsErrObjectAlreadyRemoved(obj.Error) && !client.IsErrObjectNotFound(obj.Error) {
|
||||
return obj
|
||||
}
|
||||
n.reqLogger(ctx).Debug(logs.CouldntDeleteObjectFromStorageContinueDeleting,
|
||||
zap.Stringer("cid", bkt.CID), zap.String("oid", obj.VersionID), zap.Error(obj.Error))
|
||||
}
|
||||
}
|
||||
|
||||
if obj.Error = n.treeService.RemoveVersion(ctx, bkt, nodeVersion.ID); obj.Error != nil {
|
||||
return obj
|
||||
if nullVersionToDelete != nil {
|
||||
if obj.DeleteMarkVersion, obj.Error = n.removeOldVersion(ctx, bkt, nullVersionToDelete, obj); obj.Error != nil {
|
||||
return n.handleObjectDeleteErrors(ctx, bkt, obj, nullVersionToDelete.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -637,7 +797,7 @@ func (n *Layer) deleteObject(ctx context.Context, bkt *data.BucketInfo, settings
|
|||
return obj
|
||||
}
|
||||
|
||||
func (n *Layer) handleNotFoundError(bkt *data.BucketInfo, obj *VersionedObject) *VersionedObject {
|
||||
func (n *layer) handleNotFoundError(bkt *data.BucketInfo, obj *VersionedObject) *VersionedObject {
|
||||
if isNotFoundError(obj.Error) {
|
||||
obj.Error = nil
|
||||
n.cache.CleanListCacheEntriesContainingObject(obj.Name, bkt.CID)
|
||||
|
@ -647,73 +807,39 @@ func (n *Layer) handleNotFoundError(bkt *data.BucketInfo, obj *VersionedObject)
|
|||
return obj
|
||||
}
|
||||
|
||||
func (n *layer) handleObjectDeleteErrors(ctx context.Context, bkt *data.BucketInfo, obj *VersionedObject, nodeID uint64) *VersionedObject {
|
||||
if !client.IsErrObjectAlreadyRemoved(obj.Error) && !client.IsErrObjectNotFound(obj.Error) {
|
||||
return obj
|
||||
}
|
||||
|
||||
n.reqLogger(ctx).Debug(logs.CouldntDeleteObjectFromStorageContinueDeleting,
|
||||
zap.Stringer("cid", bkt.CID), zap.String("oid", obj.VersionID), zap.Error(obj.Error))
|
||||
|
||||
obj.Error = n.treeService.RemoveVersion(ctx, bkt, nodeID)
|
||||
if obj.Error == nil {
|
||||
n.cache.DeleteObjectName(bkt.CID, bkt.Name, obj.Name)
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
func isNotFoundError(err error) bool {
|
||||
return errors.IsS3Error(err, errors.ErrNoSuchKey) ||
|
||||
errors.IsS3Error(err, errors.ErrNoSuchVersion)
|
||||
}
|
||||
|
||||
func (n *Layer) getNodeVersionsToDelete(ctx context.Context, bkt *data.BucketInfo, obj *VersionedObject) ([]*data.NodeVersion, error) {
|
||||
var versionsToDelete []*data.NodeVersion
|
||||
versions, err := n.treeService.GetVersions(ctx, bkt, obj.Name)
|
||||
if err != nil {
|
||||
if stderrors.Is(err, ErrNodeNotFound) {
|
||||
return nil, fmt.Errorf("%w: %s", s3errors.GetAPIError(s3errors.ErrNoSuchKey), err.Error())
|
||||
}
|
||||
return nil, err
|
||||
func (n *layer) getNodeVersionToDelete(ctx context.Context, bkt *data.BucketInfo, obj *VersionedObject) (*data.NodeVersion, error) {
|
||||
objVersion := &data.ObjectVersion{
|
||||
BktInfo: bkt,
|
||||
ObjectName: obj.Name,
|
||||
VersionID: obj.VersionID,
|
||||
NoErrorOnDeleteMarker: true,
|
||||
}
|
||||
|
||||
if len(versions) == 0 {
|
||||
return nil, fmt.Errorf("%w: there isn't tree node with requested version id", s3errors.GetAPIError(s3errors.ErrNoSuchVersion))
|
||||
}
|
||||
|
||||
sort.Slice(versions, func(i, j int) bool {
|
||||
return versions[i].Timestamp < versions[j].Timestamp
|
||||
})
|
||||
|
||||
var matchFn func(nv *data.NodeVersion) bool
|
||||
|
||||
switch {
|
||||
case obj.VersionID == data.UnversionedObjectVersionID:
|
||||
matchFn = func(nv *data.NodeVersion) bool {
|
||||
return nv.IsUnversioned
|
||||
}
|
||||
case len(obj.VersionID) == 0:
|
||||
latest := versions[len(versions)-1]
|
||||
if latest.IsUnversioned {
|
||||
matchFn = func(nv *data.NodeVersion) bool {
|
||||
return nv.IsUnversioned
|
||||
}
|
||||
} else {
|
||||
matchFn = func(nv *data.NodeVersion) bool {
|
||||
return nv.ID == latest.ID
|
||||
}
|
||||
}
|
||||
default:
|
||||
matchFn = func(nv *data.NodeVersion) bool {
|
||||
return nv.OID.EncodeToString() == obj.VersionID
|
||||
}
|
||||
}
|
||||
|
||||
var oids []string
|
||||
for _, v := range versions {
|
||||
if matchFn(v) {
|
||||
versionsToDelete = append(versionsToDelete, v)
|
||||
if !v.IsDeleteMarker {
|
||||
oids = append(oids, v.OID.EncodeToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(versionsToDelete) == 0 {
|
||||
return nil, fmt.Errorf("%w: there isn't tree node with requested version id", s3errors.GetAPIError(s3errors.ErrNoSuchVersion))
|
||||
}
|
||||
|
||||
n.reqLogger(ctx).Debug(logs.GetTreeNodeToDelete, zap.Stringer("cid", bkt.CID), zap.Strings("oids", oids))
|
||||
|
||||
return versionsToDelete, nil
|
||||
return n.getNodeVersion(ctx, objVersion)
|
||||
}
|
||||
|
||||
func (n *Layer) getLastNodeVersion(ctx context.Context, bkt *data.BucketInfo, obj *VersionedObject) (*data.NodeVersion, error) {
|
||||
func (n *layer) getLastNodeVersion(ctx context.Context, bkt *data.BucketInfo, obj *VersionedObject) (*data.NodeVersion, error) {
|
||||
objVersion := &data.ObjectVersion{
|
||||
BktInfo: bkt,
|
||||
ObjectName: obj.Name,
|
||||
|
@ -724,7 +850,7 @@ func (n *Layer) getLastNodeVersion(ctx context.Context, bkt *data.BucketInfo, ob
|
|||
return n.getNodeVersion(ctx, objVersion)
|
||||
}
|
||||
|
||||
func (n *Layer) removeOldVersion(ctx context.Context, bkt *data.BucketInfo, nodeVersion *data.NodeVersion, obj *VersionedObject) (string, error) {
|
||||
func (n *layer) removeOldVersion(ctx context.Context, bkt *data.BucketInfo, nodeVersion *data.NodeVersion, obj *VersionedObject) (string, error) {
|
||||
if nodeVersion.IsDeleteMarker {
|
||||
return obj.VersionID, nil
|
||||
}
|
||||
|
@ -736,14 +862,14 @@ func (n *Layer) removeOldVersion(ctx context.Context, bkt *data.BucketInfo, node
|
|||
return "", n.objectDelete(ctx, bkt, nodeVersion.OID)
|
||||
}
|
||||
|
||||
func (n *Layer) removeCombinedObject(ctx context.Context, bkt *data.BucketInfo, nodeVersion *data.NodeVersion) error {
|
||||
func (n *layer) removeCombinedObject(ctx context.Context, bkt *data.BucketInfo, nodeVersion *data.NodeVersion) error {
|
||||
combinedObj, err := n.objectGet(ctx, bkt, nodeVersion.OID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get combined object '%s': %w", nodeVersion.OID.EncodeToString(), err)
|
||||
}
|
||||
|
||||
var parts []*data.PartInfo
|
||||
if err = json.NewDecoder(combinedObj.Payload).Decode(&parts); err != nil {
|
||||
if err = json.Unmarshal(combinedObj.Payload(), &parts); err != nil {
|
||||
return fmt.Errorf("unmarshal combined object parts: %w", err)
|
||||
}
|
||||
|
||||
|
@ -764,7 +890,7 @@ func (n *Layer) removeCombinedObject(ctx context.Context, bkt *data.BucketInfo,
|
|||
}
|
||||
|
||||
// DeleteObjects from the storage.
|
||||
func (n *Layer) DeleteObjects(ctx context.Context, p *DeleteObjectParams) []*VersionedObject {
|
||||
func (n *layer) DeleteObjects(ctx context.Context, p *DeleteObjectParams) []*VersionedObject {
|
||||
for i, obj := range p.Objects {
|
||||
p.Objects[i] = n.deleteObject(ctx, p.BktInfo, p.Settings, obj)
|
||||
if p.IsMultiple && p.Objects[i].Error != nil {
|
||||
|
@ -775,7 +901,7 @@ func (n *Layer) DeleteObjects(ctx context.Context, p *DeleteObjectParams) []*Ver
|
|||
return p.Objects
|
||||
}
|
||||
|
||||
func (n *Layer) CreateBucket(ctx context.Context, p *CreateBucketParams) (*data.BucketInfo, error) {
|
||||
func (n *layer) CreateBucket(ctx context.Context, p *CreateBucketParams) (*data.BucketInfo, error) {
|
||||
bktInfo, err := n.GetBucketInfo(ctx, p.Name)
|
||||
if err != nil {
|
||||
if errors.IsS3Error(err, errors.ErrNoSuchBucket) {
|
||||
|
@ -791,7 +917,7 @@ func (n *Layer) CreateBucket(ctx context.Context, p *CreateBucketParams) (*data.
|
|||
return nil, errors.GetAPIError(errors.ErrBucketAlreadyExists)
|
||||
}
|
||||
|
||||
func (n *Layer) ResolveBucket(ctx context.Context, name string) (cid.ID, error) {
|
||||
func (n *layer) ResolveBucket(ctx context.Context, name string) (cid.ID, error) {
|
||||
var cnrID cid.ID
|
||||
if err := cnrID.DecodeString(name); err != nil {
|
||||
if cnrID, err = n.resolver.Resolve(ctx, name); err != nil {
|
||||
|
@ -804,35 +930,116 @@ func (n *Layer) ResolveBucket(ctx context.Context, name string) (cid.ID, error)
|
|||
return cnrID, nil
|
||||
}
|
||||
|
||||
func (n *Layer) DeleteBucket(ctx context.Context, p *DeleteBucketParams) error {
|
||||
if !p.SkipCheck {
|
||||
res, _, err := n.getAllObjectsVersions(ctx, commonVersionsListingParams{
|
||||
BktInfo: p.BktInfo,
|
||||
MaxKeys: 1,
|
||||
})
|
||||
func (n *layer) DeleteBucket(ctx context.Context, p *DeleteBucketParams) error {
|
||||
res, _, err := n.getAllObjectsVersions(ctx, commonVersionsListingParams{
|
||||
BktInfo: p.BktInfo,
|
||||
MaxKeys: 1,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(res) != 0 {
|
||||
return errors.GetAPIError(errors.ErrBucketNotEmpty)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(res) != 0 {
|
||||
return errors.GetAPIError(errors.ErrBucketNotEmpty)
|
||||
}
|
||||
|
||||
n.cache.DeleteBucket(p.BktInfo)
|
||||
return n.frostFS.DeleteContainer(ctx, p.BktInfo.CID, p.SessionToken)
|
||||
}
|
||||
|
||||
corsObj, err := n.treeService.GetBucketCORS(ctx, p.BktInfo)
|
||||
if err != nil {
|
||||
n.reqLogger(ctx).Error(logs.GetBucketCors, zap.Error(err))
|
||||
func wrapPatchReader(payload, rngPayload io.Reader, rng *RangeParams, bufSize int) io.Reader {
|
||||
if payload == nil || rngPayload == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = n.frostFS.DeleteContainer(ctx, p.BktInfo.CID, p.SessionToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete container: %w", err)
|
||||
}
|
||||
r, w := io.Pipe()
|
||||
go func() {
|
||||
var buf = make([]byte, bufSize)
|
||||
|
||||
if !corsObj.Container().Equals(p.BktInfo.CID) && !corsObj.Container().Equals(cid.ID{}) {
|
||||
n.deleteCORSObject(ctx, p.BktInfo, corsObj)
|
||||
if rng.Start == 0 {
|
||||
err := readRange(rngPayload, w, buf)
|
||||
if err != nil {
|
||||
_ = w.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
|
||||
var readSize uint64
|
||||
for {
|
||||
n, err := payload.Read(buf)
|
||||
if err != nil && !stderrors.Is(err, io.EOF) {
|
||||
_ = w.CloseWithError(err)
|
||||
break
|
||||
}
|
||||
readSize += uint64(n)
|
||||
if readSize > rng.End+1 {
|
||||
var start uint64
|
||||
if readSize-rng.End-1 < uint64(n) {
|
||||
start = uint64(n) - (readSize - rng.End - 1)
|
||||
}
|
||||
_, _ = w.Write(buf[start:n])
|
||||
}
|
||||
if stderrors.Is(err, io.EOF) {
|
||||
_ = w.CloseWithError(err)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var (
|
||||
readSize uint64
|
||||
readRng bool
|
||||
)
|
||||
|
||||
for {
|
||||
n, err := payload.Read(buf)
|
||||
if err != nil && !stderrors.Is(err, io.EOF) {
|
||||
_ = w.CloseWithError(err)
|
||||
break
|
||||
}
|
||||
readSize += uint64(n)
|
||||
if readSize <= rng.Start {
|
||||
_, _ = w.Write(buf[:n])
|
||||
continue
|
||||
}
|
||||
if readSize-rng.Start < uint64(n) {
|
||||
_, _ = w.Write(buf[:n-int(readSize-rng.Start)])
|
||||
}
|
||||
if !readRng {
|
||||
err = readRange(rngPayload, w, buf)
|
||||
if err != nil {
|
||||
_ = w.CloseWithError(err)
|
||||
break
|
||||
}
|
||||
readRng = true
|
||||
}
|
||||
if readSize > rng.End+1 {
|
||||
var start uint64
|
||||
if readSize-rng.End-1 < uint64(n) {
|
||||
start = uint64(n) - (readSize - rng.End - 1)
|
||||
}
|
||||
_, _ = w.Write(buf[start:n])
|
||||
}
|
||||
if stderrors.Is(err, io.EOF) {
|
||||
_ = w.CloseWithError(err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return r
|
||||
}
|
||||
|
||||
func readRange(r io.Reader, w *io.PipeWriter, buf []byte) error {
|
||||
for {
|
||||
n, err := r.Read(buf)
|
||||
if n > 0 {
|
||||
_, _ = w.Write(buf[:n])
|
||||
}
|
||||
if err != nil {
|
||||
if !stderrors.Is(err, io.EOF) {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
102
api/layer/layer_test.go
Normal file
102
api/layer/layer_test.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package layer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWrapPatchReader(t *testing.T) {
|
||||
payload := "abcdefghijklmn"
|
||||
rngPayload := "123"
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
rng *RangeParams
|
||||
bufSize int
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "patch object start, buffer is less than range size",
|
||||
rng: &RangeParams{
|
||||
Start: 0,
|
||||
End: 2,
|
||||
},
|
||||
bufSize: 2,
|
||||
expected: "123defghijklmn",
|
||||
},
|
||||
{
|
||||
name: "patch object start, buffer is equal to range size",
|
||||
rng: &RangeParams{
|
||||
Start: 0,
|
||||
End: 2,
|
||||
},
|
||||
bufSize: 3,
|
||||
expected: "123defghijklmn",
|
||||
},
|
||||
{
|
||||
name: "patch object start, buffer is greater than range size",
|
||||
rng: &RangeParams{
|
||||
Start: 0,
|
||||
End: 2,
|
||||
},
|
||||
bufSize: 4,
|
||||
expected: "123defghijklmn",
|
||||
},
|
||||
{
|
||||
name: "patch object middle, range at the beginning of buffer",
|
||||
rng: &RangeParams{
|
||||
Start: 5,
|
||||
End: 7,
|
||||
},
|
||||
bufSize: 5,
|
||||
expected: "abcde123ijklmn",
|
||||
},
|
||||
{
|
||||
name: "patch object middle, range in the middle of buffer",
|
||||
rng: &RangeParams{
|
||||
Start: 6,
|
||||
End: 8,
|
||||
},
|
||||
bufSize: 5,
|
||||
expected: "abcdef123jklmn",
|
||||
},
|
||||
{
|
||||
name: "patch object middle, range in the end of buffer",
|
||||
rng: &RangeParams{
|
||||
Start: 7,
|
||||
End: 9,
|
||||
},
|
||||
bufSize: 5,
|
||||
expected: "abcdefg123klmn",
|
||||
},
|
||||
{
|
||||
name: "patch object end, increase size",
|
||||
rng: &RangeParams{
|
||||
Start: 12,
|
||||
End: 14,
|
||||
},
|
||||
bufSize: 4,
|
||||
expected: "abcdefghijkl123",
|
||||
},
|
||||
{
|
||||
name: "patch object end",
|
||||
rng: &RangeParams{
|
||||
Start: 11,
|
||||
End: 13,
|
||||
},
|
||||
bufSize: 4,
|
||||
expected: "abcdefghijk123",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
wrappedReader := wrapPatchReader(bytes.NewBufferString(payload), bytes.NewBufferString(rngPayload), tc.rng, tc.bufSize)
|
||||
|
||||
res, err := io.ReadAll(wrappedReader)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expected, string(res))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -96,7 +96,7 @@ const (
|
|||
)
|
||||
|
||||
// ListObjectsV1 returns objects in a bucket for requests of Version 1.
|
||||
func (n *Layer) ListObjectsV1(ctx context.Context, p *ListObjectsParamsV1) (*ListObjectsInfoV1, error) {
|
||||
func (n *layer) ListObjectsV1(ctx context.Context, p *ListObjectsParamsV1) (*ListObjectsInfoV1, error) {
|
||||
var result ListObjectsInfoV1
|
||||
|
||||
prm := commonLatestVersionsListingParams{
|
||||
|
@ -127,7 +127,7 @@ func (n *Layer) ListObjectsV1(ctx context.Context, p *ListObjectsParamsV1) (*Lis
|
|||
}
|
||||
|
||||
// ListObjectsV2 returns objects in a bucket for requests of Version 2.
|
||||
func (n *Layer) ListObjectsV2(ctx context.Context, p *ListObjectsParamsV2) (*ListObjectsInfoV2, error) {
|
||||
func (n *layer) ListObjectsV2(ctx context.Context, p *ListObjectsParamsV2) (*ListObjectsInfoV2, error) {
|
||||
var result ListObjectsInfoV2
|
||||
|
||||
prm := commonLatestVersionsListingParams{
|
||||
|
@ -157,7 +157,7 @@ func (n *Layer) ListObjectsV2(ctx context.Context, p *ListObjectsParamsV2) (*Lis
|
|||
return &result, nil
|
||||
}
|
||||
|
||||
func (n *Layer) ListObjectVersions(ctx context.Context, p *ListObjectVersionsParams) (*ListObjectVersionsInfo, error) {
|
||||
func (n *layer) ListObjectVersions(ctx context.Context, p *ListObjectVersionsParams) (*ListObjectVersionsInfo, error) {
|
||||
prm := commonVersionsListingParams{
|
||||
BktInfo: p.BktInfo,
|
||||
Delimiter: p.Delimiter,
|
||||
|
@ -188,7 +188,7 @@ func (n *Layer) ListObjectVersions(ctx context.Context, p *ListObjectVersionsPar
|
|||
return res, nil
|
||||
}
|
||||
|
||||
func (n *Layer) getLatestObjectsVersions(ctx context.Context, p commonLatestVersionsListingParams) (objects []*data.ExtendedNodeVersion, next *data.ExtendedNodeVersion, err error) {
|
||||
func (n *layer) getLatestObjectsVersions(ctx context.Context, p commonLatestVersionsListingParams) (objects []*data.ExtendedNodeVersion, next *data.ExtendedNodeVersion, err error) {
|
||||
if p.MaxKeys == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
@ -225,7 +225,7 @@ func (n *Layer) getLatestObjectsVersions(ctx context.Context, p commonLatestVers
|
|||
return
|
||||
}
|
||||
|
||||
func (n *Layer) getAllObjectsVersions(ctx context.Context, p commonVersionsListingParams) ([]*data.ExtendedNodeVersion, bool, error) {
|
||||
func (n *layer) getAllObjectsVersions(ctx context.Context, p commonVersionsListingParams) ([]*data.ExtendedNodeVersion, bool, error) {
|
||||
if p.MaxKeys == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
@ -301,15 +301,15 @@ func formVersionsListRow(objects []*data.ExtendedNodeVersion, rowStartIndex int,
|
|||
}
|
||||
}
|
||||
|
||||
func (n *Layer) getListLatestVersionsSession(ctx context.Context, p commonLatestVersionsListingParams) (*data.ListSession, error) {
|
||||
func (n *layer) getListLatestVersionsSession(ctx context.Context, p commonLatestVersionsListingParams) (*data.ListSession, error) {
|
||||
return n.getListVersionsSession(ctx, p.commonVersionsListingParams, true)
|
||||
}
|
||||
|
||||
func (n *Layer) getListAllVersionsSession(ctx context.Context, p commonVersionsListingParams) (*data.ListSession, error) {
|
||||
func (n *layer) getListAllVersionsSession(ctx context.Context, p commonVersionsListingParams) (*data.ListSession, error) {
|
||||
return n.getListVersionsSession(ctx, p, false)
|
||||
}
|
||||
|
||||
func (n *Layer) getListVersionsSession(ctx context.Context, p commonVersionsListingParams, latestOnly bool) (*data.ListSession, error) {
|
||||
func (n *layer) getListVersionsSession(ctx context.Context, p commonVersionsListingParams, latestOnly bool) (*data.ListSession, error) {
|
||||
owner := n.BearerOwner(ctx)
|
||||
|
||||
cacheKey := cache.CreateListSessionCacheKey(p.BktInfo.CID, p.Prefix, p.Bookmark)
|
||||
|
@ -329,7 +329,7 @@ func (n *Layer) getListVersionsSession(ctx context.Context, p commonVersionsList
|
|||
return session, nil
|
||||
}
|
||||
|
||||
func (n *Layer) initNewVersionsByPrefixSession(ctx context.Context, p commonVersionsListingParams, latestOnly bool) (session *data.ListSession, err error) {
|
||||
func (n *layer) initNewVersionsByPrefixSession(ctx context.Context, p commonVersionsListingParams, latestOnly bool) (session *data.ListSession, err error) {
|
||||
session = &data.ListSession{NamesMap: make(map[string]struct{})}
|
||||
session.Context, session.Cancel = context.WithCancel(context.Background())
|
||||
|
||||
|
@ -345,7 +345,7 @@ func (n *Layer) initNewVersionsByPrefixSession(ctx context.Context, p commonVers
|
|||
return session, nil
|
||||
}
|
||||
|
||||
func (n *Layer) putListLatestVersionsSession(ctx context.Context, p commonLatestVersionsListingParams, session *data.ListSession, allObjects []*data.ExtendedNodeVersion) {
|
||||
func (n *layer) putListLatestVersionsSession(ctx context.Context, p commonLatestVersionsListingParams, session *data.ListSession, allObjects []*data.ExtendedNodeVersion) {
|
||||
if len(allObjects) <= p.MaxKeys {
|
||||
return
|
||||
}
|
||||
|
@ -366,7 +366,7 @@ func (n *Layer) putListLatestVersionsSession(ctx context.Context, p commonLatest
|
|||
n.cache.PutListSession(n.BearerOwner(ctx), cacheKey, session)
|
||||
}
|
||||
|
||||
func (n *Layer) putListAllVersionsSession(ctx context.Context, p commonVersionsListingParams, session *data.ListSession, allObjects []*data.ExtendedNodeVersion) {
|
||||
func (n *layer) putListAllVersionsSession(ctx context.Context, p commonVersionsListingParams, session *data.ListSession, allObjects []*data.ExtendedNodeVersion) {
|
||||
if len(allObjects) <= p.MaxKeys {
|
||||
return
|
||||
}
|
||||
|
@ -498,7 +498,7 @@ func nodesGeneratorVersions(ctx context.Context, p commonVersionsListingParams,
|
|||
return nodeCh, errCh
|
||||
}
|
||||
|
||||
func (n *Layer) initWorkerPool(ctx context.Context, size int, p commonVersionsListingParams, input <-chan *data.ExtendedNodeVersion) (<-chan *data.ExtendedNodeVersion, error) {
|
||||
func (n *layer) initWorkerPool(ctx context.Context, size int, p commonVersionsListingParams, input <-chan *data.ExtendedNodeVersion) (<-chan *data.ExtendedNodeVersion, error) {
|
||||
reqLog := n.reqLogger(ctx)
|
||||
pool, err := ants.NewPool(size, ants.WithLogger(&logWrapper{reqLog}))
|
||||
if err != nil {
|
||||
|
@ -637,7 +637,7 @@ func triageExtendedObjects(allObjects []*data.ExtendedNodeVersion) (prefixes []s
|
|||
return
|
||||
}
|
||||
|
||||
func (n *Layer) objectInfoFromObjectsCacheOrFrostFS(ctx context.Context, bktInfo *data.BucketInfo, node *data.NodeVersion) (oi *data.ObjectInfo) {
|
||||
func (n *layer) objectInfoFromObjectsCacheOrFrostFS(ctx context.Context, bktInfo *data.BucketInfo, node *data.NodeVersion) (oi *data.ObjectInfo) {
|
||||
owner := n.BearerOwner(ctx)
|
||||
if extInfo := n.cache.GetObject(owner, newAddress(bktInfo.CID, node.OID)); extInfo != nil {
|
||||
return extInfo.ObjectInfo
|
||||
|
|
|
@ -36,6 +36,7 @@ const (
|
|||
MultipartObjectSize = "S3-Multipart-Object-Size"
|
||||
|
||||
metaPrefix = "meta-"
|
||||
aclPrefix = "acl-"
|
||||
|
||||
MaxSizeUploadsList = 1000
|
||||
MaxSizePartsList = 1000
|
||||
|
@ -61,7 +62,8 @@ type (
|
|||
}
|
||||
|
||||
UploadData struct {
|
||||
TagSet map[string]string
|
||||
TagSet map[string]string
|
||||
ACLHeaders map[string]string
|
||||
}
|
||||
|
||||
UploadPartParams struct {
|
||||
|
@ -144,9 +146,10 @@ type (
|
|||
}
|
||||
)
|
||||
|
||||
func (n *Layer) CreateMultipartUpload(ctx context.Context, p *CreateMultipartParams) error {
|
||||
func (n *layer) CreateMultipartUpload(ctx context.Context, p *CreateMultipartParams) error {
|
||||
metaSize := len(p.Header)
|
||||
if p.Data != nil {
|
||||
metaSize += len(p.Data.ACLHeaders)
|
||||
metaSize += len(p.Data.TagSet)
|
||||
}
|
||||
|
||||
|
@ -164,6 +167,10 @@ func (n *Layer) CreateMultipartUpload(ctx context.Context, p *CreateMultipartPar
|
|||
}
|
||||
|
||||
if p.Data != nil {
|
||||
for key, val := range p.Data.ACLHeaders {
|
||||
info.Meta[aclPrefix+key] = val
|
||||
}
|
||||
|
||||
for key, val := range p.Data.TagSet {
|
||||
info.Meta[tagPrefix+key] = val
|
||||
}
|
||||
|
@ -178,7 +185,7 @@ func (n *Layer) CreateMultipartUpload(ctx context.Context, p *CreateMultipartPar
|
|||
return n.treeService.CreateMultipartUpload(ctx, p.Info.Bkt, info)
|
||||
}
|
||||
|
||||
func (n *Layer) UploadPart(ctx context.Context, p *UploadPartParams) (string, error) {
|
||||
func (n *layer) UploadPart(ctx context.Context, p *UploadPartParams) (string, error) {
|
||||
multipartInfo, err := n.treeService.GetMultipartUpload(ctx, p.Info.Bkt, p.Info.Key, p.Info.UploadID)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNodeNotFound) {
|
||||
|
@ -199,7 +206,7 @@ func (n *Layer) UploadPart(ctx context.Context, p *UploadPartParams) (string, er
|
|||
return objInfo.ETag(n.features.MD5Enabled()), nil
|
||||
}
|
||||
|
||||
func (n *Layer) uploadPart(ctx context.Context, multipartInfo *data.MultipartInfo, p *UploadPartParams) (*data.ObjectInfo, error) {
|
||||
func (n *layer) uploadPart(ctx context.Context, multipartInfo *data.MultipartInfo, p *UploadPartParams) (*data.ObjectInfo, error) {
|
||||
encInfo := FormEncryptionInfo(multipartInfo.Meta)
|
||||
if err := p.Info.Encryption.MatchObjectEncryption(encInfo); err != nil {
|
||||
n.reqLogger(ctx).Warn(logs.MismatchedObjEncryptionInfo, zap.Error(err))
|
||||
|
@ -312,7 +319,7 @@ func (n *Layer) uploadPart(ctx context.Context, multipartInfo *data.MultipartInf
|
|||
return objInfo, nil
|
||||
}
|
||||
|
||||
func (n *Layer) UploadPartCopy(ctx context.Context, p *UploadCopyParams) (*data.ObjectInfo, error) {
|
||||
func (n *layer) UploadPartCopy(ctx context.Context, p *UploadCopyParams) (*data.ObjectInfo, error) {
|
||||
multipartInfo, err := n.treeService.GetMultipartUpload(ctx, p.Info.Bkt, p.Info.Key, p.Info.UploadID)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNodeNotFound) {
|
||||
|
@ -360,7 +367,7 @@ func (n *Layer) UploadPartCopy(ctx context.Context, p *UploadCopyParams) (*data.
|
|||
return n.uploadPart(ctx, multipartInfo, params)
|
||||
}
|
||||
|
||||
func (n *Layer) CompleteMultipartUpload(ctx context.Context, p *CompleteMultipartParams) (*UploadData, *data.ExtendedObjectInfo, error) {
|
||||
func (n *layer) CompleteMultipartUpload(ctx context.Context, p *CompleteMultipartParams) (*UploadData, *data.ExtendedObjectInfo, error) {
|
||||
for i := 1; i < len(p.Parts); i++ {
|
||||
if p.Parts[i].PartNumber <= p.Parts[i-1].PartNumber {
|
||||
return nil, nil, s3errors.GetAPIError(s3errors.ErrInvalidPartOrder)
|
||||
|
@ -425,13 +432,16 @@ func (n *Layer) CompleteMultipartUpload(ctx context.Context, p *CompleteMultipar
|
|||
initMetadata[MultipartObjectSize] = strconv.FormatUint(multipartObjetSize, 10)
|
||||
|
||||
uploadData := &UploadData{
|
||||
TagSet: make(map[string]string),
|
||||
TagSet: make(map[string]string),
|
||||
ACLHeaders: make(map[string]string),
|
||||
}
|
||||
for key, val := range multipartInfo.Meta {
|
||||
if strings.HasPrefix(key, metaPrefix) {
|
||||
initMetadata[strings.TrimPrefix(key, metaPrefix)] = val
|
||||
} else if strings.HasPrefix(key, tagPrefix) {
|
||||
uploadData.TagSet[strings.TrimPrefix(key, tagPrefix)] = val
|
||||
} else if strings.HasPrefix(key, aclPrefix) {
|
||||
uploadData.ACLHeaders[strings.TrimPrefix(key, aclPrefix)] = val
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -482,7 +492,7 @@ func (n *Layer) CompleteMultipartUpload(ctx context.Context, p *CompleteMultipar
|
|||
return uploadData, extObjInfo, n.treeService.DeleteMultipartUpload(ctx, p.Info.Bkt, multipartInfo)
|
||||
}
|
||||
|
||||
func (n *Layer) ListMultipartUploads(ctx context.Context, p *ListMultipartUploadsParams) (*ListMultipartUploadsInfo, error) {
|
||||
func (n *layer) ListMultipartUploads(ctx context.Context, p *ListMultipartUploadsParams) (*ListMultipartUploadsInfo, error) {
|
||||
var result ListMultipartUploadsInfo
|
||||
if p.MaxUploads == 0 {
|
||||
return &result, nil
|
||||
|
@ -542,7 +552,7 @@ func (n *Layer) ListMultipartUploads(ctx context.Context, p *ListMultipartUpload
|
|||
return &result, nil
|
||||
}
|
||||
|
||||
func (n *Layer) AbortMultipartUpload(ctx context.Context, p *UploadInfoParams) error {
|
||||
func (n *layer) AbortMultipartUpload(ctx context.Context, p *UploadInfoParams) error {
|
||||
multipartInfo, parts, err := n.getUploadParts(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -558,7 +568,7 @@ func (n *Layer) AbortMultipartUpload(ctx context.Context, p *UploadInfoParams) e
|
|||
return n.treeService.DeleteMultipartUpload(ctx, p.Bkt, multipartInfo)
|
||||
}
|
||||
|
||||
func (n *Layer) ListParts(ctx context.Context, p *ListPartsParams) (*ListPartsInfo, error) {
|
||||
func (n *layer) ListParts(ctx context.Context, p *ListPartsParams) (*ListPartsInfo, error) {
|
||||
var res ListPartsInfo
|
||||
multipartInfo, partsInfo, err := n.getUploadParts(ctx, p.Info)
|
||||
if err != nil {
|
||||
|
@ -603,16 +613,16 @@ func (n *Layer) ListParts(ctx context.Context, p *ListPartsParams) (*ListPartsIn
|
|||
|
||||
if len(parts) > p.MaxParts {
|
||||
res.IsTruncated = true
|
||||
res.NextPartNumberMarker = parts[p.MaxParts-1].PartNumber
|
||||
parts = parts[:p.MaxParts]
|
||||
}
|
||||
|
||||
res.NextPartNumberMarker = parts[len(parts)-1].PartNumber
|
||||
res.Parts = parts
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (n *Layer) getUploadParts(ctx context.Context, p *UploadInfoParams) (*data.MultipartInfo, map[int]*data.PartInfo, error) {
|
||||
func (n *layer) getUploadParts(ctx context.Context, p *UploadInfoParams) (*data.MultipartInfo, map[int]*data.PartInfo, error) {
|
||||
multipartInfo, err := n.treeService.GetMultipartUpload(ctx, p.Bkt, p.Key, p.UploadID)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNodeNotFound) {
|
||||
|
|
89
api/layer/notifications.go
Normal file
89
api/layer/notifications.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package layer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/xml"
|
||||
errorsStd "errors"
|
||||
"fmt"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type PutBucketNotificationConfigurationParams struct {
|
||||
RequestInfo *middleware.ReqInfo
|
||||
BktInfo *data.BucketInfo
|
||||
Configuration *data.NotificationConfiguration
|
||||
CopiesNumbers []uint32
|
||||
}
|
||||
|
||||
func (n *layer) PutBucketNotificationConfiguration(ctx context.Context, p *PutBucketNotificationConfigurationParams) error {
|
||||
confXML, err := xml.Marshal(p.Configuration)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal notify configuration: %w", err)
|
||||
}
|
||||
|
||||
prm := PrmObjectCreate{
|
||||
Container: p.BktInfo.CID,
|
||||
Payload: bytes.NewReader(confXML),
|
||||
Filepath: p.BktInfo.NotificationConfigurationObjectName(),
|
||||
CreationTime: TimeNow(ctx),
|
||||
CopiesNumber: p.CopiesNumbers,
|
||||
}
|
||||
|
||||
_, objID, _, _, err := n.objectPutAndHash(ctx, prm, p.BktInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
objIDToDelete, err := n.treeService.PutNotificationConfigurationNode(ctx, p.BktInfo, objID)
|
||||
objIDToDeleteNotFound := errorsStd.Is(err, ErrNoNodeToRemove)
|
||||
if err != nil && !objIDToDeleteNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
if !objIDToDeleteNotFound {
|
||||
if err = n.objectDelete(ctx, p.BktInfo, objIDToDelete); err != nil {
|
||||
n.reqLogger(ctx).Error(logs.CouldntDeleteNotificationConfigurationObject, zap.Error(err),
|
||||
zap.String("cid", p.BktInfo.CID.EncodeToString()),
|
||||
zap.String("oid", objIDToDelete.EncodeToString()))
|
||||
}
|
||||
}
|
||||
|
||||
n.cache.PutNotificationConfiguration(n.BearerOwner(ctx), p.BktInfo, p.Configuration)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *layer) GetBucketNotificationConfiguration(ctx context.Context, bktInfo *data.BucketInfo) (*data.NotificationConfiguration, error) {
|
||||
owner := n.BearerOwner(ctx)
|
||||
if conf := n.cache.GetNotificationConfiguration(owner, bktInfo); conf != nil {
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
objID, err := n.treeService.GetNotificationConfigurationNode(ctx, bktInfo)
|
||||
objIDNotFound := errorsStd.Is(err, ErrNodeNotFound)
|
||||
if err != nil && !objIDNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf := &data.NotificationConfiguration{}
|
||||
|
||||
if !objIDNotFound {
|
||||
obj, err := n.objectGet(ctx, bktInfo, objID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = xml.Unmarshal(obj.Payload(), &conf); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal notify configuration: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
n.cache.PutNotificationConfiguration(owner, bktInfo, conf)
|
||||
|
||||
return conf, nil
|
||||
}
|
|
@ -67,18 +67,24 @@ func newAddress(cnr cid.ID, obj oid.ID) oid.Address {
|
|||
}
|
||||
|
||||
// objectHead returns all object's headers.
|
||||
func (n *Layer) objectHead(ctx context.Context, bktInfo *data.BucketInfo, idObj oid.ID) (*object.Object, error) {
|
||||
prm := PrmObjectHead{
|
||||
Container: bktInfo.CID,
|
||||
Object: idObj,
|
||||
func (n *layer) objectHead(ctx context.Context, bktInfo *data.BucketInfo, idObj oid.ID) (*object.Object, error) {
|
||||
prm := PrmObjectRead{
|
||||
Container: bktInfo.CID,
|
||||
Object: idObj,
|
||||
WithHeader: true,
|
||||
}
|
||||
|
||||
n.prepareAuthParameters(ctx, &prm.PrmAuth, bktInfo.Owner)
|
||||
|
||||
return n.frostFS.HeadObject(ctx, prm)
|
||||
res, err := n.frostFS.ReadObject(ctx, prm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res.Head, nil
|
||||
}
|
||||
|
||||
func (n *Layer) initObjectPayloadReader(ctx context.Context, p getParams) (io.Reader, error) {
|
||||
func (n *layer) initObjectPayloadReader(ctx context.Context, p getParams) (io.Reader, error) {
|
||||
if _, isCombined := p.objInfo.Headers[MultipartObjectSize]; !isCombined {
|
||||
return n.initFrostFSObjectPayloadReader(ctx, getFrostFSParams{
|
||||
off: p.off,
|
||||
|
@ -94,7 +100,7 @@ func (n *Layer) initObjectPayloadReader(ctx context.Context, p getParams) (io.Re
|
|||
}
|
||||
|
||||
var parts []*data.PartInfo
|
||||
if err = json.NewDecoder(combinedObj.Payload).Decode(&parts); err != nil {
|
||||
if err = json.Unmarshal(combinedObj.Payload(), &parts); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal combined object parts: %w", err)
|
||||
}
|
||||
|
||||
|
@ -125,28 +131,17 @@ func (n *Layer) initObjectPayloadReader(ctx context.Context, p getParams) (io.Re
|
|||
|
||||
// initializes payload reader of the FrostFS object.
|
||||
// Zero range corresponds to full payload (panics if only offset is set).
|
||||
func (n *Layer) initFrostFSObjectPayloadReader(ctx context.Context, p getFrostFSParams) (io.Reader, error) {
|
||||
var prmAuth PrmAuth
|
||||
n.prepareAuthParameters(ctx, &prmAuth, p.bktInfo.Owner)
|
||||
|
||||
if p.off+p.ln != 0 {
|
||||
prm := PrmObjectRange{
|
||||
PrmAuth: prmAuth,
|
||||
Container: p.bktInfo.CID,
|
||||
Object: p.oid,
|
||||
PayloadRange: [2]uint64{p.off, p.ln},
|
||||
}
|
||||
|
||||
return n.frostFS.RangeObject(ctx, prm)
|
||||
func (n *layer) initFrostFSObjectPayloadReader(ctx context.Context, p getFrostFSParams) (io.Reader, error) {
|
||||
prm := PrmObjectRead{
|
||||
Container: p.bktInfo.CID,
|
||||
Object: p.oid,
|
||||
WithPayload: true,
|
||||
PayloadRange: [2]uint64{p.off, p.ln},
|
||||
}
|
||||
|
||||
prm := PrmObjectGet{
|
||||
PrmAuth: prmAuth,
|
||||
Container: p.bktInfo.CID,
|
||||
Object: p.oid,
|
||||
}
|
||||
n.prepareAuthParameters(ctx, &prm.PrmAuth, p.bktInfo.Owner)
|
||||
|
||||
res, err := n.frostFS.GetObject(ctx, prm)
|
||||
res, err := n.frostFS.ReadObject(ctx, prm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -155,25 +150,22 @@ func (n *Layer) initFrostFSObjectPayloadReader(ctx context.Context, p getFrostFS
|
|||
}
|
||||
|
||||
// objectGet returns an object with payload in the object.
|
||||
func (n *Layer) objectGet(ctx context.Context, bktInfo *data.BucketInfo, objID oid.ID) (*Object, error) {
|
||||
return n.objectGetBase(ctx, bktInfo, objID, PrmAuth{})
|
||||
}
|
||||
|
||||
// objectGetWithAuth returns an object with payload in the object. Uses provided PrmAuth.
|
||||
func (n *Layer) objectGetWithAuth(ctx context.Context, bktInfo *data.BucketInfo, objID oid.ID, auth PrmAuth) (*Object, error) {
|
||||
return n.objectGetBase(ctx, bktInfo, objID, auth)
|
||||
}
|
||||
|
||||
func (n *Layer) objectGetBase(ctx context.Context, bktInfo *data.BucketInfo, objID oid.ID, auth PrmAuth) (*Object, error) {
|
||||
prm := PrmObjectGet{
|
||||
PrmAuth: auth,
|
||||
Container: bktInfo.CID,
|
||||
Object: objID,
|
||||
func (n *layer) objectGet(ctx context.Context, bktInfo *data.BucketInfo, objID oid.ID) (*object.Object, error) {
|
||||
prm := PrmObjectRead{
|
||||
Container: bktInfo.CID,
|
||||
Object: objID,
|
||||
WithHeader: true,
|
||||
WithPayload: true,
|
||||
}
|
||||
|
||||
n.prepareAuthParameters(ctx, &prm.PrmAuth, bktInfo.Owner)
|
||||
|
||||
return n.frostFS.GetObject(ctx, prm)
|
||||
res, err := n.frostFS.ReadObject(ctx, prm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res.Head, nil
|
||||
}
|
||||
|
||||
// MimeByFilePath detect mime type by file path extension.
|
||||
|
@ -222,7 +214,7 @@ func ParseCompletedPartHeader(hdr string) (*Part, error) {
|
|||
}
|
||||
|
||||
// PutObject stores object into FrostFS, took payload from io.Reader.
|
||||
func (n *Layer) PutObject(ctx context.Context, p *PutObjectParams) (*data.ExtendedObjectInfo, error) {
|
||||
func (n *layer) PutObject(ctx context.Context, p *PutObjectParams) (*data.ExtendedObjectInfo, error) {
|
||||
bktSettings, err := n.GetBucketSettings(ctx, p.BktInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't get versioning settings object: %w", err)
|
||||
|
@ -371,7 +363,7 @@ func (n *Layer) PutObject(ctx context.Context, p *PutObjectParams) (*data.Extend
|
|||
return extendedObjInfo, nil
|
||||
}
|
||||
|
||||
func (n *Layer) headLastVersionIfNotDeleted(ctx context.Context, bkt *data.BucketInfo, objectName string) (*data.ExtendedObjectInfo, error) {
|
||||
func (n *layer) headLastVersionIfNotDeleted(ctx context.Context, bkt *data.BucketInfo, objectName string) (*data.ExtendedObjectInfo, error) {
|
||||
owner := n.BearerOwner(ctx)
|
||||
if extObjInfo := n.cache.GetLastObject(owner, bkt.Name, objectName); extObjInfo != nil {
|
||||
return extObjInfo, nil
|
||||
|
@ -409,7 +401,7 @@ func (n *Layer) headLastVersionIfNotDeleted(ctx context.Context, bkt *data.Bucke
|
|||
return extObjInfo, nil
|
||||
}
|
||||
|
||||
func (n *Layer) headVersion(ctx context.Context, bkt *data.BucketInfo, p *HeadObjectParams) (*data.ExtendedObjectInfo, error) {
|
||||
func (n *layer) headVersion(ctx context.Context, bkt *data.BucketInfo, p *HeadObjectParams) (*data.ExtendedObjectInfo, error) {
|
||||
var err error
|
||||
var foundVersion *data.NodeVersion
|
||||
if p.VersionID == data.UnversionedObjectVersionID {
|
||||
|
@ -467,18 +459,8 @@ func (n *Layer) headVersion(ctx context.Context, bkt *data.BucketInfo, p *HeadOb
|
|||
}
|
||||
|
||||
// objectDelete puts tombstone object into frostfs.
|
||||
func (n *Layer) objectDelete(ctx context.Context, bktInfo *data.BucketInfo, idObj oid.ID) error {
|
||||
return n.objectDeleteBase(ctx, bktInfo, idObj, PrmAuth{})
|
||||
}
|
||||
|
||||
// objectDeleteWithAuth puts tombstone object into frostfs. Uses provided PrmAuth.
|
||||
func (n *Layer) objectDeleteWithAuth(ctx context.Context, bktInfo *data.BucketInfo, idObj oid.ID, auth PrmAuth) error {
|
||||
return n.objectDeleteBase(ctx, bktInfo, idObj, auth)
|
||||
}
|
||||
|
||||
func (n *Layer) objectDeleteBase(ctx context.Context, bktInfo *data.BucketInfo, idObj oid.ID, auth PrmAuth) error {
|
||||
func (n *layer) objectDelete(ctx context.Context, bktInfo *data.BucketInfo, idObj oid.ID) error {
|
||||
prm := PrmObjectDelete{
|
||||
PrmAuth: auth,
|
||||
Container: bktInfo.CID,
|
||||
Object: idObj,
|
||||
}
|
||||
|
@ -492,7 +474,7 @@ func (n *Layer) objectDeleteBase(ctx context.Context, bktInfo *data.BucketInfo,
|
|||
|
||||
// objectPutAndHash prepare auth parameters and invoke frostfs.CreateObject.
|
||||
// Returns object ID and payload sha256 hash.
|
||||
func (n *Layer) objectPutAndHash(ctx context.Context, prm PrmObjectCreate, bktInfo *data.BucketInfo) (uint64, oid.ID, []byte, []byte, error) {
|
||||
func (n *layer) objectPutAndHash(ctx context.Context, prm PrmObjectCreate, bktInfo *data.BucketInfo) (uint64, oid.ID, []byte, []byte, error) {
|
||||
n.prepareAuthParameters(ctx, &prm.PrmAuth, bktInfo.Owner)
|
||||
prm.ClientCut = n.features.ClientCut()
|
||||
prm.BufferMaxSize = n.features.BufferMaxSizeForPut()
|
||||
|
|
|
@ -31,6 +31,8 @@ func TestWrapReader(t *testing.T) {
|
|||
|
||||
func TestGoroutinesDontLeakInPutAndHash(t *testing.T) {
|
||||
tc := prepareContext(t)
|
||||
l, ok := tc.layer.(*layer)
|
||||
require.True(t, ok)
|
||||
|
||||
content := make([]byte, 128*1024)
|
||||
_, err := rand.Read(content)
|
||||
|
@ -44,7 +46,7 @@ func TestGoroutinesDontLeakInPutAndHash(t *testing.T) {
|
|||
|
||||
expErr := errors.New("some error")
|
||||
tc.testFrostFS.SetObjectPutError(tc.obj, expErr)
|
||||
_, _, _, _, err = tc.layer.objectPutAndHash(tc.ctx, prm, tc.bktInfo)
|
||||
_, _, _, _, err = l.objectPutAndHash(tc.ctx, prm, tc.bktInfo)
|
||||
require.ErrorIs(t, err, expErr)
|
||||
require.Empty(t, payload.Len(), "body must be read out otherwise goroutines can leak in wrapReader")
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
)
|
||||
|
||||
|
@ -27,7 +26,7 @@ type PutLockInfoParams struct {
|
|||
NodeVersion *data.NodeVersion // optional
|
||||
}
|
||||
|
||||
func (n *Layer) PutLockInfo(ctx context.Context, p *PutLockInfoParams) (err error) {
|
||||
func (n *layer) PutLockInfo(ctx context.Context, p *PutLockInfoParams) (err error) {
|
||||
newLock := p.NewLock
|
||||
versionNode := p.NodeVersion
|
||||
// sometimes node version can be provided from executing context
|
||||
|
@ -101,7 +100,7 @@ func (n *Layer) PutLockInfo(ctx context.Context, p *PutLockInfoParams) (err erro
|
|||
return nil
|
||||
}
|
||||
|
||||
func (n *Layer) getNodeVersionFromCacheOrFrostfs(ctx context.Context, objVersion *data.ObjectVersion) (nodeVersion *data.NodeVersion, err error) {
|
||||
func (n *layer) getNodeVersionFromCacheOrFrostfs(ctx context.Context, objVersion *data.ObjectVersion) (nodeVersion *data.NodeVersion, err error) {
|
||||
// check cache if node version is stored inside extendedObjectVersion
|
||||
nodeVersion = n.getNodeVersionFromCache(n.BearerOwner(ctx), objVersion)
|
||||
if nodeVersion == nil {
|
||||
|
@ -112,7 +111,7 @@ func (n *Layer) getNodeVersionFromCacheOrFrostfs(ctx context.Context, objVersion
|
|||
return nodeVersion, nil
|
||||
}
|
||||
|
||||
func (n *Layer) putLockObject(ctx context.Context, bktInfo *data.BucketInfo, objID oid.ID, lock *data.ObjectLock, copiesNumber []uint32) (oid.ID, error) {
|
||||
func (n *layer) putLockObject(ctx context.Context, bktInfo *data.BucketInfo, objID oid.ID, lock *data.ObjectLock, copiesNumber []uint32) (oid.ID, error) {
|
||||
prm := PrmObjectCreate{
|
||||
Container: bktInfo.CID,
|
||||
Locks: []oid.ID{objID},
|
||||
|
@ -130,7 +129,7 @@ func (n *Layer) putLockObject(ctx context.Context, bktInfo *data.BucketInfo, obj
|
|||
return id, err
|
||||
}
|
||||
|
||||
func (n *Layer) GetLockInfo(ctx context.Context, objVersion *data.ObjectVersion) (*data.LockInfo, error) {
|
||||
func (n *layer) GetLockInfo(ctx context.Context, objVersion *data.ObjectVersion) (*data.LockInfo, error) {
|
||||
owner := n.BearerOwner(ctx)
|
||||
if lockInfo := n.cache.GetLockInfo(owner, lockObjectKey(objVersion)); lockInfo != nil {
|
||||
return lockInfo, nil
|
||||
|
@ -154,36 +153,30 @@ func (n *Layer) GetLockInfo(ctx context.Context, objVersion *data.ObjectVersion)
|
|||
return lockInfo, nil
|
||||
}
|
||||
|
||||
func (n *Layer) getCORS(ctx context.Context, bkt *data.BucketInfo) (*data.CORSConfiguration, error) {
|
||||
func (n *layer) getCORS(ctx context.Context, bkt *data.BucketInfo) (*data.CORSConfiguration, error) {
|
||||
owner := n.BearerOwner(ctx)
|
||||
if cors := n.cache.GetCORS(owner, bkt); cors != nil {
|
||||
return cors, nil
|
||||
}
|
||||
|
||||
addr, err := n.treeService.GetBucketCORS(ctx, bkt)
|
||||
objNotFound := errorsStd.Is(err, ErrNodeNotFound)
|
||||
if err != nil && !objNotFound {
|
||||
objID, err := n.treeService.GetBucketCORS(ctx, bkt)
|
||||
objIDNotFound := errorsStd.Is(err, ErrNodeNotFound)
|
||||
if err != nil && !objIDNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if objNotFound {
|
||||
if objIDNotFound {
|
||||
return nil, fmt.Errorf("%w: %s", errors.GetAPIError(errors.ErrNoSuchCORSConfiguration), err.Error())
|
||||
}
|
||||
|
||||
var prmAuth PrmAuth
|
||||
corsBkt := bkt
|
||||
if !addr.Container().Equals(bkt.CID) && !addr.Container().Equals(cid.ID{}) {
|
||||
corsBkt = &data.BucketInfo{CID: addr.Container()}
|
||||
prmAuth.PrivateKey = &n.gateKey.PrivateKey
|
||||
}
|
||||
|
||||
obj, err := n.objectGetWithAuth(ctx, corsBkt, addr.Object(), prmAuth)
|
||||
obj, err := n.objectGet(ctx, bkt, objID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get cors object: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cors := &data.CORSConfiguration{}
|
||||
if err = xml.NewDecoder(obj.Payload).Decode(&cors); err != nil {
|
||||
|
||||
if err = xml.Unmarshal(obj.Payload(), &cors); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal cors: %w", err)
|
||||
}
|
||||
|
||||
|
@ -197,7 +190,7 @@ func lockObjectKey(objVersion *data.ObjectVersion) string {
|
|||
return ".lock." + objVersion.BktInfo.CID.EncodeToString() + "." + objVersion.ObjectName + "." + objVersion.VersionID
|
||||
}
|
||||
|
||||
func (n *Layer) GetBucketSettings(ctx context.Context, bktInfo *data.BucketInfo) (*data.BucketSettings, error) {
|
||||
func (n *layer) GetBucketSettings(ctx context.Context, bktInfo *data.BucketInfo) (*data.BucketSettings, error) {
|
||||
owner := n.BearerOwner(ctx)
|
||||
if settings := n.cache.GetSettings(owner, bktInfo); settings != nil {
|
||||
return settings, nil
|
||||
|
@ -216,7 +209,7 @@ func (n *Layer) GetBucketSettings(ctx context.Context, bktInfo *data.BucketInfo)
|
|||
return settings, nil
|
||||
}
|
||||
|
||||
func (n *Layer) PutBucketSettings(ctx context.Context, p *PutSettingsParams) error {
|
||||
func (n *layer) PutBucketSettings(ctx context.Context, p *PutSettingsParams) error {
|
||||
if err := n.treeService.PutSettingsNode(ctx, p.BktInfo, p.Settings); err != nil {
|
||||
return fmt.Errorf("failed to get settings node: %w", err)
|
||||
}
|
||||
|
@ -226,7 +219,7 @@ func (n *Layer) PutBucketSettings(ctx context.Context, p *PutSettingsParams) err
|
|||
return nil
|
||||
}
|
||||
|
||||
func (n *Layer) attributesFromLock(ctx context.Context, lock *data.ObjectLock) ([][2]string, error) {
|
||||
func (n *layer) attributesFromLock(ctx context.Context, lock *data.ObjectLock) ([][2]string, error) {
|
||||
var (
|
||||
err error
|
||||
expEpoch uint64
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (n *Layer) GetObjectTagging(ctx context.Context, p *data.GetObjectTaggingParams) (string, map[string]string, error) {
|
||||
func (n *layer) GetObjectTagging(ctx context.Context, p *data.GetObjectTaggingParams) (string, map[string]string, error) {
|
||||
var err error
|
||||
owner := n.BearerOwner(ctx)
|
||||
|
||||
|
@ -50,12 +50,12 @@ func (n *Layer) GetObjectTagging(ctx context.Context, p *data.GetObjectTaggingPa
|
|||
return p.ObjectVersion.VersionID, tags, nil
|
||||
}
|
||||
|
||||
func (n *Layer) PutObjectTagging(ctx context.Context, p *data.PutObjectTaggingParams) (err error) {
|
||||
nodeVersion := p.NodeVersion
|
||||
func (n *layer) PutObjectTagging(ctx context.Context, p *data.PutObjectTaggingParams) (nodeVersion *data.NodeVersion, err error) {
|
||||
nodeVersion = p.NodeVersion
|
||||
if nodeVersion == nil {
|
||||
nodeVersion, err = n.getNodeVersionFromCacheOrFrostfs(ctx, p.ObjectVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
p.ObjectVersion.VersionID = nodeVersion.OID.EncodeToString()
|
||||
|
@ -63,38 +63,38 @@ func (n *Layer) PutObjectTagging(ctx context.Context, p *data.PutObjectTaggingPa
|
|||
err = n.treeService.PutObjectTagging(ctx, p.ObjectVersion.BktInfo, nodeVersion, p.TagSet)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNodeNotFound) {
|
||||
return fmt.Errorf("%w: %s", s3errors.GetAPIError(s3errors.ErrNoSuchKey), err.Error())
|
||||
return nil, fmt.Errorf("%w: %s", s3errors.GetAPIError(s3errors.ErrNoSuchKey), err.Error())
|
||||
}
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n.cache.PutTagging(n.BearerOwner(ctx), objectTaggingCacheKey(p.ObjectVersion), p.TagSet)
|
||||
|
||||
return nil
|
||||
return nodeVersion, nil
|
||||
}
|
||||
|
||||
func (n *Layer) DeleteObjectTagging(ctx context.Context, p *data.ObjectVersion) error {
|
||||
func (n *layer) DeleteObjectTagging(ctx context.Context, p *data.ObjectVersion) (*data.NodeVersion, error) {
|
||||
version, err := n.getNodeVersion(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = n.treeService.DeleteObjectTagging(ctx, p.BktInfo, version)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNodeNotFound) {
|
||||
return fmt.Errorf("%w: %s", s3errors.GetAPIError(s3errors.ErrNoSuchKey), err.Error())
|
||||
return nil, fmt.Errorf("%w: %s", s3errors.GetAPIError(s3errors.ErrNoSuchKey), err.Error())
|
||||
}
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.VersionID = version.OID.EncodeToString()
|
||||
|
||||
n.cache.DeleteTagging(objectTaggingCacheKey(p))
|
||||
|
||||
return nil
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func (n *Layer) GetBucketTagging(ctx context.Context, bktInfo *data.BucketInfo) (map[string]string, error) {
|
||||
func (n *layer) GetBucketTagging(ctx context.Context, bktInfo *data.BucketInfo) (map[string]string, error) {
|
||||
owner := n.BearerOwner(ctx)
|
||||
|
||||
if tags := n.cache.GetTagging(owner, bucketTaggingCacheKey(bktInfo.CID)); tags != nil {
|
||||
|
@ -111,7 +111,7 @@ func (n *Layer) GetBucketTagging(ctx context.Context, bktInfo *data.BucketInfo)
|
|||
return tags, nil
|
||||
}
|
||||
|
||||
func (n *Layer) PutBucketTagging(ctx context.Context, bktInfo *data.BucketInfo, tagSet map[string]string) error {
|
||||
func (n *layer) PutBucketTagging(ctx context.Context, bktInfo *data.BucketInfo, tagSet map[string]string) error {
|
||||
if err := n.treeService.PutBucketTagging(ctx, bktInfo, tagSet); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -121,7 +121,7 @@ func (n *Layer) PutBucketTagging(ctx context.Context, bktInfo *data.BucketInfo,
|
|||
return nil
|
||||
}
|
||||
|
||||
func (n *Layer) DeleteBucketTagging(ctx context.Context, bktInfo *data.BucketInfo) error {
|
||||
func (n *layer) DeleteBucketTagging(ctx context.Context, bktInfo *data.BucketInfo) error {
|
||||
n.cache.DeleteTagging(bucketTaggingCacheKey(bktInfo.CID))
|
||||
|
||||
return n.treeService.DeleteBucketTagging(ctx, bktInfo)
|
||||
|
@ -135,7 +135,7 @@ func bucketTaggingCacheKey(cnrID cid.ID) string {
|
|||
return ".tagset." + cnrID.EncodeToString()
|
||||
}
|
||||
|
||||
func (n *Layer) getNodeVersion(ctx context.Context, objVersion *data.ObjectVersion) (*data.NodeVersion, error) {
|
||||
func (n *layer) getNodeVersion(ctx context.Context, objVersion *data.ObjectVersion) (*data.NodeVersion, error) {
|
||||
var err error
|
||||
var version *data.NodeVersion
|
||||
|
||||
|
@ -173,7 +173,7 @@ func (n *Layer) getNodeVersion(ctx context.Context, objVersion *data.ObjectVersi
|
|||
return version, err
|
||||
}
|
||||
|
||||
func (n *Layer) getNodeVersionFromCache(owner user.ID, o *data.ObjectVersion) *data.NodeVersion {
|
||||
func (n *layer) getNodeVersionFromCache(owner user.ID, o *data.ObjectVersion) *data.NodeVersion {
|
||||
if len(o.VersionID) == 0 || o.VersionID == data.UnversionedObjectVersionID {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -110,39 +110,44 @@ func (t *TreeServiceMock) GetSettingsNode(_ context.Context, bktInfo *data.Bucke
|
|||
return settings, nil
|
||||
}
|
||||
|
||||
func (t *TreeServiceMock) GetBucketCORS(_ context.Context, bktInfo *data.BucketInfo) (oid.Address, error) {
|
||||
func (t *TreeServiceMock) GetNotificationConfigurationNode(context.Context, *data.BucketInfo) (oid.ID, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (t *TreeServiceMock) PutNotificationConfigurationNode(context.Context, *data.BucketInfo, oid.ID) (oid.ID, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (t *TreeServiceMock) GetBucketCORS(_ context.Context, bktInfo *data.BucketInfo) (oid.ID, error) {
|
||||
systemMap, ok := t.system[bktInfo.CID.EncodeToString()]
|
||||
if !ok {
|
||||
return oid.Address{}, nil
|
||||
return oid.ID{}, nil
|
||||
}
|
||||
|
||||
node, ok := systemMap["cors"]
|
||||
if !ok {
|
||||
return oid.Address{}, nil
|
||||
return oid.ID{}, nil
|
||||
}
|
||||
|
||||
var addr oid.Address
|
||||
addr.SetContainer(bktInfo.CID)
|
||||
addr.SetObject(node.OID)
|
||||
return addr, nil
|
||||
return node.OID, nil
|
||||
}
|
||||
|
||||
func (t *TreeServiceMock) PutBucketCORS(_ context.Context, bktInfo *data.BucketInfo, addr oid.Address) ([]oid.Address, error) {
|
||||
func (t *TreeServiceMock) PutBucketCORS(_ context.Context, bktInfo *data.BucketInfo, objID oid.ID) (oid.ID, error) {
|
||||
systemMap, ok := t.system[bktInfo.CID.EncodeToString()]
|
||||
if !ok {
|
||||
systemMap = make(map[string]*data.BaseNodeVersion)
|
||||
}
|
||||
|
||||
systemMap["cors"] = &data.BaseNodeVersion{
|
||||
OID: addr.Object(),
|
||||
OID: objID,
|
||||
}
|
||||
|
||||
t.system[bktInfo.CID.EncodeToString()] = systemMap
|
||||
|
||||
return nil, ErrNoNodeToRemove
|
||||
return oid.ID{}, ErrNoNodeToRemove
|
||||
}
|
||||
|
||||
func (t *TreeServiceMock) DeleteBucketCORS(context.Context, *data.BucketInfo) ([]oid.Address, error) {
|
||||
func (t *TreeServiceMock) DeleteBucketCORS(context.Context, *data.BucketInfo) (oid.ID, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
|
|
|
@ -18,20 +18,31 @@ type TreeService interface {
|
|||
// If tree node is not found returns ErrNodeNotFound error.
|
||||
GetSettingsNode(ctx context.Context, bktInfo *data.BucketInfo) (*data.BucketSettings, error)
|
||||
|
||||
// GetNotificationConfigurationNode gets an object id that corresponds to object with bucket CORS.
|
||||
//
|
||||
// If tree node is not found returns ErrNodeNotFound error.
|
||||
GetNotificationConfigurationNode(ctx context.Context, bktInfo *data.BucketInfo) (oid.ID, error)
|
||||
|
||||
// PutNotificationConfigurationNode puts a node to a system tree
|
||||
// and returns objectID of a previous notif config which must be deleted in FrostFS.
|
||||
//
|
||||
// If object id to remove is not found returns ErrNoNodeToRemove error.
|
||||
PutNotificationConfigurationNode(ctx context.Context, bktInfo *data.BucketInfo, objID oid.ID) (oid.ID, error)
|
||||
|
||||
// GetBucketCORS gets an object id that corresponds to object with bucket CORS.
|
||||
//
|
||||
// If object id is not found returns ErrNodeNotFound error.
|
||||
GetBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) (oid.Address, error)
|
||||
GetBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) (oid.ID, error)
|
||||
|
||||
// PutBucketCORS puts a node to a system tree and returns objectID of a previous cors config which must be deleted in FrostFS.
|
||||
//
|
||||
// If object ids to remove is not found returns ErrNoNodeToRemove error.
|
||||
PutBucketCORS(ctx context.Context, bktInfo *data.BucketInfo, addr oid.Address) ([]oid.Address, error)
|
||||
// If object id to remove is not found returns ErrNoNodeToRemove error.
|
||||
PutBucketCORS(ctx context.Context, bktInfo *data.BucketInfo, objID oid.ID) (oid.ID, error)
|
||||
|
||||
// DeleteBucketCORS removes a node from a system tree and returns objID which must be deleted in FrostFS.
|
||||
//
|
||||
// If object ids to remove is not found returns ErrNoNodeToRemove error.
|
||||
DeleteBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) ([]oid.Address, error)
|
||||
// If object id to remove is not found returns ErrNoNodeToRemove error.
|
||||
DeleteBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) (oid.ID, error)
|
||||
|
||||
GetObjectTagging(ctx context.Context, bktInfo *data.BucketInfo, objVersion *data.NodeVersion) (map[string]string, error)
|
||||
PutObjectTagging(ctx context.Context, bktInfo *data.BucketInfo, objVersion *data.NodeVersion, tagSet map[string]string) error
|
||||
|
|
|
@ -130,7 +130,7 @@ func (tc *testContext) getObjectByID(objID oid.ID) *object.Object {
|
|||
type testContext struct {
|
||||
t *testing.T
|
||||
ctx context.Context
|
||||
layer *Layer
|
||||
layer Client
|
||||
bktInfo *data.BucketInfo
|
||||
obj string
|
||||
testFrostFS *TestFrostFS
|
||||
|
|
|
@ -74,6 +74,7 @@ const (
|
|||
AbortMultipartUploadOperation = "AbortMultipartUpload"
|
||||
DeleteObjectTaggingOperation = "DeleteObjectTagging"
|
||||
DeleteObjectOperation = "DeleteObject"
|
||||
PatchObjectOperation = "PatchObject"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -32,9 +32,7 @@ const (
|
|||
amzTagging = "x-amz-tagging"
|
||||
)
|
||||
|
||||
// In these operations we don't check resource tags because
|
||||
// * they haven't been created yet
|
||||
// * resource tags shouldn't be checked by AWS spec.
|
||||
// At the beginning of these operations resources haven't yet been created.
|
||||
var withoutResourceOps = []string{
|
||||
CreateBucketOperation,
|
||||
CreateMultipartUploadOperation,
|
||||
|
@ -45,12 +43,11 @@ var withoutResourceOps = []string{
|
|||
ListPartsOperation,
|
||||
PutObjectOperation,
|
||||
CopyObjectOperation,
|
||||
DeleteObjectOperation,
|
||||
DeleteMultipleObjectsOperation,
|
||||
}
|
||||
|
||||
type PolicySettings interface {
|
||||
PolicyDenyByDefault() bool
|
||||
ACLEnabled() bool
|
||||
}
|
||||
|
||||
type FrostFSIDInformer interface {
|
||||
|
@ -149,7 +146,12 @@ func policyCheck(r *http.Request, cfg PolicyConfig) error {
|
|||
return apiErr.GetAPIErrorWithError(apiErr.ErrAccessDenied, fmt.Errorf("policy check: %s", st.String()))
|
||||
}
|
||||
|
||||
if cfg.Settings.PolicyDenyByDefault() {
|
||||
isAPE := !cfg.Settings.ACLEnabled()
|
||||
if bktInfo != nil {
|
||||
isAPE = bktInfo.APEEnabled
|
||||
}
|
||||
|
||||
if isAPE && cfg.Settings.PolicyDenyByDefault() {
|
||||
return apiErr.GetAPIErrorWithError(apiErr.ErrAccessDenied, fmt.Errorf("policy check: %s", st.String()))
|
||||
}
|
||||
|
||||
|
@ -188,16 +190,15 @@ func getPolicyRequest(r *http.Request, cfg PolicyConfig, reqType ReqType, bktNam
|
|||
res = fmt.Sprintf(s3.ResourceFormatS3Bucket, bktName)
|
||||
}
|
||||
|
||||
requestProps, resourceProps, err := determineProperties(r, cfg.Decoder, cfg.BucketResolver, cfg.Tagging, reqType, op, bktName, objName, owner, groups, tags)
|
||||
properties, err := determineProperties(r, cfg.Decoder, cfg.BucketResolver, cfg.Tagging, reqType, op, bktName, objName, owner, groups, tags)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("determine properties: %w", err)
|
||||
}
|
||||
|
||||
reqLogOrDefault(r.Context(), cfg.Log).Debug(logs.PolicyRequest, zap.String("action", op),
|
||||
zap.String("resource", res), zap.Any("request properties", requestProps),
|
||||
zap.Any("resource properties", resourceProps))
|
||||
zap.String("resource", res), zap.Any("properties", properties))
|
||||
|
||||
return testutil.NewRequest(op, testutil.NewResource(res, resourceProps), requestProps), pk, groups, nil
|
||||
return testutil.NewRequest(op, testutil.NewResource(res, nil), properties), pk, groups, nil
|
||||
}
|
||||
|
||||
type ReqType int
|
||||
|
@ -357,6 +358,8 @@ func determineObjectOperation(r *http.Request) string {
|
|||
switch r.Method {
|
||||
case http.MethodOptions:
|
||||
return OptionsObjectOperation
|
||||
case http.MethodPatch:
|
||||
return PatchObjectOperation
|
||||
case http.MethodHead:
|
||||
return HeadObjectOperation
|
||||
case http.MethodGet:
|
||||
|
@ -426,59 +429,72 @@ func determineGeneralOperation(r *http.Request) string {
|
|||
}
|
||||
|
||||
func determineProperties(r *http.Request, decoder XMLDecoder, resolver BucketResolveFunc, tagging ResourceTagging, reqType ReqType,
|
||||
op, bktName, objName, owner string, groups []string, userClaims map[string]string) (requestProperties map[string]string, resourceProperties map[string]string, err error) {
|
||||
requestProperties = map[string]string{
|
||||
op, bktName, objName, owner string, groups []string, tags map[string]string) (map[string]string, error) {
|
||||
res := map[string]string{
|
||||
s3.PropertyKeyOwner: owner,
|
||||
common.PropertyKeyFrostFSIDGroupID: chain.FormCondSliceContainsValue(groups),
|
||||
common.PropertyKeyFrostFSSourceIP: GetReqInfo(r.Context()).RemoteHost,
|
||||
}
|
||||
queries := GetReqInfo(r.Context()).URL.Query()
|
||||
|
||||
for k, v := range userClaims {
|
||||
requestProperties[fmt.Sprintf(common.PropertyKeyFormatFrostFSIDUserClaim, k)] = v
|
||||
for k, v := range tags {
|
||||
res[fmt.Sprintf(common.PropertyKeyFormatFrostFSIDUserClaim, k)] = v
|
||||
}
|
||||
|
||||
if reqType == objectType {
|
||||
if versionID := queries.Get(QueryVersionID); len(versionID) > 0 {
|
||||
requestProperties[s3.PropertyKeyVersionID] = versionID
|
||||
res[s3.PropertyKeyVersionID] = versionID
|
||||
}
|
||||
}
|
||||
|
||||
if reqType == bucketType && (strings.HasSuffix(op, ListObjectsV1Operation) || strings.HasSuffix(op, ListObjectsV2Operation) ||
|
||||
strings.HasSuffix(op, ListBucketObjectVersionsOperation) || strings.HasSuffix(op, ListMultipartUploadsOperation)) {
|
||||
if prefix := queries.Get(QueryPrefix); len(prefix) > 0 {
|
||||
requestProperties[s3.PropertyKeyPrefix] = prefix
|
||||
res[s3.PropertyKeyPrefix] = prefix
|
||||
}
|
||||
if delimiter := queries.Get(QueryDelimiter); len(delimiter) > 0 {
|
||||
requestProperties[s3.PropertyKeyDelimiter] = delimiter
|
||||
res[s3.PropertyKeyDelimiter] = delimiter
|
||||
}
|
||||
if maxKeys := queries.Get(QueryMaxKeys); len(maxKeys) > 0 {
|
||||
requestProperties[s3.PropertyKeyMaxKeys] = maxKeys
|
||||
res[s3.PropertyKeyMaxKeys] = maxKeys
|
||||
}
|
||||
}
|
||||
|
||||
requestProperties[s3.PropertyKeyAccessBoxAttrMFA] = "false"
|
||||
tags, err := determineTags(r, decoder, resolver, tagging, reqType, op, bktName, objName, queries.Get(QueryVersionID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("determine tags: %w", err)
|
||||
}
|
||||
for k, v := range tags {
|
||||
res[k] = v
|
||||
}
|
||||
|
||||
res[s3.PropertyKeyAccessBoxAttrMFA] = "false"
|
||||
attrs, err := GetAccessBoxAttrs(r.Context())
|
||||
if err == nil {
|
||||
for _, attr := range attrs {
|
||||
requestProperties[fmt.Sprintf(s3.PropertyKeyFormatAccessBoxAttr, attr.Key())] = attr.Value()
|
||||
res[fmt.Sprintf(s3.PropertyKeyFormatAccessBoxAttr, attr.Key())] = attr.Value()
|
||||
}
|
||||
}
|
||||
|
||||
reqTags, err := determineRequestTags(r, decoder, op)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func determineTags(r *http.Request, decoder XMLDecoder, resolver BucketResolveFunc, tagging ResourceTagging, reqType ReqType,
|
||||
op, bktName, objName, versionID string) (map[string]string, error) {
|
||||
res, err := determineRequestTags(r, decoder, op)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("determine request tags: %w", err)
|
||||
}
|
||||
for k, v := range reqTags {
|
||||
requestProperties[k] = v
|
||||
return nil, fmt.Errorf("determine request tags: %w", err)
|
||||
}
|
||||
|
||||
resourceProperties, err = determineResourceTags(r.Context(), reqType, op, bktName, objName, queries.Get(QueryVersionID), resolver, tagging)
|
||||
tags, err := determineResourceTags(r.Context(), reqType, op, bktName, objName, versionID, resolver, tagging)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("determine resource tags: %w", err)
|
||||
return nil, fmt.Errorf("determine resource tags: %w", err)
|
||||
}
|
||||
for k, v := range tags {
|
||||
res[k] = v
|
||||
}
|
||||
|
||||
return requestProperties, resourceProperties, nil
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func determineRequestTags(r *http.Request, decoder XMLDecoder, op string) (map[string]string, error) {
|
||||
|
|
|
@ -69,10 +69,8 @@ var deploymentID = uuid.Must(uuid.NewRandom())
|
|||
|
||||
var (
|
||||
// De-facto standard header keys.
|
||||
xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For")
|
||||
xRealIP = http.CanonicalHeaderKey("X-Real-IP")
|
||||
xForwardedProto = http.CanonicalHeaderKey("X-Forwarded-Proto")
|
||||
xForwardedScheme = http.CanonicalHeaderKey("X-Forwarded-Scheme")
|
||||
xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For")
|
||||
xRealIP = http.CanonicalHeaderKey("X-Real-IP")
|
||||
|
||||
// RFC7239 defines a new "Forwarded: " header designed to replace the
|
||||
// existing use of X-Forwarded-* headers.
|
||||
|
@ -81,9 +79,6 @@ var (
|
|||
// Allows for a sub-match of the first value after 'for=' to the next
|
||||
// comma, semi-colon or space. The match is case-insensitive.
|
||||
forRegex = regexp.MustCompile(`(?i)(?:for=)([^(;|, )]+)(.*)`)
|
||||
// Allows for a sub-match for the first instance of scheme (http|https)
|
||||
// prefixed by 'proto='. The match is case-insensitive.
|
||||
protoRegex = regexp.MustCompile(`(?i)^(;|,| )+(?:proto=)(https|http)`)
|
||||
)
|
||||
|
||||
// NewReqInfo returns new ReqInfo based on parameters.
|
||||
|
@ -296,31 +291,3 @@ func getSourceIP(r *http.Request) string {
|
|||
}
|
||||
return raddr
|
||||
}
|
||||
|
||||
// GetSourceScheme retrieves the scheme from the X-Forwarded-Proto and RFC7239
|
||||
// Forwarded headers (in that order).
|
||||
func GetSourceScheme(r *http.Request) string {
|
||||
var scheme string
|
||||
|
||||
// Retrieve the scheme from X-Forwarded-Proto.
|
||||
if proto := r.Header.Get(xForwardedProto); proto != "" {
|
||||
scheme = strings.ToLower(proto)
|
||||
} else if proto = r.Header.Get(xForwardedScheme); proto != "" {
|
||||
scheme = strings.ToLower(proto)
|
||||
} else if proto := r.Header.Get(forwarded); proto != "" {
|
||||
// match should contain at least two elements if the protocol was
|
||||
// specified in the Forwarded header. The first element will always be
|
||||
// the 'for=', which we ignore, subsequently we proceed to look for
|
||||
// 'proto=' which should precede right after `for=` if not
|
||||
// we simply ignore the values and return empty. This is in line
|
||||
// with the approach we took for returning first ip from multiple
|
||||
// params.
|
||||
if match := forRegex.FindStringSubmatch(proto); len(match) > 1 {
|
||||
if match = protoRegex.FindStringSubmatch(match[2]); len(match) > 1 {
|
||||
scheme = strings.ToLower(match[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scheme
|
||||
}
|
||||
|
|
263
api/notifications/controller.go
Normal file
263
api/notifications/controller.go
Normal file
|
@ -0,0 +1,263 @@
|
|||
package notifications
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/handler"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
"github.com/nats-io/nats.go"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultTimeout = 30 * time.Second
|
||||
|
||||
// EventVersion23 is used for lifecycle, tiering, objectACL, objectTagging, object restoration notifications.
|
||||
EventVersion23 = "2.3"
|
||||
// EventVersion22 is used for replication notifications.
|
||||
EventVersion22 = "2.2"
|
||||
// EventVersion21 is used for all other notification types.
|
||||
EventVersion21 = "2.1"
|
||||
)
|
||||
|
||||
type (
|
||||
Options struct {
|
||||
URL string
|
||||
TLSCertFilepath string
|
||||
TLSAuthPrivateKeyFilePath string
|
||||
Timeout time.Duration
|
||||
RootCAFiles []string
|
||||
}
|
||||
|
||||
Controller struct {
|
||||
logger *zap.Logger
|
||||
taskQueueConnection *nats.Conn
|
||||
jsClient nats.JetStreamContext
|
||||
handlers map[string]Stream
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
Stream struct {
|
||||
h layer.MsgHandler
|
||||
ch chan *nats.Msg
|
||||
}
|
||||
|
||||
TestEvent struct {
|
||||
Service string
|
||||
Event string
|
||||
Time time.Time
|
||||
Bucket string
|
||||
RequestID string
|
||||
HostID string
|
||||
}
|
||||
|
||||
Event struct {
|
||||
Records []EventRecord `json:"Records"`
|
||||
}
|
||||
|
||||
EventRecord struct {
|
||||
EventVersion string `json:"eventVersion"`
|
||||
EventSource string `json:"eventSource"` // frostfs:s3
|
||||
AWSRegion string `json:"awsRegion,omitempty"` // empty
|
||||
EventTime time.Time `json:"eventTime"`
|
||||
EventName string `json:"eventName"`
|
||||
UserIdentity UserIdentity `json:"userIdentity"`
|
||||
RequestParameters RequestParameters `json:"requestParameters"`
|
||||
ResponseElements map[string]string `json:"responseElements"`
|
||||
S3 S3Entity `json:"s3"`
|
||||
}
|
||||
|
||||
UserIdentity struct {
|
||||
PrincipalID string `json:"principalId"`
|
||||
}
|
||||
|
||||
RequestParameters struct {
|
||||
SourceIPAddress string `json:"sourceIPAddress"`
|
||||
}
|
||||
|
||||
S3Entity struct {
|
||||
SchemaVersion string `json:"s3SchemaVersion"`
|
||||
ConfigurationID string `json:"configurationId,omitempty"`
|
||||
Bucket Bucket `json:"bucket"`
|
||||
Object Object `json:"object"`
|
||||
}
|
||||
|
||||
Bucket struct {
|
||||
Name string `json:"name"`
|
||||
OwnerIdentity UserIdentity `json:"ownerIdentity,omitempty"`
|
||||
Arn string `json:"arn,omitempty"`
|
||||
}
|
||||
|
||||
Object struct {
|
||||
Key string `json:"key"`
|
||||
Size uint64 `json:"size,omitempty"`
|
||||
VersionID string `json:"versionId,omitempty"`
|
||||
ETag string `json:"eTag,omitempty"`
|
||||
Sequencer string `json:"sequencer,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewController(p *Options, l *zap.Logger) (*Controller, error) {
|
||||
ncopts := []nats.Option{
|
||||
nats.Timeout(p.Timeout),
|
||||
}
|
||||
|
||||
if len(p.TLSCertFilepath) != 0 && len(p.TLSAuthPrivateKeyFilePath) != 0 {
|
||||
ncopts = append(ncopts, nats.ClientCert(p.TLSCertFilepath, p.TLSAuthPrivateKeyFilePath))
|
||||
}
|
||||
if len(p.RootCAFiles) != 0 {
|
||||
ncopts = append(ncopts, nats.RootCAs(p.RootCAFiles...))
|
||||
}
|
||||
|
||||
nc, err := nats.Connect(p.URL, ncopts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect to nats: %w", err)
|
||||
}
|
||||
|
||||
js, err := nc.JetStream()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get jet stream: %w", err)
|
||||
}
|
||||
|
||||
return &Controller{
|
||||
logger: l,
|
||||
taskQueueConnection: nc,
|
||||
jsClient: js,
|
||||
handlers: make(map[string]Stream),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Controller) Subscribe(_ context.Context, topic string, handler layer.MsgHandler) error {
|
||||
ch := make(chan *nats.Msg, 1)
|
||||
|
||||
c.mu.RLock()
|
||||
_, ok := c.handlers[topic]
|
||||
c.mu.RUnlock()
|
||||
if ok {
|
||||
return fmt.Errorf("already subscribed to topic '%s'", topic)
|
||||
}
|
||||
|
||||
if _, err := c.jsClient.AddStream(&nats.StreamConfig{Name: topic}); err != nil {
|
||||
return fmt.Errorf("add stream: %w", err)
|
||||
}
|
||||
|
||||
if _, err := c.jsClient.ChanSubscribe(topic, ch); err != nil {
|
||||
return fmt.Errorf("could not subscribe: %w", err)
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.handlers[topic] = Stream{
|
||||
h: handler,
|
||||
ch: ch,
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) Listen(ctx context.Context) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
for _, stream := range c.handlers {
|
||||
go func(stream Stream) {
|
||||
for {
|
||||
select {
|
||||
case msg := <-stream.ch:
|
||||
if err := stream.h.HandleMessage(ctx, msg); err != nil {
|
||||
c.logger.Error(logs.CouldNotHandleMessage, zap.Error(err))
|
||||
} else if err = msg.Ack(); err != nil {
|
||||
c.logger.Error(logs.CouldNotACKMessage, zap.Error(err))
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}(stream)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) SendNotifications(topics map[string]string, p *handler.SendNotificationParams) error {
|
||||
event := prepareEvent(p)
|
||||
|
||||
for id, topic := range topics {
|
||||
event.Records[0].S3.ConfigurationID = id
|
||||
msg, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
c.logger.Error(logs.CouldntMarshalAnEvent, zap.String("subject", topic), zap.Error(err))
|
||||
}
|
||||
if err = c.publish(topic, msg); err != nil {
|
||||
c.logger.Error(logs.CouldntSendAnEventToTopic, zap.String("subject", topic), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) SendTestNotification(topic, bucketName, requestID, HostID string, now time.Time) error {
|
||||
event := &TestEvent{
|
||||
Service: "FrostFS S3",
|
||||
Event: "s3:TestEvent",
|
||||
Time: now,
|
||||
Bucket: bucketName,
|
||||
RequestID: requestID,
|
||||
HostID: HostID,
|
||||
}
|
||||
|
||||
msg, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't marshal test event: %w", err)
|
||||
}
|
||||
|
||||
return c.publish(topic, msg)
|
||||
}
|
||||
|
||||
func prepareEvent(p *handler.SendNotificationParams) *Event {
|
||||
return &Event{
|
||||
Records: []EventRecord{
|
||||
{
|
||||
EventVersion: EventVersion21,
|
||||
EventSource: "frostfs:s3",
|
||||
AWSRegion: "",
|
||||
EventTime: p.Time,
|
||||
EventName: p.Event,
|
||||
UserIdentity: UserIdentity{
|
||||
PrincipalID: p.User,
|
||||
},
|
||||
RequestParameters: RequestParameters{
|
||||
SourceIPAddress: p.ReqInfo.RemoteHost,
|
||||
},
|
||||
ResponseElements: nil,
|
||||
S3: S3Entity{
|
||||
SchemaVersion: "1.0",
|
||||
// ConfigurationID is skipped and will be placed later
|
||||
Bucket: Bucket{
|
||||
Name: p.BktInfo.Name,
|
||||
OwnerIdentity: UserIdentity{PrincipalID: p.BktInfo.Owner.String()},
|
||||
Arn: p.BktInfo.Name,
|
||||
},
|
||||
Object: Object{
|
||||
Key: p.NotificationInfo.Name,
|
||||
Size: p.NotificationInfo.Size,
|
||||
VersionID: p.NotificationInfo.Version,
|
||||
ETag: p.NotificationInfo.HashSum,
|
||||
Sequencer: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) publish(topic string, msg []byte) error {
|
||||
if _, err := c.jsClient.Publish(topic, msg); err != nil {
|
||||
return fmt.Errorf("couldn't send event: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -87,6 +87,7 @@ type (
|
|||
AbortMultipartUploadHandler(http.ResponseWriter, *http.Request)
|
||||
ListPartsHandler(w http.ResponseWriter, r *http.Request)
|
||||
ListMultipartUploadsHandler(http.ResponseWriter, *http.Request)
|
||||
PatchHandler(http.ResponseWriter, *http.Request)
|
||||
|
||||
ResolveBucket(ctx context.Context, bucket string) (*data.BucketInfo, error)
|
||||
ResolveCID(ctx context.Context, bucket string) (cid.ID, error)
|
||||
|
@ -376,6 +377,8 @@ func objectRouter(h Handler, l *zap.Logger) chi.Router {
|
|||
|
||||
objRouter.Head("/*", named(s3middleware.HeadObjectOperation, h.HeadObjectHandler))
|
||||
|
||||
objRouter.Patch("/*", named(s3middleware.PatchObjectOperation, h.PatchHandler))
|
||||
|
||||
// GET method handlers
|
||||
objRouter.Group(func(r chi.Router) {
|
||||
r.Method(http.MethodGet, "/*", NewHandlerFilter().
|
||||
|
|
|
@ -72,6 +72,7 @@ func (c *centerMock) Authenticate(*http.Request) (*middleware.Box, error) {
|
|||
|
||||
type middlewareSettingsMock struct {
|
||||
denyByDefault bool
|
||||
aclEnabled bool
|
||||
sourceIPHeader string
|
||||
}
|
||||
|
||||
|
@ -91,6 +92,10 @@ func (r *middlewareSettingsMock) PolicyDenyByDefault() bool {
|
|||
return r.denyByDefault
|
||||
}
|
||||
|
||||
func (r *middlewareSettingsMock) ACLEnabled() bool {
|
||||
return r.aclEnabled
|
||||
}
|
||||
|
||||
type frostFSIDMock struct {
|
||||
tags map[string]string
|
||||
validateError bool
|
||||
|
@ -424,7 +429,8 @@ func (h *handlerMock) CreateBucketHandler(w http.ResponseWriter, r *http.Request
|
|||
reqInfo := middleware.GetReqInfo(r.Context())
|
||||
|
||||
h.buckets[reqInfo.Namespace+reqInfo.BucketName] = &data.BucketInfo{
|
||||
Name: reqInfo.BucketName,
|
||||
Name: reqInfo.BucketName,
|
||||
APEEnabled: !h.cfg.ACLEnabled(),
|
||||
}
|
||||
|
||||
res := &handlerResult{
|
||||
|
@ -534,6 +540,10 @@ func (h *handlerMock) ListMultipartUploadsHandler(w http.ResponseWriter, r *http
|
|||
h.writeResponse(w, res)
|
||||
}
|
||||
|
||||
func (h *handlerMock) PatchHandler(http.ResponseWriter, *http.Request) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (h *handlerMock) ResolveBucket(ctx context.Context, name string) (*data.BucketInfo, error) {
|
||||
reqInfo := middleware.GetReqInfo(ctx)
|
||||
bktInfo, ok := h.buckets[reqInfo.Namespace+name]
|
||||
|
|
|
@ -37,7 +37,6 @@ type routerMock struct {
|
|||
cfg Config
|
||||
middlewareSettings *middlewareSettingsMock
|
||||
policyChecker engine.LocalOverrideEngine
|
||||
handler *handlerMock
|
||||
}
|
||||
|
||||
func (m *routerMock) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -65,14 +64,12 @@ func prepareRouter(t *testing.T, opts ...option) *routerMock {
|
|||
Enabled: true,
|
||||
}
|
||||
|
||||
handlerTestMock := &handlerMock{t: t, cfg: middlewareSettings, buckets: map[string]*data.BucketInfo{}}
|
||||
|
||||
cfg := Config{
|
||||
Throttle: middleware.ThrottleOpts{
|
||||
Limit: 10,
|
||||
BacklogTimeout: 30 * time.Second,
|
||||
},
|
||||
Handler: handlerTestMock,
|
||||
Handler: &handlerMock{t: t, cfg: middlewareSettings, buckets: map[string]*data.BucketInfo{}},
|
||||
Center: ¢erMock{t: t},
|
||||
Log: logger,
|
||||
Metrics: metrics.NewAppMetrics(metricsConfig),
|
||||
|
@ -94,7 +91,6 @@ func prepareRouter(t *testing.T, opts ...option) *routerMock {
|
|||
cfg: cfg,
|
||||
middlewareSettings: middlewareSettings,
|
||||
policyChecker: policyChecker,
|
||||
handler: handlerTestMock,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -303,6 +299,102 @@ func TestDefaultPolicyCheckerWithUserTags(t *testing.T) {
|
|||
createBucket(router, ns, bktName)
|
||||
}
|
||||
|
||||
func TestACLAPE(t *testing.T) {
|
||||
t.Run("acl disabled, ape deny by default", func(t *testing.T) {
|
||||
router := prepareRouter(t)
|
||||
|
||||
ns, bktName, objName := "", "bucket", "object"
|
||||
bktNameOld, bktNameNew := "old-bucket", "new-bucket"
|
||||
createOldBucket(router, bktNameOld)
|
||||
createNewBucket(router, bktNameNew)
|
||||
|
||||
router.middlewareSettings.aclEnabled = false
|
||||
router.middlewareSettings.denyByDefault = true
|
||||
|
||||
// Allow because of using old bucket
|
||||
putObject(router, ns, bktNameOld, objName, nil)
|
||||
// Deny because of deny by default
|
||||
putObjectErr(router, ns, bktNameNew, objName, nil, apiErrors.ErrAccessDenied)
|
||||
|
||||
// Deny because of deny by default
|
||||
createBucketErr(router, ns, bktName, nil, apiErrors.ErrAccessDenied)
|
||||
listBucketsErr(router, ns, apiErrors.ErrAccessDenied)
|
||||
|
||||
// Allow operations and check
|
||||
allowOperations(router, ns, []string{"s3:CreateBucket", "s3:ListAllMyBuckets"}, nil)
|
||||
createBucket(router, ns, bktName)
|
||||
listBuckets(router, ns)
|
||||
})
|
||||
|
||||
t.Run("acl disabled, ape allow by default", func(t *testing.T) {
|
||||
router := prepareRouter(t)
|
||||
|
||||
ns, bktName, objName := "", "bucket", "object"
|
||||
bktNameOld, bktNameNew := "old-bucket", "new-bucket"
|
||||
createOldBucket(router, bktNameOld)
|
||||
createNewBucket(router, bktNameNew)
|
||||
|
||||
router.middlewareSettings.aclEnabled = false
|
||||
router.middlewareSettings.denyByDefault = false
|
||||
|
||||
// Allow because of using old bucket
|
||||
putObject(router, ns, bktNameOld, objName, nil)
|
||||
// Allow because of allow by default
|
||||
putObject(router, ns, bktNameNew, objName, nil)
|
||||
|
||||
// Allow because of deny by default
|
||||
createBucket(router, ns, bktName)
|
||||
listBuckets(router, ns)
|
||||
|
||||
// Deny operations and check
|
||||
denyOperations(router, ns, []string{"s3:CreateBucket", "s3:ListAllMyBuckets"}, nil)
|
||||
createBucketErr(router, ns, bktName, nil, apiErrors.ErrAccessDenied)
|
||||
listBucketsErr(router, ns, apiErrors.ErrAccessDenied)
|
||||
})
|
||||
|
||||
t.Run("acl enabled, ape deny by default", func(t *testing.T) {
|
||||
router := prepareRouter(t)
|
||||
|
||||
ns, bktName, objName := "", "bucket", "object"
|
||||
bktNameOld, bktNameNew := "old-bucket", "new-bucket"
|
||||
createOldBucket(router, bktNameOld)
|
||||
createNewBucket(router, bktNameNew)
|
||||
|
||||
router.middlewareSettings.aclEnabled = true
|
||||
router.middlewareSettings.denyByDefault = true
|
||||
|
||||
// Allow because of using old bucket
|
||||
putObject(router, ns, bktNameOld, objName, nil)
|
||||
// Deny because of deny by default
|
||||
putObjectErr(router, ns, bktNameNew, objName, nil, apiErrors.ErrAccessDenied)
|
||||
|
||||
// Allow because of old behavior
|
||||
createBucket(router, ns, bktName)
|
||||
listBuckets(router, ns)
|
||||
})
|
||||
|
||||
t.Run("acl enabled, ape allow by default", func(t *testing.T) {
|
||||
router := prepareRouter(t)
|
||||
|
||||
ns, bktName, objName := "", "bucket", "object"
|
||||
bktNameOld, bktNameNew := "old-bucket", "new-bucket"
|
||||
createOldBucket(router, bktNameOld)
|
||||
createNewBucket(router, bktNameNew)
|
||||
|
||||
router.middlewareSettings.aclEnabled = true
|
||||
router.middlewareSettings.denyByDefault = false
|
||||
|
||||
// Allow because of using old bucket
|
||||
putObject(router, ns, bktNameOld, objName, nil)
|
||||
// Allow because of allow by default
|
||||
putObject(router, ns, bktNameNew, objName, nil)
|
||||
|
||||
// Allow because of old behavior
|
||||
createBucket(router, ns, bktName)
|
||||
listBuckets(router, ns)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRequestParametersCheck(t *testing.T) {
|
||||
t.Run("prefix parameter, allow specific value", func(t *testing.T) {
|
||||
router := prepareRouter(t)
|
||||
|
@ -625,6 +717,21 @@ func addPolicy(router *routerMock, ns string, id string, effect engineiam.Effect
|
|||
require.NoError(router.t, err)
|
||||
}
|
||||
|
||||
func createOldBucket(router *routerMock, bktName string) {
|
||||
createSpecificBucket(router, bktName, true)
|
||||
}
|
||||
|
||||
func createNewBucket(router *routerMock, bktName string) {
|
||||
createSpecificBucket(router, bktName, false)
|
||||
}
|
||||
|
||||
func createSpecificBucket(router *routerMock, bktName string, old bool) {
|
||||
aclEnabled := router.middlewareSettings.ACLEnabled()
|
||||
router.middlewareSettings.aclEnabled = old
|
||||
createBucket(router, "", bktName)
|
||||
router.middlewareSettings.aclEnabled = aclEnabled
|
||||
}
|
||||
|
||||
func createBucket(router *routerMock, namespace, bktName string) {
|
||||
w := createBucketBase(router, namespace, bktName, nil)
|
||||
resp := readResponse(router.t, w)
|
||||
|
@ -647,6 +754,24 @@ func createBucketBase(router *routerMock, namespace, bktName string, header http
|
|||
return w
|
||||
}
|
||||
|
||||
func listBuckets(router *routerMock, namespace string) {
|
||||
w := listBucketsBase(router, namespace)
|
||||
resp := readResponse(router.t, w)
|
||||
require.Equal(router.t, s3middleware.ListBucketsOperation, resp.Method)
|
||||
}
|
||||
|
||||
func listBucketsErr(router *routerMock, namespace string, errCode apiErrors.ErrorCode) {
|
||||
w := listBucketsBase(router, namespace)
|
||||
assertAPIError(router.t, w, errCode)
|
||||
}
|
||||
|
||||
func listBucketsBase(router *routerMock, namespace string) *httptest.ResponseRecorder {
|
||||
w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
r.Header.Set(FrostfsNamespaceHeader, namespace)
|
||||
router.ServeHTTP(w, r)
|
||||
return w
|
||||
}
|
||||
|
||||
func getBucketErr(router *routerMock, namespace, bktName string, errCode apiErrors.ErrorCode) {
|
||||
w := getBucketBase(router, namespace, bktName)
|
||||
assertAPIError(router.t, w, errCode)
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
|
@ -101,6 +102,7 @@ type (
|
|||
Container ContainerOptions
|
||||
FrostFSKey *keys.PrivateKey
|
||||
GatesPublicKeys []*keys.PublicKey
|
||||
EACLRules []byte
|
||||
Impersonate bool
|
||||
SessionTokenRules []byte
|
||||
SkipSessionRules bool
|
||||
|
@ -439,11 +441,47 @@ func (a *Agent) ObtainSecret(ctx context.Context, w io.Writer, options *ObtainSe
|
|||
return enc.Encode(or)
|
||||
}
|
||||
|
||||
func buildBearerToken(key *keys.PrivateKey, impersonate bool, lifetime lifetimeOptions, gateKey *keys.PublicKey) (*bearer.Token, error) {
|
||||
func buildEACLTable(eaclTable []byte) (*eacl.Table, error) {
|
||||
table := eacl.NewTable()
|
||||
if len(eaclTable) != 0 {
|
||||
return table, table.UnmarshalJSON(eaclTable)
|
||||
}
|
||||
|
||||
record := eacl.NewRecord()
|
||||
record.SetOperation(eacl.OperationGet)
|
||||
record.SetAction(eacl.ActionAllow)
|
||||
eacl.AddFormedTarget(record, eacl.RoleOthers)
|
||||
table.AddRecord(record)
|
||||
|
||||
for _, rec := range restrictedRecords() {
|
||||
table.AddRecord(rec)
|
||||
}
|
||||
|
||||
return table, nil
|
||||
}
|
||||
|
||||
func restrictedRecords() (records []*eacl.Record) {
|
||||
for op := eacl.OperationGet; op <= eacl.OperationRangeHash; op++ {
|
||||
record := eacl.NewRecord()
|
||||
record.SetOperation(op)
|
||||
record.SetAction(eacl.ActionDeny)
|
||||
eacl.AddFormedTarget(record, eacl.RoleOthers)
|
||||
records = append(records, record)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func buildBearerToken(key *keys.PrivateKey, impersonate bool, table *eacl.Table, lifetime lifetimeOptions, gateKey *keys.PublicKey) (*bearer.Token, error) {
|
||||
var ownerID user.ID
|
||||
user.IDFromKey(&ownerID, (ecdsa.PublicKey)(*gateKey))
|
||||
|
||||
var bearerToken bearer.Token
|
||||
|
||||
if !impersonate {
|
||||
bearerToken.SetEACLTable(*table)
|
||||
}
|
||||
|
||||
bearerToken.ForUser(ownerID)
|
||||
bearerToken.SetExp(lifetime.Exp)
|
||||
bearerToken.SetIat(lifetime.Iat)
|
||||
|
@ -458,10 +496,10 @@ func buildBearerToken(key *keys.PrivateKey, impersonate bool, lifetime lifetimeO
|
|||
return &bearerToken, nil
|
||||
}
|
||||
|
||||
func buildBearerTokens(key *keys.PrivateKey, impersonate bool, lifetime lifetimeOptions, gatesKeys []*keys.PublicKey) ([]*bearer.Token, error) {
|
||||
func buildBearerTokens(key *keys.PrivateKey, impersonate bool, table *eacl.Table, lifetime lifetimeOptions, gatesKeys []*keys.PublicKey) ([]*bearer.Token, error) {
|
||||
bearerTokens := make([]*bearer.Token, 0, len(gatesKeys))
|
||||
for _, gateKey := range gatesKeys {
|
||||
tkn, err := buildBearerToken(key, impersonate, lifetime, gateKey)
|
||||
tkn, err := buildBearerToken(key, impersonate, table, lifetime, gateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build bearer token: %w", err)
|
||||
}
|
||||
|
@ -506,7 +544,12 @@ func buildSessionTokens(key *keys.PrivateKey, lifetime lifetimeOptions, ctxs []s
|
|||
func createTokens(options *IssueSecretOptions, lifetime lifetimeOptions) ([]*accessbox.GateData, error) {
|
||||
gates := make([]*accessbox.GateData, len(options.GatesPublicKeys))
|
||||
|
||||
bearerTokens, err := buildBearerTokens(options.FrostFSKey, options.Impersonate, lifetime, options.GatesPublicKeys)
|
||||
table, err := buildEACLTable(options.EACLRules)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build eacl table: %w", err)
|
||||
}
|
||||
|
||||
bearerTokens, err := buildBearerTokens(options.FrostFSKey, options.Impersonate, table, lifetime, options.GatesPublicKeys)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build bearer tokens: %w", err)
|
||||
}
|
||||
|
@ -534,14 +577,9 @@ func createTokens(options *IssueSecretOptions, lifetime lifetimeOptions) ([]*acc
|
|||
|
||||
func formTokensToUpdate(options tokenUpdateOptions) ([]*accessbox.GateData, error) {
|
||||
btoken := options.box.Gate.BearerToken
|
||||
table := btoken.EACLTable()
|
||||
|
||||
btokenv2 := new(acl.BearerToken)
|
||||
btoken.WriteToV2(btokenv2)
|
||||
if btokenv2.GetBody().GetEACL() != nil {
|
||||
return nil, errors.New("EACL table in bearer token isn't supported")
|
||||
}
|
||||
|
||||
bearerTokens, err := buildBearerTokens(options.frostFSKey, btoken.Impersonate(), options.lifetime, options.gatesPublicKeys)
|
||||
bearerTokens, err := buildBearerTokens(options.frostFSKey, btoken.Impersonate(), &table, options.lifetime, options.gatesPublicKeys)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build bearer tokens: %w", err)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package authmate
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
apisession "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
||||
|
@ -56,11 +55,22 @@ func buildContext(rules []byte) ([]sessionTokenContext, error) {
|
|||
return nil, fmt.Errorf("failed to unmarshal rules for session token: %w", err)
|
||||
}
|
||||
|
||||
var (
|
||||
containsPut = false
|
||||
containsSetEACL = false
|
||||
)
|
||||
for _, d := range sessionCtxs {
|
||||
if d.verb == session.VerbContainerSetEACL {
|
||||
return nil, errors.New("verb container SetEACL isn't supported")
|
||||
if d.verb == session.VerbContainerPut {
|
||||
containsPut = true
|
||||
} else if d.verb == session.VerbContainerSetEACL {
|
||||
containsSetEACL = true
|
||||
}
|
||||
}
|
||||
if containsPut && !containsSetEACL {
|
||||
sessionCtxs = append(sessionCtxs, sessionTokenContext{
|
||||
verb: session.VerbContainerSetEACL,
|
||||
})
|
||||
}
|
||||
|
||||
return sessionCtxs, nil
|
||||
}
|
||||
|
@ -68,5 +78,6 @@ func buildContext(rules []byte) ([]sessionTokenContext, error) {
|
|||
return []sessionTokenContext{
|
||||
{verb: session.VerbContainerPut},
|
||||
{verb: session.VerbContainerDelete},
|
||||
{verb: session.VerbContainerSetEACL},
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -17,15 +17,20 @@ func TestContainerSessionRules(t *testing.T) {
|
|||
{
|
||||
"verb": "DELETE",
|
||||
"containerID": "6CcWg8LkcbfMUC8pt7wiy5zM1fyS3psNoxgfppcCgig1"
|
||||
},
|
||||
{
|
||||
"verb": "SETEACL"
|
||||
}
|
||||
]`)
|
||||
|
||||
sessionContext, err := buildContext(jsonRules)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, sessionContext, 2)
|
||||
require.Len(t, sessionContext, 3)
|
||||
require.Equal(t, sessionContext[0].verb, session.VerbContainerPut)
|
||||
require.Zero(t, sessionContext[0].containerID)
|
||||
require.Equal(t, sessionContext[1].verb, session.VerbContainerDelete)
|
||||
require.NotNil(t, sessionContext[1].containerID)
|
||||
require.Equal(t, sessionContext[2].verb, session.VerbContainerSetEACL)
|
||||
require.Zero(t, sessionContext[2].containerID)
|
||||
}
|
||||
|
|
|
@ -16,10 +16,6 @@ type (
|
|||
frostFSIDInitError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
policyInitError struct {
|
||||
err error
|
||||
}
|
||||
)
|
||||
|
||||
func wrapPreparationError(e error) error {
|
||||
|
@ -54,14 +50,6 @@ func (e frostFSIDInitError) Error() string {
|
|||
return e.err.Error()
|
||||
}
|
||||
|
||||
func wrapPolicyInitError(e error) error {
|
||||
return policyInitError{e}
|
||||
}
|
||||
|
||||
func (e policyInitError) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
// ExitCode picks corresponding error code depending on the type of error provided.
|
||||
// Returns 1 if error type is unknown.
|
||||
func ExitCode(e error) int {
|
||||
|
@ -74,8 +62,6 @@ func ExitCode(e error) int {
|
|||
return 4
|
||||
case frostFSIDInitError:
|
||||
return 4
|
||||
case policyInitError:
|
||||
return 5
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
|
|
@ -2,11 +2,13 @@ package modules
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/frostfsid/contract"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
|
@ -27,6 +29,8 @@ const (
|
|||
walletFlag = "wallet"
|
||||
addressFlag = "address"
|
||||
peerFlag = "peer"
|
||||
bearerRulesFlag = "bearer-rules"
|
||||
disableImpersonateFlag = "disable-impersonate"
|
||||
gatePublicKeyFlag = "gate-public-key"
|
||||
containerIDFlag = "container-id"
|
||||
containerFriendlyNameFlag = "container-friendly-name"
|
||||
|
@ -35,10 +39,16 @@ const (
|
|||
lifetimeFlag = "lifetime"
|
||||
containerPolicyFlag = "container-policy"
|
||||
awsCLICredentialFlag = "aws-cli-credentials"
|
||||
frostfsIDFlag = "frostfsid"
|
||||
frostfsIDProxyFlag = "frostfsid-proxy"
|
||||
frostfsIDNamespaceFlag = "frostfsid-namespace"
|
||||
rpcEndpointFlag = "rpc-endpoint"
|
||||
attributesFlag = "attributes"
|
||||
)
|
||||
|
||||
const walletPassphraseCfg = "wallet.passphrase"
|
||||
const (
|
||||
walletPassphraseCfg = "wallet.passphrase"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultAccessBoxLifetime = 30 * 24 * time.Hour
|
||||
|
@ -60,6 +70,8 @@ func initIssueSecretCmd() {
|
|||
issueSecretCmd.Flags().String(walletFlag, "", "Path to the wallet that will be owner of the credentials")
|
||||
issueSecretCmd.Flags().String(addressFlag, "", "Address of the wallet account")
|
||||
issueSecretCmd.Flags().String(peerFlag, "", "Address of a frostfs peer to connect to")
|
||||
issueSecretCmd.Flags().String(bearerRulesFlag, "", "Rules for bearer token (filepath or a plain json string are allowed, can be used only with --disable-impersonate)")
|
||||
issueSecretCmd.Flags().Bool(disableImpersonateFlag, false, "Mark token as not impersonate to don't consider token signer as request owner (must be provided to use --bearer-rules flag)")
|
||||
issueSecretCmd.Flags().StringSlice(gatePublicKeyFlag, nil, "Public 256r1 key of a gate (use flags repeatedly for multiple gates or separate them by comma)")
|
||||
issueSecretCmd.Flags().String(containerIDFlag, "", "Auth container id to put the secret into (if not provided new container will be created)")
|
||||
issueSecretCmd.Flags().String(containerFriendlyNameFlag, "", "Friendly name of auth container to put the secret into (flag value will be used only if --container-id is missed)")
|
||||
|
@ -72,6 +84,10 @@ func initIssueSecretCmd() {
|
|||
issueSecretCmd.Flags().Duration(poolHealthcheckTimeoutFlag, defaultPoolHealthcheckTimeout, "Timeout for request to node to decide if it is alive")
|
||||
issueSecretCmd.Flags().Duration(poolRebalanceIntervalFlag, defaultPoolRebalanceInterval, "Interval for updating nodes health status")
|
||||
issueSecretCmd.Flags().Duration(poolStreamTimeoutFlag, defaultPoolStreamTimeout, "Timeout for individual operation in streaming RPC")
|
||||
issueSecretCmd.Flags().String(frostfsIDFlag, "", "FrostfsID contract hash (LE) or name in NNS to register public key in contract (rpc-endpoint flag also must be provided)")
|
||||
issueSecretCmd.Flags().String(frostfsIDProxyFlag, "", "Proxy contract hash (LE) or name in NNS to use when interact with frostfsid contract")
|
||||
issueSecretCmd.Flags().String(frostfsIDNamespaceFlag, "", "Namespace to register public key in frostfsid contract")
|
||||
issueSecretCmd.Flags().String(rpcEndpointFlag, "", "NEO node RPC address")
|
||||
issueSecretCmd.Flags().String(attributesFlag, "", "User attributes in form of Key1=Value1,Key2=Value2 (note: you cannot override system attributes)")
|
||||
|
||||
_ = issueSecretCmd.MarkFlagRequired(walletFlag)
|
||||
|
@ -118,6 +134,17 @@ func runIssueSecretCmd(cmd *cobra.Command, _ []string) error {
|
|||
return wrapPreparationError(fmt.Errorf("couldn't parse container policy: %s", err.Error()))
|
||||
}
|
||||
|
||||
disableImpersonate := viper.GetBool(disableImpersonateFlag)
|
||||
eaclRules := viper.GetString(bearerRulesFlag)
|
||||
if !disableImpersonate && eaclRules != "" {
|
||||
return wrapPreparationError(errors.New("--bearer-rules flag can be used only with --disable-impersonate"))
|
||||
}
|
||||
|
||||
bearerRules, err := getJSONRules(eaclRules)
|
||||
if err != nil {
|
||||
return wrapPreparationError(fmt.Errorf("couldn't parse 'bearer-rules' flag: %s", err.Error()))
|
||||
}
|
||||
|
||||
sessionRules, skipSessionRules, err := getSessionRules(viper.GetString(sessionTokensFlag))
|
||||
if err != nil {
|
||||
return wrapPreparationError(fmt.Errorf("couldn't parse 'session-tokens' flag: %s", err.Error()))
|
||||
|
@ -137,6 +164,29 @@ func runIssueSecretCmd(cmd *cobra.Command, _ []string) error {
|
|||
return wrapFrostFSInitError(fmt.Errorf("failed to create FrostFS component: %s", err))
|
||||
}
|
||||
|
||||
frostFSID := viper.GetString(frostfsIDFlag)
|
||||
if frostFSID != "" {
|
||||
rpcAddress := viper.GetString(rpcEndpointFlag)
|
||||
if rpcAddress == "" {
|
||||
return wrapPreparationError(fmt.Errorf("you can use '%s' flag only along with '%s'", frostfsIDFlag, rpcEndpointFlag))
|
||||
}
|
||||
cfg := contract.Config{
|
||||
RPCAddress: rpcAddress,
|
||||
Contract: frostFSID,
|
||||
ProxyContract: viper.GetString(frostfsIDProxyFlag),
|
||||
Key: key,
|
||||
}
|
||||
|
||||
frostfsIDClient, err := createFrostFSID(ctx, log, cfg)
|
||||
if err != nil {
|
||||
return wrapFrostFSIDInitError(err)
|
||||
}
|
||||
|
||||
if err = registerPublicKey(frostfsIDClient, viper.GetString(frostfsIDNamespaceFlag), key.PublicKey()); err != nil {
|
||||
return wrapBusinessLogicError(fmt.Errorf("failed to register key in frostfsid: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
customAttrs, err := parseObjectAttrs(viper.GetString(attributesFlag))
|
||||
if err != nil {
|
||||
return wrapPreparationError(fmt.Errorf("failed to parse attributes: %s", err))
|
||||
|
@ -150,7 +200,8 @@ func runIssueSecretCmd(cmd *cobra.Command, _ []string) error {
|
|||
},
|
||||
FrostFSKey: key,
|
||||
GatesPublicKeys: gatesPublicKeys,
|
||||
Impersonate: true,
|
||||
EACLRules: bearerRules,
|
||||
Impersonate: !disableImpersonate,
|
||||
SessionTokenRules: sessionRules,
|
||||
SkipSessionRules: skipSessionRules,
|
||||
ContainerPolicies: policies,
|
||||
|
|
|
@ -1,202 +0,0 @@
|
|||
package modules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-contract/policy"
|
||||
ffsidContract "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/frostfsid/contract"
|
||||
policyContact "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/policy/contract"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var registerUserCmd = &cobra.Command{
|
||||
Use: "register-user",
|
||||
Short: "Register user and add allowed policy to him",
|
||||
Long: "Register user in FrostFSID contract and add allowed policies. This is need to get access to s3-gw operations.",
|
||||
Example: `frostfs-s3-authmate register-user --wallet wallet.json --rpc-endpoint http://morph-chain.frostfs.devenv:30333
|
||||
frostfs-s3-authmate register-user --wallet wallet.json --contract-wallet contract-wallet.json --rpc-endpoint http://morph-chain.frostfs.devenv:30333 --namespace namespace --frostfsid-name devenv --frostfsid-contract frostfsid.frostfs --proxy-contract proxy.frostfs --policy-contract policy.frostfs`,
|
||||
RunE: runRegisterUserCmd,
|
||||
}
|
||||
|
||||
const (
|
||||
frostfsIDContractFlag = "frostfsid-contract"
|
||||
proxyContractFlag = "proxy-contract"
|
||||
usernameFlag = "username"
|
||||
namespaceFlag = "namespace"
|
||||
policyContractFlag = "policy-contract"
|
||||
contractWalletFlag = "contract-wallet"
|
||||
contractWalletAddressFlag = "contract-wallet-address"
|
||||
rpcEndpointFlag = "rpc-endpoint"
|
||||
)
|
||||
|
||||
const walletContractPassphraseCfg = "wallet.contract.passphrase"
|
||||
|
||||
func initRegisterUserCmd() {
|
||||
registerUserCmd.Flags().String(walletFlag, "", "Path to the wallet with account of the user that will be registered in FrostFS ID contract")
|
||||
registerUserCmd.Flags().String(addressFlag, "", "Address of the user wallet that will be registered in FrostFS ID contract")
|
||||
registerUserCmd.Flags().String(frostfsIDContractFlag, "frostfsid.frostfs", "FrostfsID contract hash (LE) or name in NNS to register public key in contract")
|
||||
registerUserCmd.Flags().String(proxyContractFlag, "proxy.frostfs", "Proxy contract hash (LE) or name in NNS to use when interact with frostfsid contract")
|
||||
registerUserCmd.Flags().String(namespaceFlag, "", "Namespace to register public key in frostfsid contract and add policy chains")
|
||||
registerUserCmd.Flags().String(usernameFlag, "", "Username to set for public key in frostfsid contract")
|
||||
registerUserCmd.Flags().String(contractWalletFlag, "", "Path to wallet that will be used to interact with contracts (if missing key from wallet flag be used)")
|
||||
registerUserCmd.Flags().String(contractWalletAddressFlag, "", "Address of the contract wallet account")
|
||||
registerUserCmd.Flags().String(policyContractFlag, "policy.frostfs", "Policy contract hash (LE) or name in NNS to save allowed chains for key")
|
||||
registerUserCmd.Flags().String(rpcEndpointFlag, "", "NEO node RPC address")
|
||||
|
||||
_ = registerUserCmd.MarkFlagRequired(walletFlag)
|
||||
_ = registerUserCmd.MarkFlagRequired(rpcEndpointFlag)
|
||||
}
|
||||
|
||||
func runRegisterUserCmd(cmd *cobra.Command, _ []string) error {
|
||||
ctx, cancel := context.WithTimeout(cmd.Context(), viper.GetDuration(timeoutFlag))
|
||||
defer cancel()
|
||||
|
||||
log := getLogger()
|
||||
|
||||
key, contractKey, err := parseKeys()
|
||||
if err != nil {
|
||||
return wrapPreparationError(err)
|
||||
}
|
||||
|
||||
frostfsIDClient, err := initFrostFSIDContract(ctx, log, contractKey)
|
||||
if err != nil {
|
||||
return wrapFrostFSIDInitError(err)
|
||||
}
|
||||
|
||||
if err = registerPublicKey(log, frostfsIDClient, key.PublicKey()); err != nil {
|
||||
return wrapBusinessLogicError(err)
|
||||
}
|
||||
|
||||
policyClient, err := initPolicyContract(ctx, log, contractKey)
|
||||
if err != nil {
|
||||
return wrapPolicyInitError(err)
|
||||
}
|
||||
|
||||
if err = addAllowedPolicyChains(cmd, log, policyClient, key.PublicKey()); err != nil {
|
||||
return wrapBusinessLogicError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseKeys() (userKey *keys.PrivateKey, contractKey *keys.PrivateKey, err error) {
|
||||
password := wallet.GetPassword(viper.GetViper(), walletPassphraseCfg)
|
||||
key, err := wallet.GetKeyFromPath(viper.GetString(walletFlag), viper.GetString(addressFlag), password)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to load frostfs private key: %s", err)
|
||||
}
|
||||
|
||||
contractKey = key
|
||||
if contractWallet := viper.GetString(contractWalletFlag); contractWallet != "" {
|
||||
password = wallet.GetPassword(viper.GetViper(), walletContractPassphraseCfg)
|
||||
contractKey, err = wallet.GetKeyFromPath(contractWallet, viper.GetString(contractWalletAddressFlag), password)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to load contract private key: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return key, contractKey, nil
|
||||
}
|
||||
|
||||
func initFrostFSIDContract(ctx context.Context, log *zap.Logger, key *keys.PrivateKey) (*ffsidContract.FrostFSID, error) {
|
||||
log.Debug(logs.PrepareFrostfsIDClient)
|
||||
|
||||
cfg := ffsidContract.Config{
|
||||
RPCAddress: viper.GetString(rpcEndpointFlag),
|
||||
Contract: viper.GetString(frostfsIDContractFlag),
|
||||
ProxyContract: viper.GetString(proxyContractFlag),
|
||||
Key: key,
|
||||
}
|
||||
|
||||
cli, err := ffsidContract.New(ctx, cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create frostfsid client: %w", err)
|
||||
}
|
||||
|
||||
return cli, nil
|
||||
}
|
||||
|
||||
func registerPublicKey(log *zap.Logger, cli *ffsidContract.FrostFSID, key *keys.PublicKey) error {
|
||||
namespace := viper.GetString(namespaceFlag)
|
||||
|
||||
log.Debug(logs.CreateSubjectInFrostFSID)
|
||||
err := cli.Wait(cli.CreateSubject(namespace, key))
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "subject already exists") {
|
||||
log.Debug(logs.SubjectAlreadyExistsInFrostFSID, zap.String("address", key.Address()))
|
||||
} else {
|
||||
return fmt.Errorf("create subject in frostfsid: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
name := viper.GetString(usernameFlag)
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debug(logs.SetSubjectNameInFrostFSID)
|
||||
if err = cli.Wait(cli.SetSubjectName(key, name)); err != nil {
|
||||
return fmt.Errorf("set subject name in frostfsid: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initPolicyContract(ctx context.Context, log *zap.Logger, key *keys.PrivateKey) (*policyContact.Client, error) {
|
||||
log.Debug(logs.PreparePolicyClient)
|
||||
|
||||
cfg := policyContact.Config{
|
||||
RPCAddress: viper.GetString(rpcEndpointFlag),
|
||||
Contract: viper.GetString(policyContractFlag),
|
||||
ProxyContract: viper.GetString(proxyContractFlag),
|
||||
Key: key,
|
||||
}
|
||||
|
||||
cli, err := policyContact.New(ctx, cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create policy client: %w", err)
|
||||
}
|
||||
|
||||
return cli, nil
|
||||
}
|
||||
|
||||
func addAllowedPolicyChains(cmd *cobra.Command, log *zap.Logger, cli *policyContact.Client, key *keys.PublicKey) error {
|
||||
log.Debug(logs.AddPolicyChainRules)
|
||||
namespace := viper.GetString(namespaceFlag)
|
||||
|
||||
allowAllRule := chain.Rule{
|
||||
Status: chain.Allow,
|
||||
Actions: chain.Actions{Names: []string{"*"}},
|
||||
Resources: chain.Resources{Names: []string{"*"}},
|
||||
}
|
||||
|
||||
chains := []*chain.Chain{
|
||||
{ID: chain.ID(chain.S3 + ":authmate"), Rules: []chain.Rule{allowAllRule}},
|
||||
{ID: chain.ID(chain.Ingress + ":authmate"), Rules: []chain.Rule{allowAllRule}},
|
||||
}
|
||||
|
||||
kind := policy.Kind(policy.User)
|
||||
entity := namespace + ":" + key.Address()
|
||||
tx := cli.StartTx()
|
||||
for _, ch := range chains {
|
||||
tx.AddChain(kind, entity, ch.ID, ch.Bytes())
|
||||
}
|
||||
|
||||
if err := cli.SendTx(tx); err != nil {
|
||||
return fmt.Errorf("add policy chain: %w", err)
|
||||
}
|
||||
|
||||
cmd.Printf("Added policy rules:\nkind: '%c'\nentity: '%s'\nchains:\n", kind, entity)
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
return enc.Encode(chains)
|
||||
}
|
|
@ -65,7 +65,4 @@ GoVersion: {{ runtimeVersion }}
|
|||
|
||||
rootCmd.AddCommand(updateSecretCmd)
|
||||
initUpdateSecretCmd()
|
||||
|
||||
rootCmd.AddCommand(registerUserCmd)
|
||||
initRegisterUserCmd()
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/frostfsid/contract"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
|
@ -39,6 +40,10 @@ func initUpdateSecretCmd() {
|
|||
updateSecretCmd.Flags().Duration(poolHealthcheckTimeoutFlag, defaultPoolHealthcheckTimeout, "Timeout for request to node to decide if it is alive")
|
||||
updateSecretCmd.Flags().Duration(poolRebalanceIntervalFlag, defaultPoolRebalanceInterval, "Interval for updating nodes health status")
|
||||
updateSecretCmd.Flags().Duration(poolStreamTimeoutFlag, defaultPoolStreamTimeout, "Timeout for individual operation in streaming RPC")
|
||||
updateSecretCmd.Flags().String(frostfsIDFlag, "", "FrostfsID contract hash (LE) or name in NNS to register public key in contract (rpc-endpoint flag also must be provided)")
|
||||
updateSecretCmd.Flags().String(frostfsIDProxyFlag, "", "Proxy contract hash (LE) or name in NNS to use when interact with frostfsid contract")
|
||||
updateSecretCmd.Flags().String(frostfsIDNamespaceFlag, "", "Namespace to register public key in frostfsid contract")
|
||||
updateSecretCmd.Flags().String(rpcEndpointFlag, "", "NEO node RPC address")
|
||||
updateSecretCmd.Flags().String(attributesFlag, "", "User attributes in form of Key1=Value1,Key2=Value2 (note: you cannot override system attributes)")
|
||||
|
||||
_ = updateSecretCmd.MarkFlagRequired(walletFlag)
|
||||
|
@ -95,6 +100,29 @@ func runUpdateSecretCmd(cmd *cobra.Command, _ []string) error {
|
|||
return wrapFrostFSInitError(fmt.Errorf("failed to create FrostFS component: %s", err))
|
||||
}
|
||||
|
||||
frostFSID := viper.GetString(frostfsIDFlag)
|
||||
if frostFSID != "" {
|
||||
rpcAddress := viper.GetString(rpcEndpointFlag)
|
||||
if rpcAddress == "" {
|
||||
return wrapPreparationError(fmt.Errorf("you can use '%s' flag only along with '%s'", frostfsIDFlag, rpcEndpointFlag))
|
||||
}
|
||||
cfg := contract.Config{
|
||||
RPCAddress: rpcAddress,
|
||||
Contract: frostFSID,
|
||||
ProxyContract: viper.GetString(frostfsIDProxyFlag),
|
||||
Key: key,
|
||||
}
|
||||
|
||||
frostfsIDClient, err := createFrostFSID(ctx, log, cfg)
|
||||
if err != nil {
|
||||
return wrapFrostFSIDInitError(err)
|
||||
}
|
||||
|
||||
if err = registerPublicKey(frostfsIDClient, viper.GetString(frostfsIDNamespaceFlag), key.PublicKey()); err != nil {
|
||||
return wrapBusinessLogicError(fmt.Errorf("failed to register key in frostfsid: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
customAttrs, err := parseObjectAttrs(viper.GetString(attributesFlag))
|
||||
if err != nil {
|
||||
return wrapPreparationError(fmt.Errorf("failed to parse attributes: %s", err))
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/frostfsid/contract"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
|
||||
|
@ -50,7 +51,7 @@ func createFrostFS(ctx context.Context, log *zap.Logger, cfg PoolConfig) (*frost
|
|||
return nil, fmt.Errorf("dial pool: %w", err)
|
||||
}
|
||||
|
||||
return frostfs.NewAuthmateFrostFS(frostfs.NewFrostFS(p, cfg.Key), log), nil
|
||||
return frostfs.NewAuthmateFrostFS(frostfs.NewFrostFS(p, cfg.Key)), nil
|
||||
}
|
||||
|
||||
func parsePolicies(val string) (authmate.ContainerPolicies, error) {
|
||||
|
@ -144,6 +145,26 @@ func getLogger() *zap.Logger {
|
|||
return log
|
||||
}
|
||||
|
||||
func createFrostFSID(ctx context.Context, log *zap.Logger, cfg contract.Config) (*contract.FrostFSID, error) {
|
||||
log.Debug(logs.PrepareFrostfsIDClient)
|
||||
|
||||
cli, err := contract.New(ctx, cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create frostfsid client: %w", err)
|
||||
}
|
||||
|
||||
return cli, nil
|
||||
}
|
||||
|
||||
func registerPublicKey(cli *contract.FrostFSID, namespace string, key *keys.PublicKey) error {
|
||||
err := cli.Wait(cli.CreateSubject(namespace, key))
|
||||
if err != nil && !strings.Contains(err.Error(), "subject already exists") {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseObjectAttrs(attributes string) ([]object.Attribute, error) {
|
||||
if len(attributes) == 0 {
|
||||
return nil, nil
|
||||
|
|
192
cmd/s3-gw/app.go
192
cmd/s3-gw/app.go
|
@ -7,6 +7,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
@ -21,10 +22,10 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/handler"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
|
||||
s3middleware "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/notifications"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/resolver"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs"
|
||||
|
@ -37,9 +38,9 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/version"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/metrics"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/control"
|
||||
controlSvc "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/control/server"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/tree"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
|
||||
treepool "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool/tree"
|
||||
|
@ -64,7 +65,8 @@ type (
|
|||
pool *pool.Pool
|
||||
treePool *treepool.Pool
|
||||
key *keys.PrivateKey
|
||||
obj *layer.Layer
|
||||
nc *notifications.Controller
|
||||
obj layer.Client
|
||||
api api.Handler
|
||||
|
||||
frostfsid *frostfsid.FrostFSID
|
||||
|
@ -75,6 +77,8 @@ type (
|
|||
unbindServers []ServerInfo
|
||||
mu sync.RWMutex
|
||||
|
||||
controlAPI *grpc.Server
|
||||
|
||||
metrics *metrics.AppMetrics
|
||||
bucketResolver *resolver.BucketResolver
|
||||
services []*Service
|
||||
|
@ -89,6 +93,7 @@ type (
|
|||
maxClient maxClientsConfig
|
||||
defaultMaxAge int
|
||||
reconnectInterval time.Duration
|
||||
notificatorEnabled bool
|
||||
resolveZoneList []string
|
||||
isResolveListAllow bool // True if ResolveZoneList contains allowed zones
|
||||
frostfsidValidation bool
|
||||
|
@ -100,14 +105,15 @@ type (
|
|||
clientCut bool
|
||||
maxBufferSizeForPut uint64
|
||||
md5Enabled bool
|
||||
aclEnabled bool
|
||||
namespaceHeader string
|
||||
defaultNamespaces []string
|
||||
authorizedControlAPIKeys [][]byte
|
||||
policyDenyByDefault bool
|
||||
sourceIPHeader string
|
||||
retryMaxAttempts int
|
||||
retryMaxBackoff time.Duration
|
||||
retryStrategy handler.RetryStrategy
|
||||
domains []string
|
||||
}
|
||||
|
||||
maxClientsConfig struct {
|
||||
|
@ -125,7 +131,7 @@ func newApp(ctx context.Context, log *Logger, v *viper.Viper) *App {
|
|||
objPool, treePool, key := getPools(ctx, log.logger, v)
|
||||
|
||||
cfg := tokens.Config{
|
||||
FrostFS: frostfs.NewAuthmateFrostFS(frostfs.NewFrostFS(objPool, key), log.logger),
|
||||
FrostFS: frostfs.NewAuthmateFrostFS(frostfs.NewFrostFS(objPool, key)),
|
||||
Key: key,
|
||||
CacheConfig: getAccessBoxCacheConfig(v, log.logger),
|
||||
RemovingCheckAfterDurations: fetchRemovingCheckInterval(v, log.logger),
|
||||
|
@ -145,7 +151,7 @@ func newApp(ctx context.Context, log *Logger, v *viper.Viper) *App {
|
|||
webDone: make(chan struct{}, 1),
|
||||
wrkDone: make(chan struct{}, 1),
|
||||
|
||||
settings: newAppSettings(log, v),
|
||||
settings: newAppSettings(log, v, key),
|
||||
}
|
||||
|
||||
app.init(ctx)
|
||||
|
@ -159,6 +165,7 @@ func (a *App) init(ctx context.Context) {
|
|||
a.initPolicyStorage(ctx)
|
||||
a.initAPI(ctx)
|
||||
a.initMetrics()
|
||||
a.initControlAPI()
|
||||
a.initServers(ctx)
|
||||
a.initTracing(ctx)
|
||||
}
|
||||
|
@ -175,14 +182,6 @@ func (a *App) initLayer(ctx context.Context) {
|
|||
var gateOwner user.ID
|
||||
user.IDFromKey(&gateOwner, a.key.PrivateKey.PublicKey)
|
||||
|
||||
var corsCnrInfo *data.BucketInfo
|
||||
if a.cfg.IsSet(cfgContainersCORS) {
|
||||
corsCnrInfo, err = a.fetchContainerInfo(ctx, cfgContainersCORS)
|
||||
if err != nil {
|
||||
a.log.Fatal(logs.CouldNotFetchCORSContainerInfo, zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
layerCfg := &layer.Config{
|
||||
Cache: layer.NewCache(getCacheOptions(a.cfg, a.log)),
|
||||
AnonKey: layer.AnonymousKey{
|
||||
|
@ -192,20 +191,31 @@ func (a *App) initLayer(ctx context.Context) {
|
|||
Resolver: a.bucketResolver,
|
||||
TreeService: tree.NewTree(services.NewPoolWrapper(a.treePool), a.log),
|
||||
Features: a.settings,
|
||||
GateKey: a.key,
|
||||
CORSCnrInfo: corsCnrInfo,
|
||||
}
|
||||
|
||||
// prepare object layer
|
||||
a.obj = layer.NewLayer(a.log, frostfs.NewFrostFS(a.pool, a.key), layerCfg)
|
||||
|
||||
if a.cfg.GetBool(cfgEnableNATS) {
|
||||
nopts := getNotificationsOptions(a.cfg, a.log)
|
||||
a.nc, err = notifications.NewController(nopts, a.log)
|
||||
if err != nil {
|
||||
a.log.Fatal(logs.FailedToEnableNotifications, zap.Error(err))
|
||||
}
|
||||
|
||||
if err = a.obj.Initialize(ctx, a.nc); err != nil {
|
||||
a.log.Fatal(logs.CouldntInitializeLayer, zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newAppSettings(log *Logger, v *viper.Viper) *appSettings {
|
||||
func newAppSettings(log *Logger, v *viper.Viper, key *keys.PrivateKey) *appSettings {
|
||||
settings := &appSettings{
|
||||
logLevel: log.lvl,
|
||||
maxClient: newMaxClients(v),
|
||||
defaultMaxAge: fetchDefaultMaxAge(v, log.logger),
|
||||
reconnectInterval: fetchReconnectInterval(v),
|
||||
notificatorEnabled: v.GetBool(cfgEnableNATS),
|
||||
frostfsidValidation: v.GetBool(cfgFrostfsIDValidationEnabled),
|
||||
}
|
||||
|
||||
|
@ -215,24 +225,25 @@ func newAppSettings(log *Logger, v *viper.Viper) *appSettings {
|
|||
settings.resolveZoneList = v.GetStringSlice(cfgResolveBucketDeny)
|
||||
}
|
||||
|
||||
settings.update(v, log.logger)
|
||||
settings.update(v, log.logger, key)
|
||||
|
||||
return settings
|
||||
}
|
||||
|
||||
func (s *appSettings) update(v *viper.Viper, log *zap.Logger) {
|
||||
func (s *appSettings) update(v *viper.Viper, log *zap.Logger, key *keys.PrivateKey) {
|
||||
s.updateNamespacesSettings(v, log)
|
||||
s.useDefaultXMLNamespace(v.GetBool(cfgKludgeUseDefaultXMLNS))
|
||||
s.setACLEnabled(v.GetBool(cfgKludgeACLEnabled))
|
||||
s.setBypassContentEncodingInChunks(v.GetBool(cfgKludgeBypassContentEncodingCheckInChunks))
|
||||
s.setClientCut(v.GetBool(cfgClientCut))
|
||||
s.setBufferMaxSizeForPut(v.GetUint64(cfgBufferMaxSizeForPut))
|
||||
s.setMD5Enabled(v.GetBool(cfgMD5Enabled))
|
||||
s.setAuthorizedControlAPIKeys(append(fetchAuthorizedKeys(log, v), key.PublicKey()))
|
||||
s.setPolicyDenyByDefault(v.GetBool(cfgPolicyDenyByDefault))
|
||||
s.setSourceIPHeader(v.GetString(cfgSourceIPHeader))
|
||||
s.setRetryMaxAttempts(fetchRetryMaxAttempts(v))
|
||||
s.setRetryMaxBackoff(fetchRetryMaxBackoff(v))
|
||||
s.setRetryStrategy(fetchRetryStrategy(v))
|
||||
s.setVHSSettings(v, log)
|
||||
}
|
||||
|
||||
func (s *appSettings) updateNamespacesSettings(v *viper.Viper, log *zap.Logger) {
|
||||
|
@ -247,15 +258,6 @@ func (s *appSettings) updateNamespacesSettings(v *viper.Viper, log *zap.Logger)
|
|||
s.namespaces = nsConfig.Namespaces
|
||||
}
|
||||
|
||||
func (s *appSettings) setVHSSettings(v *viper.Viper, _ *zap.Logger) {
|
||||
domains := v.GetStringSlice(cfgListenDomains)
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.domains = domains
|
||||
}
|
||||
|
||||
func (s *appSettings) BypassContentEncodingInChunks() bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
@ -349,6 +351,10 @@ func (s *appSettings) DefaultMaxAge() int {
|
|||
return s.defaultMaxAge
|
||||
}
|
||||
|
||||
func (s *appSettings) NotificatorEnabled() bool {
|
||||
return s.notificatorEnabled
|
||||
}
|
||||
|
||||
func (s *appSettings) ResolveZoneList() []string {
|
||||
return s.resolveZoneList
|
||||
}
|
||||
|
@ -369,6 +375,18 @@ func (s *appSettings) setMD5Enabled(md5Enabled bool) {
|
|||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *appSettings) setACLEnabled(enableACL bool) {
|
||||
s.mu.Lock()
|
||||
s.aclEnabled = enableACL
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *appSettings) ACLEnabled() bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.aclEnabled
|
||||
}
|
||||
|
||||
func (s *appSettings) NamespaceHeader() string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
@ -390,6 +408,23 @@ func (s *appSettings) isDefaultNamespace(ns string) bool {
|
|||
return slices.Contains(namespaces, ns)
|
||||
}
|
||||
|
||||
func (s *appSettings) FetchRawKeys() [][]byte {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.authorizedControlAPIKeys
|
||||
}
|
||||
|
||||
func (s *appSettings) setAuthorizedControlAPIKeys(keys keys.PublicKeys) {
|
||||
rawPubs := make([][]byte, len(keys))
|
||||
for i := range keys {
|
||||
rawPubs[i] = keys[i].Bytes()
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.authorizedControlAPIKeys = rawPubs
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *appSettings) ResolveNamespaceAlias(namespace string) string {
|
||||
if s.isDefaultNamespace(namespace) {
|
||||
return defaultNamespace
|
||||
|
@ -458,17 +493,23 @@ func (s *appSettings) RetryStrategy() handler.RetryStrategy {
|
|||
return s.retryStrategy
|
||||
}
|
||||
|
||||
func (s *appSettings) Domains() []string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.domains
|
||||
}
|
||||
|
||||
func (a *App) initAPI(ctx context.Context) {
|
||||
a.initLayer(ctx)
|
||||
a.initHandler()
|
||||
}
|
||||
|
||||
func (a *App) initControlAPI() {
|
||||
svc := controlSvc.New(
|
||||
controlSvc.WithSettings(a.settings),
|
||||
controlSvc.WithLogger(a.log),
|
||||
controlSvc.WithChainStorage(a.policyStorage.LocalStorage()),
|
||||
)
|
||||
|
||||
a.controlAPI = grpc.NewServer()
|
||||
|
||||
control.RegisterControlServiceServer(a.controlAPI, svc)
|
||||
}
|
||||
|
||||
func (a *App) initMetrics() {
|
||||
cfg := metrics.AppMetricsConfig{
|
||||
Logger: a.log,
|
||||
|
@ -701,7 +742,8 @@ func (a *App) setHealthStatus() {
|
|||
// Serve runs HTTP server to handle S3 API requests.
|
||||
func (a *App) Serve(ctx context.Context) {
|
||||
// Attach S3 API:
|
||||
a.log.Info(logs.FetchDomainsPrepareToUseAPI, zap.Strings("domains", a.settings.Domains()))
|
||||
domains := a.cfg.GetStringSlice(cfgListenDomains)
|
||||
a.log.Info(logs.FetchDomainsPrepareToUseAPI, zap.Strings("domains", domains))
|
||||
|
||||
cfg := api.Config{
|
||||
Throttle: middleware.ThrottleOpts{
|
||||
|
@ -712,7 +754,7 @@ func (a *App) Serve(ctx context.Context) {
|
|||
Center: a.ctr,
|
||||
Log: a.log,
|
||||
Metrics: a.metrics,
|
||||
Domains: a.settings.Domains(),
|
||||
Domains: domains,
|
||||
|
||||
MiddlewareSettings: a.settings,
|
||||
PolicyChecker: a.policyStorage,
|
||||
|
@ -754,6 +796,16 @@ func (a *App) Serve(ctx context.Context) {
|
|||
a.scheduleReconnect(ctx, srv)
|
||||
}
|
||||
|
||||
go func() {
|
||||
address := a.cfg.GetString(cfgControlGRPCEndpoint)
|
||||
a.log.Info(logs.StartingControlAPI, zap.String("address", address))
|
||||
if listener, err := net.Listen("tcp", address); err != nil {
|
||||
a.log.Fatal(logs.ListenAndServe, zap.Error(err))
|
||||
} else if err = a.controlAPI.Serve(listener); err != nil {
|
||||
a.log.Fatal(logs.ListenAndServe, zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGHUP)
|
||||
|
||||
|
@ -772,6 +824,7 @@ LOOP:
|
|||
|
||||
a.log.Info(logs.StoppingServer, zap.Error(srv.Shutdown(ctx)))
|
||||
|
||||
a.stopControlAPI()
|
||||
a.metrics.Shutdown()
|
||||
a.stopServices()
|
||||
a.shutdownTracing()
|
||||
|
@ -783,6 +836,25 @@ func shutdownContext() (context.Context, context.CancelFunc) {
|
|||
return context.WithTimeout(context.Background(), defaultShutdownTimeout)
|
||||
}
|
||||
|
||||
func (a *App) stopControlAPI() {
|
||||
ctx, cancel := shutdownContext()
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
a.controlAPI.GracefulStop()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
a.log.Info(logs.ControlAPICannotShutdownGracefully)
|
||||
a.controlAPI.Stop()
|
||||
}
|
||||
|
||||
a.log.Info(logs.ControlAPIServiceStopped)
|
||||
}
|
||||
|
||||
func (a *App) configReload(ctx context.Context) {
|
||||
a.log.Info(logs.SIGHUPConfigReloadStarted)
|
||||
|
||||
|
@ -824,7 +896,7 @@ func (a *App) updateSettings() {
|
|||
a.settings.logLevel.SetLevel(lvl)
|
||||
}
|
||||
|
||||
a.settings.update(a.cfg, a.log)
|
||||
a.settings.update(a.cfg, a.log, a.key)
|
||||
}
|
||||
|
||||
func (a *App) startServices() {
|
||||
|
@ -903,6 +975,17 @@ func (a *App) stopServices() {
|
|||
}
|
||||
}
|
||||
|
||||
func getNotificationsOptions(v *viper.Viper, l *zap.Logger) *notifications.Options {
|
||||
cfg := notifications.Options{}
|
||||
cfg.URL = v.GetString(cfgNATSEndpoint)
|
||||
cfg.Timeout = fetchNATSTimeout(v, l)
|
||||
cfg.TLSCertFilepath = v.GetString(cfgNATSTLSCertFile)
|
||||
cfg.TLSAuthPrivateKeyFilePath = v.GetString(cfgNATSAuthPrivateKeyFile)
|
||||
cfg.RootCAFiles = v.GetStringSlice(cfgNATSRootCAFiles)
|
||||
|
||||
return &cfg
|
||||
}
|
||||
|
||||
func getCacheOptions(v *viper.Viper, l *zap.Logger) *layer.CachesConfig {
|
||||
cacheCfg := layer.DefaultCachesConfigs(l)
|
||||
|
||||
|
@ -960,7 +1043,7 @@ func getFrostfsIDCacheConfig(v *viper.Viper, l *zap.Logger) *cache.Config {
|
|||
func (a *App) initHandler() {
|
||||
var err error
|
||||
|
||||
a.api, err = handler.New(a.log, a.obj, a.settings, a.policyStorage, a.frostfsid)
|
||||
a.api, err = handler.New(a.log, a.obj, a.nc, a.settings, a.policyStorage, a.frostfsid)
|
||||
if err != nil {
|
||||
a.log.Fatal(logs.CouldNotInitializeAPIHandler, zap.Error(err))
|
||||
}
|
||||
|
@ -1063,32 +1146,3 @@ func (a *App) tryReconnect(ctx context.Context, sr *http.Server) bool {
|
|||
|
||||
return len(a.unbindServers) == 0
|
||||
}
|
||||
|
||||
func (a *App) fetchContainerInfo(ctx context.Context, cfgKey string) (info *data.BucketInfo, err error) {
|
||||
containerString := a.cfg.GetString(cfgKey)
|
||||
|
||||
var id cid.ID
|
||||
if err = id.DecodeString(containerString); err != nil {
|
||||
if id, err = a.bucketResolver.Resolve(ctx, containerString); err != nil {
|
||||
return nil, fmt.Errorf("resolve container name %s: %w", containerString, err)
|
||||
}
|
||||
}
|
||||
|
||||
return getContainerInfo(ctx, id, a.pool)
|
||||
}
|
||||
|
||||
func getContainerInfo(ctx context.Context, id cid.ID, frostFSPool *pool.Pool) (*data.BucketInfo, error) {
|
||||
prm := pool.PrmContainerGet{
|
||||
ContainerID: id,
|
||||
}
|
||||
|
||||
res, err := frostFSPool.GetContainer(ctx, prm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &data.BucketInfo{
|
||||
CID: id,
|
||||
HomomorphicHashDisabled: container.IsHomomorphicHashingDisabled(res),
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -14,12 +14,14 @@ import (
|
|||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/handler"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/notifications"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/resolver"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/version"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
|
||||
"git.frostfs.info/TrueCloudLab/zapjournald"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/ssgreg/journald"
|
||||
|
@ -88,6 +90,10 @@ const ( // Settings.
|
|||
cfgTLSKeyFile = "tls.key_file"
|
||||
cfgTLSCertFile = "tls.cert_file"
|
||||
|
||||
// Control API.
|
||||
cfgControlAuthorizedKeys = "control.authorized_keys"
|
||||
cfgControlGRPCEndpoint = "control.grpc.endpoint"
|
||||
|
||||
// Pool config.
|
||||
cfgConnectTimeout = "connect_timeout"
|
||||
cfgStreamTimeout = "stream_timeout"
|
||||
|
@ -119,6 +125,14 @@ const ( // Settings.
|
|||
|
||||
cfgAccessBoxCacheRemovingCheckInterval = "cache.accessbox.removing_check_interval"
|
||||
|
||||
// NATS.
|
||||
cfgEnableNATS = "nats.enabled"
|
||||
cfgNATSEndpoint = "nats.endpoint"
|
||||
cfgNATSTimeout = "nats.timeout"
|
||||
cfgNATSTLSCertFile = "nats.cert_file"
|
||||
cfgNATSAuthPrivateKeyFile = "nats.key_file"
|
||||
cfgNATSRootCAFiles = "nats.root_ca"
|
||||
|
||||
// Policy.
|
||||
cfgPolicyDefault = "placement_policy.default"
|
||||
cfgPolicyRegionMapFile = "placement_policy.region_mapping"
|
||||
|
@ -160,6 +174,8 @@ const ( // Settings.
|
|||
cfgKludgeUseDefaultXMLNS = "kludge.use_default_xmlns"
|
||||
cfgKludgeBypassContentEncodingCheckInChunks = "kludge.bypass_content_encoding_check_in_chunks"
|
||||
cfgKludgeDefaultNamespaces = "kludge.default_namespaces"
|
||||
cfgKludgeACLEnabled = "kludge.acl_enabled"
|
||||
|
||||
// Web.
|
||||
cfgWebReadTimeout = "web.read_timeout"
|
||||
cfgWebReadHeaderTimeout = "web.read_header_timeout"
|
||||
|
@ -176,9 +192,6 @@ const ( // Settings.
|
|||
|
||||
cfgSourceIPHeader = "source_ip_header"
|
||||
|
||||
// Containers.
|
||||
cfgContainersCORS = "containers.cors"
|
||||
|
||||
// Command line args.
|
||||
cmdHelp = "help"
|
||||
cmdVersion = "version"
|
||||
|
@ -368,6 +381,19 @@ func fetchDefaultPolicy(l *zap.Logger, cfg *viper.Viper) netmap.PlacementPolicy
|
|||
return policy
|
||||
}
|
||||
|
||||
func fetchNATSTimeout(cfg *viper.Viper, l *zap.Logger) time.Duration {
|
||||
timeout := cfg.GetDuration(cfgNATSTimeout)
|
||||
if timeout <= 0 {
|
||||
l.Error(logs.InvalidLifetimeUsingDefaultValue,
|
||||
zap.String("parameter", cfgNATSTimeout),
|
||||
zap.Duration("value in config", timeout),
|
||||
zap.Duration("default", notifications.DefaultTimeout))
|
||||
timeout = notifications.DefaultTimeout
|
||||
}
|
||||
|
||||
return timeout
|
||||
}
|
||||
|
||||
func fetchCacheLifetime(v *viper.Viper, l *zap.Logger, cfgEntry string, defaultValue time.Duration) time.Duration {
|
||||
if v.IsSet(cfgEntry) {
|
||||
lifetime := v.GetDuration(cfgEntry)
|
||||
|
@ -667,6 +693,23 @@ func fetchServers(v *viper.Viper, log *zap.Logger) []ServerInfo {
|
|||
return servers
|
||||
}
|
||||
|
||||
func fetchAuthorizedKeys(l *zap.Logger, v *viper.Viper) keys.PublicKeys {
|
||||
strKeys := v.GetStringSlice(cfgControlAuthorizedKeys)
|
||||
pubs := make(keys.PublicKeys, 0, len(strKeys))
|
||||
|
||||
for i := range strKeys {
|
||||
pub, err := keys.NewPublicKeyFromString(strKeys[i])
|
||||
if err != nil {
|
||||
l.Warn(logs.FailedToParsePublicKey, zap.String("key", strKeys[i]))
|
||||
continue
|
||||
}
|
||||
|
||||
pubs = append(pubs, pub)
|
||||
}
|
||||
|
||||
return pubs
|
||||
}
|
||||
|
||||
func newSettings() *viper.Viper {
|
||||
v := viper.New()
|
||||
|
||||
|
@ -725,6 +768,8 @@ func newSettings() *viper.Viper {
|
|||
v.SetDefault(cfgPProfAddress, "localhost:8085")
|
||||
v.SetDefault(cfgPrometheusAddress, "localhost:8086")
|
||||
|
||||
v.SetDefault(cfgControlGRPCEndpoint, "localhost:8083")
|
||||
|
||||
// frostfs
|
||||
v.SetDefault(cfgBufferMaxSizeForPut, 1024*1024) // 1mb
|
||||
|
||||
|
@ -732,6 +777,7 @@ func newSettings() *viper.Viper {
|
|||
v.SetDefault(cfgKludgeUseDefaultXMLNS, false)
|
||||
v.SetDefault(cfgKludgeBypassContentEncodingCheckInChunks, false)
|
||||
v.SetDefault(cfgKludgeDefaultNamespaces, defaultDefaultNamespaces)
|
||||
v.SetDefault(cfgKludgeACLEnabled, false)
|
||||
|
||||
// web
|
||||
v.SetDefault(cfgWebReadHeaderTimeout, defaultReadHeaderTimeout)
|
||||
|
|
|
@ -36,6 +36,12 @@ S3_GW_SERVER_1_TLS_KEY_FILE=/path/to/tls/key
|
|||
# How often to reconnect to the servers
|
||||
S3_GW_RECONNECT_INTERVAL: 1m
|
||||
|
||||
# Control API
|
||||
# List of hex-encoded public keys that have rights to use the Control Service
|
||||
S3_GW_CONTROL_AUTHORIZED_KEYS=035839e45d472a3b7769a2a1bd7d54c4ccd4943c3b40f547870e83a8fcbfb3ce11 028f42cfcb74499d7b15b35d9bff260a1c8d27de4f446a627406a382d8961486d6
|
||||
# Endpoint that is listened by the Control Service
|
||||
S3_GW_CONTROL_GRPC_ENDPOINT=localhost:8083
|
||||
|
||||
# Domains to be able to use virtual-hosted-style access to bucket.
|
||||
S3_GW_LISTEN_DOMAINS=s3dev.frostfs.devenv
|
||||
|
||||
|
@ -88,7 +94,7 @@ S3_GW_CACHE_BUCKETS_SIZE=1000
|
|||
# Cache which contains mapping of nice name to object addresses
|
||||
S3_GW_CACHE_NAMES_LIFETIME=1m
|
||||
S3_GW_CACHE_NAMES_SIZE=10000
|
||||
# Cache for system objects in a bucket: bucket settings etc
|
||||
# Cache for system objects in a bucket: bucket settings, notification configuration etc
|
||||
S3_GW_CACHE_SYSTEM_LIFETIME=5m
|
||||
S3_GW_CACHE_SYSTEM_SIZE=100000
|
||||
# Cache which stores access box with tokens by its address
|
||||
|
@ -105,6 +111,14 @@ S3_GW_CACHE_MORPH_POLICY_SIZE=10000
|
|||
S3_GW_CACHE_FROSTFSID_LIFETIME=1m
|
||||
S3_GW_CACHE_FROSTFSID_SIZE=10000
|
||||
|
||||
# NATS
|
||||
S3_GW_NATS_ENABLED=true
|
||||
S3_GW_NATS_ENDPOINT=nats://nats.frostfs.devenv:4222
|
||||
S3_GW_NATS_TIMEOUT=30s
|
||||
S3_GW_NATS_CERT_FILE=/path/to/cert
|
||||
S3_GW_NATS_KEY_FILE=/path/to/key
|
||||
S3_GW_NATS_ROOT_CA=/path/to/ca
|
||||
|
||||
# Default policy of placing containers in FrostFS
|
||||
# If a user sends a request `CreateBucket` and doesn't define policy for placing of a container in FrostFS, the S3 Gateway
|
||||
# will put the container with default policy. It can be specified via environment variable, e.g.:
|
||||
|
@ -154,6 +168,8 @@ S3_GW_KLUDGE_USE_DEFAULT_XMLNS=false
|
|||
S3_GW_KLUDGE_BYPASS_CONTENT_ENCODING_CHECK_IN_CHUNKS=false
|
||||
# Namespaces that should be handled as default
|
||||
S3_GW_KLUDGE_DEFAULT_NAMESPACES="" "root"
|
||||
# Enable bucket/object ACL support for newly created buckets.
|
||||
S3_GW_KLUDGE_ACL_ENABLED=false
|
||||
|
||||
S3_GW_TRACING_ENABLED=false
|
||||
S3_GW_TRACING_ENDPOINT="localhost:4318"
|
||||
|
@ -216,5 +232,3 @@ S3_GW_RETRY_MAX_BACKOFF=30s
|
|||
# Backoff strategy. `exponential` and `constant` are allowed.
|
||||
S3_GW_RETRY_STRATEGY=exponential
|
||||
|
||||
# Containers properties
|
||||
S3_GW_CONTAINERS_CORS=AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
|
||||
|
|
|
@ -39,6 +39,15 @@ server:
|
|||
cert_file: /path/to/cert
|
||||
key_file: /path/to/key
|
||||
|
||||
control:
|
||||
# List of hex-encoded public keys that have rights to use the Control Service
|
||||
authorized_keys:
|
||||
- 035839e45d472a3b7769a2a1bd7d54c4ccd4943c3b40f547870e83a8fcbfb3ce11
|
||||
- 028f42cfcb74499d7b15b35d9bff260a1c8d27de4f446a627406a382d8961486d6
|
||||
grpc:
|
||||
# Endpoint that is listened by the Control Service
|
||||
endpoint: localhost:8083
|
||||
|
||||
# Domains to be able to use virtual-hosted-style access to bucket.
|
||||
listen_domains:
|
||||
- s3dev.frostfs.devenv
|
||||
|
@ -105,7 +114,7 @@ cache:
|
|||
buckets:
|
||||
lifetime: 1m
|
||||
size: 500
|
||||
# Cache for system objects in a bucket: bucket settings etc
|
||||
# Cache for system objects in a bucket: bucket settings, notification configuration etc
|
||||
system:
|
||||
lifetime: 2m
|
||||
size: 1000
|
||||
|
@ -127,6 +136,14 @@ cache:
|
|||
lifetime: 1m
|
||||
size: 10000
|
||||
|
||||
nats:
|
||||
enabled: true
|
||||
endpoint: nats://localhost:4222
|
||||
timeout: 30s
|
||||
cert_file: /path/to/cert
|
||||
key_file: /path/to/key
|
||||
root_ca: /path/to/ca
|
||||
|
||||
# Parameters of FrostFS container placement policy
|
||||
placement_policy:
|
||||
# Default policy of placing containers in FrostFS
|
||||
|
@ -182,6 +199,8 @@ kludge:
|
|||
bypass_content_encoding_check_in_chunks: false
|
||||
# Namespaces that should be handled as default
|
||||
default_namespaces: [ "", "root" ]
|
||||
# Enable bucket/object ACL support for newly created buckets.
|
||||
acl_enabled: false
|
||||
|
||||
runtime:
|
||||
soft_memory_limit: 1gb
|
||||
|
@ -252,7 +271,3 @@ retry:
|
|||
max_backoff: 30s
|
||||
# Backoff strategy. `exponential` and `constant` are allowed.
|
||||
strategy: exponential
|
||||
|
||||
# Containers properties
|
||||
containers:
|
||||
cors: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
|
||||
|
|
|
@ -54,6 +54,11 @@ func (g *GateData) SessionTokenForDelete() *session.Container {
|
|||
return g.containerSessionToken(session.VerbContainerDelete)
|
||||
}
|
||||
|
||||
// SessionTokenForSetEACL returns the first suitable container session context for SetEACL operation.
|
||||
func (g *GateData) SessionTokenForSetEACL() *session.Container {
|
||||
return g.containerSessionToken(session.VerbContainerSetEACL)
|
||||
}
|
||||
|
||||
// SessionToken returns the first container session context.
|
||||
func (g *GateData) SessionToken() *session.Container {
|
||||
if len(g.SessionTokens) != 0 {
|
||||
|
@ -73,7 +78,7 @@ func (g *GateData) containerSessionToken(verb session.ContainerVerb) *session.Co
|
|||
|
||||
func isAppropriateContainerContext(tok *session.Container, verb session.ContainerVerb) bool {
|
||||
switch verb {
|
||||
case session.VerbContainerDelete, session.VerbContainerPut:
|
||||
case session.VerbContainerSetEACL, session.VerbContainerDelete, session.VerbContainerPut:
|
||||
return tok.AssertVerb(verb)
|
||||
default:
|
||||
return false
|
||||
|
|
|
@ -205,6 +205,18 @@ func TestGateDataSessionToken(t *testing.T) {
|
|||
require.Equal(t, sessionTknDelete, sessionTkn)
|
||||
})
|
||||
|
||||
t.Run("session token for set eACL", func(t *testing.T) {
|
||||
gate.SessionTokens = []*session.Container{}
|
||||
sessionTkn := gate.SessionTokenForSetEACL()
|
||||
require.Nil(t, sessionTkn)
|
||||
|
||||
sessionTknSetEACL := new(session.Container)
|
||||
sessionTknSetEACL.ForVerb(session.VerbContainerSetEACL)
|
||||
gate.SessionTokens = []*session.Container{sessionTknSetEACL}
|
||||
sessionTkn = gate.SessionTokenForSetEACL()
|
||||
require.Equal(t, sessionTknSetEACL, sessionTkn)
|
||||
})
|
||||
|
||||
t.Run("session token", func(t *testing.T) {
|
||||
gate.SessionTokens = []*session.Container{}
|
||||
sessionTkn := gate.SessionToken()
|
||||
|
|
|
@ -92,7 +92,6 @@ type FrostFS interface {
|
|||
//
|
||||
// It returns exactly one non-nil value. It returns any error encountered which
|
||||
// prevented the object payload from being read.
|
||||
// Object must contain full payload.
|
||||
GetCredsObject(context.Context, oid.Address) (*object.Object, error)
|
||||
}
|
||||
|
||||
|
|
|
@ -6,23 +6,23 @@ This document describes s3-gw authentication and authorization mechanism.
|
|||
|
||||
Basic provisions:
|
||||
|
||||
* A request to s3-gw can be signed or not (request that isn't signed we will call anonymous or just anon)
|
||||
* A request to s3-gw can be signed or not (request that isn't signed we will cal anonymous or just anon)
|
||||
* To manage resources (buckets/objects) using s3-gw you must have appropriate access rights
|
||||
|
||||
Each request must be authenticated (at least as anonymous) and authorized. The following scheme shows components that
|
||||
are involved in this
|
||||
are involved to this
|
||||
process.
|
||||
|
||||
<a>
|
||||
<img src="images/authentication/auth-overview.svg" alt="Auth general overview"/>
|
||||
</a>
|
||||
|
||||
There are several participants in this process:
|
||||
There are several participants of this process:
|
||||
|
||||
1. User that make a request
|
||||
2. S3-GW that accepts a request
|
||||
3. FrostFS Storage that stores AccessObjects (objects are needed for authentication)
|
||||
4. Blockchain smart contracts (`frostfsid`, `policy`) that store user info and access rules.
|
||||
4. Blockchain smart contracts (`frostfsid`, `policy`) that stores user info and access rules.
|
||||
|
||||
## Data auth process
|
||||
|
||||
|
@ -32,23 +32,23 @@ Let's look at the process in more detail:
|
|||
<img src="images/authentication/auth-sequence.svg" alt="Auth sequence diagram"/>
|
||||
</a>
|
||||
|
||||
* First of all, someone makes a request. If request is signed we will check its signature (`Authentication`) after that
|
||||
we will check access rights using policies (`Auhorization`). For anonymous requests only authorization is performed.
|
||||
* First of all, someone make a request. If request is signed we will check its signature (`Authentication`) after that
|
||||
we will check access rights using policies (`Auhorization`). For anonymous requests only authorization be performed.
|
||||
|
||||
* **Authentication steps**:
|
||||
* Each signed request is provided with `AccessKeyId` and signature. So if request is signed we must check its
|
||||
signature. To do this we must know the `AccessKeyId`/`SecretAccessKey` pair (For how the signature is calculated
|
||||
using this pair, see [signing](#aws-signing). Client and server (s3-gw) use the same credentials and algorithm to
|
||||
compute signature). The `AccessKeyId` is a public part of credentials, and it's passed to the gate in request. The
|
||||
signature. To do this we must know the `AccessKeyId`/`SecretAccessKey` pair (How the signature is calculated
|
||||
using this pair see [signing](#aws-signing). Client and server (s3-gw) use the same credentials and algorithm to
|
||||
compute signature). The `AccessKeyId` is a public part of credentials, and it's passed to gate in request. The
|
||||
private part of credentials is `SecretAccessKey` and it's encrypted and stored in [AccessBox](#accessbox). So on
|
||||
this step we must find appropriate `AccessBox` in FrostFS storage node (For how to find appropriate `AccessBox`
|
||||
knowing `AccessKeyId`, see [search algorithm](#search-algorithm)). On this stage we can get `AccessDenied` from
|
||||
this step we must find appropriate `AccessBox` in FrostFS storage node (How to find appropriate `AccessBox`
|
||||
knowing `AccessKeyId` see [search algorithm](#search-algorithm)). On this stage we can get `AccessDenied` from
|
||||
FrostFS storage node if the s3-gw doesn't have permission to read this `AccessBox` object.
|
||||
|
||||
* After successfully retrieving the object we must extract `SecretAccessKey` from it. Since it's encrypted, the s3-gw must
|
||||
decrypt (see [encryption](#encryption)) this object using its own private key and `SeedKey` from `AccessBox`
|
||||
(see [AccessBox inner structure](#accessbox)). After s3-gw got the `AccessKeyId`/`SecretAccessKey` pair it
|
||||
[calculates signature](#aws-signing) and compares this signature with one provided by the request. If signature doesn't
|
||||
* After successful retrieving object we must extract `SecretAccessKey` from it. Since it's encrypted the s3-gw must
|
||||
decrypt (see [encryption](#encryption)) this object using own private key and `SeedKey` from `AccessBox`
|
||||
(see [AccessBox inner structure](#accessbox)). After s3-gw have got the `AccessKeyId`/`SecretAccessKey` pair it
|
||||
[calculate signature](#aws-signing) and compare got signature with provided withing request. If signature doesn't
|
||||
match the `AccessDenied` is returned.
|
||||
|
||||
* `AccessBox` also contains `OwnerID` that is related to `AccessKeyId` that was provided. So we have to check if
|
||||
|
@ -63,7 +63,7 @@ Let's look at the process in more detail:
|
|||
|
||||
* After successful authentication and authorization the request will be processed by s3-gw business logic and finally be
|
||||
propagated to FrostFS storage node which also performs some auth checks and can return `AccessDenied`. If this happens
|
||||
s3-gw also returns `AccessDenied` as a response.
|
||||
s3-gw also returns `AccessDenied` as response.
|
||||
|
||||
### AWS Signing
|
||||
|
||||
|
@ -77,7 +77,7 @@ authentication with the AWS Signature Version 4 algorithm. More info in AWS docu
|
|||
|
||||
You can express authentication information by using one of the following methods:
|
||||
|
||||
* **HTTP Authorization header** - Using the HTTP Authorization header is the most common method of authenticating
|
||||
* **HTTP Authorization header** - Using the HTTP Authorization header is the most common method of authenticating an
|
||||
FrostFS S3 request. All the FrostFS S3 REST operations (except for browser-based uploads using POST requests) require
|
||||
this header. For more information about the Authorization header value, and how to calculate signature and related
|
||||
options,
|
||||
|
@ -114,7 +114,7 @@ parameters for authentication, you use a varying combination of request elements
|
|||
HTTP POST request, the POST policy in the request is the string you sign. For more information about computing string to
|
||||
sign, follow links provided at the end of this section.
|
||||
|
||||
For signing key, the diagram shows series of calculations, where the result of each step you feed into the next step. The
|
||||
For signing key, the diagram shows series of calculations, where result of each step you feed into the next step. The
|
||||
final step is the signing key.
|
||||
|
||||
Upon receiving an authenticated request, FrostFS S3 servers re-create the signature by using the authentication
|
||||
|
@ -139,7 +139,7 @@ See detains in [AWS documentation](https://docs.aws.amazon.com/AmazonS3/latest/A
|
|||
|
||||
#### s3-gw
|
||||
|
||||
s3-gw supports the following ways to provide the singed request:
|
||||
s3-gw support the following ways to provide the singed request:
|
||||
|
||||
* [HTTP Authorization header](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html)
|
||||
* [Query string parameters](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html)
|
||||
|
@ -153,10 +153,10 @@ if they don't match the access denied is returned.
|
|||
### AccessBox
|
||||
|
||||
`AccessBox` is an ordinary object in FrostFS storage. It contains all information that can be used by s3-gw to
|
||||
successfully authenticate request. Also, it contains data that is required for successful authentication in FrostFS
|
||||
successfully authenticate request. Also, it contains data that is required to successful authentication in FrostFS
|
||||
storage node.
|
||||
|
||||
Object s3 credentials are formed based on:
|
||||
Based on this object s3 credentials are formed:
|
||||
|
||||
* `AccessKeyId` - is concatenated container id and object id (`<cid>0<oid>`) of `AccessBox` (
|
||||
e.g. `2XGRML5EW3LMHdf64W2DkBy1Nkuu4y4wGhUj44QjbXBi05ZNvs8WVwy1XTmSEkcVkydPKzCgtmR7U3zyLYTj3Snxf`)
|
||||
|
@ -173,9 +173,9 @@ Object s3 credentials are formed based on:
|
|||
|
||||
**Headers:**
|
||||
|
||||
`AccessBox` object has the following attributes (at least them, it also can contain custom ones):
|
||||
`AccessBox` object has the following attributes (at least them, it also can contain custom one):
|
||||
|
||||
* `Timestamp` - unix timestamp indicating when the object was created
|
||||
* `Timestamp` - unix timestamp when object was created
|
||||
* `__SYSTEM__EXPIRATION_EPOCH` - epoch after which the object isn't available anymore
|
||||
* `S3-CRDT-Versions-Add` - comma separated list of previous versions of `AccessBox` (
|
||||
see [AccessBox versions](#accessbox-versions))
|
||||
|
@ -190,7 +190,7 @@ It contains:
|
|||
|
||||
* Seed key - hex-encoded public seed key to compute shared secret using ECDH (see [encryption](#encryption))
|
||||
* List of gate data:
|
||||
* Gate public key (so that gate (when it will decrypt data later) know which item from the list it should process)
|
||||
* Gate public key (so that gate (when it will decrypt data later) know which one item from list it should process)
|
||||
* Encrypted tokens:
|
||||
* `SecretAccessKey` - hex-encoded random generated 32 bytes
|
||||
* Marshaled bearer token - more detail
|
||||
|
@ -207,16 +207,16 @@ It contains:
|
|||
|
||||
Imagine the following scenario:
|
||||
|
||||
* There is a system where only one s3-gw exists
|
||||
* There is an `AccessBox` that can be used by this s3-gw
|
||||
* User has s3 credentials (`AccessKeyId`/`SecretAccessKey`) related to corresponding `AccessBox` and can successfully
|
||||
* There is a system where only one s3-gw exist
|
||||
* There is a `AccessBox` that can be used by this s3-gw
|
||||
* User has s3 credentials (`AccessKeyId`/`SecretAccessKey`) related to corresponded `AccessBox` and can successfully
|
||||
make request to s3-gw
|
||||
* The system is expanded and a new s3-gw is added
|
||||
* User must be able to use the credentials (that he has already had) to make request to the new s3-gw
|
||||
* The system is expanded and new one s3-gw is added
|
||||
* User must be able to use the credentials (that he has already had) to make request to new one s3-gw
|
||||
|
||||
Since `AccessBox` object is immutable and `SecretAccessKey` is encrypted only for restricted list of keys (can be used
|
||||
(decrypted) only by limited number of s3-gw) we have to create a new `AccessBox` that has encrypted secrets for a new list
|
||||
of s3-gw and is related to the initial s3 credentials (`AccessKeyId`/`SecretAccessKey`). Such relation is done
|
||||
(decrypted) only by limited number of s3-gw) we have to create new `AccessBox` that has encrypted secrets for new list
|
||||
of s3-gw and be related to initial s3 credentials (`AccessKeyId`/`SecretAccessKey`). Such relationship is done
|
||||
by `S3-Access-Box-CRDT-Name`.
|
||||
|
||||
##### Search algorithm
|
||||
|
@ -285,10 +285,10 @@ is performed the following algorithm is applied:
|
|||
* If no rules were matched return `deny` status.
|
||||
|
||||
To local and contract policies `deny first` scheme is applied. This means that if several rules were matched for
|
||||
reqeust (with both statuses `allow` and `deny`) the resulting status is `deny`.
|
||||
reqeust (with both statuses `allow` and `deny`) the resulting status be `deny`.
|
||||
|
||||
Policy rules validate if specified request can be performed on the specific resource. Request and resource can contain
|
||||
some properties, and rules can contain conditions on some of these properties.
|
||||
some properties and rules can contain conditions on some such properties.
|
||||
|
||||
In s3-gw resource is `/bucket/object`, `/bucket` or just `/` (if request is trying to list buckets).
|
||||
Currently, request that is checked contains the following properties (so policy rule can contain conditions on them):
|
||||
|
|
246
docs/authmate.md
246
docs/authmate.md
|
@ -20,10 +20,10 @@ potentially).
|
|||
|
||||
1. [Generation of wallet](#generation-of-wallet)
|
||||
2. [Issuance of a secret](#issuance-of-a-secret)
|
||||
1. [CLI parameters](#cli-parameters)
|
||||
2. [Bearer tokens](#bearer-tokens)
|
||||
3. [Session tokens](#session-tokens)
|
||||
4. [Containers policy](#containers-policy)
|
||||
1. [CLI parameters](#cli-parameters)
|
||||
2. [Bearer tokens](#bearer-tokens)
|
||||
3. [Session tokens](#session-tokens)
|
||||
4. [Containers policy](#containers-policy)
|
||||
3. [Obtainment of a secret](#obtaining-credential-secrets)
|
||||
4. [Generate presigned url](#generate-presigned-url)
|
||||
5. [Update secrets](#update-secret)
|
||||
|
@ -75,7 +75,6 @@ wallet is successfully created, the file location is wallet.json
|
|||
```
|
||||
|
||||
To get the public key from the wallet:
|
||||
|
||||
```shell
|
||||
$ ./bin/neo-go wallet dump-keys -w wallet.json
|
||||
|
||||
|
@ -91,25 +90,22 @@ put them as an object into a container on the FrostFS network.
|
|||
### CLI parameters
|
||||
|
||||
**Required parameters:**
|
||||
|
||||
* `--wallet` is a path to a wallet `.json` file. You can provide a passphrase to decrypt
|
||||
a wallet via environment variable `AUTHMATE_WALLET_PASSPHRASE`, or you will be asked to enter a passphrase
|
||||
interactively. You can also specify an account address to use from a wallet using the `--address` parameter.
|
||||
a wallet via environment variable `AUTHMATE_WALLET_PASSPHRASE`, or you will be asked to enter a passphrase
|
||||
interactively. You can also specify an account address to use from a wallet using the `--address` parameter.
|
||||
* `--peer` is an address of a FrostFS peer to connect to
|
||||
* `--gate-public-key` is a public `secp256r1` 33-byte short key of a gate (use flags repeatedly for multiple gates). The
|
||||
tokens are encrypted by a set of gateway keys, so you need to pass them as well.
|
||||
* `--gate-public-key` is a public `secp256r1` 33-byte short key of a gate (use flags repeatedly for multiple gates). The tokens are encrypted
|
||||
by a set of gateway keys, so you need to pass them as well.
|
||||
|
||||
You can issue a secret using the parameters above only. The tool will
|
||||
|
||||
1. create a new container
|
||||
1. without a friendly name
|
||||
2. with ACL `0x3c8c8cce` -- all operations are forbidden for `OTHERS` and `BEARER` user groups, except for `GET`
|
||||
3. with policy `REP 2 IN X CBF 3 SELECT 2 FROM * AS X`
|
||||
2. put bearer and session tokens with default rules (details in [Bearer tokens](#bearer-tokens) and
|
||||
[Session tokens](#session-tokens))
|
||||
1. create a new container
|
||||
1. without a friendly name
|
||||
2. with ACL `0x3c8c8cce` -- all operations are forbidden for `OTHERS` and `BEARER` user groups, except for `GET`
|
||||
3. with policy `REP 2 IN X CBF 3 SELECT 2 FROM * AS X`
|
||||
2. put bearer and session tokens with default rules (details in [Bearer tokens](#Bearer tokens) and
|
||||
[Session tokens](#Session tokens))
|
||||
|
||||
E.g.:
|
||||
|
||||
```shell
|
||||
$ frostfs-s3-authmate issue-secret --wallet wallet.json \
|
||||
--peer 192.168.130.71:8080 \
|
||||
|
@ -132,72 +128,134 @@ $ frostfs-s3-authmate issue-secret --wallet wallet.json \
|
|||
`initial_access_key_id` contains the first credentials in the chain of credentials versions
|
||||
(can be useful when you update your credentials).
|
||||
|
||||
`access_key_id` consists of Base58 encoded containerID(cid) and objectID(oid) stored on the FrostFS network and
|
||||
containing
|
||||
`access_key_id` consists of Base58 encoded containerID(cid) and objectID(oid) stored on the FrostFS network and containing
|
||||
the secret. Format of `access_key_id`: `%cid0%oid`, where 0(zero) is a delimiter.
|
||||
|
||||
**Optional parameters:**
|
||||
|
||||
* `--container-id` - you can put the tokens into an existing container, but this way is ***not recommended***.
|
||||
* `--container-friendly-name` -- name of a container with tokens, by default container will not have a friendly name
|
||||
* `--container-placement-policy` - placement policy of auth container to put the secret into. Default value is
|
||||
`REP 2 IN X CBF 3 SELECT 2 FROM * AS X`
|
||||
* `--lifetime`-- lifetime of tokens. For example 50h30m (note: max time unit is an hour so to set a day you should use
|
||||
24h). Default value is `720h` (30 days). It will be ceil rounded to the nearest amount of epoch
|
||||
* `--container-placement-policy` - placement policy of auth container to put the secret into. Default value is
|
||||
`REP 2 IN X CBF 3 SELECT 2 FROM * AS X`
|
||||
* `--lifetime`-- lifetime of tokens. For example 50h30m (note: max time unit is an hour so to set a day you should use
|
||||
24h). Default value is `720h` (30 days). It will be ceil rounded to the nearest amount of epoch
|
||||
* `--aws-cli-credentials` - path to the aws cli credentials file, where authmate will write `access_key_id` and
|
||||
`secret_access_key` to
|
||||
`secret_access_key` to
|
||||
* `--access-key-id` -- credentials that you want to update (e.g. to add more gates that can use your creds)
|
||||
without changing values of `aws_access_key_id` and `aws_secret_access_key`. If you want to update credential you MUST
|
||||
provide also secret key using `AUTHMATE_SECRET_ACCESS_KEY` env variable.
|
||||
* `--frostfsid` -- FrostfsID contract hash (LE) or name in NNS to register public key in contract
|
||||
(`--rpc-endpoint` flag also must be provided).
|
||||
* `--rpc-endpoint` -- NEO node RPC address.
|
||||
|
||||
### Bearer tokens
|
||||
|
||||
Creation of bearer tokens is mandatory.
|
||||
|
||||
Bearer token will be created with `impersonate` flag. It means that gate (which will use such token to interact with
|
||||
node) can have access to your private containers or to containers in which eACL grants access to you by public key.
|
||||
By default, bearer token will be created with `impersonate` flag and won't have eACL table. It means that gate which will use such token
|
||||
to interact with node can have access to your private containers or to containers in which eACL grants access to you
|
||||
by public key.
|
||||
|
||||
Rules for a bearer token can be set via parameter `--bearer-rules` (json-string and file path allowed).
|
||||
But you must provide `--disable-impersonate` flag:
|
||||
|
||||
```shell
|
||||
$ frostfs-s3-authmate issue-secret --wallet wallet.json \
|
||||
--peer 192.168.130.71:8080 \
|
||||
--gate-public-key 0313b1ac3a8076e155a7e797b24f0b650cccad5941ea59d7cfd51a024a8b2a06bf \
|
||||
--bearer-rules bearer-rules.json \
|
||||
--disable-impersonate
|
||||
```
|
||||
where content of `bearer-rules.json`:
|
||||
```json
|
||||
{
|
||||
"records": [
|
||||
{"operation": "PUT", "action": "ALLOW", "filters": [], "targets": [{"role": "OTHERS", "keys": []}]},
|
||||
{"operation": "GET", "action": "ALLOW", "filters": [], "targets": [{"role": "OTHERS", "keys": []}]},
|
||||
{"operation": "HEAD", "action": "ALLOW", "filters": [], "targets": [{"role": "OTHERS", "keys": []}]},
|
||||
{"operation": "DELETE", "action": "ALLOW", "filters": [], "targets": [{"role": "OTHERS", "keys": []}]},
|
||||
{"operation": "SEARCH", "action": "ALLOW", "filters": [], "targets": [{"role": "OTHERS", "keys": []}]},
|
||||
{"operation": "GETRANGE", "action": "ALLOW", "filters": [], "targets": [{"role": "OTHERS", "keys": []}]},
|
||||
{"operation": "GETRANGEHASH", "action": "ALLOW", "filters": [], "targets": [{"role": "OTHERS", "keys": []}]}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** such rules allow all operations for all users (the same behavior when records are empty).
|
||||
To restrict access you MUST provide records with `DENY` action. That's why we recommend always place such records
|
||||
at the end of records (see default rules below) to prevent undesirable access violation.
|
||||
Since the rules are applied from top to bottom, they do not override what was previously allowed.
|
||||
|
||||
If bearer rules are not set, a token will be auto-generated with a value:
|
||||
```json
|
||||
{
|
||||
"version": {
|
||||
"major": 2,
|
||||
"minor": 11
|
||||
},
|
||||
"containerID": {
|
||||
"value": null
|
||||
},
|
||||
"records": [
|
||||
{"operation": "GET", "action": "ALLOW", "filters": [], "targets": [{"role": "OTHERS", "keys": []}]},
|
||||
|
||||
{"operation": "GET", "action": "DENY", "filters": [], "targets": [{"role": "OTHERS", "keys": []}]},
|
||||
{"operation": "HEAD", "action": "DENY", "filters": [], "targets": [{"role": "OTHERS", "keys": []}]},
|
||||
{"operation": "PUT", "action": "DENY", "filters": [], "targets": [{"role": "OTHERS", "keys": []}]},
|
||||
{"operation": "DELETE", "action": "DENY", "filters": [], "targets": [{"role": "OTHERS", "keys": []}]},
|
||||
{"operation": "SEARCH", "action": "DENY", "filters": [], "targets": [{"role": "OTHERS", "keys": []}]},
|
||||
{"operation": "GETRANGE", "action": "DENY", "filters": [], "targets": [{"role": "OTHERS", "keys": []}]},
|
||||
{"operation": "GETRANGEHASH", "action": "DENY", "filters": [], "targets": [{"role": "OTHERS", "keys": []}]}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Session tokens
|
||||
|
||||
With a session token, there are 3 options:
|
||||
|
||||
1. append `--session-tokens` parameter with your custom rules in json format (as a string or file path). E.g.:
|
||||
```shell
|
||||
$ frostfs-s3-authmate issue-secret --wallet wallet.json \
|
||||
--peer 192.168.130.71:8080 \
|
||||
--gate-public-key 0313b1ac3a8076e155a7e797b24f0b650cccad5941ea59d7cfd51a024a8b2a06bf \
|
||||
--session-tokens session.json
|
||||
```
|
||||
where content of `session.json`:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"verb": "PUT",
|
||||
"containerID": null
|
||||
},
|
||||
{
|
||||
"verb": "DELETE",
|
||||
"containerID": null
|
||||
}
|
||||
]
|
||||
```
|
||||
```shell
|
||||
$ frostfs-s3-authmate issue-secret --wallet wallet.json \
|
||||
--peer 192.168.130.71:8080 \
|
||||
--gate-public-key 0313b1ac3a8076e155a7e797b24f0b650cccad5941ea59d7cfd51a024a8b2a06bf \
|
||||
--session-tokens session.json
|
||||
```
|
||||
where content of `session.json`:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"verb": "PUT",
|
||||
"containerID": null
|
||||
},
|
||||
{
|
||||
"verb": "DELETE",
|
||||
"containerID": null
|
||||
},
|
||||
{
|
||||
"verb": "SETEACL",
|
||||
"containerID": null
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Available `verb` values: `PUT`, `DELETE`.
|
||||
Available `verb` values: `PUT`, `DELETE`, `SETEACL`.
|
||||
|
||||
If `containerID` is `null` or omitted, then session token rule will be applied
|
||||
to all containers. Otherwise, specify `containerID` value in human-readable
|
||||
format (base58 encoded string).
|
||||
If `containerID` is `null` or omitted, then session token rule will be applied
|
||||
to all containers. Otherwise, specify `containerID` value in human-redabale
|
||||
format (base58 encoded string).
|
||||
|
||||
> **_NB!_** To create buckets in FrostFS it's necessary to have session tokens with `PUT` permissions.
|
||||
> **_NB!_** To create buckets in FrostFS it's necessary to have session tokens with `PUT` and `SETEACL` permissions, that's why
|
||||
the authmate creates a `SETEACL` session token automatically in case when a user specified the token rule with `PUT` and
|
||||
forgot about the rule with `SETEACL`.
|
||||
|
||||
2. append `--session-tokens` parameter with the value `none` -- no session token will be created
|
||||
3. skip the parameter, and `authmate` will create session tokens with default rules (the same as in `session.json`
|
||||
in example above)
|
||||
in example above)
|
||||
|
||||
### Containers policy
|
||||
|
||||
Rules for mapping
|
||||
of `LocationConstraint` ([aws spec](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateBucket.html#API_CreateBucket_RequestBody))
|
||||
Rules for mapping of `LocationConstraint` ([aws spec](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateBucket.html#API_CreateBucket_RequestBody))
|
||||
to `PlacementPolicy`
|
||||
can be set via parameter `--container-policy` (json-string and file path allowed):
|
||||
|
||||
```json
|
||||
{
|
||||
"rep-3": "REP 3",
|
||||
|
@ -206,57 +264,6 @@ can be set via parameter `--container-policy` (json-string and file path allowed
|
|||
}
|
||||
```
|
||||
|
||||
## User registration
|
||||
|
||||
To be able to interact with FrostFS Storage using s3-gw the user (whose credential is used) must be registered in
|
||||
`frostfsid` contract and also must have allowed chain rules in `policy` contract.
|
||||
|
||||
### CLI parameters
|
||||
|
||||
**Required parameters:**
|
||||
|
||||
* `--wallet` is a path to a wallet `.json` file. You can provide a passphrase to decrypt
|
||||
a wallet via environment variable `AUTHMATE_WALLET_PASSPHRASE`, or you will be asked to enter a passphrase
|
||||
interactively. You can also specify an account address to use from a wallet using the `--address` parameter. Key from
|
||||
this wallet will be registered in `frostfsid` contract and for this key allowed rules will be added.
|
||||
* `--rpc-endpoint` -- NEO node RPC address.
|
||||
|
||||
**Optional parameters:**
|
||||
|
||||
* `--frostfsid-contract` -- FrostfsID contract hash (LE) or name in NNS to register public key in contract (
|
||||
default: `frostfsid.frostfs`).
|
||||
* `--username` -- Username to set for public key in frostfsid contract.
|
||||
* `--namespace` -- Namespace to register public key in frostfsid contract and add allowed rules (default: `""`).
|
||||
* `--contract-wallet` -- is a path to a contract wallet `.json` file. This wallet will be used to sign transactions
|
||||
(if missing key from wallet flag be used). You can provide a passphrase to decrypt
|
||||
a wallet via environment variable `AUTHMATE_WALLET_CONTRACT_PASSPHRASE`, or you will be asked to enter a passphrase
|
||||
interactively. You can also specify an account address to use from a wallet using the `--contract-wallet-address`
|
||||
parameter.
|
||||
* `--policy-contract` -- Policy contract hash (LE) or name in NNS to save allowed chains for key (
|
||||
default: `policy.frostfs`).
|
||||
* `--proxy-contract` -- Proxy contract hash (LE) or name in NNS to use when interact with frostfsid contract (
|
||||
default: `proxy.frostfs`).
|
||||
|
||||
Example command that not only create access keys for key, but also register key in frostfsid contract and
|
||||
create allowed rules in policy contract:
|
||||
|
||||
```shell
|
||||
$ frostfs-s3-authmate register-user --wallet subject-wallet.json \
|
||||
--contract-wallet wallet-registered-in-proxy-contract.json \
|
||||
--frostfsid-contract frostfsid.frostfs \
|
||||
--namespace "" \
|
||||
--usrername devenv \
|
||||
--proxy-contract proxy.frostfs \
|
||||
--policy-contract policy.frostfs \
|
||||
--rpc-endpoint http://morph-chain.frostfs.devenv:30333
|
||||
|
||||
Added policy rules:
|
||||
kind: 'u'
|
||||
entity: ':NbUgTSFvPmsRxmGeWpuuGeJUoRoi6PErcM'
|
||||
chains:
|
||||
[{"ID":"czM6YXV0aG1hdGU=","Rules":[{"Status":"Allow","Actions":{"Inverted":false,"Names":["*"]},"Resources":{"Inverted":false,"Names":["*"]},"Any":false,"Condition":null}],"MatchType":"DenyPriority"},{"ID":"aW5ncmVzczphdXRobWF0ZQ==","Rules":[{"Status":"Allow","Actions":{"Inverted":false,"Names":["*"]},"Resources":{"Inverted":false,"Names":["*"]},"Any":false,"Condition":null}],"MatchType":"DenyPriority"}]
|
||||
```
|
||||
|
||||
## Obtaining credential secrets
|
||||
|
||||
You can get a secret access key and bearer token associated with an access key ID by obtaining a
|
||||
|
@ -295,6 +302,7 @@ Enter password for gate-wallet.json >
|
|||
}
|
||||
```
|
||||
|
||||
|
||||
## Generate presigned URL
|
||||
|
||||
You can generate [presigned url](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html)
|
||||
|
@ -324,7 +332,6 @@ $ frostfs-s3-authmate generate-presigned-url --endpoint http://localhost:8084 \
|
|||
```
|
||||
|
||||
### AWS CLI
|
||||
|
||||
You can also can get the presigned URL (only for GET) using aws cli v2:
|
||||
|
||||
```shell
|
||||
|
@ -334,22 +341,19 @@ http://localhost:8084/pregigned/obj?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Crede
|
|||
```
|
||||
|
||||
## Update secret
|
||||
|
||||
You can extend list of s3 gates that can accept already issued credentials.
|
||||
To do this use `frostfs-s3-authmate update-secret` command:
|
||||
|
||||
**Required parameters:**
|
||||
|
||||
* `--wallet` is a path to a user wallet `.json` file. You can provide a passphrase to decrypt
|
||||
a wallet via environment variable `AUTHMATE_WALLET_PASSPHRASE`, or you will be asked to enter a passphrase
|
||||
interactively. You can also specify an account address to use from a wallet using the `--address` parameter.
|
||||
* `--gate-wallet` is a path to a gate wallet `.json` file (need to decrypt current access box version). You can provide
|
||||
a passphrase to decrypt
|
||||
* `--gate-wallet` is a path to a gate wallet `.json` file (need to decrypt current access box version). You can provide a passphrase to decrypt
|
||||
a wallet via environment variable `AUTHMATE_WALLET_GATE_PASSPHRASE`, or you will be asked to enter a passphrase
|
||||
interactively. You can also specify an account address to use from a wallet using the `--gate-address` parameter.
|
||||
* `--peer` is an address of a FrostFS peer to connect to
|
||||
* `--gate-public-key` is a public `secp256r1` 33-byte short key of a gate (use flags repeatedly for multiple gates).
|
||||
* `--access-key-id` is a credential id to update.
|
||||
* `--gate-public-key` is a public `secp256r1` 33-byte short key of a gate (use flags repeatedly for multiple gates).
|
||||
* `--access-key-id` is a credential id to update.
|
||||
|
||||
```shell
|
||||
$ frostfs-s3-authmate update-secret --wallet wallet.json --gate-wallet s3-wallet.json \
|
||||
|
@ -376,9 +380,9 @@ Enter password for s3-wallet.json >
|
|||
|
||||
There are several non-zero exit codes added at the moment.
|
||||
|
||||
| Code | Description |
|
||||
|------|--------------------------------------------------------------------------------------------|
|
||||
| 1 | Any unknown errors, or errors generated by the parser of command line parameters. |
|
||||
| 2 | Preparation errors: malformed configuration, issues with input data parsing. |
|
||||
| 3 | FrostFS errors: connectivity problems, misconfiguration. |
|
||||
| 4 | Business logic errors: `authmate` could not execute its task because of some restrictions. |
|
||||
| Code | Description |
|
||||
|-------|--------------------------------------------------------------------------------------------|
|
||||
| 1 | Any unknown errors, or errors generated by the parser of command line parameters. |
|
||||
| 2 | Preparation errors: malformed configuration, issues with input data parsing. |
|
||||
| 3 | FrostFS errors: connectivity problems, misconfiguration. |
|
||||
| 4 | Business logic errors: `authmate` could not execute its task because of some restrictions. |
|
||||
|
|
|
@ -175,8 +175,10 @@ There are some custom types used for brevity:
|
|||
| `peers` | [Nodes configuration](#peers-section) |
|
||||
| `placement_policy` | [Placement policy configuration](#placement_policy-section) |
|
||||
| `server` | [Server configuration](#server-section) |
|
||||
| `control` | [Control API configuration](#control-section) |
|
||||
| `logger` | [Logger configuration](#logger-section) |
|
||||
| `cache` | [Cache configuration](#cache-section) |
|
||||
| `nats` | [NATS configuration](#nats-section) |
|
||||
| `cors` | [CORS configuration](#cors-section) |
|
||||
| `pprof` | [Pprof configuration](#pprof-section) |
|
||||
| `prometheus` | [Prometheus configuration](#prometheus-section) |
|
||||
|
@ -192,7 +194,6 @@ There are some custom types used for brevity:
|
|||
| `proxy` | [Proxy contract configuration](#proxy-section) |
|
||||
| `namespaces` | [Namespaces configuration](#namespaces-section) |
|
||||
| `retry` | [Retry configuration](#retry-section) |
|
||||
| `containers` | [Containers configuration](#containers-section) |
|
||||
|
||||
### General section
|
||||
|
||||
|
@ -361,6 +362,24 @@ server:
|
|||
| `tls.cert_file` | `string` | yes | | Path to the TLS certificate. |
|
||||
| `tls.key_file` | `string` | yes | | Path to the key. |
|
||||
|
||||
### `control` section
|
||||
|
||||
Control API parameters.
|
||||
|
||||
```yaml
|
||||
control:
|
||||
authorized_keys:
|
||||
- 035839e45d472a3b7769a2a1bd7d54c4ccd4943c3b40f547870e83a8fcbfb3ce11
|
||||
- 028f42cfcb74499d7b15b35d9bff260a1c8d27de4f446a627406a382d8961486d6
|
||||
grpc:
|
||||
endpoint: localhost:8083
|
||||
```
|
||||
|
||||
| Parameter | Type | SIGHUP reload | Default value | Description |
|
||||
|-------------------|------------|---------------|------------------|------------------------------------------------------------------------------|
|
||||
| `authorized_keys` | `[]string` | yes | | List of hex-encoded public keys that have rights to use the Control Service. |
|
||||
| `grpc.endpoint` | `string` | | `localhost:8083` | Endpoint that is listened by the Control Service. |
|
||||
|
||||
### `logger` section
|
||||
|
||||
```yaml
|
||||
|
@ -411,18 +430,18 @@ cache:
|
|||
size: 10000
|
||||
```
|
||||
|
||||
| Parameter | Type | Default value | Description |
|
||||
|-----------------|-------------------------------------------------|-----------------------------------|----------------------------------------------------------------|
|
||||
| `objects` | [Cache config](#cache-subsection) | `lifetime: 5m`<br>`size: 1000000` | Cache for objects (FrostFS headers). |
|
||||
| `list` | [Cache config](#cache-subsection) | `lifetime: 60s`<br>`size: 100000` | Cache which keeps lists of objects in buckets. |
|
||||
| `list_session` | [Cache config](#cache-subsection) | `lifetime: 60s`<br>`size: 100` | Cache which keeps listing session. |
|
||||
| `names` | [Cache config](#cache-subsection) | `lifetime: 60s`<br>`size: 10000` | Cache which contains mapping of nice name to object addresses. |
|
||||
| `buckets` | [Cache config](#cache-subsection) | `lifetime: 60s`<br>`size: 1000` | Cache which contains mapping of bucket name to bucket info. |
|
||||
| `system` | [Cache config](#cache-subsection) | `lifetime: 5m`<br>`size: 10000` | Cache for system objects in a bucket: bucket settings etc. |
|
||||
| `accessbox` | [Accessbox cache config](#accessbox-subsection) | `lifetime: 10m`<br>`size: 100` | Cache which stores access box with tokens by its address. |
|
||||
| `accesscontrol` | [Cache config](#cache-subsection) | `lifetime: 1m`<br>`size: 100000` | Cache which stores owner to cache operation mapping. |
|
||||
| `morph_policy` | [Cache config](#cache-subsection) | `lifetime: 1m`<br>`size: 10000` | Cache which stores list of policy chains. |
|
||||
| `frostfsid` | [Cache config](#cache-subsection) | `lifetime: 1m`<br>`size: 10000` | Cache which stores FrostfsID subject info. |
|
||||
| Parameter | Type | Default value | Description |
|
||||
|-----------------|-------------------------------------------------|-----------------------------------|----------------------------------------------------------------------------------------|
|
||||
| `objects` | [Cache config](#cache-subsection) | `lifetime: 5m`<br>`size: 1000000` | Cache for objects (FrostFS headers). |
|
||||
| `list` | [Cache config](#cache-subsection) | `lifetime: 60s`<br>`size: 100000` | Cache which keeps lists of objects in buckets. |
|
||||
| `list_session` | [Cache config](#cache-subsection) | `lifetime: 60s`<br>`size: 100` | Cache which keeps listing session. |
|
||||
| `names` | [Cache config](#cache-subsection) | `lifetime: 60s`<br>`size: 10000` | Cache which contains mapping of nice name to object addresses. |
|
||||
| `buckets` | [Cache config](#cache-subsection) | `lifetime: 60s`<br>`size: 1000` | Cache which contains mapping of bucket name to bucket info. |
|
||||
| `system` | [Cache config](#cache-subsection) | `lifetime: 5m`<br>`size: 10000` | Cache for system objects in a bucket: bucket settings, notification configuration etc. |
|
||||
| `accessbox` | [Accessbox cache config](#accessbox-subsection) | `lifetime: 10m`<br>`size: 100` | Cache which stores access box with tokens by its address. |
|
||||
| `accesscontrol` | [Cache config](#cache-subsection) | `lifetime: 1m`<br>`size: 100000` | Cache which stores owner to cache operation mapping. |
|
||||
| `morph_policy` | [Cache config](#cache-subsection) | `lifetime: 1m`<br>`size: 10000` | Cache which stores list of policy chains. |
|
||||
| `frostfsid` | [Cache config](#cache-subsection) | `lifetime: 1m`<br>`size: 10000` | Cache which stores FrostfsID subject info. |
|
||||
|
||||
#### `cache` subsection
|
||||
|
||||
|
@ -450,6 +469,36 @@ size: 100
|
|||
| `lifetime` | `duration` | '10m' | Lifetime of entries in cache. |
|
||||
| `size` | `int` | '100 | LRU cache size. |
|
||||
|
||||
### `nats` section
|
||||
|
||||
This is an advanced section, use with caution.
|
||||
You can turn on notifications about successful completions of basic operations, and the gateway will send notifications
|
||||
via NATS JetStream.
|
||||
|
||||
1. to configure the NATS server with JetStream
|
||||
2. to specify NATS parameters for the S3 GW. It's ***necessary*** to define a values of `nats.enable` or
|
||||
`S3_GW_NATS_ENABLED` as `True`
|
||||
3. to configure notifications in a bucket
|
||||
|
||||
```yaml
|
||||
nats:
|
||||
enabled: true
|
||||
endpoint: nats://localhost:4222
|
||||
timeout: 30s
|
||||
cert_file: /path/to/cert
|
||||
key_file: /path/to/key
|
||||
root_ca: /path/to/ca
|
||||
```
|
||||
|
||||
| Parameter | Type | Default value | Description |
|
||||
|---------------|------------|---------------|------------------------------------------------------|
|
||||
| `enabled` | `bool` | `false` | Flag to enable the service. |
|
||||
| `endpoint` | `string` | | NATS endpoint to connect to. |
|
||||
| `timeout` | `duration` | `30s` | Timeout for the object notification operation. |
|
||||
| `certificate` | `string` | | Path to the client certificate. |
|
||||
| `key` | `string` | | Path to the client key. |
|
||||
| `ca` | `string` | | Override root CA used to verify server certificates. |
|
||||
|
||||
### `cors` section
|
||||
|
||||
```yaml
|
||||
|
@ -559,6 +608,7 @@ kludge:
|
|||
use_default_xmlns: false
|
||||
bypass_content_encoding_check_in_chunks: false
|
||||
default_namespaces: [ "", "root" ]
|
||||
acl_enabled: false
|
||||
```
|
||||
|
||||
| Parameter | Type | SIGHUP reload | Default value | Description |
|
||||
|
@ -566,6 +616,7 @@ kludge:
|
|||
| `use_default_xmlns` | `bool` | yes | `false` | Enable using default xml namespace `http://s3.amazonaws.com/doc/2006-03-01/` when parse xml bodies. |
|
||||
| `bypass_content_encoding_check_in_chunks` | `bool` | yes | `false` | Use this flag to be able to use [chunked upload approach](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html) without having `aws-chunked` value in `Content-Encoding` header. |
|
||||
| `default_namespaces` | `[]string` | yes | `["","root"]` | Namespaces that should be handled as default. |
|
||||
| `acl_enabled` | `bool` | yes | `false` | Enable bucket/object ACL support for newly created buckets. |
|
||||
|
||||
# `runtime` section
|
||||
Contains runtime parameters.
|
||||
|
@ -709,15 +760,3 @@ retry:
|
|||
| `max_backoff` | `duration` | yes | `30s` | Max delay before next attempt. |
|
||||
| `strategy` | `string` | yes | `exponential` | Backoff strategy. `exponential` and `constant` are allowed. |
|
||||
|
||||
# `containers` section
|
||||
|
||||
Section for well-known containers to store s3-related data and settings.
|
||||
|
||||
```yaml
|
||||
containers:
|
||||
cors: AZjLTXfK4vs4ovxMic2xEJKSymMNLqdwq9JT64ASFCRj
|
||||
```
|
||||
|
||||
| Parameter | Type | SIGHUP reload | Default value | Description |
|
||||
|-----------|----------|---------------|---------------|--------------------------------------------------------------------------------------|
|
||||
| `cors` | `string` | no | | Container name for CORS configurations. If not set, container of the bucket is used. |
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
# S3 API Extension
|
||||
|
||||
## Bucket operations management
|
||||
|
||||
### Action to delete bucket (DeleteBucket)
|
||||
|
||||
Deletes bucket with all objects in it.
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
- **Bucket**
|
||||
|
||||
Specifies the bucket being deleted.
|
||||
|
||||
|
||||
#### Errors
|
||||
|
||||
- **NoSuchEntity**
|
||||
|
||||
The request was rejected because it referenced a resource entity that does not exist.
|
||||
|
||||
HTTP Status Code: 404
|
||||
|
||||
- **ServiceFailure**
|
||||
|
||||
The request processing has failed because of an unknown error, exception or failure.
|
||||
|
||||
HTTP Status Code: 500
|
||||
|
||||
|
||||
#### Example
|
||||
|
||||
Sample Request
|
||||
|
||||
```text
|
||||
DELETE / HTTP/1.1
|
||||
X-Amz-Force-Delete-Bucket: true
|
||||
Host: data.s3.<Region>.frostfs-s3-gw.com
|
||||
Date: Wed, 01 Mar 2024 12:00:00 GMT
|
||||
Authorization: authorization string
|
||||
```
|
||||
|
||||
Sample Response
|
||||
|
||||
```text
|
||||
HTTP/1.1 204 No Content
|
||||
x-amz-id-2: JuKZqmXuiwFeDQxhD7M8KtsKobSzWA1QEjLbTMTagkKdBX2z7Il/jGhDeJ3j6s80
|
||||
x-amz-request-id: 32FE2CEB32F5EE25
|
||||
Date: Wed, 01 Mar 2006 12:00:00 GMT
|
||||
Connection: close
|
||||
Server: AmazonS3
|
||||
```
|
|
@ -13,5 +13,6 @@ Each node keeps one of the types of data as a set of **key-value pairs**:
|
|||
|
||||
Some data takes up a lot of memory, so we store it in FrostFS nodes as an object with payload.
|
||||
But we keep these objects' metadata in the Tree service too:
|
||||
* Notification configuration
|
||||
* CORS
|
||||
* Metadata of parts of active multipart uploads
|
||||
|
|
56
go.mod
56
go.mod
|
@ -1,38 +1,39 @@
|
|||
module git.frostfs.info/TrueCloudLab/frostfs-s3-gw
|
||||
|
||||
go 1.21
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240716113920-f517e3949164
|
||||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240621131249-49e5270f673e
|
||||
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240530152826-2f6d3209e1d3
|
||||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240409115729-6eb492025bdd
|
||||
git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20230531082742-c97d21411eb6
|
||||
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240722121227-fa89999d919c
|
||||
git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240611102930-ac965e8d176a
|
||||
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240531132048-ebd8fcd1685f
|
||||
git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240527065402-303a81cdc6db
|
||||
git.frostfs.info/TrueCloudLab/zapjournald v0.0.0-20240124114243-cb2e66427d02
|
||||
github.com/aws/aws-sdk-go v1.44.6
|
||||
github.com/aws/aws-sdk-go-v2 v1.18.1
|
||||
github.com/bluele/gcache v0.0.2
|
||||
github.com/go-chi/chi/v5 v5.0.8
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/minio/sio v0.3.0
|
||||
github.com/nspcc-dev/neo-go v0.106.2
|
||||
github.com/nats-io/nats.go v1.13.1-0.20220121202836-972a071d373d
|
||||
github.com/nspcc-dev/neo-go v0.105.0
|
||||
github.com/panjf2000/ants/v2 v2.5.0
|
||||
github.com/prometheus/client_golang v1.19.0
|
||||
github.com/prometheus/client_model v0.5.0
|
||||
github.com/prometheus/client_golang v1.15.1
|
||||
github.com/prometheus/client_model v0.3.0
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.15.0
|
||||
github.com/ssgreg/journald v1.0.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/urfave/cli/v2 v2.3.0
|
||||
go.opentelemetry.io/otel v1.16.0
|
||||
go.opentelemetry.io/otel/trace v1.16.0
|
||||
go.uber.org/zap v1.27.0
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/crypto v0.21.0
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225
|
||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63
|
||||
golang.org/x/net v0.23.0
|
||||
golang.org/x/text v0.14.0
|
||||
google.golang.org/grpc v1.62.0
|
||||
google.golang.org/grpc v1.59.0
|
||||
google.golang.org/protobuf v1.33.0
|
||||
)
|
||||
|
||||
|
@ -54,24 +55,29 @@ require (
|
|||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hashicorp/golang-lru v0.6.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.2 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/nspcc-dev/go-ordered-json v0.0.0-20240301084351-0246b013f8b2 // indirect
|
||||
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20240521091047-78685785716d // indirect
|
||||
github.com/nspcc-dev/rfc6979 v0.2.1 // indirect
|
||||
github.com/nats-io/nats-server/v2 v2.7.1 // indirect
|
||||
github.com/nats-io/nkeys v0.3.0 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/nspcc-dev/go-ordered-json v0.0.0-20231123160306-3374ff1e7a3c // indirect
|
||||
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231127165613-b35f351f0ba0 // indirect
|
||||
github.com/nspcc-dev/rfc6979 v0.2.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.9.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/spf13/afero v1.9.3 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
|
@ -80,7 +86,7 @@ require (
|
|||
github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 // indirect
|
||||
github.com/twmb/murmur3 v1.1.8 // indirect
|
||||
github.com/urfave/cli v1.22.5 // indirect
|
||||
go.etcd.io/bbolt v1.3.9 // indirect
|
||||
go.etcd.io/bbolt v1.3.8 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 // indirect
|
||||
|
@ -89,12 +95,12 @@ require (
|
|||
go.opentelemetry.io/otel/sdk v1.16.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/term v0.18.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect
|
||||
google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
146
go.sum
146
go.sum
|
@ -36,20 +36,20 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
|
|||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240716113920-f517e3949164 h1:XxvwQKJT/f16qS3df5PBQPRYKkhy0/A7zH6644QpKD0=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240716113920-f517e3949164/go.mod h1:OBDSr+DqV1z4VDouoX3YMleNc4DPBVBWTG3WDT2PK1o=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240621131249-49e5270f673e h1:kcBqZBiFIUBATUqEuvVigtkJJWQ2Gug/eYXn967o3M4=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240621131249-49e5270f673e/go.mod h1:F/fe1OoIDKr5Bz99q4sriuHDuf3aZefZy9ZsCqEtgxc=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240530152826-2f6d3209e1d3 h1:H5GvrVlowIMWfzqQkhY0p0myooJxQ1sMRVSFfXawwWg=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240530152826-2f6d3209e1d3/go.mod h1:OBDSr+DqV1z4VDouoX3YMleNc4DPBVBWTG3WDT2PK1o=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240409115729-6eb492025bdd h1:fujTUMMn0wnpEKNDWLejFL916EPuaYD1MdZpk1ZokU8=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240409115729-6eb492025bdd/go.mod h1:F/fe1OoIDKr5Bz99q4sriuHDuf3aZefZy9ZsCqEtgxc=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0 h1:FxqFDhQYYgpe41qsIHVOcdzSVCB8JNSfPG7Uk4r2oSk=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0/go.mod h1:RUIKZATQLJ+TaYQa60X2fTDwfuhMfm8Ar60bQ5fr+vU=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20230531082742-c97d21411eb6 h1:aGQ6QaAnTerQ5Dq5b2/f9DUQtSqPkZZ/bkMx/HKuLCo=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20230531082742-c97d21411eb6/go.mod h1:W8Nn08/l6aQ7UlIbpF7FsQou7TVpcRD1ZT1KG4TrFhE=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240722121227-fa89999d919c h1:8ZS6eUFnOhzUo9stFqwq1Zyq+Y5YNcYAidCGICcZVL4=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240722121227-fa89999d919c/go.mod h1:vluJ/+yQMcq8ZIZZSA7Te+JKClr0lgtRErjICvb8wto=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240531132048-ebd8fcd1685f h1:vBLC1OSGMSn7lRJv/p1of0veifuBdZdztVrF9Vn+UFk=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240531132048-ebd8fcd1685f/go.mod h1:4AObM67VUqkXQJlODTFThFnuMGEuK8h9DrAXHDZqvCU=
|
||||
git.frostfs.info/TrueCloudLab/hrw v1.2.1 h1:ccBRK21rFvY5R1WotI6LNoPlizk7qSvdfD8lNIRudVc=
|
||||
git.frostfs.info/TrueCloudLab/hrw v1.2.1/go.mod h1:C1Ygde2n843yTZEQ0FP69jYiuaYV0kriLvP4zm8JuvM=
|
||||
git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240611102930-ac965e8d176a h1:Bk1fB4cQASPKgAVGCdlBOEp5ohZfDxqK6fZM8eP+Emo=
|
||||
git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240611102930-ac965e8d176a/go.mod h1:SgioiGhQNWqiV5qpFAXRDJF81SEFRBhtwGEiU0FViyA=
|
||||
git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240527065402-303a81cdc6db h1:SVtRixp8gYn4orflpXaq3m7ET284kF8dogczIxbQRWs=
|
||||
git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240527065402-303a81cdc6db/go.mod h1:SgioiGhQNWqiV5qpFAXRDJF81SEFRBhtwGEiU0FViyA=
|
||||
git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0 h1:M2KR3iBj7WpY3hP10IevfIB9MURr4O9mwVfJ+SjT3HA=
|
||||
git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0/go.mod h1:okpbKfVYf/BpejtfFTfhZqFP+sZ8rsHrP8Rr/jYPNRc=
|
||||
git.frostfs.info/TrueCloudLab/tzhash v1.8.0 h1:UFMnUIk0Zh17m8rjGHJMqku2hCgaXDqjqZzS4gsb4UA=
|
||||
|
@ -71,7 +71,6 @@ github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J
|
|||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bits-and-blooms/bitset v1.8.0 h1:FD+XqgOZDUxxZ8hzoBFuV9+cGWY9CslN6d5MS5JVb4c=
|
||||
github.com/bits-and-blooms/bitset v1.8.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
|
||||
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
|
@ -94,9 +93,7 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH
|
|||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ=
|
||||
github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI=
|
||||
github.com/consensys/gnark-crypto v0.12.2-0.20231013160410-1f65e75b6dfb h1:f0BMgIjhZy4lSRHCXFbQst85f5agZAjtDMixQqBWNpc=
|
||||
github.com/consensys/gnark-crypto v0.12.2-0.20231013160410-1f65e75b6dfb/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
|
@ -114,7 +111,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
|
|||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
|
||||
github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
|
@ -132,8 +128,7 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
|||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||
github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
|
||||
github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||
github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
|
@ -178,8 +173,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
|
@ -195,25 +189,26 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
|
|||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 h1:lLT7ZLSzGLI08vc9cpd+tYmNWjdKDqyr/2L+f6U12Fk=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
|
||||
github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.2 h1:Dwmkdr5Nc/oBiXgJS3CDHNhJtIHkuZ3DZF5twqnfBdU=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.2/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU=
|
||||
github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
|
||||
github.com/holiman/uint256 v1.2.0 h1:gpSYcPLWGv4sG43I2mVLiDZCNDh/EpGjSk8tmtxitHM=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
|
@ -228,34 +223,44 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
|
|||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.13.4 h1:0zhec2I8zGnjWcKyLl6i3gPqKANCCn5e9xmviEEeX6s=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/minio/highwayhash v1.0.1 h1:dZ6IIu8Z14VlC0VpfKofAhCy74wu/Qb5gcn52yWoz/0=
|
||||
github.com/minio/sio v0.3.0 h1:syEFBewzOMOYVzSTFpp1MqpSZk8rUNbz8VIIc+PNzus=
|
||||
github.com/minio/sio v0.3.0/go.mod h1:8b0yPp2avGThviy/+OCJBI6OMpvxoUuiLvE6F1lebhw=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY=
|
||||
github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/nspcc-dev/go-ordered-json v0.0.0-20240301084351-0246b013f8b2 h1:mD9hU3v+zJcnHAVmHnZKt3I++tvn30gBj2rP2PocZMk=
|
||||
github.com/nspcc-dev/go-ordered-json v0.0.0-20240301084351-0246b013f8b2/go.mod h1:U5VfmPNM88P4RORFb6KSUVBdJBDhlqggJZYGXGPxOcc=
|
||||
github.com/nspcc-dev/neo-go v0.106.2 h1:KXSJ2J5Oacc7LrX3r4jvnC8ihKqHs5NB21q4f2S3r9o=
|
||||
github.com/nspcc-dev/neo-go v0.106.2/go.mod h1:Ojwfx3/lv0VTeEHMpQ17g0wTnXcCSoFQVq5GEeCZmGo=
|
||||
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20240521091047-78685785716d h1:Vcb7YkZuUSSIC+WF/xV3UDfHbAxZgyT2zGleJP3Ig5k=
|
||||
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20240521091047-78685785716d/go.mod h1:/vrbWSHc7YS1KSYhVOyyeucXW/e+1DkVBOgnBEXUCeY=
|
||||
github.com/nspcc-dev/rfc6979 v0.2.1 h1:8wWxkamHWFmO790GsewSoKUSJjVnL1fmdRpokU/RgRM=
|
||||
github.com/nspcc-dev/rfc6979 v0.2.1/go.mod h1:Tk7h5kyUWkhjyO3zUgFFhy1v2vQv3BvQEntakdtqrWc=
|
||||
github.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296 h1:vU9tpM3apjYlLLeY23zRWJ9Zktr5jp+mloR942LEOpY=
|
||||
github.com/nats-io/nats-server/v2 v2.7.1 h1:SDj8R0PJPVekw3EgHxGtTfJUuMbsuaul1nwWFI3xTyk=
|
||||
github.com/nats-io/nats-server/v2 v2.7.1/go.mod h1:tckmrt0M6bVaDT3kmh9UrIq/CBOBBse+TpXQi5ldaa8=
|
||||
github.com/nats-io/nats.go v1.13.1-0.20220121202836-972a071d373d h1:GRSmEJutHkdoxKsRypP575IIdoXe7Bm6yHQF6GcDBnA=
|
||||
github.com/nats-io/nats.go v1.13.1-0.20220121202836-972a071d373d/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w=
|
||||
github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8=
|
||||
github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/nspcc-dev/go-ordered-json v0.0.0-20231123160306-3374ff1e7a3c h1:OOQeE613BH93ICPq3eke5N78gWNeMjcBWkmD2NKyXVg=
|
||||
github.com/nspcc-dev/go-ordered-json v0.0.0-20231123160306-3374ff1e7a3c/go.mod h1:79bEUDEviBHJMFV6Iq6in57FEOCMcRhfQnfaf0ETA5U=
|
||||
github.com/nspcc-dev/neo-go v0.105.0 h1:vtNZYFEFySK8zRDhLzQYha849VzWrcKezlnq/oNQg/w=
|
||||
github.com/nspcc-dev/neo-go v0.105.0/go.mod h1:6pchIHg5okeZO955RxpTh5q0sUI0vtpgPM6Q+no1rlI=
|
||||
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231127165613-b35f351f0ba0 h1:N+dMIBmteXjJpkH6UZ7HmNftuFxkqszfGLbhsEctnv0=
|
||||
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20231127165613-b35f351f0ba0/go.mod h1:J/Mk6+nKeKSW4wygkZQFLQ6SkLOSGX5Ga0RuuuktEag=
|
||||
github.com/nspcc-dev/rfc6979 v0.2.0 h1:3e1WNxrN60/6N0DW7+UYisLeZJyfqZTNOjeV/toYvOE=
|
||||
github.com/nspcc-dev/rfc6979 v0.2.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso=
|
||||
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
|
@ -273,19 +278,18 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||
github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
|
||||
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
|
||||
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
|
||||
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
||||
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
|
@ -315,8 +319,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 h1:xQdMZ1WLrgkkvOZ/LDQxjVxMLdby7osSh4ZEVa5sIjs=
|
||||
|
@ -327,12 +331,14 @@ github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
|
|||
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo=
|
||||
github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
|
||||
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
|
||||
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
|
||||
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
|
@ -358,18 +364,18 @@ go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLk
|
|||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw=
|
||||
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
|
@ -384,8 +390,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
|||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ=
|
||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
|
@ -409,8 +415,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
@ -469,8 +474,9 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -535,6 +541,7 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
|
@ -582,14 +589,12 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
|
|||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
||||
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
|
@ -654,12 +659,12 @@ google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6D
|
|||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
|
||||
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 h1:x9PwdEgd11LgK+orcck69WVRo7DezSO4VUMPI4xpc8A=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014/go.mod h1:rbHMSEDyoYX62nRVLOCc4Qt1HbsdytAYoVwgjiOhF3I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
|
||||
google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f h1:Vn+VyHU5guc9KjB5KrjI2q0wCOWEOIh0OEsleqakHJg=
|
||||
google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f/go.mod h1:nWSwAFPb+qfNJXsoeO3Io7zf4tMSfN8EA8RlDA04GhY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 h1:DC7wcm+i+P1rN3Ff07vL+OndGg5OhNddHyTA+ocPqYE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
|
@ -680,8 +685,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
|
|||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk=
|
||||
google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
|
||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
@ -700,7 +705,6 @@ google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHh
|
|||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
|
@ -713,7 +717,6 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
@ -729,4 +732,3 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8
|
|||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU=
|
||||
rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=
|
||||
|
|
|
@ -4,22 +4,18 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
objectv2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/crdt"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -29,12 +25,11 @@ const (
|
|||
// AuthmateFrostFS is a mediator which implements authmate.FrostFS through pool.Pool.
|
||||
type AuthmateFrostFS struct {
|
||||
frostFS layer.FrostFS
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
// NewAuthmateFrostFS creates new AuthmateFrostFS using provided pool.Pool.
|
||||
func NewAuthmateFrostFS(frostFS layer.FrostFS, log *zap.Logger) *AuthmateFrostFS {
|
||||
return &AuthmateFrostFS{frostFS: frostFS, log: log}
|
||||
func NewAuthmateFrostFS(frostFS layer.FrostFS) *AuthmateFrostFS {
|
||||
return &AuthmateFrostFS{frostFS: frostFS}
|
||||
}
|
||||
|
||||
// ContainerExists implements authmate.FrostFS interface method.
|
||||
|
@ -84,27 +79,17 @@ func (x *AuthmateFrostFS) GetCredsObject(ctx context.Context, addr oid.Address)
|
|||
credObjID = last.ObjID
|
||||
}
|
||||
|
||||
res, err := x.frostFS.GetObject(ctx, layer.PrmObjectGet{
|
||||
Container: addr.Container(),
|
||||
Object: credObjID,
|
||||
res, err := x.frostFS.ReadObject(ctx, layer.PrmObjectRead{
|
||||
Container: addr.Container(),
|
||||
Object: credObjID,
|
||||
WithPayload: true,
|
||||
WithHeader: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if closeErr := res.Payload.Close(); closeErr != nil {
|
||||
x.reqLogger(ctx).Warn(logs.CloseCredsObjectPayload, zap.Error(closeErr))
|
||||
}
|
||||
}()
|
||||
|
||||
data, err := io.ReadAll(res.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.Header.SetPayload(data)
|
||||
|
||||
return &res.Header, err
|
||||
return res.Head, err
|
||||
}
|
||||
|
||||
// CreateObject implements authmate.FrostFS interface method.
|
||||
|
@ -158,28 +143,21 @@ func (x *AuthmateFrostFS) getCredVersions(ctx context.Context, addr oid.Address)
|
|||
versions := crdt.NewObjectVersions(objCredSystemName)
|
||||
|
||||
for _, id := range credVersions {
|
||||
objVersion, err := x.frostFS.HeadObject(ctx, layer.PrmObjectHead{
|
||||
Container: addr.Container(),
|
||||
Object: id,
|
||||
objVersion, err := x.frostFS.ReadObject(ctx, layer.PrmObjectRead{
|
||||
Container: addr.Container(),
|
||||
Object: id,
|
||||
WithHeader: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("head crdt access box '%s': %w", id.EncodeToString(), err)
|
||||
}
|
||||
|
||||
versions.AppendVersion(crdt.NewObjectVersion(objVersion))
|
||||
versions.AppendVersion(crdt.NewObjectVersion(objVersion.Head))
|
||||
}
|
||||
|
||||
return versions, nil
|
||||
}
|
||||
|
||||
func (x *AuthmateFrostFS) reqLogger(ctx context.Context) *zap.Logger {
|
||||
reqLogger := middleware.GetReqLog(ctx)
|
||||
if reqLogger != nil {
|
||||
return reqLogger
|
||||
}
|
||||
return x.log
|
||||
}
|
||||
|
||||
func credVersionSysName(cnrID cid.ID, objID oid.ID) string {
|
||||
return cnrID.EncodeToString() + "0" + objID.EncodeToString()
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
func TestGetCredsObject(t *testing.T) {
|
||||
|
@ -36,7 +35,7 @@ func TestGetCredsObject(t *testing.T) {
|
|||
},
|
||||
}})
|
||||
|
||||
frostfs := NewAuthmateFrostFS(layer.NewTestFrostFS(key), zaptest.NewLogger(t))
|
||||
frostfs := NewAuthmateFrostFS(layer.NewTestFrostFS(key))
|
||||
|
||||
cid, err := frostfs.CreateContainer(ctx, authmate.PrmContainerCreate{
|
||||
FriendlyName: bktName,
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
|
||||
|
@ -161,6 +162,29 @@ func (x *FrostFS) UserContainers(ctx context.Context, layerPrm layer.PrmUserCont
|
|||
return r, handleObjectError("list user containers via connection pool", err)
|
||||
}
|
||||
|
||||
// SetContainerEACL implements frostfs.FrostFS interface method.
|
||||
func (x *FrostFS) SetContainerEACL(ctx context.Context, table eacl.Table, sessionToken *session.Container) error {
|
||||
prm := pool.PrmContainerSetEACL{Table: table, Session: sessionToken, WaitParams: &x.await}
|
||||
|
||||
err := x.pool.SetEACL(ctx, prm)
|
||||
return handleObjectError("save eACL via connection pool", err)
|
||||
}
|
||||
|
||||
// ContainerEACL implements frostfs.FrostFS interface method.
|
||||
func (x *FrostFS) ContainerEACL(ctx context.Context, layerPrm layer.PrmContainerEACL) (*eacl.Table, error) {
|
||||
prm := pool.PrmContainerEACL{
|
||||
ContainerID: layerPrm.ContainerID,
|
||||
Session: layerPrm.SessionToken,
|
||||
}
|
||||
|
||||
res, err := x.pool.GetEACL(ctx, prm)
|
||||
if err != nil {
|
||||
return nil, handleObjectError("read eACL via connection pool", err)
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
// DeleteContainer implements frostfs.FrostFS interface method.
|
||||
func (x *FrostFS) DeleteContainer(ctx context.Context, id cid.ID, token *session.Container) error {
|
||||
prm := pool.PrmContainerDelete{ContainerID: id, Session: token, WaitParams: &x.await}
|
||||
|
@ -237,12 +261,8 @@ func (x *FrostFS) CreateObject(ctx context.Context, prm layer.PrmObjectCreate) (
|
|||
prmPut.UseKey(prm.PrivateKey)
|
||||
}
|
||||
|
||||
res, err := x.pool.PutObject(ctx, prmPut)
|
||||
if err = handleObjectError("save object via connection pool", err); err != nil {
|
||||
return oid.ID{}, err
|
||||
}
|
||||
|
||||
return res.ObjectID, nil
|
||||
idObj, err := x.pool.PutObject(ctx, prmPut)
|
||||
return idObj, handleObjectError("save object via connection pool", err)
|
||||
}
|
||||
|
||||
// wraps io.ReadCloser and transforms Read errors related to access violation
|
||||
|
@ -259,31 +279,8 @@ func (x payloadReader) Read(p []byte) (int, error) {
|
|||
return n, handleObjectError("read payload", err)
|
||||
}
|
||||
|
||||
// HeadObject implements frostfs.FrostFS interface method.
|
||||
func (x *FrostFS) HeadObject(ctx context.Context, prm layer.PrmObjectHead) (*object.Object, error) {
|
||||
var addr oid.Address
|
||||
addr.SetContainer(prm.Container)
|
||||
addr.SetObject(prm.Object)
|
||||
|
||||
var prmHead pool.PrmObjectHead
|
||||
prmHead.SetAddress(addr)
|
||||
|
||||
if prm.BearerToken != nil {
|
||||
prmHead.UseBearer(*prm.BearerToken)
|
||||
} else {
|
||||
prmHead.UseKey(prm.PrivateKey)
|
||||
}
|
||||
|
||||
res, err := x.pool.HeadObject(ctx, prmHead)
|
||||
if err != nil {
|
||||
return nil, handleObjectError("read object header via connection pool", err)
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
// GetObject implements frostfs.FrostFS interface method.
|
||||
func (x *FrostFS) GetObject(ctx context.Context, prm layer.PrmObjectGet) (*layer.Object, error) {
|
||||
// ReadObject implements frostfs.FrostFS interface method.
|
||||
func (x *FrostFS) ReadObject(ctx context.Context, prm layer.PrmObjectRead) (*layer.ObjectPart, error) {
|
||||
var addr oid.Address
|
||||
addr.SetContainer(prm.Container)
|
||||
addr.SetObject(prm.Object)
|
||||
|
@ -297,23 +294,55 @@ func (x *FrostFS) GetObject(ctx context.Context, prm layer.PrmObjectGet) (*layer
|
|||
prmGet.UseKey(prm.PrivateKey)
|
||||
}
|
||||
|
||||
res, err := x.pool.GetObject(ctx, prmGet)
|
||||
if err != nil {
|
||||
return nil, handleObjectError("init full object reading via connection pool", err)
|
||||
if prm.WithHeader {
|
||||
if prm.WithPayload {
|
||||
res, err := x.pool.GetObject(ctx, prmGet)
|
||||
if err != nil {
|
||||
return nil, handleObjectError("init full object reading via connection pool", err)
|
||||
}
|
||||
|
||||
defer res.Payload.Close()
|
||||
|
||||
payload, err := io.ReadAll(res.Payload)
|
||||
if err != nil {
|
||||
return nil, handleObjectError("read full object payload", err)
|
||||
}
|
||||
|
||||
res.Header.SetPayload(payload)
|
||||
|
||||
return &layer.ObjectPart{
|
||||
Head: &res.Header,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var prmHead pool.PrmObjectHead
|
||||
prmHead.SetAddress(addr)
|
||||
|
||||
if prm.BearerToken != nil {
|
||||
prmHead.UseBearer(*prm.BearerToken)
|
||||
} else {
|
||||
prmHead.UseKey(prm.PrivateKey)
|
||||
}
|
||||
|
||||
hdr, err := x.pool.HeadObject(ctx, prmHead)
|
||||
if err != nil {
|
||||
return nil, handleObjectError("read object header via connection pool", err)
|
||||
}
|
||||
|
||||
return &layer.ObjectPart{
|
||||
Head: &hdr,
|
||||
}, nil
|
||||
} else if prm.PayloadRange[0]+prm.PayloadRange[1] == 0 {
|
||||
res, err := x.pool.GetObject(ctx, prmGet)
|
||||
if err != nil {
|
||||
return nil, handleObjectError("init full payload range reading via connection pool", err)
|
||||
}
|
||||
|
||||
return &layer.ObjectPart{
|
||||
Payload: res.Payload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &layer.Object{
|
||||
Header: res.Header,
|
||||
Payload: res.Payload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RangeObject implements frostfs.FrostFS interface method.
|
||||
func (x *FrostFS) RangeObject(ctx context.Context, prm layer.PrmObjectRange) (io.ReadCloser, error) {
|
||||
var addr oid.Address
|
||||
addr.SetContainer(prm.Container)
|
||||
addr.SetObject(prm.Object)
|
||||
|
||||
var prmRange pool.PrmObjectRange
|
||||
prmRange.SetAddress(addr)
|
||||
prmRange.SetOffset(prm.PayloadRange[0])
|
||||
|
@ -330,7 +359,9 @@ func (x *FrostFS) RangeObject(ctx context.Context, prm layer.PrmObjectRange) (io
|
|||
return nil, handleObjectError("init payload range reading via connection pool", err)
|
||||
}
|
||||
|
||||
return payloadReader{&res}, nil
|
||||
return &layer.ObjectPart{
|
||||
Payload: payloadReader{&res},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteObject implements frostfs.FrostFS interface method.
|
||||
|
|
|
@ -78,10 +78,6 @@ func (f *FrostFSID) CreateSubject(namespace string, key *keys.PublicKey) (util.U
|
|||
return f.cli.CreateSubject(namespace, key)
|
||||
}
|
||||
|
||||
func (f *FrostFSID) SetSubjectName(key *keys.PublicKey, name string) (util.Uint256, uint32, error) {
|
||||
return f.cli.SetSubjectName(key.GetScriptHash(), name)
|
||||
}
|
||||
|
||||
func (f *FrostFSID) Wait(tx util.Uint256, vub uint32, err error) error {
|
||||
_, err = f.cli.Wait(tx, vub, err)
|
||||
return err
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
@ -15,6 +16,8 @@ type Storage struct {
|
|||
router engine.ChainRouter
|
||||
|
||||
morph *MorphRuleChainStorage
|
||||
|
||||
local engine.LocalOverrideStorage
|
||||
}
|
||||
|
||||
type StorageConfig struct {
|
||||
|
@ -40,6 +43,8 @@ type Contract interface {
|
|||
var _ handler.APE = (*Storage)(nil)
|
||||
|
||||
func NewStorage(cfg StorageConfig) *Storage {
|
||||
local := inmemory.NewInmemoryLocalStorage()
|
||||
|
||||
morph := NewMorphRuleChainStorage(&MorphRuleChainStorageConfig{
|
||||
Contract: cfg.Contract,
|
||||
Cache: cfg.Cache,
|
||||
|
@ -47,8 +52,9 @@ func NewStorage(cfg StorageConfig) *Storage {
|
|||
})
|
||||
|
||||
return &Storage{
|
||||
router: engine.NewDefaultChainRouter(morph),
|
||||
router: engine.NewDefaultChainRouterWithLocalOverrides(morph, local),
|
||||
morph: morph,
|
||||
local: local,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,6 +62,10 @@ func (s *Storage) IsAllowed(name chain.Name, target engine.RequestTarget, r reso
|
|||
return s.router.IsAllowed(name, target, r)
|
||||
}
|
||||
|
||||
func (s *Storage) LocalStorage() engine.LocalOverrideStorage {
|
||||
return s.local
|
||||
}
|
||||
|
||||
func (s *Storage) PutBucketPolicy(ns string, cnrID cid.ID, policy []byte, policyChains []*chain.Chain) error {
|
||||
return s.morph.PutBucketPolicy(ns, cnrID, policy, policyChains)
|
||||
}
|
||||
|
|
|
@ -19,16 +19,16 @@ type GetNodeByPathResponseInfoWrapper struct {
|
|||
response *grpcService.GetNodeByPathResponse_Info
|
||||
}
|
||||
|
||||
func (n GetNodeByPathResponseInfoWrapper) GetNodeID() []uint64 {
|
||||
return []uint64{n.response.GetNodeId()}
|
||||
func (n GetNodeByPathResponseInfoWrapper) GetNodeID() uint64 {
|
||||
return n.response.GetNodeId()
|
||||
}
|
||||
|
||||
func (n GetNodeByPathResponseInfoWrapper) GetParentID() []uint64 {
|
||||
return []uint64{n.response.GetParentId()}
|
||||
func (n GetNodeByPathResponseInfoWrapper) GetParentID() uint64 {
|
||||
return n.response.GetParentId()
|
||||
}
|
||||
|
||||
func (n GetNodeByPathResponseInfoWrapper) GetTimestamp() []uint64 {
|
||||
return []uint64{n.response.GetTimestamp()}
|
||||
func (n GetNodeByPathResponseInfoWrapper) GetTimestamp() uint64 {
|
||||
return n.response.GetTimestamp()
|
||||
}
|
||||
|
||||
func (n GetNodeByPathResponseInfoWrapper) GetMeta() []tree.Meta {
|
||||
|
@ -43,21 +43,15 @@ type GetSubTreeResponseBodyWrapper struct {
|
|||
response *grpcService.GetSubTreeResponse_Body
|
||||
}
|
||||
|
||||
func (n GetSubTreeResponseBodyWrapper) GetNodeID() []uint64 {
|
||||
func (n GetSubTreeResponseBodyWrapper) GetNodeID() uint64 {
|
||||
return n.response.GetNodeId()
|
||||
}
|
||||
|
||||
func (n GetSubTreeResponseBodyWrapper) GetParentID() []uint64 {
|
||||
resp := n.response.GetParentId()
|
||||
if resp == nil {
|
||||
// storage sends nil that should be interpreted as []uint64{0}
|
||||
// due to protobuf compatibility, see 'GetSubTree' function
|
||||
return []uint64{0}
|
||||
}
|
||||
return resp
|
||||
func (n GetSubTreeResponseBodyWrapper) GetParentID() uint64 {
|
||||
return n.response.GetParentId()
|
||||
}
|
||||
|
||||
func (n GetSubTreeResponseBodyWrapper) GetTimestamp() []uint64 {
|
||||
func (n GetSubTreeResponseBodyWrapper) GetTimestamp() uint64 {
|
||||
return n.response.GetTimestamp()
|
||||
}
|
||||
|
||||
|
@ -102,21 +96,13 @@ func (w *PoolWrapper) GetNodes(ctx context.Context, prm *tree.GetNodesParams) ([
|
|||
return res, nil
|
||||
}
|
||||
|
||||
func (w *PoolWrapper) GetSubTree(ctx context.Context, bktInfo *data.BucketInfo, treeID string, rootID []uint64, depth uint32) ([]tree.NodeResponse, error) {
|
||||
func (w *PoolWrapper) GetSubTree(ctx context.Context, bktInfo *data.BucketInfo, treeID string, rootID uint64, depth uint32) ([]tree.NodeResponse, error) {
|
||||
poolPrm := treepool.GetSubTreeParams{
|
||||
CID: bktInfo.CID,
|
||||
TreeID: treeID,
|
||||
RootID: rootID,
|
||||
Depth: depth,
|
||||
BearerToken: getBearer(ctx, bktInfo),
|
||||
Order: treepool.AscendingOrder,
|
||||
}
|
||||
if len(rootID) == 1 && rootID[0] == 0 {
|
||||
// storage node interprets 'nil' value as []uint64{0}
|
||||
// gate wants to send 'nil' value instead of []uint64{0}, because
|
||||
// it provides compatibility with previous tree service api where
|
||||
// single uint64(0) value is dropped from signature
|
||||
poolPrm.RootID = nil
|
||||
}
|
||||
|
||||
subTreeReader, err := w.p.GetSubTree(ctx, poolPrm)
|
||||
|
@ -176,7 +162,7 @@ func (s *SubTreeStreamImpl) Next() (tree.NodeResponse, error) {
|
|||
return s.Next()
|
||||
}
|
||||
|
||||
func (w *PoolWrapper) GetSubTreeStream(ctx context.Context, bktInfo *data.BucketInfo, treeID string, rootID []uint64, depth uint32) (tree.SubTreeStream, error) {
|
||||
func (w *PoolWrapper) GetSubTreeStream(ctx context.Context, bktInfo *data.BucketInfo, treeID string, rootID uint64, depth uint32) (tree.SubTreeStream, error) {
|
||||
poolPrm := treepool.GetSubTreeParams{
|
||||
CID: bktInfo.CID,
|
||||
TreeID: treeID,
|
||||
|
@ -185,13 +171,6 @@ func (w *PoolWrapper) GetSubTreeStream(ctx context.Context, bktInfo *data.Bucket
|
|||
BearerToken: getBearer(ctx, bktInfo),
|
||||
Order: treepool.AscendingOrder,
|
||||
}
|
||||
if len(rootID) == 1 && rootID[0] == 0 {
|
||||
// storage node interprets 'nil' value as []uint64{0}
|
||||
// gate wants to send 'nil' value instead of []uint64{0}, because
|
||||
// it provides compatibility with previous tree service api where
|
||||
// single uint64(0) value is dropped from signature
|
||||
poolPrm.RootID = nil
|
||||
}
|
||||
|
||||
subTreeReader, err := w.p.GetSubTree(ctx, poolPrm)
|
||||
if err != nil {
|
||||
|
|
|
@ -22,6 +22,7 @@ const (
|
|||
ApplicationFinished = "application finished" // Info in ../../cmd/s3-gw/app.go
|
||||
FetchDomainsPrepareToUseAPI = "fetch domains, prepare to use API" // Info in ../../cmd/s3-gw/app.go
|
||||
StartingServer = "starting server" // Info in ../../cmd/s3-gw/app.go
|
||||
StartingControlAPI = "starting control API server" // Info in ../../cmd/s3-gw/app.go
|
||||
StoppingServer = "stopping server" // Info in ../../cmd/s3-gw/app.go
|
||||
SIGHUPConfigReloadStarted = "SIGHUP config reload started" // Info in ../../cmd/s3-gw/app.go
|
||||
FailedToReloadConfigBecauseItsMissed = "failed to reload config because it's missed" // Warn in ../../cmd/s3-gw/app.go
|
||||
|
@ -33,6 +34,8 @@ const (
|
|||
FailedToAddServer = "failed to add server" // Warn in ../../cmd/s3-gw/app.go
|
||||
AddServer = "add server" // Info in ../../cmd/s3-gw/app.go
|
||||
ResolverNNSWontBeUsedSinceRPCEndpointIsntProvided = "resolver 'nns' won't be used since 'rpc_endpoint' isn't provided" // Warn in ../../cmd/s3-gw/app.go
|
||||
ControlAPICannotShutdownGracefully = "control API cannot shutdown gracefully, forcing stop" // Info in ../../cmd/s3-gw/app.go
|
||||
ControlAPIServiceStopped = "control API service stopped" // Info in ../../cmd/s3-gw/app.go
|
||||
InvalidLifetimeUsingDefaultValue = "invalid lifetime, using default value (in seconds)" // Error in ../../cmd/s3-gw/app_settings.go
|
||||
InvalidCacheSizeUsingDefaultValue = "invalid cache size, using default value" // Error in ../../cmd/s3-gw/app_settings.go
|
||||
FailedToParseDefaultLocationConstraint = "failed to parse 'default' location constraint, default one will be used" // Warn in cmd/s3-gw/app_settings.go
|
||||
|
@ -41,6 +44,7 @@ const (
|
|||
FailedToParseLocationConstraint = "failed to parse location constraint, it cannot be used" // Warn in cmd/s3-gw/app_settings.go
|
||||
FailedToParseDefaultCopiesNumbers = "failed to parse 'default' copies numbers, default one will be used" // Warn in cmd/s3-gw/app_settings.go
|
||||
FailedToParseCopiesNumbers = "failed to parse copies numbers, skip" // Warn in cmd/s3-gw/app_settings.go
|
||||
FailedToParsePublicKey = "failed to parse public key, skip" // Warn in cmd/s3-gw/app_settings.go
|
||||
DefaultNamespacesCannotBeEmpty = "default namespaces cannot be empty, defaults will be used" // Warn in cmd/s3-gw/app_settings.go
|
||||
FailedToParseNamespacesConfig = "failed to unmarshal namespaces config" // Warn in cmd/s3-gw/app_settings.go
|
||||
DefaultNamespaceConfigValuesBeOverwritten = "default namespace config value be overwritten by values from 'namespaces.config'" // Warn in cmd/s3-gw/app_settings.go
|
||||
|
@ -51,19 +55,19 @@ const (
|
|||
AddedStoragePeer = "added storage peer" // Info in ../../cmd/s3-gw/app_settings.go
|
||||
PrepareConnectionPool = "prepare connection pool" // Debug in ../../cmd/s3-authmate/modules/utils.go
|
||||
PrepareFrostfsIDClient = "prepare frostfsid client" // Debug in ../../cmd/s3-authmate/modules/utils.go
|
||||
PreparePolicyClient = "prepare policy client" // Debug in ../../cmd/s3-authmate/modules/utils.go
|
||||
CreateSubjectInFrostFSID = "create subject in frostfsid" // Debug in ../../cmd/s3-authmate/modules/utils.go
|
||||
SubjectAlreadyExistsInFrostFSID = "subject already exists in frostfsid" // Debug in ../../cmd/s3-authmate/modules/utils.go
|
||||
SetSubjectNameInFrostFSID = "set subject name in frostfsid" // Debug in ../../cmd/s3-authmate/modules/utils.go
|
||||
AddPolicyChainRules = "add policy chain rules" // Debug in ../../cmd/s3-authmate/modules/utils.go
|
||||
InvalidCacheEntryType = "invalid cache entry type" // Warn in ../../api/cache/*
|
||||
InvalidCacheKeyType = "invalid cache key type" // Warn in ../../api/cache/objectslist.go
|
||||
ObjectIsCopied = "object is copied" // Info in ../../api/handler/copy.go
|
||||
CouldntSendNotification = "couldn't send notification: %w" // Error in ../../api/handler/*
|
||||
FailedToSendTestEventBecauseNotificationsIsDisabled = "failed to send test event because notifications is disabled" // Warn in ../../api/handler/notifications.go
|
||||
RequestFailed = "request failed" // Error in ../../api/handler/util.go
|
||||
GetBucketInfo = "get bucket info" // Warn in ../../api/handler/cors.go
|
||||
GetBucketCors = "get bucket cors" // Warn in ../../api/handler/cors.go
|
||||
SomeACLNotFullyMapped = "some acl not fully mapped" // Warn in ../../api/handler/acl.go
|
||||
CouldntDeleteObject = "couldn't delete object" // Error in ../../api/layer/layer.go
|
||||
NotificatorIsDisabledS3WontProduceNotificationEvents = "notificator is disabled, s3 won't produce notification events" // Warn in ../../api/handler/api.go
|
||||
BucketIsCreated = "bucket is created" // Info in ../../api/handler/put.go
|
||||
CouldntDeleteNotificationConfigurationObject = "couldn't delete notification configuration object" // Error in ../../api/layer/notifications.go
|
||||
CouldNotParseContainerObjectLockEnabledAttribute = "could not parse container object lock enabled attribute" // Error in ../../api/layer/container.go
|
||||
CouldNotListUserContainers = "could not list user containers" // Error in ../../api/layer/container.go
|
||||
CouldNotFetchContainerInfo = "could not fetch container info" // Error in ../../api/layer/container.go
|
||||
|
@ -83,7 +87,6 @@ const (
|
|||
FailedToSubmitTaskToPool = "failed to submit task to pool" // Warn in ../../api/layer/object.go
|
||||
CouldNotFetchObjectMeta = "could not fetch object meta" // Warn in ../../api/layer/object.go
|
||||
GetTreeNode = "get tree node" // Debug in ../../api/layer/tagging.go
|
||||
GetTreeNodeToDelete = "get tree node to delete" // Debug in ../../api/layer/tagging.go
|
||||
CouldntPutBucketInfoIntoCache = "couldn't put bucket info into cache" // Warn in ../../api/layer/cache.go
|
||||
CouldntAddObjectToCache = "couldn't add object to cache" // Warn in ../../api/layer/cache.go
|
||||
CouldntCacheAccessControlOperation = "couldn't cache access control operation" // Warn in ../../api/layer/cache.go
|
||||
|
@ -94,6 +97,7 @@ const (
|
|||
CouldntCacheLockInfo = "couldn't cache lock info" // Error in ../../api/layer/cache.go
|
||||
CouldntCacheBucketSettings = "couldn't cache bucket settings" // Warn in ../../api/layer/cache.go
|
||||
CouldntCacheCors = "couldn't cache cors" // Warn in ../../api/layer/cache.go
|
||||
CouldntCacheNotificationConfiguration = "couldn't cache notification configuration" // Warn in ../../api/layer/cache.go
|
||||
CouldntCacheListPolicyChains = "couldn't cache list policy chains" // Warn in ../../api/layer/cache.go
|
||||
RequestEnd = "request end" // Info in ../../api/middleware/response.go
|
||||
CouldntReceiveAccessBoxForGateKeyRandomKeyWillBeUsed = "couldn't receive access box for gate key, random key will be used" // Debug in ../../api/middleware/auth.go
|
||||
|
@ -101,9 +105,15 @@ const (
|
|||
FailedToResolveCID = "failed to resolve CID" // Debug in ../../api/middleware/metrics.go
|
||||
RequestStart = "request start" // Info in ../../api/middleware/reqinfo.go
|
||||
FailedToUnescapeObjectName = "failed to unescape object name" // Warn in ../../api/middleware/reqinfo.go
|
||||
CouldNotHandleMessage = "could not handle message" // Error in ../../api/notifications/controller.go
|
||||
CouldNotACKMessage = "could not ACK message" // Error in ../../api/notifications/controller.go
|
||||
CouldntMarshalAnEvent = "couldn't marshal an event" // Error in ../../api/notifications/controller.go
|
||||
CouldntSendAnEventToTopic = "couldn't send an event to topic" // Error in ../../api/notifications/controller.go
|
||||
InvalidDefaultMaxAge = "invalid defaultMaxAge" // Fatal in ../../cmd/s3-gw/app_settings.go
|
||||
CantShutDownService = "can't shut down service" // Panic in ../../cmd/s3-gw/service.go
|
||||
CouldntGenerateRandomKey = "couldn't generate random key" // Fatal in ../../cmd/s3-gw/app.go
|
||||
FailedToEnableNotifications = "failed to enable notifications" // Fatal in ../../cmd/s3-gw/app.go
|
||||
CouldntInitializeLayer = "couldn't initialize layer" // Fatal in ../../cmd/s3-gw/app.go
|
||||
FailedToCreateResolver = "failed to create resolver" // Fatal in ../../cmd/s3-gw/app.go
|
||||
CouldNotLoadFrostFSPrivateKey = "could not load FrostFS private key" // Fatal in ../../cmd/s3-gw/app.go
|
||||
FailedToCreateConnectionPool = "failed to create connection pool" // Fatal in ../../cmd/s3-gw/app.go
|
||||
|
@ -119,6 +129,11 @@ const (
|
|||
FrostfsIDValidationFailed = "FrostfsID validation failed" // Error in ../../api/middleware/auth.go
|
||||
InitFrostfsIDContractFailed = "init frostfsid contract failed" // Fatal in ../../cmd/s3-gw/app.go
|
||||
InitPolicyContractFailed = "init policy contract failed" // Fatal in ../../cmd/s3-gw/app.go
|
||||
ControlAPIHealthcheck = "healthcheck request"
|
||||
ControlAPIPutPolicies = "put policies request"
|
||||
ControlAPIRemovePolicies = "remove policies request"
|
||||
ControlAPIGetPolicy = "get policy request"
|
||||
ControlAPIListPolicies = "list policies request"
|
||||
PolicyValidationFailed = "policy validation failed"
|
||||
ServerReconnecting = "reconnecting server..."
|
||||
ServerReconnectedSuccessfully = "server reconnected successfully"
|
||||
|
@ -142,16 +157,4 @@ const (
|
|||
CouldntCacheSubject = "couldn't cache subject info"
|
||||
UserGroupsListIsEmpty = "user groups list is empty, subject not found"
|
||||
CouldntCacheUserKey = "couldn't cache user key"
|
||||
ObjectTaggingNodeHasMultipleIDs = "object tagging node has multiple ids"
|
||||
BucketTaggingNodeHasMultipleIDs = "bucket tagging node has multiple ids"
|
||||
BucketSettingsNodeHasMultipleIDs = "bucket settings node has multiple ids"
|
||||
BucketCORSNodeHasMultipleIDs = "bucket cors node has multiple ids"
|
||||
SystemNodeHasMultipleIDs = "system node has multiple ids"
|
||||
FailedToRemoveOldSystemNode = "failed to remove old system node"
|
||||
FailedToParseAddressInTreeNode = "failed to parse object addr in tree node"
|
||||
UnexpectedMultiNodeIDsInSubTreeMultiParts = "unexpected multi node ids in sub tree multi parts"
|
||||
FoundSeveralSystemNodes = "found several system nodes"
|
||||
FailedToParsePartInfo = "failed to parse part info"
|
||||
CouldNotFetchCORSContainerInfo = "couldn't fetch CORS container info"
|
||||
CloseCredsObjectPayload = "close creds object payload"
|
||||
)
|
||||
|
|
184
pkg/service/control/client/client.go
Normal file
184
pkg/service/control/client/client.go
Normal file
|
@ -0,0 +1,184 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
policycontract "git.frostfs.info/TrueCloudLab/frostfs-contract/policy"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/control"
|
||||
controlSvc "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/control/server"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
svc control.ControlServiceClient
|
||||
key *keys.PrivateKey
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Logger *zap.Logger
|
||||
}
|
||||
|
||||
type PolicyData struct {
|
||||
Kind policycontract.Kind
|
||||
Name string
|
||||
Chain *chain.Chain
|
||||
}
|
||||
|
||||
type PolicyInfo struct {
|
||||
Kind policycontract.Kind
|
||||
Name string
|
||||
ChainID chain.ID
|
||||
}
|
||||
|
||||
func kindToTarget(k policycontract.Kind) control.PolicyTarget {
|
||||
switch k {
|
||||
case policycontract.Container:
|
||||
return control.PolicyTarget_CONTAINER
|
||||
case policycontract.Namespace:
|
||||
return control.PolicyTarget_NAMESPACE
|
||||
case 'u':
|
||||
return control.PolicyTarget_USER
|
||||
case 'g':
|
||||
return control.PolicyTarget_GROUP
|
||||
default:
|
||||
return control.PolicyTarget_TARGET_UNDEFINED
|
||||
}
|
||||
}
|
||||
|
||||
func New(ctx context.Context, addr string, key *keys.PrivateKey) (*Client, error) {
|
||||
conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to dial s3 gw control api: %w", err)
|
||||
}
|
||||
|
||||
svc := control.NewControlServiceClient(conn)
|
||||
|
||||
cli := &Client{
|
||||
svc: svc,
|
||||
key: key,
|
||||
}
|
||||
|
||||
return cli, cli.Healthcheck(ctx)
|
||||
}
|
||||
|
||||
func (c *Client) Healthcheck(ctx context.Context) error {
|
||||
req := &control.HealthCheckRequest{}
|
||||
if err := controlSvc.SignMessage(&c.key.PrivateKey, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := c.svc.HealthCheck(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.Body.HealthStatus != control.HealthStatus_READY {
|
||||
return fmt.Errorf("service isn't ready, status: %s", res.Body.HealthStatus)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) PutPolicies(ctx context.Context, policies []PolicyData) error {
|
||||
chainDatas := make([]*control.PutPoliciesRequest_ChainData, len(policies))
|
||||
for i := range policies {
|
||||
chainDatas[i] = &control.PutPoliciesRequest_ChainData{
|
||||
Target: kindToTarget(policies[i].Kind),
|
||||
Name: policies[i].Name,
|
||||
Chain: policies[i].Chain.Bytes(),
|
||||
}
|
||||
}
|
||||
|
||||
req := &control.PutPoliciesRequest{
|
||||
Body: &control.PutPoliciesRequest_Body{
|
||||
ChainDatas: chainDatas,
|
||||
},
|
||||
}
|
||||
|
||||
if err := controlSvc.SignMessage(&c.key.PrivateKey, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := c.svc.PutPolicies(ctx, req)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) RemovePolicies(ctx context.Context, policies []PolicyInfo) error {
|
||||
chainInfos := make([]*control.RemovePoliciesRequest_ChainInfo, len(policies))
|
||||
for i := range policies {
|
||||
chainInfos[i] = &control.RemovePoliciesRequest_ChainInfo{
|
||||
Target: kindToTarget(policies[i].Kind),
|
||||
Name: policies[i].Name,
|
||||
ChainID: []byte(policies[i].ChainID),
|
||||
}
|
||||
}
|
||||
|
||||
req := &control.RemovePoliciesRequest{
|
||||
Body: &control.RemovePoliciesRequest_Body{
|
||||
ChainInfos: chainInfos,
|
||||
},
|
||||
}
|
||||
|
||||
if err := controlSvc.SignMessage(&c.key.PrivateKey, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := c.svc.RemovePolicies(ctx, req)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) GetPolicy(ctx context.Context, kind policycontract.Kind, name string, chainID chain.ID) (*chain.Chain, error) {
|
||||
req := &control.GetPolicyRequest{
|
||||
Body: &control.GetPolicyRequest_Body{
|
||||
Target: kindToTarget(kind),
|
||||
Name: name,
|
||||
ChainID: []byte(chainID),
|
||||
},
|
||||
}
|
||||
|
||||
if err := controlSvc.SignMessage(&c.key.PrivateKey, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.svc.GetPolicy(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var policyChain chain.Chain
|
||||
if err = policyChain.DecodeBytes(resp.GetBody().GetChain()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &policyChain, nil
|
||||
}
|
||||
|
||||
func (c *Client) ListPolicies(ctx context.Context, kind policycontract.Kind, name string) ([]chain.ID, error) {
|
||||
req := &control.ListPoliciesRequest{
|
||||
Body: &control.ListPoliciesRequest_Body{
|
||||
Target: kindToTarget(kind),
|
||||
Name: name,
|
||||
},
|
||||
}
|
||||
|
||||
if err := controlSvc.SignMessage(&c.key.PrivateKey, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.svc.ListPolicies(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := make([]chain.ID, len(resp.GetBody().GetChainIDs()))
|
||||
for i, chainID := range resp.GetBody().GetChainIDs() {
|
||||
res[i] = chain.ID(chainID)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
352
pkg/service/control/server/server.go
Normal file
352
pkg/service/control/server/server.go
Normal file
|
@ -0,0 +1,352 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/control"
|
||||
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
|
||||
frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
*cfg
|
||||
}
|
||||
|
||||
type Settings interface {
|
||||
ResolveNamespaceAlias(ns string) string
|
||||
FetchRawKeys() [][]byte
|
||||
}
|
||||
|
||||
type defaultSettings struct{}
|
||||
|
||||
func (f defaultSettings) FetchRawKeys() [][]byte { return nil }
|
||||
func (f defaultSettings) ResolveNamespaceAlias(ns string) string { return ns }
|
||||
|
||||
// Option of the Server's constructor.
|
||||
type Option func(*cfg)
|
||||
|
||||
type cfg struct {
|
||||
log *zap.Logger
|
||||
|
||||
settings Settings
|
||||
|
||||
chainStorage engine.LocalOverrideStorage
|
||||
}
|
||||
|
||||
func defaultCfg() *cfg {
|
||||
return &cfg{
|
||||
log: zap.NewNop(),
|
||||
settings: defaultSettings{},
|
||||
chainStorage: inmemory.NewInmemoryLocalStorage(),
|
||||
}
|
||||
}
|
||||
|
||||
// New creates, initializes and returns new Server instance.
|
||||
func New(opts ...Option) *Server {
|
||||
c := defaultCfg()
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
|
||||
c.log = c.log.With(zap.String("service", "control API"))
|
||||
|
||||
return &Server{
|
||||
cfg: c,
|
||||
}
|
||||
}
|
||||
|
||||
// WithSettings returns option to add settings to use Control service.
|
||||
func WithSettings(settings Settings) Option {
|
||||
return func(c *cfg) {
|
||||
c.settings = settings
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger returns option to set logger.
|
||||
func WithLogger(log *zap.Logger) Option {
|
||||
return func(c *cfg) {
|
||||
c.log = log
|
||||
}
|
||||
}
|
||||
|
||||
// WithChainStorage returns option to set logger.
|
||||
func WithChainStorage(chainStorage engine.LocalOverrideStorage) Option {
|
||||
return func(c *cfg) {
|
||||
c.chainStorage = chainStorage
|
||||
}
|
||||
}
|
||||
|
||||
// HealthCheck returns health status of the local node.
|
||||
//
|
||||
// If request is unsigned or signed by disallowed key, permission error returns.
|
||||
func (s *Server) HealthCheck(_ context.Context, req *control.HealthCheckRequest) (*control.HealthCheckResponse, error) {
|
||||
s.log.Info(logs.ControlAPIHealthcheck, zap.String("key", hex.EncodeToString(req.Signature.Key)))
|
||||
|
||||
// verify request
|
||||
if err := s.isValidRequest(req); err != nil {
|
||||
return nil, status.Error(codes.PermissionDenied, err.Error())
|
||||
}
|
||||
|
||||
resp := &control.HealthCheckResponse{
|
||||
Body: &control.HealthCheckResponse_Body{
|
||||
HealthStatus: control.HealthStatus_READY,
|
||||
},
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// PutPolicies replaces existing policies.
|
||||
//
|
||||
// If request is unsigned or signed by disallowed key, permission error returns.
|
||||
func (s *Server) PutPolicies(_ context.Context, req *control.PutPoliciesRequest) (*control.PutPoliciesResponse, error) {
|
||||
s.log.Info(logs.ControlAPIPutPolicies, zap.String("key", hex.EncodeToString(req.Signature.Key)))
|
||||
|
||||
// verify request
|
||||
if err := s.isValidRequest(req); err != nil {
|
||||
return nil, status.Error(codes.PermissionDenied, err.Error())
|
||||
}
|
||||
|
||||
for _, data := range req.GetBody().GetChainDatas() {
|
||||
if err := s.putPolicy(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &control.PutPoliciesResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *Server) putPolicy(data *control.PutPoliciesRequest_ChainData) error {
|
||||
var overrideChain chain.Chain
|
||||
if err := overrideChain.DecodeBytes(data.GetChain()); err != nil {
|
||||
return status.Error(codes.InvalidArgument, fmt.Sprintf("failed to parse body: %s", err.Error()))
|
||||
}
|
||||
|
||||
if len(overrideChain.ID) == 0 {
|
||||
return status.Error(codes.InvalidArgument, "missing chain id")
|
||||
}
|
||||
|
||||
t, err := s.parseTarget(data.Target, data.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := s.chainStorage.AddOverride(chain.S3, t, &overrideChain); err != nil {
|
||||
return status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) parseTarget(target control.PolicyTarget, name string) (engine.Target, error) {
|
||||
var (
|
||||
t engine.Target
|
||||
err error
|
||||
)
|
||||
switch target {
|
||||
case control.PolicyTarget_NAMESPACE:
|
||||
ns := s.settings.ResolveNamespaceAlias(name)
|
||||
t = engine.NamespaceTarget(ns)
|
||||
case control.PolicyTarget_CONTAINER:
|
||||
t = engine.ContainerTarget(name)
|
||||
case control.PolicyTarget_USER:
|
||||
t = engine.UserTarget(name)
|
||||
case control.PolicyTarget_GROUP:
|
||||
t = engine.GroupTarget(name)
|
||||
default:
|
||||
err = status.Error(codes.InvalidArgument, fmt.Sprintf("invalid target: %s", target.String()))
|
||||
}
|
||||
return t, err
|
||||
}
|
||||
|
||||
// RemovePolicies removes existing policies.
|
||||
//
|
||||
// If request is unsigned or signed by disallowed key, permission error returns.
|
||||
func (s *Server) RemovePolicies(_ context.Context, req *control.RemovePoliciesRequest) (*control.RemovePoliciesResponse, error) {
|
||||
s.log.Info(logs.ControlAPIRemovePolicies, zap.String("key", hex.EncodeToString(req.Signature.Key)))
|
||||
|
||||
// verify request
|
||||
if err := s.isValidRequest(req); err != nil {
|
||||
return nil, status.Error(codes.PermissionDenied, err.Error())
|
||||
}
|
||||
|
||||
for _, info := range req.GetBody().GetChainInfos() {
|
||||
if err := s.removePolicy(info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &control.RemovePoliciesResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *Server) removePolicy(info *control.RemovePoliciesRequest_ChainInfo) error {
|
||||
t, err := s.parseTarget(info.Target, info.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.chainStorage.RemoveOverride(chain.S3, t, chain.ID(info.GetChainID()))
|
||||
if err != nil {
|
||||
if isNotFoundError(err) {
|
||||
return status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
return status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPolicy returns existing policy.
|
||||
//
|
||||
// If request is unsigned or signed by disallowed key, permission error returns.
|
||||
func (s *Server) GetPolicy(_ context.Context, req *control.GetPolicyRequest) (*control.GetPolicyResponse, error) {
|
||||
s.log.Info(logs.ControlAPIGetPolicy, zap.Stringer("target", req.GetBody().GetTarget()), zap.String("name", req.GetBody().GetName()),
|
||||
zap.Binary("chainId", req.GetBody().GetChainID()), zap.String("key", hex.EncodeToString(req.Signature.Key)))
|
||||
|
||||
t, err := s.parseTarget(req.GetBody().GetTarget(), req.GetBody().GetName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// verify request
|
||||
if err := s.isValidRequest(req); err != nil {
|
||||
return nil, status.Error(codes.PermissionDenied, err.Error())
|
||||
}
|
||||
|
||||
overrideChain, err := s.chainStorage.GetOverride(chain.S3, t, chain.ID(req.GetBody().GetChainID()))
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
return &control.GetPolicyResponse{Body: &control.GetPolicyResponse_Body{Chain: overrideChain.Bytes()}}, nil
|
||||
}
|
||||
|
||||
// ListPolicies lists existing policies.
|
||||
//
|
||||
// If request is unsigned or signed by disallowed key, permission error returns.
|
||||
func (s *Server) ListPolicies(_ context.Context, req *control.ListPoliciesRequest) (*control.ListPoliciesResponse, error) {
|
||||
s.log.Info(logs.ControlAPIListPolicies, zap.Stringer("target", req.GetBody().GetTarget()), zap.String("name", req.GetBody().GetName()),
|
||||
zap.String("key", hex.EncodeToString(req.Signature.Key)))
|
||||
|
||||
t, err := s.parseTarget(req.GetBody().GetTarget(), req.GetBody().GetName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// verify request
|
||||
if err := s.isValidRequest(req); err != nil {
|
||||
return nil, status.Error(codes.PermissionDenied, err.Error())
|
||||
}
|
||||
|
||||
chains, err := s.chainStorage.ListOverrides(chain.S3, t)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
res := make([][]byte, len(chains))
|
||||
for i := range chains {
|
||||
res[i] = []byte(chains[i].ID)
|
||||
}
|
||||
|
||||
return &control.ListPoliciesResponse{Body: &control.ListPoliciesResponse_Body{ChainIDs: res}}, nil
|
||||
}
|
||||
|
||||
// SignedMessage is an interface of Control service message.
|
||||
type SignedMessage interface {
|
||||
ReadSignedData([]byte) ([]byte, error)
|
||||
GetSignature() *control.Signature
|
||||
SetSignature(*control.Signature)
|
||||
}
|
||||
|
||||
var errDisallowedKey = errors.New("key is not in the allowed list")
|
||||
var errMissingSignature = errors.New("missing signature")
|
||||
var errInvalidSignature = errors.New("invalid signature")
|
||||
|
||||
func (s *Server) isValidRequest(req SignedMessage) error {
|
||||
sign := req.GetSignature()
|
||||
if sign == nil {
|
||||
return errMissingSignature
|
||||
}
|
||||
|
||||
var (
|
||||
key = sign.GetKey()
|
||||
allowed = false
|
||||
)
|
||||
|
||||
// check if key is allowed
|
||||
for _, authKey := range s.settings.FetchRawKeys() {
|
||||
if allowed = bytes.Equal(authKey, key); allowed {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !allowed {
|
||||
return errDisallowedKey
|
||||
}
|
||||
|
||||
// verify signature
|
||||
binBody, err := req.ReadSignedData(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal request body: %w", err)
|
||||
}
|
||||
|
||||
var sigV2 refs.Signature
|
||||
sigV2.SetKey(sign.GetKey())
|
||||
sigV2.SetSign(sign.GetSign())
|
||||
sigV2.SetScheme(refs.ECDSA_SHA512)
|
||||
|
||||
var sig frostfscrypto.Signature
|
||||
if err := sig.ReadFromV2(sigV2); err != nil {
|
||||
return fmt.Errorf("can't read signature: %w", err)
|
||||
}
|
||||
|
||||
if !sig.Verify(binBody) {
|
||||
return errInvalidSignature
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SignMessage signs Control service message with private key.
|
||||
func SignMessage(key *ecdsa.PrivateKey, msg SignedMessage) error {
|
||||
binBody, err := msg.ReadSignedData(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal request body: %w", err)
|
||||
}
|
||||
|
||||
var sig frostfscrypto.Signature
|
||||
|
||||
err = sig.Calculate(frostfsecdsa.Signer(*key), binBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("calculate signature: %w", err)
|
||||
}
|
||||
|
||||
var sigV2 refs.Signature
|
||||
sig.WriteToV2(&sigV2)
|
||||
|
||||
var sigControl control.Signature
|
||||
sigControl.Key = sigV2.GetKey()
|
||||
sigControl.Sign = sigV2.GetSign()
|
||||
|
||||
msg.SetSignature(&sigControl)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isNotFoundError(err error) bool {
|
||||
return errors.Is(err, engine.ErrChainNameNotFound) ||
|
||||
errors.Is(err, engine.ErrChainNotFound) ||
|
||||
errors.Is(err, engine.ErrResourceNotFound)
|
||||
}
|
1951
pkg/service/control/service.pb.go
Normal file
1951
pkg/service/control/service.pb.go
Normal file
File diff suppressed because it is too large
Load diff
206
pkg/service/control/service.proto
Normal file
206
pkg/service/control/service.proto
Normal file
|
@ -0,0 +1,206 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package s3gw.control;
|
||||
|
||||
option go_package = "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/control";
|
||||
|
||||
// `ControlService` provides an interface for internal work with the storage node.
|
||||
service ControlService {
|
||||
// Performs health check of the storage node.
|
||||
rpc HealthCheck (HealthCheckRequest) returns (HealthCheckResponse);
|
||||
|
||||
rpc PutPolicies (PutPoliciesRequest) returns (PutPoliciesResponse);
|
||||
|
||||
rpc RemovePolicies (RemovePoliciesRequest) returns (RemovePoliciesResponse);
|
||||
|
||||
rpc GetPolicy (GetPolicyRequest) returns (GetPolicyResponse);
|
||||
|
||||
rpc ListPolicies (ListPoliciesRequest) returns (ListPoliciesResponse);
|
||||
}
|
||||
|
||||
// Signature of some message.
|
||||
message Signature {
|
||||
// Public key used for signing.
|
||||
bytes key = 1 [json_name = "key"];
|
||||
|
||||
// Binary signature.
|
||||
bytes sign = 2 [json_name = "signature"];
|
||||
}
|
||||
|
||||
// Health check request.
|
||||
message HealthCheckRequest {
|
||||
message Body {
|
||||
}
|
||||
|
||||
// Body of health check request message.
|
||||
Body body = 1;
|
||||
|
||||
// Body signature.
|
||||
Signature signature = 2;
|
||||
}
|
||||
|
||||
// Health check response.
|
||||
message HealthCheckResponse {
|
||||
// Health check response body
|
||||
message Body {
|
||||
// Health status of storage node application.
|
||||
HealthStatus health_status = 1;
|
||||
}
|
||||
|
||||
// Body of health check response message.
|
||||
Body body = 1;
|
||||
|
||||
Signature signature = 2;
|
||||
}
|
||||
|
||||
|
||||
// Health status of the storage node application.
|
||||
enum HealthStatus {
|
||||
// Undefined status, default value.
|
||||
HEALTH_STATUS_UNDEFINED = 0;
|
||||
|
||||
// Storage node application is starting.
|
||||
STARTING = 1;
|
||||
|
||||
// Storage node application is started and serves all services.
|
||||
READY = 2;
|
||||
|
||||
// Storage node application is shutting down.
|
||||
SHUTTING_DOWN = 3;
|
||||
}
|
||||
|
||||
// Policy target to store chains.
|
||||
enum PolicyTarget {
|
||||
// Undefined target, invalid to use.
|
||||
TARGET_UNDEFINED = 0;
|
||||
|
||||
// Container target for bucket policies.
|
||||
CONTAINER = 1;
|
||||
|
||||
// Namespace target for namespace policies.
|
||||
NAMESPACE = 2;
|
||||
|
||||
// User target for namespace user policies.
|
||||
USER = 3;
|
||||
|
||||
// Group target for namespace target policies.
|
||||
GROUP = 4;
|
||||
}
|
||||
|
||||
// Put policies request.
|
||||
message PutPoliciesRequest {
|
||||
message ChainData {
|
||||
// Policy entity type.
|
||||
PolicyTarget target = 1;
|
||||
// Policy name.
|
||||
string name = 2;
|
||||
// Chain rules.
|
||||
bytes chain = 3;
|
||||
}
|
||||
|
||||
message Body {
|
||||
repeated ChainData chainDatas = 1;
|
||||
}
|
||||
|
||||
Body body = 1;
|
||||
|
||||
// Body signature.
|
||||
Signature signature = 2;
|
||||
}
|
||||
|
||||
// Put policies response.
|
||||
message PutPoliciesResponse {
|
||||
message Body {
|
||||
}
|
||||
|
||||
Body body = 1;
|
||||
|
||||
Signature signature = 2;
|
||||
}
|
||||
|
||||
// Remove policies request.
|
||||
message RemovePoliciesRequest {
|
||||
message ChainInfo {
|
||||
// Policy entity type.
|
||||
PolicyTarget target = 1;
|
||||
// Policy name.
|
||||
string name = 2;
|
||||
// Chain id to remove.
|
||||
bytes chainID = 3;
|
||||
}
|
||||
|
||||
message Body {
|
||||
repeated ChainInfo chainInfos = 1;
|
||||
}
|
||||
|
||||
Body body = 1;
|
||||
|
||||
// Body signature.
|
||||
Signature signature = 2;
|
||||
}
|
||||
|
||||
// Remove policies response.
|
||||
message RemovePoliciesResponse {
|
||||
message Body {
|
||||
}
|
||||
|
||||
Body body = 1;
|
||||
|
||||
Signature signature = 2;
|
||||
}
|
||||
|
||||
// Get policy request.
|
||||
message GetPolicyRequest {
|
||||
message Body {
|
||||
// Policy entity type.
|
||||
PolicyTarget target = 1;
|
||||
// Policy name.
|
||||
string name = 2;
|
||||
// Chain id to get.
|
||||
bytes chainID = 3;
|
||||
}
|
||||
|
||||
Body body = 1;
|
||||
|
||||
// Body signature.
|
||||
Signature signature = 2;
|
||||
}
|
||||
|
||||
// Get policy response.
|
||||
message GetPolicyResponse {
|
||||
message Body {
|
||||
// Chain rules.
|
||||
bytes chain = 1;
|
||||
}
|
||||
|
||||
Body body = 1;
|
||||
|
||||
Signature signature = 2;
|
||||
}
|
||||
|
||||
// List policies request.
|
||||
message ListPoliciesRequest {
|
||||
message Body {
|
||||
// Policy entity type.
|
||||
PolicyTarget target = 1;
|
||||
// Policy name.
|
||||
string name = 2;
|
||||
}
|
||||
|
||||
Body body = 1;
|
||||
|
||||
// Body signature.
|
||||
Signature signature = 2;
|
||||
}
|
||||
|
||||
// List policies response.
|
||||
message ListPoliciesResponse {
|
||||
message Body {
|
||||
// Chain ids.
|
||||
repeated bytes chainIDs = 1;
|
||||
}
|
||||
|
||||
Body body = 1;
|
||||
|
||||
Signature signature = 2;
|
||||
}
|
955
pkg/service/control/service_frostfs.pb.go
Normal file
955
pkg/service/control/service_frostfs.pb.go
Normal file
|
@ -0,0 +1,955 @@
|
|||
// Code generated by protoc-gen-go-frostfs. DO NOT EDIT.
|
||||
|
||||
package control
|
||||
|
||||
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/util/proto"
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *Signature) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
size += proto.BytesSize(1, x.Key)
|
||||
size += proto.BytesSize(2, x.Sign)
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *Signature) StableMarshal(buf []byte) []byte {
|
||||
if x == nil {
|
||||
return []byte{}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, x.StableSize())
|
||||
}
|
||||
var offset int
|
||||
offset += proto.BytesMarshal(1, buf[offset:], x.Key)
|
||||
offset += proto.BytesMarshal(2, buf[offset:], x.Sign)
|
||||
return buf
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *HealthCheckRequest_Body) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *HealthCheckRequest_Body) StableMarshal(buf []byte) []byte {
|
||||
return buf
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *HealthCheckRequest) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
size += proto.NestedStructureSize(1, x.Body)
|
||||
size += proto.NestedStructureSize(2, x.Signature)
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *HealthCheckRequest) StableMarshal(buf []byte) []byte {
|
||||
if x == nil {
|
||||
return []byte{}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, x.StableSize())
|
||||
}
|
||||
var offset int
|
||||
offset += proto.NestedStructureMarshal(1, buf[offset:], x.Body)
|
||||
offset += proto.NestedStructureMarshal(2, buf[offset:], x.Signature)
|
||||
return buf
|
||||
}
|
||||
|
||||
// ReadSignedData fills buf with signed data of x.
|
||||
// If buffer length is less than x.SignedDataSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same signed data.
|
||||
func (x *HealthCheckRequest) SignedDataSize() int {
|
||||
return x.GetBody().StableSize()
|
||||
}
|
||||
|
||||
// SignedDataSize returns size of the request signed data in bytes.
|
||||
//
|
||||
// Structures with the same field values have the same signed data size.
|
||||
func (x *HealthCheckRequest) ReadSignedData(buf []byte) ([]byte, error) {
|
||||
return x.GetBody().StableMarshal(buf), nil
|
||||
}
|
||||
|
||||
func (x *HealthCheckRequest) SetSignature(sig *Signature) {
|
||||
x.Signature = sig
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *HealthCheckResponse_Body) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
size += proto.EnumSize(1, int32(x.HealthStatus))
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *HealthCheckResponse_Body) StableMarshal(buf []byte) []byte {
|
||||
if x == nil {
|
||||
return []byte{}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, x.StableSize())
|
||||
}
|
||||
var offset int
|
||||
offset += proto.EnumMarshal(1, buf[offset:], int32(x.HealthStatus))
|
||||
return buf
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *HealthCheckResponse) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
size += proto.NestedStructureSize(1, x.Body)
|
||||
size += proto.NestedStructureSize(2, x.Signature)
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *HealthCheckResponse) StableMarshal(buf []byte) []byte {
|
||||
if x == nil {
|
||||
return []byte{}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, x.StableSize())
|
||||
}
|
||||
var offset int
|
||||
offset += proto.NestedStructureMarshal(1, buf[offset:], x.Body)
|
||||
offset += proto.NestedStructureMarshal(2, buf[offset:], x.Signature)
|
||||
return buf
|
||||
}
|
||||
|
||||
// ReadSignedData fills buf with signed data of x.
|
||||
// If buffer length is less than x.SignedDataSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same signed data.
|
||||
func (x *HealthCheckResponse) SignedDataSize() int {
|
||||
return x.GetBody().StableSize()
|
||||
}
|
||||
|
||||
// SignedDataSize returns size of the request signed data in bytes.
|
||||
//
|
||||
// Structures with the same field values have the same signed data size.
|
||||
func (x *HealthCheckResponse) ReadSignedData(buf []byte) ([]byte, error) {
|
||||
return x.GetBody().StableMarshal(buf), nil
|
||||
}
|
||||
|
||||
func (x *HealthCheckResponse) SetSignature(sig *Signature) {
|
||||
x.Signature = sig
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *PutPoliciesRequest_ChainData) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
size += proto.EnumSize(1, int32(x.Target))
|
||||
size += proto.StringSize(2, x.Name)
|
||||
size += proto.BytesSize(3, x.Chain)
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *PutPoliciesRequest_ChainData) StableMarshal(buf []byte) []byte {
|
||||
if x == nil {
|
||||
return []byte{}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, x.StableSize())
|
||||
}
|
||||
var offset int
|
||||
offset += proto.EnumMarshal(1, buf[offset:], int32(x.Target))
|
||||
offset += proto.StringMarshal(2, buf[offset:], x.Name)
|
||||
offset += proto.BytesMarshal(3, buf[offset:], x.Chain)
|
||||
return buf
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *PutPoliciesRequest_Body) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
for i := range x.ChainDatas {
|
||||
size += proto.NestedStructureSize(1, x.ChainDatas[i])
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *PutPoliciesRequest_Body) StableMarshal(buf []byte) []byte {
|
||||
if x == nil {
|
||||
return []byte{}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, x.StableSize())
|
||||
}
|
||||
var offset int
|
||||
for i := range x.ChainDatas {
|
||||
offset += proto.NestedStructureMarshal(1, buf[offset:], x.ChainDatas[i])
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *PutPoliciesRequest) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
size += proto.NestedStructureSize(1, x.Body)
|
||||
size += proto.NestedStructureSize(2, x.Signature)
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *PutPoliciesRequest) StableMarshal(buf []byte) []byte {
|
||||
if x == nil {
|
||||
return []byte{}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, x.StableSize())
|
||||
}
|
||||
var offset int
|
||||
offset += proto.NestedStructureMarshal(1, buf[offset:], x.Body)
|
||||
offset += proto.NestedStructureMarshal(2, buf[offset:], x.Signature)
|
||||
return buf
|
||||
}
|
||||
|
||||
// ReadSignedData fills buf with signed data of x.
|
||||
// If buffer length is less than x.SignedDataSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same signed data.
|
||||
func (x *PutPoliciesRequest) SignedDataSize() int {
|
||||
return x.GetBody().StableSize()
|
||||
}
|
||||
|
||||
// SignedDataSize returns size of the request signed data in bytes.
|
||||
//
|
||||
// Structures with the same field values have the same signed data size.
|
||||
func (x *PutPoliciesRequest) ReadSignedData(buf []byte) ([]byte, error) {
|
||||
return x.GetBody().StableMarshal(buf), nil
|
||||
}
|
||||
|
||||
func (x *PutPoliciesRequest) SetSignature(sig *Signature) {
|
||||
x.Signature = sig
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *PutPoliciesResponse_Body) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *PutPoliciesResponse_Body) StableMarshal(buf []byte) []byte {
|
||||
return buf
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *PutPoliciesResponse) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
size += proto.NestedStructureSize(1, x.Body)
|
||||
size += proto.NestedStructureSize(2, x.Signature)
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *PutPoliciesResponse) StableMarshal(buf []byte) []byte {
|
||||
if x == nil {
|
||||
return []byte{}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, x.StableSize())
|
||||
}
|
||||
var offset int
|
||||
offset += proto.NestedStructureMarshal(1, buf[offset:], x.Body)
|
||||
offset += proto.NestedStructureMarshal(2, buf[offset:], x.Signature)
|
||||
return buf
|
||||
}
|
||||
|
||||
// ReadSignedData fills buf with signed data of x.
|
||||
// If buffer length is less than x.SignedDataSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same signed data.
|
||||
func (x *PutPoliciesResponse) SignedDataSize() int {
|
||||
return x.GetBody().StableSize()
|
||||
}
|
||||
|
||||
// SignedDataSize returns size of the request signed data in bytes.
|
||||
//
|
||||
// Structures with the same field values have the same signed data size.
|
||||
func (x *PutPoliciesResponse) ReadSignedData(buf []byte) ([]byte, error) {
|
||||
return x.GetBody().StableMarshal(buf), nil
|
||||
}
|
||||
|
||||
func (x *PutPoliciesResponse) SetSignature(sig *Signature) {
|
||||
x.Signature = sig
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *RemovePoliciesRequest_ChainInfo) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
size += proto.EnumSize(1, int32(x.Target))
|
||||
size += proto.StringSize(2, x.Name)
|
||||
size += proto.BytesSize(3, x.ChainID)
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *RemovePoliciesRequest_ChainInfo) StableMarshal(buf []byte) []byte {
|
||||
if x == nil {
|
||||
return []byte{}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, x.StableSize())
|
||||
}
|
||||
var offset int
|
||||
offset += proto.EnumMarshal(1, buf[offset:], int32(x.Target))
|
||||
offset += proto.StringMarshal(2, buf[offset:], x.Name)
|
||||
offset += proto.BytesMarshal(3, buf[offset:], x.ChainID)
|
||||
return buf
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *RemovePoliciesRequest_Body) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
for i := range x.ChainInfos {
|
||||
size += proto.NestedStructureSize(1, x.ChainInfos[i])
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *RemovePoliciesRequest_Body) StableMarshal(buf []byte) []byte {
|
||||
if x == nil {
|
||||
return []byte{}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, x.StableSize())
|
||||
}
|
||||
var offset int
|
||||
for i := range x.ChainInfos {
|
||||
offset += proto.NestedStructureMarshal(1, buf[offset:], x.ChainInfos[i])
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *RemovePoliciesRequest) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
size += proto.NestedStructureSize(1, x.Body)
|
||||
size += proto.NestedStructureSize(2, x.Signature)
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *RemovePoliciesRequest) StableMarshal(buf []byte) []byte {
|
||||
if x == nil {
|
||||
return []byte{}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, x.StableSize())
|
||||
}
|
||||
var offset int
|
||||
offset += proto.NestedStructureMarshal(1, buf[offset:], x.Body)
|
||||
offset += proto.NestedStructureMarshal(2, buf[offset:], x.Signature)
|
||||
return buf
|
||||
}
|
||||
|
||||
// ReadSignedData fills buf with signed data of x.
|
||||
// If buffer length is less than x.SignedDataSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same signed data.
|
||||
func (x *RemovePoliciesRequest) SignedDataSize() int {
|
||||
return x.GetBody().StableSize()
|
||||
}
|
||||
|
||||
// SignedDataSize returns size of the request signed data in bytes.
|
||||
//
|
||||
// Structures with the same field values have the same signed data size.
|
||||
func (x *RemovePoliciesRequest) ReadSignedData(buf []byte) ([]byte, error) {
|
||||
return x.GetBody().StableMarshal(buf), nil
|
||||
}
|
||||
|
||||
func (x *RemovePoliciesRequest) SetSignature(sig *Signature) {
|
||||
x.Signature = sig
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *RemovePoliciesResponse_Body) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *RemovePoliciesResponse_Body) StableMarshal(buf []byte) []byte {
|
||||
return buf
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *RemovePoliciesResponse) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
size += proto.NestedStructureSize(1, x.Body)
|
||||
size += proto.NestedStructureSize(2, x.Signature)
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *RemovePoliciesResponse) StableMarshal(buf []byte) []byte {
|
||||
if x == nil {
|
||||
return []byte{}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, x.StableSize())
|
||||
}
|
||||
var offset int
|
||||
offset += proto.NestedStructureMarshal(1, buf[offset:], x.Body)
|
||||
offset += proto.NestedStructureMarshal(2, buf[offset:], x.Signature)
|
||||
return buf
|
||||
}
|
||||
|
||||
// ReadSignedData fills buf with signed data of x.
|
||||
// If buffer length is less than x.SignedDataSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same signed data.
|
||||
func (x *RemovePoliciesResponse) SignedDataSize() int {
|
||||
return x.GetBody().StableSize()
|
||||
}
|
||||
|
||||
// SignedDataSize returns size of the request signed data in bytes.
|
||||
//
|
||||
// Structures with the same field values have the same signed data size.
|
||||
func (x *RemovePoliciesResponse) ReadSignedData(buf []byte) ([]byte, error) {
|
||||
return x.GetBody().StableMarshal(buf), nil
|
||||
}
|
||||
|
||||
func (x *RemovePoliciesResponse) SetSignature(sig *Signature) {
|
||||
x.Signature = sig
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *GetPolicyRequest_Body) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
size += proto.EnumSize(1, int32(x.Target))
|
||||
size += proto.StringSize(2, x.Name)
|
||||
size += proto.BytesSize(3, x.ChainID)
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *GetPolicyRequest_Body) StableMarshal(buf []byte) []byte {
|
||||
if x == nil {
|
||||
return []byte{}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, x.StableSize())
|
||||
}
|
||||
var offset int
|
||||
offset += proto.EnumMarshal(1, buf[offset:], int32(x.Target))
|
||||
offset += proto.StringMarshal(2, buf[offset:], x.Name)
|
||||
offset += proto.BytesMarshal(3, buf[offset:], x.ChainID)
|
||||
return buf
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *GetPolicyRequest) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
size += proto.NestedStructureSize(1, x.Body)
|
||||
size += proto.NestedStructureSize(2, x.Signature)
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *GetPolicyRequest) StableMarshal(buf []byte) []byte {
|
||||
if x == nil {
|
||||
return []byte{}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, x.StableSize())
|
||||
}
|
||||
var offset int
|
||||
offset += proto.NestedStructureMarshal(1, buf[offset:], x.Body)
|
||||
offset += proto.NestedStructureMarshal(2, buf[offset:], x.Signature)
|
||||
return buf
|
||||
}
|
||||
|
||||
// ReadSignedData fills buf with signed data of x.
|
||||
// If buffer length is less than x.SignedDataSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same signed data.
|
||||
func (x *GetPolicyRequest) SignedDataSize() int {
|
||||
return x.GetBody().StableSize()
|
||||
}
|
||||
|
||||
// SignedDataSize returns size of the request signed data in bytes.
|
||||
//
|
||||
// Structures with the same field values have the same signed data size.
|
||||
func (x *GetPolicyRequest) ReadSignedData(buf []byte) ([]byte, error) {
|
||||
return x.GetBody().StableMarshal(buf), nil
|
||||
}
|
||||
|
||||
func (x *GetPolicyRequest) SetSignature(sig *Signature) {
|
||||
x.Signature = sig
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *GetPolicyResponse_Body) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
size += proto.BytesSize(1, x.Chain)
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *GetPolicyResponse_Body) StableMarshal(buf []byte) []byte {
|
||||
if x == nil {
|
||||
return []byte{}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, x.StableSize())
|
||||
}
|
||||
var offset int
|
||||
offset += proto.BytesMarshal(1, buf[offset:], x.Chain)
|
||||
return buf
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *GetPolicyResponse) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
size += proto.NestedStructureSize(1, x.Body)
|
||||
size += proto.NestedStructureSize(2, x.Signature)
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *GetPolicyResponse) StableMarshal(buf []byte) []byte {
|
||||
if x == nil {
|
||||
return []byte{}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, x.StableSize())
|
||||
}
|
||||
var offset int
|
||||
offset += proto.NestedStructureMarshal(1, buf[offset:], x.Body)
|
||||
offset += proto.NestedStructureMarshal(2, buf[offset:], x.Signature)
|
||||
return buf
|
||||
}
|
||||
|
||||
// ReadSignedData fills buf with signed data of x.
|
||||
// If buffer length is less than x.SignedDataSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same signed data.
|
||||
func (x *GetPolicyResponse) SignedDataSize() int {
|
||||
return x.GetBody().StableSize()
|
||||
}
|
||||
|
||||
// SignedDataSize returns size of the request signed data in bytes.
|
||||
//
|
||||
// Structures with the same field values have the same signed data size.
|
||||
func (x *GetPolicyResponse) ReadSignedData(buf []byte) ([]byte, error) {
|
||||
return x.GetBody().StableMarshal(buf), nil
|
||||
}
|
||||
|
||||
func (x *GetPolicyResponse) SetSignature(sig *Signature) {
|
||||
x.Signature = sig
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *ListPoliciesRequest_Body) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
size += proto.EnumSize(1, int32(x.Target))
|
||||
size += proto.StringSize(2, x.Name)
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *ListPoliciesRequest_Body) StableMarshal(buf []byte) []byte {
|
||||
if x == nil {
|
||||
return []byte{}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, x.StableSize())
|
||||
}
|
||||
var offset int
|
||||
offset += proto.EnumMarshal(1, buf[offset:], int32(x.Target))
|
||||
offset += proto.StringMarshal(2, buf[offset:], x.Name)
|
||||
return buf
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *ListPoliciesRequest) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
size += proto.NestedStructureSize(1, x.Body)
|
||||
size += proto.NestedStructureSize(2, x.Signature)
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *ListPoliciesRequest) StableMarshal(buf []byte) []byte {
|
||||
if x == nil {
|
||||
return []byte{}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, x.StableSize())
|
||||
}
|
||||
var offset int
|
||||
offset += proto.NestedStructureMarshal(1, buf[offset:], x.Body)
|
||||
offset += proto.NestedStructureMarshal(2, buf[offset:], x.Signature)
|
||||
return buf
|
||||
}
|
||||
|
||||
// ReadSignedData fills buf with signed data of x.
|
||||
// If buffer length is less than x.SignedDataSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same signed data.
|
||||
func (x *ListPoliciesRequest) SignedDataSize() int {
|
||||
return x.GetBody().StableSize()
|
||||
}
|
||||
|
||||
// SignedDataSize returns size of the request signed data in bytes.
|
||||
//
|
||||
// Structures with the same field values have the same signed data size.
|
||||
func (x *ListPoliciesRequest) ReadSignedData(buf []byte) ([]byte, error) {
|
||||
return x.GetBody().StableMarshal(buf), nil
|
||||
}
|
||||
|
||||
func (x *ListPoliciesRequest) SetSignature(sig *Signature) {
|
||||
x.Signature = sig
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *ListPoliciesResponse_Body) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
size += proto.RepeatedBytesSize(1, x.ChainIDs)
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *ListPoliciesResponse_Body) StableMarshal(buf []byte) []byte {
|
||||
if x == nil {
|
||||
return []byte{}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, x.StableSize())
|
||||
}
|
||||
var offset int
|
||||
offset += proto.RepeatedBytesMarshal(1, buf[offset:], x.ChainIDs)
|
||||
return buf
|
||||
}
|
||||
|
||||
// StableSize returns the size of x in protobuf format.
|
||||
//
|
||||
// Structures with the same field values have the same binary size.
|
||||
func (x *ListPoliciesResponse) StableSize() (size int) {
|
||||
if x == nil {
|
||||
return 0
|
||||
}
|
||||
size += proto.NestedStructureSize(1, x.Body)
|
||||
size += proto.NestedStructureSize(2, x.Signature)
|
||||
return size
|
||||
}
|
||||
|
||||
// StableMarshal marshals x in protobuf binary format with stable field order.
|
||||
//
|
||||
// If buffer length is less than x.StableSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same binary format.
|
||||
func (x *ListPoliciesResponse) StableMarshal(buf []byte) []byte {
|
||||
if x == nil {
|
||||
return []byte{}
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, x.StableSize())
|
||||
}
|
||||
var offset int
|
||||
offset += proto.NestedStructureMarshal(1, buf[offset:], x.Body)
|
||||
offset += proto.NestedStructureMarshal(2, buf[offset:], x.Signature)
|
||||
return buf
|
||||
}
|
||||
|
||||
// ReadSignedData fills buf with signed data of x.
|
||||
// If buffer length is less than x.SignedDataSize(), new buffer is allocated.
|
||||
//
|
||||
// Returns any error encountered which did not allow writing the data completely.
|
||||
// Otherwise, returns the buffer in which the data is written.
|
||||
//
|
||||
// Structures with the same field values have the same signed data.
|
||||
func (x *ListPoliciesResponse) SignedDataSize() int {
|
||||
return x.GetBody().StableSize()
|
||||
}
|
||||
|
||||
// SignedDataSize returns size of the request signed data in bytes.
|
||||
//
|
||||
// Structures with the same field values have the same signed data size.
|
||||
func (x *ListPoliciesResponse) ReadSignedData(buf []byte) ([]byte, error) {
|
||||
return x.GetBody().StableMarshal(buf), nil
|
||||
}
|
||||
|
||||
func (x *ListPoliciesResponse) SetSignature(sig *Signature) {
|
||||
x.Signature = sig
|
||||
}
|
249
pkg/service/control/service_grpc.pb.go
Normal file
249
pkg/service/control/service_grpc.pb.go
Normal file
|
@ -0,0 +1,249 @@
|
|||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.21.12
|
||||
// source: pkg/service/control/service.proto
|
||||
|
||||
package control
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.32.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion7
|
||||
|
||||
// ControlServiceClient is the client API for ControlService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type ControlServiceClient interface {
|
||||
// Performs health check of the storage node.
|
||||
HealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error)
|
||||
PutPolicies(ctx context.Context, in *PutPoliciesRequest, opts ...grpc.CallOption) (*PutPoliciesResponse, error)
|
||||
RemovePolicies(ctx context.Context, in *RemovePoliciesRequest, opts ...grpc.CallOption) (*RemovePoliciesResponse, error)
|
||||
GetPolicy(ctx context.Context, in *GetPolicyRequest, opts ...grpc.CallOption) (*GetPolicyResponse, error)
|
||||
ListPolicies(ctx context.Context, in *ListPoliciesRequest, opts ...grpc.CallOption) (*ListPoliciesResponse, error)
|
||||
}
|
||||
|
||||
type controlServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewControlServiceClient(cc grpc.ClientConnInterface) ControlServiceClient {
|
||||
return &controlServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *controlServiceClient) HealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) {
|
||||
out := new(HealthCheckResponse)
|
||||
err := c.cc.Invoke(ctx, "/s3gw.control.ControlService/HealthCheck", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *controlServiceClient) PutPolicies(ctx context.Context, in *PutPoliciesRequest, opts ...grpc.CallOption) (*PutPoliciesResponse, error) {
|
||||
out := new(PutPoliciesResponse)
|
||||
err := c.cc.Invoke(ctx, "/s3gw.control.ControlService/PutPolicies", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *controlServiceClient) RemovePolicies(ctx context.Context, in *RemovePoliciesRequest, opts ...grpc.CallOption) (*RemovePoliciesResponse, error) {
|
||||
out := new(RemovePoliciesResponse)
|
||||
err := c.cc.Invoke(ctx, "/s3gw.control.ControlService/RemovePolicies", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *controlServiceClient) GetPolicy(ctx context.Context, in *GetPolicyRequest, opts ...grpc.CallOption) (*GetPolicyResponse, error) {
|
||||
out := new(GetPolicyResponse)
|
||||
err := c.cc.Invoke(ctx, "/s3gw.control.ControlService/GetPolicy", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *controlServiceClient) ListPolicies(ctx context.Context, in *ListPoliciesRequest, opts ...grpc.CallOption) (*ListPoliciesResponse, error) {
|
||||
out := new(ListPoliciesResponse)
|
||||
err := c.cc.Invoke(ctx, "/s3gw.control.ControlService/ListPolicies", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ControlServiceServer is the server API for ControlService service.
|
||||
// All implementations should embed UnimplementedControlServiceServer
|
||||
// for forward compatibility
|
||||
type ControlServiceServer interface {
|
||||
// Performs health check of the storage node.
|
||||
HealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error)
|
||||
PutPolicies(context.Context, *PutPoliciesRequest) (*PutPoliciesResponse, error)
|
||||
RemovePolicies(context.Context, *RemovePoliciesRequest) (*RemovePoliciesResponse, error)
|
||||
GetPolicy(context.Context, *GetPolicyRequest) (*GetPolicyResponse, error)
|
||||
ListPolicies(context.Context, *ListPoliciesRequest) (*ListPoliciesResponse, error)
|
||||
}
|
||||
|
||||
// UnimplementedControlServiceServer should be embedded to have forward compatible implementations.
|
||||
type UnimplementedControlServiceServer struct {
|
||||
}
|
||||
|
||||
func (UnimplementedControlServiceServer) HealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method HealthCheck not implemented")
|
||||
}
|
||||
func (UnimplementedControlServiceServer) PutPolicies(context.Context, *PutPoliciesRequest) (*PutPoliciesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method PutPolicies not implemented")
|
||||
}
|
||||
func (UnimplementedControlServiceServer) RemovePolicies(context.Context, *RemovePoliciesRequest) (*RemovePoliciesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RemovePolicies not implemented")
|
||||
}
|
||||
func (UnimplementedControlServiceServer) GetPolicy(context.Context, *GetPolicyRequest) (*GetPolicyResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetPolicy not implemented")
|
||||
}
|
||||
func (UnimplementedControlServiceServer) ListPolicies(context.Context, *ListPoliciesRequest) (*ListPoliciesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListPolicies not implemented")
|
||||
}
|
||||
|
||||
// UnsafeControlServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to ControlServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeControlServiceServer interface {
|
||||
mustEmbedUnimplementedControlServiceServer()
|
||||
}
|
||||
|
||||
func RegisterControlServiceServer(s grpc.ServiceRegistrar, srv ControlServiceServer) {
|
||||
s.RegisterService(&ControlService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _ControlService_HealthCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(HealthCheckRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ControlServiceServer).HealthCheck(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/s3gw.control.ControlService/HealthCheck",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ControlServiceServer).HealthCheck(ctx, req.(*HealthCheckRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ControlService_PutPolicies_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(PutPoliciesRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ControlServiceServer).PutPolicies(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/s3gw.control.ControlService/PutPolicies",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ControlServiceServer).PutPolicies(ctx, req.(*PutPoliciesRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ControlService_RemovePolicies_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RemovePoliciesRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ControlServiceServer).RemovePolicies(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/s3gw.control.ControlService/RemovePolicies",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ControlServiceServer).RemovePolicies(ctx, req.(*RemovePoliciesRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ControlService_GetPolicy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetPolicyRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ControlServiceServer).GetPolicy(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/s3gw.control.ControlService/GetPolicy",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ControlServiceServer).GetPolicy(ctx, req.(*GetPolicyRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ControlService_ListPolicies_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListPoliciesRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ControlServiceServer).ListPolicies(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/s3gw.control.ControlService/ListPolicies",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ControlServiceServer).ListPolicies(ctx, req.(*ListPoliciesRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// ControlService_ServiceDesc is the grpc.ServiceDesc for ControlService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var ControlService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "s3gw.control.ControlService",
|
||||
HandlerType: (*ControlServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "HealthCheck",
|
||||
Handler: _ControlService_HealthCheck_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "PutPolicies",
|
||||
Handler: _ControlService_PutPolicies_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RemovePolicies",
|
||||
Handler: _ControlService_RemovePolicies_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetPolicy",
|
||||
Handler: _ControlService_GetPolicy_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListPolicies",
|
||||
Handler: _ControlService_ListPolicies_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "pkg/service/control/service.proto",
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -2,7 +2,6 @@ package tree
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
|
@ -32,16 +31,16 @@ type nodeResponse struct {
|
|||
timestamp uint64
|
||||
}
|
||||
|
||||
func (n nodeResponse) GetNodeID() []uint64 {
|
||||
return []uint64{n.nodeID}
|
||||
func (n nodeResponse) GetNodeID() uint64 {
|
||||
return n.nodeID
|
||||
}
|
||||
|
||||
func (n nodeResponse) GetParentID() []uint64 {
|
||||
return []uint64{n.parentID}
|
||||
func (n nodeResponse) GetParentID() uint64 {
|
||||
return n.parentID
|
||||
}
|
||||
|
||||
func (n nodeResponse) GetTimestamp() []uint64 {
|
||||
return []uint64{n.timestamp}
|
||||
func (n nodeResponse) GetTimestamp() uint64 {
|
||||
return n.timestamp
|
||||
}
|
||||
|
||||
func (n nodeResponse) GetMeta() []Meta {
|
||||
|
@ -234,7 +233,7 @@ func (c *ServiceClientMemory) GetNodes(_ context.Context, p *GetNodesParams) ([]
|
|||
return res2, nil
|
||||
}
|
||||
|
||||
func (c *ServiceClientMemory) GetSubTree(_ context.Context, bktInfo *data.BucketInfo, treeID string, rootID []uint64, depth uint32) ([]NodeResponse, error) {
|
||||
func (c *ServiceClientMemory) GetSubTree(_ context.Context, bktInfo *data.BucketInfo, treeID string, rootID uint64, depth uint32) ([]NodeResponse, error) {
|
||||
cnr, ok := c.containers[bktInfo.CID.EncodeToString()]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
|
@ -245,11 +244,7 @@ func (c *ServiceClientMemory) GetSubTree(_ context.Context, bktInfo *data.Bucket
|
|||
return nil, ErrNodeNotFound
|
||||
}
|
||||
|
||||
if len(rootID) != 1 {
|
||||
return nil, errors.New("invalid rootID")
|
||||
}
|
||||
|
||||
node := tr.treeData.getNode(rootID[0])
|
||||
node := tr.treeData.getNode(rootID)
|
||||
if node == nil {
|
||||
return nil, ErrNodeNotFound
|
||||
}
|
||||
|
@ -275,7 +270,7 @@ func (s *SubTreeStreamMemoryImpl) Next() (NodeResponse, error) {
|
|||
return s.res[s.offset-1], nil
|
||||
}
|
||||
|
||||
func (c *ServiceClientMemory) GetSubTreeStream(_ context.Context, bktInfo *data.BucketInfo, treeID string, rootID []uint64, depth uint32) (SubTreeStream, error) {
|
||||
func (c *ServiceClientMemory) GetSubTreeStream(_ context.Context, bktInfo *data.BucketInfo, treeID string, rootID uint64, depth uint32) (SubTreeStream, error) {
|
||||
cnr, ok := c.containers[bktInfo.CID.EncodeToString()]
|
||||
if !ok {
|
||||
return &SubTreeStreamMemoryImpl{err: ErrNodeNotFound}, nil
|
||||
|
@ -286,11 +281,7 @@ func (c *ServiceClientMemory) GetSubTreeStream(_ context.Context, bktInfo *data.
|
|||
return nil, ErrNodeNotFound
|
||||
}
|
||||
|
||||
if len(rootID) != 1 {
|
||||
return nil, errors.New("invalid rootID")
|
||||
}
|
||||
|
||||
node := tr.treeData.getNode(rootID[0])
|
||||
node := tr.treeData.getNode(rootID)
|
||||
if node == nil {
|
||||
return nil, ErrNodeNotFound
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue