forked from TrueCloudLab/frostfs-s3-gw
Compare commits
22 commits
support/v0
...
master
Author | SHA1 | Date | |
---|---|---|---|
9f29fcbd52 | |||
8307c73fef | |||
d8889fca56 | |||
61ff4702a2 | |||
6da1acc554 | |||
3ea3f971e1 | |||
cb83f7646f | |||
9c012d0a66 | |||
bda014b7b4 | |||
37d05dcefd | |||
8407b3ea4c | |||
e537675223 | |||
789464e134 | |||
a138f4954b | |||
8669bf6b50 | |||
6b8095182e | |||
348126b3b8 | |||
fbe7a784e8 | |||
bfcde09f07 | |||
94bd1dfe28 | |||
80c7b73eb9 | |||
62cc5a04a7 |
55 changed files with 2872 additions and 458 deletions
|
@ -11,6 +11,7 @@ This document outlines major changes between releases.
|
|||
- Fix possibility of panic during SIGHUP (#288)
|
||||
- Fix flaky `TestErrorTimeoutChecking` (`make test` sometimes failed) (#290)
|
||||
- Fix user owner ID in billing metrics (#321)
|
||||
- Fix HTTP/2 requests (#341)
|
||||
|
||||
### Added
|
||||
- Add new `frostfs.buffer_max_size_for_put` config param and sync TZ hash for PUT operations (#197)
|
||||
|
@ -26,6 +27,8 @@ This document outlines major changes between releases.
|
|||
- Support `policy` contract (#259)
|
||||
- Support `proxy` contract (#287)
|
||||
- Authmate: support custom attributes (#292)
|
||||
- Add new `reconnect_interval` config param (#291)
|
||||
- Support `GetBucketPolicyStatus` (#301)
|
||||
|
||||
### Changed
|
||||
- Generalise config param `use_default_xmlns_for_complete_multipart` to `use_default_xmlns` so that use default xmlns for all requests (#221)
|
||||
|
|
|
@ -91,6 +91,7 @@ const (
|
|||
ErrBucketNotEmpty
|
||||
ErrAllAccessDisabled
|
||||
ErrMalformedPolicy
|
||||
ErrMalformedPolicyNotPrincipal
|
||||
ErrMissingFields
|
||||
ErrMissingCredTag
|
||||
ErrCredMalformed
|
||||
|
@ -665,6 +666,12 @@ var errorCodes = errorCodeMap{
|
|||
Description: "Policy has invalid resource.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMalformedPolicyNotPrincipal: {
|
||||
ErrCode: ErrMalformedPolicyNotPrincipal,
|
||||
Code: "MalformedPolicy",
|
||||
Description: "Allow with NotPrincipal is not allowed.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingFields: {
|
||||
ErrCode: ErrMissingFields,
|
||||
Code: "MissingFields",
|
||||
|
|
|
@ -284,6 +284,32 @@ func (h *handler) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func (h *handler) encodeBucketCannedACL(ctx context.Context, bktInfo *data.BucketInfo, settings *data.BucketSettings) *AccessControlPolicy {
|
||||
res := h.encodePrivateCannedACL(ctx, bktInfo, settings)
|
||||
|
||||
switch settings.CannedACL {
|
||||
case basicACLPublic:
|
||||
grantee := NewGrantee(acpGroup)
|
||||
grantee.URI = allUsersGroup
|
||||
|
||||
res.AccessControlList = append(res.AccessControlList, &Grant{
|
||||
Grantee: grantee,
|
||||
Permission: aclWrite,
|
||||
})
|
||||
fallthrough
|
||||
case basicACLReadOnly:
|
||||
grantee := NewGrantee(acpGroup)
|
||||
grantee.URI = allUsersGroup
|
||||
|
||||
res.AccessControlList = append(res.AccessControlList, &Grant{
|
||||
Grantee: grantee,
|
||||
Permission: aclRead,
|
||||
})
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (h *handler) encodePrivateCannedACL(ctx context.Context, bktInfo *data.BucketInfo, settings *data.BucketSettings) *AccessControlPolicy {
|
||||
ownerDisplayName := bktInfo.Owner.EncodeToString()
|
||||
ownerEncodedID := ownerDisplayName
|
||||
|
||||
|
@ -308,26 +334,6 @@ func (h *handler) encodeBucketCannedACL(ctx context.Context, bktInfo *data.Bucke
|
|||
Permission: aclFullControl,
|
||||
}}
|
||||
|
||||
switch settings.CannedACL {
|
||||
case basicACLPublic:
|
||||
grantee := NewGrantee(acpGroup)
|
||||
grantee.URI = allUsersGroup
|
||||
|
||||
res.AccessControlList = append(res.AccessControlList, &Grant{
|
||||
Grantee: grantee,
|
||||
Permission: aclWrite,
|
||||
})
|
||||
fallthrough
|
||||
case basicACLReadOnly:
|
||||
grantee := NewGrantee(acpGroup)
|
||||
grantee.URI = allUsersGroup
|
||||
|
||||
res.AccessControlList = append(res.AccessControlList, &Grant{
|
||||
Grantee: grantee,
|
||||
Permission: aclRead,
|
||||
})
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
|
@ -444,7 +450,7 @@ func (h *handler) putBucketACLAPEHandler(w http.ResponseWriter, r *http.Request,
|
|||
}
|
||||
|
||||
chainRules := bucketCannedACLToAPERules(cannedACL, reqInfo, key, bktInfo.CID)
|
||||
if err = h.ape.SaveACLChains(reqInfo.Namespace, chainRules); err != nil {
|
||||
if err = h.ape.SaveACLChains(bktInfo.CID.EncodeToString(), chainRules); err != nil {
|
||||
h.logAndSendError(w, "failed to add morph rule chains", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
@ -513,19 +519,17 @@ func (h *handler) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
apeEnabled := bktInfo.APEEnabled
|
||||
|
||||
if !apeEnabled {
|
||||
settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "couldn't get bucket settings", reqInfo, err)
|
||||
return
|
||||
}
|
||||
apeEnabled = len(settings.CannedACL) != 0
|
||||
settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "couldn't get bucket settings", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
if apeEnabled {
|
||||
h.logAndSendError(w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported))
|
||||
if bktInfo.APEEnabled || len(settings.CannedACL) != 0 {
|
||||
if err = middleware.EncodeToResponse(w, h.encodePrivateCannedACL(ctx, bktInfo, settings)); err != nil {
|
||||
h.logAndSendError(w, "something went wrong", reqInfo, err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -543,7 +547,7 @@ func (h *handler) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
objInfo, err := h.obj.GetObjectInfo(ctx, prm)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not object info", reqInfo, err)
|
||||
h.logAndSendError(w, "could not get object info", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -646,6 +650,48 @@ func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) {
|
|||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *handler) GetBucketPolicyStatusHandler(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
|
||||
}
|
||||
|
||||
jsonPolicy, err := h.ape.GetBucketPolicy(reqInfo.Namespace, bktInfo.CID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
err = fmt.Errorf("%w: %s", errors.GetAPIError(errors.ErrNoSuchBucketPolicy), err.Error())
|
||||
}
|
||||
h.logAndSendError(w, "failed to get policy from storage", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
var bktPolicy engineiam.Policy
|
||||
if err = json.Unmarshal(jsonPolicy, &bktPolicy); err != nil {
|
||||
h.logAndSendError(w, "could not parse bucket policy", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
policyStatus := &PolicyStatus{
|
||||
IsPublic: PolicyStatusIsPublicFalse,
|
||||
}
|
||||
|
||||
for _, st := range bktPolicy.Statement {
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html#access-control-block-public-access-policy-status
|
||||
if _, ok := st.Principal[engineiam.Wildcard]; ok {
|
||||
policyStatus.IsPublic = PolicyStatusIsPublicTrue
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err = middleware.EncodeToResponse(w, policyStatus); err != nil {
|
||||
h.logAndSendError(w, "encode and write response", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
reqInfo := middleware.GetReqInfo(r.Context())
|
||||
|
||||
|
@ -681,7 +727,8 @@ func (h *handler) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Reque
|
|||
return
|
||||
}
|
||||
|
||||
if err = h.ape.DeleteBucketPolicy(reqInfo.Namespace, bktInfo.CID, getBucketChainID(chain.S3, bktInfo)); err != nil {
|
||||
chainIDs := []chain.ID{getBucketChainID(chain.S3, bktInfo), getBucketChainID(chain.Ingress, bktInfo)}
|
||||
if err = h.ape.DeleteBucketPolicy(reqInfo.Namespace, bktInfo.CID, chainIDs); err != nil {
|
||||
h.logAndSendError(w, "failed to delete policy from storage", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
@ -727,6 +774,11 @@ func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request)
|
|||
return
|
||||
}
|
||||
|
||||
if len(stat.NotPrincipal) != 0 && stat.Effect == engineiam.AllowEffect {
|
||||
h.logAndSendError(w, "invalid NotPrincipal", reqInfo, errors.GetAPIError(errors.ErrMalformedPolicyNotPrincipal))
|
||||
return
|
||||
}
|
||||
|
||||
for _, resource := range stat.Resource {
|
||||
if reqInfo.BucketName != strings.Split(strings.TrimPrefix(resource, arnAwsPrefix), "/")[0] {
|
||||
h.logAndSendError(w, "policy resource mismatched bucket", reqInfo, errors.GetAPIError(errors.ErrMalformedPolicy))
|
||||
|
@ -735,13 +787,6 @@ func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request)
|
|||
}
|
||||
}
|
||||
|
||||
nativeChain, err := engineiam.ConvertToNativeChain(bktPolicy, h.nativeResolver(reqInfo.Namespace, bktInfo))
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not convert s3 policy to native chain policy", reqInfo, err)
|
||||
return
|
||||
}
|
||||
nativeChain.ID = getBucketChainID(chain.Ingress, bktInfo)
|
||||
|
||||
s3Chain, err := engineiam.ConvertToS3Chain(bktPolicy, h.frostfsid)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not convert s3 policy to chain policy", reqInfo, err)
|
||||
|
@ -749,7 +794,22 @@ func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request)
|
|||
}
|
||||
s3Chain.ID = getBucketChainID(chain.S3, bktInfo)
|
||||
|
||||
if err = h.ape.PutBucketPolicy(reqInfo.Namespace, bktInfo.CID, jsonPolicy, []*chain.Chain{s3Chain, nativeChain}); err != nil {
|
||||
nativeChain, err := engineiam.ConvertToNativeChain(bktPolicy, h.nativeResolver(reqInfo.Namespace, bktInfo))
|
||||
if err == nil {
|
||||
nativeChain.ID = getBucketChainID(chain.Ingress, bktInfo)
|
||||
} else if !stderrors.Is(err, engineiam.ErrActionsNotApplicable) {
|
||||
h.logAndSendError(w, "could not convert s3 policy to native chain policy", reqInfo, err)
|
||||
return
|
||||
} else {
|
||||
h.reqLogger(r.Context()).Warn(logs.PolicyCouldntBeConvertedToNativeRules)
|
||||
}
|
||||
|
||||
chainsToSave := []*chain.Chain{s3Chain}
|
||||
if nativeChain != nil {
|
||||
chainsToSave = append(chainsToSave, nativeChain)
|
||||
}
|
||||
|
||||
if err = h.ape.PutBucketPolicy(reqInfo.Namespace, bktInfo.CID, jsonPolicy, chainsToSave); err != nil {
|
||||
h.logAndSendError(w, "failed to update policy in contract", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
@ -1570,6 +1630,26 @@ func isWriteOperation(op eacl.Operation) bool {
|
|||
return op == eacl.OperationDelete || op == eacl.OperationPut
|
||||
}
|
||||
|
||||
type access struct {
|
||||
recipient string
|
||||
operations []eacl.Operation
|
||||
}
|
||||
|
||||
type accessList struct {
|
||||
list []access
|
||||
}
|
||||
|
||||
func (c *accessList) addAccess(recipient string, operation eacl.Operation) {
|
||||
for i, v := range c.list {
|
||||
if v.recipient == recipient {
|
||||
c.list[i].operations = append(c.list[i].operations, operation)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.list = append(c.list, access{recipient, []eacl.Operation{operation}})
|
||||
}
|
||||
|
||||
func (h *handler) encodeObjectACL(ctx context.Context, bucketACL *layer.BucketACL, bucketName, objectVersion string) *AccessControlPolicy {
|
||||
res := &AccessControlPolicy{
|
||||
Owner: Owner{
|
||||
|
@ -1578,7 +1658,7 @@ func (h *handler) encodeObjectACL(ctx context.Context, bucketACL *layer.BucketAC
|
|||
},
|
||||
}
|
||||
|
||||
m := make(map[string][]eacl.Operation)
|
||||
m := &accessList{}
|
||||
|
||||
astList := tableToAst(bucketACL.EACL, bucketName)
|
||||
|
||||
|
@ -1593,22 +1673,20 @@ func (h *handler) encodeObjectACL(ctx context.Context, bucketACL *layer.BucketAC
|
|||
}
|
||||
|
||||
if len(op.Users) == 0 {
|
||||
list := append(m[allUsersGroup], op.Op)
|
||||
m[allUsersGroup] = list
|
||||
m.addAccess(allUsersGroup, op.Op)
|
||||
} else {
|
||||
for _, user := range op.Users {
|
||||
list := append(m[user], op.Op)
|
||||
m[user] = list
|
||||
m.addAccess(user, op.Op)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for key, val := range m {
|
||||
for _, val := range m.list {
|
||||
permission := aclFullControl
|
||||
read := true
|
||||
for op := eacl.OperationGet; op <= eacl.OperationRangeHash; op++ {
|
||||
if !contains(val, op) && !isWriteOperation(op) {
|
||||
if !contains(val.operations, op) && !isWriteOperation(op) {
|
||||
read = false
|
||||
}
|
||||
}
|
||||
|
@ -1620,12 +1698,12 @@ func (h *handler) encodeObjectACL(ctx context.Context, bucketACL *layer.BucketAC
|
|||
}
|
||||
|
||||
var grantee *Grantee
|
||||
if key == allUsersGroup {
|
||||
if val.recipient == allUsersGroup {
|
||||
grantee = NewGrantee(acpGroup)
|
||||
grantee.URI = allUsersGroup
|
||||
} else {
|
||||
grantee = NewGrantee(acpCanonicalUser)
|
||||
grantee.ID = key
|
||||
grantee.ID = val.recipient
|
||||
}
|
||||
|
||||
grant := &Grant{
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
@ -1324,60 +1325,101 @@ func TestPutBucketAPE(t *testing.T) {
|
|||
_, err := hc.tp.ContainerEACL(hc.Context(), layer.PrmContainerEACL{ContainerID: info.BktInfo.CID})
|
||||
require.ErrorContains(t, err, "not found")
|
||||
|
||||
chains, err := hc.h.ape.(*apeMock).ListChains(engine.NamespaceTarget(""))
|
||||
chains, err := hc.h.ape.(*apeMock).ListChains(engine.ContainerTarget(info.BktInfo.CID.EncodeToString()))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, chains, 2)
|
||||
}
|
||||
|
||||
func TestPutBucketObjectACLErrorAPE(t *testing.T) {
|
||||
func TestPutObjectACLErrorAPE(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
bktName, objName := "bucket-for-acl-ape", "object"
|
||||
|
||||
info := createBucket(hc, bktName)
|
||||
putObject(hc, bktName, objName)
|
||||
|
||||
putObjectWithHeadersAssertS3Error(hc, bktName, objName, map[string]string{api.AmzACL: basicACLPublic}, s3errors.ErrAccessControlListNotSupported)
|
||||
putObjectWithHeaders(hc, bktName, objName, map[string]string{api.AmzACL: basicACLPrivate}) // only `private` canned acl is allowed, that is actually ignored
|
||||
putObjectWithHeaders(hc, bktName, objName, nil)
|
||||
|
||||
aclBody := &AccessControlPolicy{}
|
||||
putBucketACLAssertS3Error(hc, bktName, info.Box, nil, aclBody, s3errors.ErrAccessControlListNotSupported)
|
||||
|
||||
putObjectACLAssertS3Error(hc, bktName, objName, info.Box, nil, aclBody, s3errors.ErrAccessControlListNotSupported)
|
||||
getObjectACLAssertS3Error(hc, bktName, objName, s3errors.ErrAccessControlListNotSupported)
|
||||
|
||||
aclRes := getObjectACL(hc, bktName, objName)
|
||||
checkPrivateACL(t, aclRes, info.Key.PublicKey())
|
||||
}
|
||||
|
||||
func TestGetBucketACLAPE(t *testing.T) {
|
||||
func TestCreateObjectACLErrorAPE(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
bktName, objName, objNameCopy := "bucket-for-acl-ape", "object", "copy"
|
||||
|
||||
createBucket(hc, bktName)
|
||||
|
||||
putObject(hc, bktName, objName)
|
||||
copyObject(hc, bktName, objName, objNameCopy, CopyMeta{Headers: map[string]string{api.AmzACL: basicACLPublic}}, http.StatusBadRequest)
|
||||
copyObject(hc, bktName, objName, objNameCopy, CopyMeta{Headers: map[string]string{api.AmzACL: basicACLPrivate}}, http.StatusOK)
|
||||
|
||||
createMultipartUploadAssertS3Error(hc, bktName, objName, map[string]string{api.AmzACL: basicACLPublic}, s3errors.ErrAccessControlListNotSupported)
|
||||
createMultipartUpload(hc, bktName, objName, map[string]string{api.AmzACL: basicACLPrivate})
|
||||
}
|
||||
|
||||
func TestPutObjectACLBackwardCompatibility(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
hc.config.aclEnabled = true
|
||||
bktName, objName := "bucket-for-acl-ape", "object"
|
||||
|
||||
info := createBucket(hc, bktName)
|
||||
|
||||
putObjectWithHeadersBase(hc, bktName, objName, map[string]string{api.AmzACL: basicACLPrivate}, info.Box, nil)
|
||||
putObjectWithHeadersBase(hc, bktName, objName, map[string]string{api.AmzACL: basicACLPublic}, info.Box, nil)
|
||||
|
||||
aclRes := getObjectACL(hc, bktName, objName)
|
||||
require.Len(t, aclRes.AccessControlList, 2)
|
||||
require.Equal(t, hex.EncodeToString(info.Key.PublicKey().Bytes()), aclRes.AccessControlList[0].Grantee.ID)
|
||||
require.Equal(t, aclFullControl, aclRes.AccessControlList[0].Permission)
|
||||
require.Equal(t, allUsersGroup, aclRes.AccessControlList[1].Grantee.URI)
|
||||
require.Equal(t, aclFullControl, aclRes.AccessControlList[1].Permission)
|
||||
|
||||
aclBody := &AccessControlPolicy{}
|
||||
putObjectACLBase(hc, bktName, objName, info.Box, nil, aclBody)
|
||||
}
|
||||
|
||||
func TestBucketACLAPE(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
bktName := "bucket-for-acl-ape"
|
||||
|
||||
info := createBucket(hc, bktName)
|
||||
|
||||
aclBody := &AccessControlPolicy{}
|
||||
putBucketACLAssertS3Error(hc, bktName, info.Box, nil, aclBody, s3errors.ErrAccessControlListNotSupported)
|
||||
|
||||
aclRes := getBucketACL(hc, bktName)
|
||||
checkPrivateBucketACL(t, aclRes, info.Key.PublicKey())
|
||||
checkPrivateACL(t, aclRes, info.Key.PublicKey())
|
||||
|
||||
putBucketACL(hc, bktName, info.Box, map[string]string{api.AmzACL: basicACLPrivate})
|
||||
aclRes = getBucketACL(hc, bktName)
|
||||
checkPrivateBucketACL(t, aclRes, info.Key.PublicKey())
|
||||
checkPrivateACL(t, aclRes, info.Key.PublicKey())
|
||||
|
||||
putBucketACL(hc, bktName, info.Box, map[string]string{api.AmzACL: basicACLReadOnly})
|
||||
aclRes = getBucketACL(hc, bktName)
|
||||
checkPublicReadBucketACL(t, aclRes, info.Key.PublicKey())
|
||||
checkPublicReadACL(t, aclRes, info.Key.PublicKey())
|
||||
|
||||
putBucketACL(hc, bktName, info.Box, map[string]string{api.AmzACL: basicACLPublic})
|
||||
aclRes = getBucketACL(hc, bktName)
|
||||
checkPublicReadWriteBucketACL(t, aclRes, info.Key.PublicKey())
|
||||
checkPublicReadWriteACL(t, aclRes, info.Key.PublicKey())
|
||||
}
|
||||
|
||||
func checkPrivateBucketACL(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey) {
|
||||
checkBucketACLOwner(t, aclRes, ownerKey, 1)
|
||||
func checkPrivateACL(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey) {
|
||||
checkACLOwner(t, aclRes, ownerKey, 1)
|
||||
}
|
||||
|
||||
func checkPublicReadBucketACL(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey) {
|
||||
checkBucketACLOwner(t, aclRes, ownerKey, 2)
|
||||
func checkPublicReadACL(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey) {
|
||||
checkACLOwner(t, aclRes, ownerKey, 2)
|
||||
|
||||
require.Equal(t, allUsersGroup, aclRes.AccessControlList[1].Grantee.URI)
|
||||
require.Equal(t, aclRead, aclRes.AccessControlList[1].Permission)
|
||||
}
|
||||
|
||||
func checkPublicReadWriteBucketACL(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey) {
|
||||
checkBucketACLOwner(t, aclRes, ownerKey, 3)
|
||||
func checkPublicReadWriteACL(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey) {
|
||||
checkACLOwner(t, aclRes, ownerKey, 3)
|
||||
|
||||
require.Equal(t, allUsersGroup, aclRes.AccessControlList[1].Grantee.URI)
|
||||
require.Equal(t, aclWrite, aclRes.AccessControlList[1].Permission)
|
||||
|
@ -1386,7 +1428,7 @@ func checkPublicReadWriteBucketACL(t *testing.T, aclRes *AccessControlPolicy, ow
|
|||
require.Equal(t, aclRead, aclRes.AccessControlList[2].Permission)
|
||||
}
|
||||
|
||||
func checkBucketACLOwner(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey, ln int) {
|
||||
func checkACLOwner(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey, ln int) {
|
||||
ownerIDStr := hex.EncodeToString(ownerKey.Bytes())
|
||||
ownerNameStr := ownerKey.Address()
|
||||
|
||||
|
@ -1409,6 +1451,7 @@ func TestBucketPolicy(t *testing.T) {
|
|||
getBucketPolicy(hc, bktName, s3errors.ErrNoSuchBucketPolicy)
|
||||
|
||||
newPolicy := engineiam.Policy{
|
||||
Version: "2012-10-17",
|
||||
Statement: []engineiam.Statement{{
|
||||
Principal: map[engineiam.PrincipalType][]string{engineiam.Wildcard: {}},
|
||||
Effect: engineiam.DenyEffect,
|
||||
|
@ -1426,6 +1469,71 @@ func TestBucketPolicy(t *testing.T) {
|
|||
require.Equal(t, newPolicy, bktPolicy)
|
||||
}
|
||||
|
||||
func TestBucketPolicyStatus(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
bktName := "bucket-for-policy"
|
||||
|
||||
createTestBucket(hc, bktName)
|
||||
|
||||
getBucketPolicy(hc, bktName, s3errors.ErrNoSuchBucketPolicy)
|
||||
|
||||
newPolicy := engineiam.Policy{
|
||||
Version: "2012-10-17",
|
||||
Statement: []engineiam.Statement{{
|
||||
NotPrincipal: engineiam.Principal{engineiam.Wildcard: {}},
|
||||
Effect: engineiam.AllowEffect,
|
||||
Action: engineiam.Action{"s3:PutObject"},
|
||||
Resource: engineiam.Resource{arnAwsPrefix + bktName + "/*"},
|
||||
}},
|
||||
}
|
||||
|
||||
putBucketPolicy(hc, bktName, newPolicy, s3errors.ErrMalformedPolicyNotPrincipal)
|
||||
|
||||
newPolicy.Statement[0].NotPrincipal = nil
|
||||
newPolicy.Statement[0].Principal = map[engineiam.PrincipalType][]string{engineiam.Wildcard: {}}
|
||||
putBucketPolicy(hc, bktName, newPolicy)
|
||||
bktPolicyStatus := getBucketPolicyStatus(hc, bktName)
|
||||
require.True(t, PolicyStatusIsPublicTrue == bktPolicyStatus.IsPublic)
|
||||
|
||||
key, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
hc.Handler().frostfsid.(*frostfsidMock).data["devenv"] = key.PublicKey()
|
||||
|
||||
newPolicy.Statement[0].Principal = map[engineiam.PrincipalType][]string{engineiam.AWSPrincipalType: {"arn:aws:iam:::user/devenv"}}
|
||||
putBucketPolicy(hc, bktName, newPolicy)
|
||||
bktPolicyStatus = getBucketPolicyStatus(hc, bktName)
|
||||
require.True(t, PolicyStatusIsPublicFalse == bktPolicyStatus.IsPublic)
|
||||
}
|
||||
|
||||
func TestDeleteBucketWithPolicy(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
|
||||
bktName := "bucket-for-policy"
|
||||
bi := createTestBucket(hc, bktName)
|
||||
|
||||
newPolicy := engineiam.Policy{
|
||||
Version: "2012-10-17",
|
||||
Statement: []engineiam.Statement{{
|
||||
Principal: map[engineiam.PrincipalType][]string{engineiam.Wildcard: {}},
|
||||
Effect: engineiam.AllowEffect,
|
||||
Action: engineiam.Action{"s3:PutObject"},
|
||||
Resource: engineiam.Resource{"arn:aws:s3:::bucket-for-policy/*"},
|
||||
}},
|
||||
}
|
||||
|
||||
putBucketPolicy(hc, bktName, newPolicy)
|
||||
|
||||
require.Len(t, hc.h.ape.(*apeMock).policyMap, 1)
|
||||
require.Len(t, hc.h.ape.(*apeMock).chainMap[engine.ContainerTarget(bi.CID.EncodeToString())], 4)
|
||||
|
||||
deleteBucket(t, hc, bktName, http.StatusNoContent)
|
||||
|
||||
require.Empty(t, hc.h.ape.(*apeMock).policyMap)
|
||||
chains, err := hc.h.ape.(*apeMock).ListChains(engine.ContainerTarget(bi.CID.EncodeToString()))
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, chains)
|
||||
}
|
||||
|
||||
func TestBucketPolicyUnmarshal(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
|
@ -1516,6 +1624,22 @@ func getBucketPolicy(hc *handlerContext, bktName string, errCode ...s3errors.Err
|
|||
return policy
|
||||
}
|
||||
|
||||
func getBucketPolicyStatus(hc *handlerContext, bktName string, errCode ...s3errors.ErrorCode) PolicyStatus {
|
||||
w, r := prepareTestRequest(hc, bktName, "", nil)
|
||||
hc.Handler().GetBucketPolicyStatusHandler(w, r)
|
||||
|
||||
var policyStatus PolicyStatus
|
||||
if len(errCode) == 0 {
|
||||
assertStatus(hc.t, w, http.StatusOK)
|
||||
err := xml.NewDecoder(w.Result().Body).Decode(&policyStatus)
|
||||
require.NoError(hc.t, err)
|
||||
} else {
|
||||
assertS3Error(hc.t, w, s3errors.GetAPIError(errCode[0]))
|
||||
}
|
||||
|
||||
return policyStatus
|
||||
}
|
||||
|
||||
func putBucketPolicy(hc *handlerContext, bktName string, bktPolicy engineiam.Policy, errCode ...s3errors.ErrorCode) {
|
||||
body, err := json.Marshal(bktPolicy)
|
||||
require.NoError(hc.t, err)
|
||||
|
@ -1661,9 +1785,12 @@ func putObjectACLBase(hc *handlerContext, bktName, objName string, box *accessbo
|
|||
return w
|
||||
}
|
||||
|
||||
func getObjectACLAssertS3Error(hc *handlerContext, bktName, objName string, code s3errors.ErrorCode) {
|
||||
func getObjectACL(hc *handlerContext, bktName, objName string) *AccessControlPolicy {
|
||||
w := getObjectACLBase(hc, bktName, objName)
|
||||
assertS3Error(hc.t, w, s3errors.GetAPIError(code))
|
||||
assertStatus(hc.t, w, http.StatusOK)
|
||||
res := &AccessControlPolicy{}
|
||||
parseTestResponse(hc.t, w, res)
|
||||
return res
|
||||
}
|
||||
|
||||
func getObjectACLBase(hc *handlerContext, bktName, objName string) *httptest.ResponseRecorder {
|
||||
|
@ -1671,3 +1798,29 @@ func getObjectACLBase(hc *handlerContext, bktName, objName string) *httptest.Res
|
|||
hc.Handler().GetObjectACLHandler(w, r)
|
||||
return w
|
||||
}
|
||||
|
||||
func putObjectWithHeaders(hc *handlerContext, bktName, objName string, headers map[string]string) http.Header {
|
||||
w := putObjectWithHeadersBase(hc, bktName, objName, headers, nil, nil)
|
||||
assertStatus(hc.t, w, http.StatusOK)
|
||||
return w.Header()
|
||||
}
|
||||
|
||||
func putObjectWithHeadersAssertS3Error(hc *handlerContext, bktName, objName string, headers map[string]string, code s3errors.ErrorCode) {
|
||||
w := putObjectWithHeadersBase(hc, bktName, objName, headers, nil, nil)
|
||||
assertS3Error(hc.t, w, s3errors.GetAPIError(code))
|
||||
}
|
||||
|
||||
func putObjectWithHeadersBase(hc *handlerContext, bktName, objName string, headers map[string]string, box *accessbox.Box, data []byte) *httptest.ResponseRecorder {
|
||||
body := bytes.NewReader(data)
|
||||
w, r := prepareTestPayloadRequest(hc, bktName, objName, body)
|
||||
|
||||
for k, v := range headers {
|
||||
r.Header.Set(k, v)
|
||||
}
|
||||
|
||||
ctx := middleware.SetBoxData(r.Context(), box)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
hc.Handler().PutObjectHandler(w, r)
|
||||
return w
|
||||
}
|
||||
|
|
|
@ -57,9 +57,9 @@ type (
|
|||
// APE is Access Policy Engine that needs to save policy and acl info to different places.
|
||||
APE interface {
|
||||
PutBucketPolicy(ns string, cnrID cid.ID, policy []byte, chains []*chain.Chain) error
|
||||
DeleteBucketPolicy(ns string, cnrID cid.ID, chainID chain.ID) error
|
||||
DeleteBucketPolicy(ns string, cnrID cid.ID, chainIDs []chain.ID) error
|
||||
GetBucketPolicy(ns string, cnrID cid.ID) ([]byte, error)
|
||||
SaveACLChains(ns string, chains []*chain.Chain) error
|
||||
SaveACLChains(cid string, chains []*chain.Chain) error
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
ctx = r.Context()
|
||||
reqInfo = middleware.GetReqInfo(ctx)
|
||||
|
||||
containsACL = containsACLHeaders(r)
|
||||
cannedACLStatus = aclHeadersStatus(r)
|
||||
)
|
||||
|
||||
src := r.Header.Get(api.AmzCopySource)
|
||||
|
@ -93,7 +93,14 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if containsACL {
|
||||
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
|
||||
|
@ -232,7 +239,7 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if containsACL {
|
||||
if needUpdateEACLTable {
|
||||
newEaclTable, err := h.getNewEAclTable(r, dstBktInfo, dstObjInfo)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get new eacl table", reqInfo, err)
|
||||
|
|
|
@ -22,6 +22,7 @@ type CopyMeta struct {
|
|||
Tags map[string]string
|
||||
MetadataDirective string
|
||||
Metadata map[string]string
|
||||
Headers map[string]string
|
||||
}
|
||||
|
||||
func TestCopyWithTaggingDirective(t *testing.T) {
|
||||
|
@ -279,6 +280,10 @@ func copyObject(hc *handlerContext, bktName, fromObject, toObject string, copyMe
|
|||
}
|
||||
r.Header.Set(api.AmzTagging, tagsQuery.Encode())
|
||||
|
||||
for key, val := range copyMeta.Headers {
|
||||
r.Header.Set(key, val)
|
||||
}
|
||||
|
||||
hc.Handler().CopyObjectHandler(w, r)
|
||||
assertStatus(hc.t, w, statusCode)
|
||||
}
|
||||
|
|
|
@ -66,7 +66,10 @@ func (h *handler) PutBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
middleware.WriteSuccessResponseHeadersOnly(w)
|
||||
if err = middleware.WriteSuccessResponseHeadersOnly(w); err != nil {
|
||||
h.logAndSendError(w, "write response", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) DeleteBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -200,7 +203,10 @@ func (h *handler) Preflight(w http.ResponseWriter, r *http.Request) {
|
|||
if o != wildcard {
|
||||
w.Header().Set(api.AccessControlAllowCredentials, "true")
|
||||
}
|
||||
middleware.WriteSuccessResponseHeadersOnly(w)
|
||||
if err = middleware.WriteSuccessResponseHeadersOnly(w); err != nil {
|
||||
h.logAndSendError(w, "write response", reqInfo, err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
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"
|
||||
)
|
||||
|
||||
|
@ -277,5 +278,17 @@ func (h *handler) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}); err != nil {
|
||||
h.logAndSendError(w, "couldn't delete bucket", reqInfo, err)
|
||||
}
|
||||
|
||||
chainIDs := []chain.ID{
|
||||
getBucketChainID(chain.S3, bktInfo),
|
||||
getBucketChainID(chain.Ingress, bktInfo),
|
||||
getBucketCannedChainID(chain.S3, bktInfo.CID),
|
||||
getBucketCannedChainID(chain.Ingress, bktInfo.CID),
|
||||
}
|
||||
if err = h.ape.DeleteBucketPolicy(reqInfo.Namespace, bktInfo.CID, chainIDs); err != nil {
|
||||
h.logAndSendError(w, "failed to delete policy from storage", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -234,24 +235,33 @@ func multipartUpload(hc *handlerContext, bktName, objName string, headers map[st
|
|||
}
|
||||
|
||||
func createMultipartUploadEncrypted(hc *handlerContext, bktName, objName string, headers map[string]string) *InitiateMultipartUploadResponse {
|
||||
return createMultipartUploadBase(hc, bktName, objName, true, headers)
|
||||
return createMultipartUploadOkBase(hc, bktName, objName, true, headers)
|
||||
}
|
||||
|
||||
func createMultipartUpload(hc *handlerContext, bktName, objName string, headers map[string]string) *InitiateMultipartUploadResponse {
|
||||
return createMultipartUploadBase(hc, bktName, objName, false, headers)
|
||||
return createMultipartUploadOkBase(hc, bktName, objName, false, headers)
|
||||
}
|
||||
|
||||
func createMultipartUploadBase(hc *handlerContext, bktName, objName string, encrypted bool, headers map[string]string) *InitiateMultipartUploadResponse {
|
||||
func createMultipartUploadOkBase(hc *handlerContext, bktName, objName string, encrypted bool, headers map[string]string) *InitiateMultipartUploadResponse {
|
||||
w := createMultipartUploadBase(hc, bktName, objName, encrypted, headers)
|
||||
multipartInitInfo := &InitiateMultipartUploadResponse{}
|
||||
readResponse(hc.t, w, http.StatusOK, multipartInitInfo)
|
||||
return multipartInitInfo
|
||||
}
|
||||
|
||||
func createMultipartUploadAssertS3Error(hc *handlerContext, bktName, objName string, headers map[string]string, code errors.ErrorCode) {
|
||||
w := createMultipartUploadBase(hc, bktName, objName, false, headers)
|
||||
assertS3Error(hc.t, w, errors.GetAPIError(code))
|
||||
}
|
||||
|
||||
func createMultipartUploadBase(hc *handlerContext, bktName, objName string, encrypted bool, headers map[string]string) *httptest.ResponseRecorder {
|
||||
w, r := prepareTestRequest(hc, bktName, objName, nil)
|
||||
if encrypted {
|
||||
setEncryptHeaders(r)
|
||||
}
|
||||
setHeaders(r, headers)
|
||||
hc.Handler().CreateMultipartUploadHandler(w, r)
|
||||
multipartInitInfo := &InitiateMultipartUploadResponse{}
|
||||
readResponse(hc.t, w, http.StatusOK, multipartInitInfo)
|
||||
|
||||
return multipartInitInfo
|
||||
return w
|
||||
}
|
||||
|
||||
func completeMultipartUpload(hc *handlerContext, bktName, objName, uploadID string, partsETags []string) {
|
||||
|
|
|
@ -4,8 +4,10 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -177,10 +179,11 @@ func prepareHandlerContextBase(t *testing.T, cacheCfg *layer.CachesConfig) *hand
|
|||
defaultPolicy: pp,
|
||||
}
|
||||
h := &handler{
|
||||
log: l,
|
||||
obj: layer.NewLayer(l, tp, layerCfg),
|
||||
cfg: cfg,
|
||||
ape: newAPEMock(),
|
||||
log: l,
|
||||
obj: layer.NewLayer(l, tp, layerCfg),
|
||||
cfg: cfg,
|
||||
ape: newAPEMock(),
|
||||
frostfsid: newFrostfsIDMock(),
|
||||
}
|
||||
|
||||
return &handlerContext{
|
||||
|
@ -267,7 +270,7 @@ func (a *apeMock) PutBucketPolicy(ns string, cnrID cid.ID, policy []byte, chain
|
|||
}
|
||||
|
||||
for i := range chain {
|
||||
if err := a.AddChain(engine.NamespaceTarget(ns), chain[i]); err != nil {
|
||||
if err := a.AddChain(engine.ContainerTarget(cnrID.EncodeToString()), chain[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -275,11 +278,17 @@ func (a *apeMock) PutBucketPolicy(ns string, cnrID cid.ID, policy []byte, chain
|
|||
return nil
|
||||
}
|
||||
|
||||
func (a *apeMock) DeleteBucketPolicy(ns string, cnrID cid.ID, chainID chain.ID) error {
|
||||
func (a *apeMock) DeleteBucketPolicy(ns string, cnrID cid.ID, chainIDs []chain.ID) error {
|
||||
if err := a.DeletePolicy(ns, cnrID); err != nil {
|
||||
return err
|
||||
}
|
||||
return a.RemoveChain(engine.NamespaceTarget(ns), chainID)
|
||||
for i := range chainIDs {
|
||||
if err := a.RemoveChain(engine.ContainerTarget(cnrID.EncodeToString()), chainIDs[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *apeMock) GetBucketPolicy(ns string, cnrID cid.ID) ([]byte, error) {
|
||||
|
@ -291,9 +300,9 @@ func (a *apeMock) GetBucketPolicy(ns string, cnrID cid.ID) ([]byte, error) {
|
|||
return policy, nil
|
||||
}
|
||||
|
||||
func (a *apeMock) SaveACLChains(ns string, chains []*chain.Chain) error {
|
||||
func (a *apeMock) SaveACLChains(cid string, chains []*chain.Chain) error {
|
||||
for i := range chains {
|
||||
if err := a.AddChain(engine.NamespaceTarget(ns), chains[i]); err != nil {
|
||||
if err := a.AddChain(engine.ContainerTarget(cid), chains[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -301,6 +310,32 @@ func (a *apeMock) SaveACLChains(ns string, chains []*chain.Chain) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type frostfsidMock struct {
|
||||
data map[string]*keys.PublicKey
|
||||
}
|
||||
|
||||
func newFrostfsIDMock() *frostfsidMock {
|
||||
return &frostfsidMock{data: map[string]*keys.PublicKey{}}
|
||||
}
|
||||
|
||||
func (f *frostfsidMock) GetUserAddress(account, user string) (string, error) {
|
||||
res, ok := f.data[account+user]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("not found")
|
||||
}
|
||||
|
||||
return res.Address(), nil
|
||||
}
|
||||
|
||||
func (f *frostfsidMock) GetUserKey(account, user string) (string, error) {
|
||||
res, ok := f.data[account+user]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("not found")
|
||||
}
|
||||
|
||||
return hex.EncodeToString(res.Bytes()), nil
|
||||
}
|
||||
|
||||
func createTestBucket(hc *handlerContext, bktName string) *data.BucketInfo {
|
||||
info := createBucket(hc, bktName)
|
||||
return info.BktInfo
|
||||
|
|
|
@ -140,7 +140,10 @@ func (h *handler) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {
|
|||
w.Header().Set(api.ContainerZone, bktInfo.Zone)
|
||||
}
|
||||
|
||||
middleware.WriteResponse(w, http.StatusOK, nil, middleware.MimeNone)
|
||||
if err = middleware.WriteResponse(w, http.StatusOK, nil, middleware.MimeNone); err != nil {
|
||||
h.logAndSendError(w, "write response", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) setLockingHeaders(bktInfo *data.BucketInfo, lockInfo data.LockInfo, header http.Header) error {
|
||||
|
|
|
@ -103,6 +103,9 @@ const (
|
|||
|
||||
func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
reqInfo := middleware.GetReqInfo(r.Context())
|
||||
uploadID := uuid.New()
|
||||
cannedACLStatus := aclHeadersStatus(r)
|
||||
additional := []zap.Field{zap.String("uploadID", uploadID.String())}
|
||||
|
||||
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
||||
if err != nil {
|
||||
|
@ -110,8 +113,17 @@ func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Re
|
|||
return
|
||||
}
|
||||
|
||||
uploadID := uuid.New()
|
||||
additional := []zap.Field{zap.String("uploadID", uploadID.String())}
|
||||
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
|
||||
}
|
||||
|
||||
p := &layer.CreateMultipartParams{
|
||||
Info: &layer.UploadInfoParams{
|
||||
|
@ -122,7 +134,8 @@ func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Re
|
|||
Data: &layer.UploadData{},
|
||||
}
|
||||
|
||||
if containsACLHeaders(r) {
|
||||
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...)
|
||||
|
@ -266,7 +279,10 @@ func (h *handler) UploadPartHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
w.Header().Set(api.ETag, data.Quote(hash))
|
||||
middleware.WriteSuccessResponseHeadersOnly(w)
|
||||
if err = middleware.WriteSuccessResponseHeadersOnly(w); err != nil {
|
||||
h.logAndSendError(w, "write response", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) UploadPartCopy(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -38,6 +38,36 @@ func TestMultipartUploadInvalidPart(t *testing.T) {
|
|||
assertS3Error(hc.t, w, s3Errors.GetAPIError(s3Errors.ErrEntityTooSmall))
|
||||
}
|
||||
|
||||
func TestDeleteMultipartAllParts(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
|
||||
partSize := layer.UploadMinSize
|
||||
objLen := 6 * partSize
|
||||
|
||||
bktName, bktName2, objName := "bucket", "bucket2", "object"
|
||||
|
||||
// unversioned bucket
|
||||
createTestBucket(hc, bktName)
|
||||
multipartUpload(hc, bktName, objName, nil, objLen, partSize)
|
||||
deleteObject(t, hc, bktName, objName, emptyVersion)
|
||||
require.Empty(t, hc.tp.Objects())
|
||||
|
||||
// encrypted multipart
|
||||
multipartUploadEncrypted(hc, bktName, objName, nil, objLen, partSize)
|
||||
deleteObject(t, hc, bktName, objName, emptyVersion)
|
||||
require.Empty(t, hc.tp.Objects())
|
||||
|
||||
// versions bucket
|
||||
createTestBucket(hc, bktName2)
|
||||
putBucketVersioning(t, hc, bktName2, true)
|
||||
multipartUpload(hc, bktName2, objName, nil, objLen, partSize)
|
||||
_, hdr := getObject(hc, bktName2, objName)
|
||||
versionID := hdr.Get("X-Amz-Version-Id")
|
||||
deleteObject(t, hc, bktName2, objName, emptyVersion)
|
||||
deleteObject(t, hc, bktName2, objName, versionID)
|
||||
require.Empty(t, hc.tp.Objects())
|
||||
}
|
||||
|
||||
func TestMultipartReUploadPart(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
|
||||
|
|
|
@ -186,12 +186,31 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
err error
|
||||
newEaclTable *eacl.Table
|
||||
sessionTokenEACL *session.Container
|
||||
containsACL = containsACLHeaders(r)
|
||||
cannedACLStatus = aclHeadersStatus(r)
|
||||
ctx = r.Context()
|
||||
reqInfo = middleware.GetReqInfo(ctx)
|
||||
)
|
||||
|
||||
if containsACL {
|
||||
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket objInfo", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := h.obj.GetBucketSettings(ctx, bktInfo)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not 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
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -204,12 +223,6 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket objInfo", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
metadata := parseMetadata(r)
|
||||
if contentType := r.Header.Get(api.ContentType); len(contentType) > 0 {
|
||||
metadata[api.ContentType] = contentType
|
||||
|
@ -261,12 +274,6 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
settings, err := h.obj.GetBucketSettings(ctx, bktInfo)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket settings", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
params.Lock, err = formObjectLock(ctx, bktInfo, settings.LockConfiguration, r.Header)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not form object lock", reqInfo, err)
|
||||
|
@ -292,7 +299,7 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
h.reqLogger(ctx).Error(logs.CouldntSendNotification, zap.Error(err))
|
||||
}
|
||||
|
||||
if containsACL {
|
||||
if needUpdateEACLTable {
|
||||
if newEaclTable, err = h.getNewEAclTable(r, bktInfo, objInfo); err != nil {
|
||||
h.logAndSendError(w, "could not get new eacl table", reqInfo, err)
|
||||
return
|
||||
|
@ -337,7 +344,10 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
w.Header().Set(api.ETag, data.Quote(objInfo.ETag(h.cfg.MD5Enabled())))
|
||||
|
||||
middleware.WriteSuccessResponseHeadersOnly(w)
|
||||
if err = middleware.WriteSuccessResponseHeadersOnly(w); err != nil {
|
||||
h.logAndSendError(w, "write response", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) getBodyReader(r *http.Request) (io.ReadCloser, error) {
|
||||
|
@ -462,7 +472,7 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) {
|
|||
ctx = r.Context()
|
||||
reqInfo = middleware.GetReqInfo(ctx)
|
||||
metadata = make(map[string]string)
|
||||
containsACL = containsACLHeaders(r)
|
||||
cannedACLStatus = aclHeadersStatus(r)
|
||||
)
|
||||
|
||||
policy, err := checkPostPolicy(r, reqInfo, metadata)
|
||||
|
@ -480,7 +490,26 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
if containsACL {
|
||||
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket objInfo", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := h.obj.GetBucketSettings(ctx, bktInfo)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not 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
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -507,12 +536,6 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
bktInfo, err := h.obj.GetBucketInfo(ctx, reqInfo.BucketName)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
params := &layer.PutObjectParams{
|
||||
BktInfo: bktInfo,
|
||||
Object: reqInfo.ObjectName,
|
||||
|
@ -579,9 +602,7 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
if settings, err := h.obj.GetBucketSettings(ctx, bktInfo); err != nil {
|
||||
h.reqLogger(ctx).Warn(logs.CouldntGetBucketVersioning, zap.String("bucket name", reqInfo.BucketName), zap.Error(err))
|
||||
} else if settings.VersioningEnabled() {
|
||||
if settings.VersioningEnabled() {
|
||||
w.Header().Set(api.AmzVersionID, objInfo.VersionID())
|
||||
}
|
||||
|
||||
|
@ -602,7 +623,11 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) {
|
|||
ETag: data.Quote(objInfo.ETag(h.cfg.MD5Enabled())),
|
||||
}
|
||||
w.WriteHeader(status)
|
||||
if _, err = w.Write(middleware.EncodeResponse(resp)); err != nil {
|
||||
respData, err := middleware.EncodeResponse(resp)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "encode response", reqInfo, err)
|
||||
}
|
||||
if _, err = w.Write(respData); err != nil {
|
||||
h.logAndSendError(w, "something went wrong", reqInfo, err)
|
||||
}
|
||||
return
|
||||
|
@ -673,9 +698,33 @@ func checkPostPolicy(r *http.Request, reqInfo *middleware.ReqInfo, metadata map[
|
|||
return policy, nil
|
||||
}
|
||||
|
||||
func containsACLHeaders(r *http.Request) bool {
|
||||
return r.Header.Get(api.AmzACL) != "" || r.Header.Get(api.AmzGrantRead) != "" ||
|
||||
r.Header.Get(api.AmzGrantFullControl) != "" || r.Header.Get(api.AmzGrantWrite) != ""
|
||||
type aclStatus int
|
||||
|
||||
const (
|
||||
// aclStatusNo means no acl headers at all.
|
||||
aclStatusNo aclStatus = iota
|
||||
// aclStatusYesAPECompatible means that only X-Amz-Acl present and equals to private.
|
||||
aclStatusYesAPECompatible
|
||||
// aclStatusYes means any other acl headers configuration.
|
||||
aclStatusYes
|
||||
)
|
||||
|
||||
func aclHeadersStatus(r *http.Request) aclStatus {
|
||||
if r.Header.Get(api.AmzGrantRead) != "" ||
|
||||
r.Header.Get(api.AmzGrantFullControl) != "" ||
|
||||
r.Header.Get(api.AmzGrantWrite) != "" {
|
||||
return aclStatusYes
|
||||
}
|
||||
|
||||
cannedACL := r.Header.Get(api.AmzACL)
|
||||
if cannedACL != "" {
|
||||
if cannedACL == basicACLPrivate {
|
||||
return aclStatusYesAPECompatible
|
||||
}
|
||||
return aclStatusYes
|
||||
}
|
||||
|
||||
return aclStatusNo
|
||||
}
|
||||
|
||||
func (h *handler) getNewEAclTable(r *http.Request, bktInfo *data.BucketInfo, objInfo *data.ObjectInfo) (*eacl.Table, error) {
|
||||
|
@ -849,7 +898,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, key, bktInfo.CID)
|
||||
if err = h.ape.SaveACLChains(reqInfo.Namespace, chains); err != nil {
|
||||
if err = h.ape.SaveACLChains(bktInfo.CID.EncodeToString(), chains); err != nil {
|
||||
h.logAndSendError(w, "failed to add morph rule chain", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
@ -873,7 +922,10 @@ func (h *handler) createBucketHandlerPolicy(w http.ResponseWriter, r *http.Reque
|
|||
return
|
||||
}
|
||||
|
||||
middleware.WriteSuccessResponseHeadersOnly(w)
|
||||
if err = middleware.WriteSuccessResponseHeadersOnly(w); err != nil {
|
||||
h.logAndSendError(w, "write response", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) createBucketHandlerACL(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -942,7 +994,10 @@ func (h *handler) createBucketHandlerACL(w http.ResponseWriter, r *http.Request)
|
|||
return
|
||||
}
|
||||
|
||||
middleware.WriteSuccessResponseHeadersOnly(w)
|
||||
if err = middleware.WriteSuccessResponseHeadersOnly(w); err != nil {
|
||||
h.logAndSendError(w, "write response", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const s3ActionPrefix = "s3:"
|
||||
|
|
|
@ -55,6 +55,19 @@ type Bucket struct {
|
|||
CreationDate string // time string of format "2006-01-02T15:04:05.000Z"
|
||||
}
|
||||
|
||||
// PolicyStatus contains status of bucket policy.
|
||||
type PolicyStatus struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ PolicyStatus" json:"-"`
|
||||
IsPublic PolicyStatusIsPublic `xml:"IsPublic"`
|
||||
}
|
||||
|
||||
type PolicyStatusIsPublic string
|
||||
|
||||
const (
|
||||
PolicyStatusIsPublicFalse = "FALSE"
|
||||
PolicyStatusIsPublicTrue = "TRUE"
|
||||
)
|
||||
|
||||
// AccessControlPolicy contains ACL.
|
||||
type AccessControlPolicy struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ AccessControlPolicy" json:"-"`
|
||||
|
|
|
@ -31,9 +31,12 @@ func (h *handler) reqLogger(ctx context.Context) *zap.Logger {
|
|||
|
||||
func (h *handler) logAndSendError(w http.ResponseWriter, logText string, reqInfo *middleware.ReqInfo, err error, additional ...zap.Field) {
|
||||
err = handleDeleteMarker(w, err)
|
||||
code := middleware.WriteErrorResponse(w, reqInfo, transformToS3Error(err))
|
||||
if code, wrErr := middleware.WriteErrorResponse(w, reqInfo, transformToS3Error(err)); wrErr != nil {
|
||||
additional = append(additional, zap.NamedError("write_response_error", wrErr))
|
||||
} else {
|
||||
additional = append(additional, zap.Int("status", code))
|
||||
}
|
||||
fields := []zap.Field{
|
||||
zap.Int("status", code),
|
||||
zap.String("request_id", reqInfo.RequestID),
|
||||
zap.String("method", reqInfo.API),
|
||||
zap.String("bucket", reqInfo.BucketName),
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"io"
|
||||
"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"
|
||||
|
@ -220,7 +221,7 @@ func (t *TestFrostFS) ReadObject(ctx context.Context, prm PrmObjectRead) (*Objec
|
|||
|
||||
if obj, ok := t.objects[sAddr]; ok {
|
||||
owner := getBearerOwner(ctx)
|
||||
if !t.checkAccess(prm.Container, owner, eacl.OperationGet) {
|
||||
if !t.checkAccess(prm.Container, owner, eacl.OperationGet, obj) {
|
||||
return nil, ErrAccessDenied
|
||||
}
|
||||
|
||||
|
@ -322,9 +323,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, eacl.OperationDelete) {
|
||||
if !t.checkAccess(prm.Container, owner, eacl.OperationDelete, obj) {
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
|
@ -376,7 +377,7 @@ func (t *TestFrostFS) ContainerEACL(_ context.Context, prm PrmContainerEACL) (*e
|
|||
return table, nil
|
||||
}
|
||||
|
||||
func (t *TestFrostFS) checkAccess(cnrID cid.ID, owner user.ID, op eacl.Operation) 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
|
||||
|
@ -392,22 +393,51 @@ func (t *TestFrostFS) checkAccess(cnrID cid.ID, owner user.ID, op eacl.Operation
|
|||
}
|
||||
|
||||
for _, rec := range table.Records() {
|
||||
if rec.Operation() == op && len(rec.Filters()) == 0 {
|
||||
for _, trgt := range rec.Targets() {
|
||||
if trgt.Role() == eacl.RoleOthers {
|
||||
return rec.Action() == eacl.ActionAllow
|
||||
}
|
||||
var targetOwner user.ID
|
||||
for _, pk := range eacl.TargetECDSAKeys(&trgt) {
|
||||
user.IDFromKey(&targetOwner, *pk)
|
||||
if targetOwner.Equals(owner) {
|
||||
return rec.Action() == eacl.ActionAllow
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -791,9 +792,40 @@ func (n *layer) removeOldVersion(ctx context.Context, bkt *data.BucketInfo, node
|
|||
return obj.VersionID, nil
|
||||
}
|
||||
|
||||
if nodeVersion.IsCombined {
|
||||
return "", n.removeCombinedObject(ctx, bkt, nodeVersion)
|
||||
}
|
||||
|
||||
return "", n.objectDelete(ctx, bkt, nodeVersion.OID)
|
||||
}
|
||||
|
||||
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.Unmarshal(combinedObj.Payload(), &parts); err != nil {
|
||||
return fmt.Errorf("unmarshal combined object parts: %w", err)
|
||||
}
|
||||
|
||||
for _, part := range parts {
|
||||
if err = n.objectDelete(ctx, bkt, part.OID); err == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !client.IsErrObjectAlreadyRemoved(err) && !client.IsErrObjectNotFound(err) {
|
||||
return fmt.Errorf("couldn't delete part '%s': %w", part.OID.EncodeToString(), err)
|
||||
}
|
||||
|
||||
n.reqLogger(ctx).Warn(logs.CouldntDeletePart, zap.String("cid", bkt.CID.EncodeToString()),
|
||||
zap.String("oid", part.OID.EncodeToString()), zap.Int("part number", part.Number), zap.Error(err))
|
||||
}
|
||||
|
||||
return n.objectDelete(ctx, bkt, nodeVersion.OID)
|
||||
}
|
||||
|
||||
// DeleteObjects from the storage.
|
||||
func (n *layer) DeleteObjects(ctx context.Context, p *DeleteObjectParams) []*VersionedObject {
|
||||
for i, obj := range p.Objects {
|
||||
|
|
|
@ -384,7 +384,7 @@ func (n *layer) headLastVersionIfNotDeleted(ctx context.Context, bkt *data.Bucke
|
|||
meta, err := n.objectHead(ctx, bkt, node.OID)
|
||||
if err != nil {
|
||||
if client.IsErrObjectNotFound(err) {
|
||||
return nil, fmt.Errorf("%w: %s", apiErrors.GetAPIError(apiErrors.ErrNoSuchKey), err.Error())
|
||||
return nil, fmt.Errorf("%w: %s; %s", apiErrors.GetAPIError(apiErrors.ErrNoSuchKey), err.Error(), node.OID.EncodeToString())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -59,7 +59,9 @@ func Auth(center Center, log *zap.Logger) Func {
|
|||
if _, ok := err.(apiErrors.Error); !ok {
|
||||
err = apiErrors.GetAPIError(apiErrors.ErrAccessDenied)
|
||||
}
|
||||
WriteErrorResponse(w, GetReqInfo(r.Context()), err)
|
||||
if _, wrErr := WriteErrorResponse(w, GetReqInfo(r.Context()), err); wrErr != nil {
|
||||
reqLogOrDefault(ctx, log).Error(logs.FailedToWriteResponse, zap.Error(wrErr))
|
||||
}
|
||||
return
|
||||
}
|
||||
} else {
|
||||
|
@ -97,7 +99,9 @@ func FrostfsIDValidation(frostfsID FrostFSIDValidator, log *zap.Logger) Func {
|
|||
|
||||
if err = validateBearerToken(frostfsID, bd.Gate.BearerToken); err != nil {
|
||||
reqLogOrDefault(ctx, log).Error(logs.FrostfsIDValidationFailed, zap.Error(err))
|
||||
WriteErrorResponse(w, GetReqInfo(r.Context()), err)
|
||||
if _, wrErr := WriteErrorResponse(w, GetReqInfo(r.Context()), err); wrErr != nil {
|
||||
reqLogOrDefault(ctx, log).Error(logs.FailedToWriteResponse, zap.Error(wrErr))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ const (
|
|||
HeadBucketOperation = "HeadBucket"
|
||||
ListMultipartUploadsOperation = "ListMultipartUploads"
|
||||
GetBucketLocationOperation = "GetBucketLocation"
|
||||
GetBucketPolicyStatusOperation = "GetBucketPolicyStatus"
|
||||
GetBucketPolicyOperation = "GetBucketPolicy"
|
||||
GetBucketLifecycleOperation = "GetBucketLifecycle"
|
||||
GetBucketEncryptionOperation = "GetBucketEncryption"
|
||||
|
@ -77,6 +78,7 @@ const (
|
|||
const (
|
||||
UploadsQuery = "uploads"
|
||||
LocationQuery = "location"
|
||||
PolicyStatusQuery = "policyStatus"
|
||||
PolicyQuery = "policy"
|
||||
LifecycleQuery = "lifecycle"
|
||||
EncryptionQuery = "encryption"
|
||||
|
|
|
@ -20,6 +20,13 @@ import (
|
|||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
QueryVersionID = "versionId"
|
||||
QueryPrefix = "prefix"
|
||||
QueryDelimiter = "delimiter"
|
||||
QueryMaxKeys = "max-keys"
|
||||
)
|
||||
|
||||
type PolicySettings interface {
|
||||
PolicyDenyByDefault() bool
|
||||
ACLEnabled() bool
|
||||
|
@ -47,7 +54,9 @@ func PolicyCheck(cfg PolicyConfig) Func {
|
|||
ctx := r.Context()
|
||||
if err := policyCheck(r, cfg); err != nil {
|
||||
reqLogOrDefault(ctx, cfg.Log).Error(logs.PolicyValidationFailed, zap.Error(err))
|
||||
WriteErrorResponse(w, GetReqInfo(ctx), err)
|
||||
if _, wrErr := WriteErrorResponse(w, GetReqInfo(ctx), err); wrErr != nil {
|
||||
reqLogOrDefault(ctx, cfg.Log).Error(logs.FailedToWriteResponse, zap.Error(wrErr))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -63,8 +72,21 @@ func policyCheck(r *http.Request, cfg PolicyConfig) error {
|
|||
return err
|
||||
}
|
||||
|
||||
var bktInfo *data.BucketInfo
|
||||
if reqType != noneType && !strings.HasSuffix(req.Operation(), CreateBucketOperation) {
|
||||
bktInfo, err = cfg.BucketResolver(r.Context(), bktName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
reqInfo := GetReqInfo(r.Context())
|
||||
target := engine.NewRequestTargetWithNamespace(reqInfo.Namespace)
|
||||
if bktInfo != nil {
|
||||
cnrTarget := engine.ContainerTarget(bktInfo.CID.EncodeToString())
|
||||
target.Container = &cnrTarget
|
||||
}
|
||||
|
||||
st, found, err := cfg.Storage.IsAllowed(chain.S3, target, req)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -81,9 +103,9 @@ func policyCheck(r *http.Request, cfg PolicyConfig) error {
|
|||
return apiErr.GetAPIErrorWithError(apiErr.ErrAccessDenied, fmt.Errorf("policy check: %s", st.String()))
|
||||
}
|
||||
|
||||
isAPE, err := isAPEBehavior(r.Context(), req, cfg, reqType, bktName)
|
||||
if err != nil {
|
||||
return err
|
||||
isAPE := !cfg.Settings.ACLEnabled()
|
||||
if bktInfo != nil {
|
||||
isAPE = bktInfo.APEEnabled
|
||||
}
|
||||
|
||||
if isAPE && cfg.Settings.PolicyDenyByDefault() {
|
||||
|
@ -93,20 +115,6 @@ func policyCheck(r *http.Request, cfg PolicyConfig) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func isAPEBehavior(ctx context.Context, req *testutil.Request, cfg PolicyConfig, reqType ReqType, bktName string) (bool, error) {
|
||||
if reqType == noneType ||
|
||||
strings.HasSuffix(req.Operation(), CreateBucketOperation) {
|
||||
return !cfg.Settings.ACLEnabled(), nil
|
||||
}
|
||||
|
||||
bktInfo, err := cfg.BucketResolver(ctx, bktName) // we cannot use reqInfo.BucketName because it hasn't been set yet
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return bktInfo.APEEnabled, nil
|
||||
}
|
||||
|
||||
func getPolicyRequest(r *http.Request, frostfsid FrostFSIDInformer, reqType ReqType, bktName string, objName string, log *zap.Logger) (*testutil.Request, error) {
|
||||
var (
|
||||
owner string
|
||||
|
@ -137,15 +145,12 @@ func getPolicyRequest(r *http.Request, frostfsid FrostFSIDInformer, reqType ReqT
|
|||
res = fmt.Sprintf(s3.ResourceFormatS3Bucket, bktName)
|
||||
}
|
||||
|
||||
reqLogOrDefault(r.Context(), log).Debug(logs.PolicyRequest, zap.String("action", op),
|
||||
zap.String("resource", res), zap.String("owner", owner))
|
||||
properties := determineProperties(ctx, reqType, op, owner, groups)
|
||||
|
||||
return testutil.NewRequest(op, testutil.NewResource(res, nil),
|
||||
map[string]string{
|
||||
s3.PropertyKeyOwner: owner,
|
||||
common.PropertyKeyFrostFSIDGroupID: chain.FormCondSliceContainsValue(groups),
|
||||
},
|
||||
), nil
|
||||
reqLogOrDefault(r.Context(), log).Debug(logs.PolicyRequest, zap.String("action", op),
|
||||
zap.String("resource", res), zap.Any("properties", properties))
|
||||
|
||||
return testutil.NewRequest(op, testutil.NewResource(res, nil), properties), nil
|
||||
}
|
||||
|
||||
type ReqType int
|
||||
|
@ -370,3 +375,32 @@ func determineGeneralOperation(r *http.Request) string {
|
|||
}
|
||||
return "UnmatchedOperation"
|
||||
}
|
||||
|
||||
func determineProperties(ctx context.Context, reqType ReqType, op, owner string, groups []string) map[string]string {
|
||||
res := map[string]string{
|
||||
s3.PropertyKeyOwner: owner,
|
||||
common.PropertyKeyFrostFSIDGroupID: chain.FormCondSliceContainsValue(groups),
|
||||
}
|
||||
queries := GetReqInfo(ctx).URL.Query()
|
||||
|
||||
if reqType == objectType {
|
||||
if versionID := queries.Get(QueryVersionID); len(versionID) > 0 {
|
||||
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 {
|
||||
res[s3.PropertyKeyPrefix] = prefix
|
||||
}
|
||||
if delimiter := queries.Get(QueryDelimiter); len(delimiter) > 0 {
|
||||
res[s3.PropertyKeyDelimiter] = delimiter
|
||||
}
|
||||
if maxKeys := queries.Get(QueryMaxKeys); len(maxKeys) > 0 {
|
||||
res[s3.PropertyKeyMaxKeys] = maxKeys
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
|
|
@ -118,7 +118,8 @@ var s3ErrorResponseMap = map[string]string{
|
|||
}
|
||||
|
||||
// WriteErrorResponse writes error headers.
|
||||
func WriteErrorResponse(w http.ResponseWriter, reqInfo *ReqInfo, err error) int {
|
||||
// returns http error code and error in case of failure of response writing.
|
||||
func WriteErrorResponse(w http.ResponseWriter, reqInfo *ReqInfo, err error) (int, error) {
|
||||
code := http.StatusInternalServerError
|
||||
|
||||
if e, ok := err.(errors.Error); ok {
|
||||
|
@ -134,9 +135,14 @@ func WriteErrorResponse(w http.ResponseWriter, reqInfo *ReqInfo, err error) int
|
|||
|
||||
// Generates error response.
|
||||
errorResponse := getAPIErrorResponse(reqInfo, err)
|
||||
encodedErrorResponse := EncodeResponse(errorResponse)
|
||||
WriteResponse(w, code, encodedErrorResponse, MimeXML)
|
||||
return code
|
||||
encodedErrorResponse, err := EncodeResponse(errorResponse)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("encode response: %w", err)
|
||||
}
|
||||
if err = WriteResponse(w, code, encodedErrorResponse, MimeXML); err != nil {
|
||||
return 0, fmt.Errorf("write response: %w", err)
|
||||
}
|
||||
return code, nil
|
||||
}
|
||||
|
||||
// Write http common headers.
|
||||
|
@ -157,7 +163,7 @@ func removeSensitiveHeaders(h http.Header) {
|
|||
}
|
||||
|
||||
// WriteResponse writes given statusCode and response into w (with mType header if set).
|
||||
func WriteResponse(w http.ResponseWriter, statusCode int, response []byte, mType mimeType) {
|
||||
func WriteResponse(w http.ResponseWriter, statusCode int, response []byte, mType mimeType) error {
|
||||
setCommonHeaders(w)
|
||||
if mType != MimeNone {
|
||||
w.Header().Set(hdrContentType, string(mType))
|
||||
|
@ -165,37 +171,46 @@ func WriteResponse(w http.ResponseWriter, statusCode int, response []byte, mType
|
|||
w.Header().Set(hdrContentLength, strconv.Itoa(len(response)))
|
||||
w.WriteHeader(statusCode)
|
||||
if response == nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
WriteResponseBody(w, response)
|
||||
return WriteResponseBody(w, response)
|
||||
}
|
||||
|
||||
// WriteResponseBody writes response into w.
|
||||
func WriteResponseBody(w http.ResponseWriter, response []byte) {
|
||||
_, _ = w.Write(response)
|
||||
func WriteResponseBody(w http.ResponseWriter, response []byte) error {
|
||||
if _, err := w.Write(response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if flusher, ok := w.(http.Flusher); ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodeResponse encodes the response headers into XML format.
|
||||
func EncodeResponse(response interface{}) []byte {
|
||||
func EncodeResponse(response interface{}) ([]byte, error) {
|
||||
var bytesBuffer bytes.Buffer
|
||||
bytesBuffer.WriteString(xml.Header)
|
||||
_ = xml.
|
||||
NewEncoder(&bytesBuffer).
|
||||
Encode(response)
|
||||
return bytesBuffer.Bytes()
|
||||
if err := xml.NewEncoder(&bytesBuffer).Encode(response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bytesBuffer.Bytes(), nil
|
||||
}
|
||||
|
||||
// EncodeResponseNoHeader encodes response without setting xml.Header.
|
||||
// Should be used with periodicXMLWriter which sends xml.Header to the client
|
||||
// with whitespaces to keep connection alive.
|
||||
func EncodeResponseNoHeader(response interface{}) []byte {
|
||||
func EncodeResponseNoHeader(response interface{}) ([]byte, error) {
|
||||
var bytesBuffer bytes.Buffer
|
||||
_ = xml.NewEncoder(&bytesBuffer).Encode(response)
|
||||
return bytesBuffer.Bytes()
|
||||
if err := xml.NewEncoder(&bytesBuffer).Encode(response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bytesBuffer.Bytes(), nil
|
||||
}
|
||||
|
||||
// EncodeToResponse encodes the response into ResponseWriter.
|
||||
|
@ -227,8 +242,8 @@ func EncodeToResponseNoHeader(w http.ResponseWriter, response interface{}) error
|
|||
|
||||
// WriteSuccessResponseHeadersOnly writes HTTP (200) OK response with no data
|
||||
// to the client.
|
||||
func WriteSuccessResponseHeadersOnly(w http.ResponseWriter) {
|
||||
WriteResponse(w, http.StatusOK, nil, MimeNone)
|
||||
func WriteSuccessResponseHeadersOnly(w http.ResponseWriter) error {
|
||||
return WriteResponse(w, http.StatusOK, nil, MimeNone)
|
||||
}
|
||||
|
||||
// Error -- Returns S3 error string.
|
||||
|
|
|
@ -37,6 +37,7 @@ type (
|
|||
PutObjectHandler(http.ResponseWriter, *http.Request)
|
||||
DeleteObjectHandler(http.ResponseWriter, *http.Request)
|
||||
GetBucketLocationHandler(http.ResponseWriter, *http.Request)
|
||||
GetBucketPolicyStatusHandler(http.ResponseWriter, *http.Request)
|
||||
GetBucketPolicyHandler(http.ResponseWriter, *http.Request)
|
||||
GetBucketLifecycleHandler(http.ResponseWriter, *http.Request)
|
||||
GetBucketEncryptionHandler(http.ResponseWriter, *http.Request)
|
||||
|
@ -178,14 +179,24 @@ func errorResponseHandler(w http.ResponseWriter, r *http.Request) {
|
|||
reqInfo := s3middleware.GetReqInfo(ctx)
|
||||
|
||||
desc := fmt.Sprintf("Unknown API request at %s", r.URL.Path)
|
||||
s3middleware.WriteErrorResponse(w, reqInfo, errors.Error{
|
||||
_, wrErr := s3middleware.WriteErrorResponse(w, reqInfo, errors.Error{
|
||||
Code: "UnknownAPIRequest",
|
||||
Description: desc,
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
})
|
||||
|
||||
if log := s3middleware.GetReqLog(ctx); log != nil {
|
||||
log.Error(logs.RequestUnmatched, zap.String("method", reqInfo.API), zap.String("http method", r.Method), zap.String("url", r.RequestURI))
|
||||
fields := []zap.Field{
|
||||
zap.String("method", reqInfo.API),
|
||||
zap.String("http method", r.Method),
|
||||
zap.String("url", r.RequestURI),
|
||||
}
|
||||
|
||||
if wrErr != nil {
|
||||
fields = append(fields, zap.NamedError("write_response_error", wrErr))
|
||||
}
|
||||
|
||||
log.Error(logs.RequestUnmatched, fields...)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -220,6 +231,9 @@ func bucketRouter(h Handler, log *zap.Logger) chi.Router {
|
|||
Add(NewFilter().
|
||||
Queries(s3middleware.LocationQuery).
|
||||
Handler(named(s3middleware.GetBucketLocationOperation, h.GetBucketLocationHandler))).
|
||||
Add(NewFilter().
|
||||
Queries(s3middleware.PolicyStatusQuery).
|
||||
Handler(named(s3middleware.GetBucketPolicyStatusOperation, h.GetBucketPolicyStatusHandler))).
|
||||
Add(NewFilter().
|
||||
Queries(s3middleware.PolicyQuery).
|
||||
Handler(named(s3middleware.GetBucketPolicyOperation, h.GetBucketPolicyHandler))).
|
||||
|
|
|
@ -186,6 +186,11 @@ func (h *handlerMock) GetBucketLocationHandler(http.ResponseWriter, *http.Reques
|
|||
panic("implement me")
|
||||
}
|
||||
|
||||
func (h *handlerMock) GetBucketPolicyStatusHandler(http.ResponseWriter, *http.Request) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (h *handlerMock) GetBucketPolicyHandler(http.ResponseWriter, *http.Request) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -196,8 +197,10 @@ func TestPolicyChecker(t *testing.T) {
|
|||
func TestPolicyCheckerReqTypeDetermination(t *testing.T) {
|
||||
chiRouter := prepareRouter(t)
|
||||
bktName, objName := "bucket", "object"
|
||||
createBucket(chiRouter, "", bktName)
|
||||
|
||||
policy := engineiam.Policy{
|
||||
Version: "2012-10-17",
|
||||
Statement: []engineiam.Statement{{
|
||||
Principal: map[engineiam.PrincipalType][]string{engineiam.Wildcard: {}},
|
||||
Effect: engineiam.AllowEffect,
|
||||
|
@ -269,7 +272,7 @@ func TestACLAPE(t *testing.T) {
|
|||
listBucketsErr(router, ns, apiErrors.ErrAccessDenied)
|
||||
|
||||
// Allow operations and check
|
||||
allowOperations(router, ns, []string{"s3:CreateBucket", "s3:ListBuckets"})
|
||||
allowOperations(router, ns, []string{"s3:CreateBucket", "s3:ListAllMyBuckets"}, nil)
|
||||
createBucket(router, ns, bktName)
|
||||
listBuckets(router, ns)
|
||||
})
|
||||
|
@ -295,7 +298,7 @@ func TestACLAPE(t *testing.T) {
|
|||
listBuckets(router, ns)
|
||||
|
||||
// Deny operations and check
|
||||
denyOperations(router, ns, []string{"s3:CreateBucket", "s3:ListBuckets"})
|
||||
denyOperations(router, ns, []string{"s3:CreateBucket", "s3:ListAllMyBuckets"}, nil)
|
||||
createBucketErr(router, ns, bktName, apiErrors.ErrAccessDenied)
|
||||
listBucketsErr(router, ns, apiErrors.ErrAccessDenied)
|
||||
})
|
||||
|
@ -343,21 +346,136 @@ func TestACLAPE(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func allowOperations(router *routerMock, ns string, operations []string) {
|
||||
addPolicy(router, ns, "allow", engineiam.AllowEffect, operations)
|
||||
func TestRequestParametersCheck(t *testing.T) {
|
||||
t.Run("prefix parameter, allow specific value", func(t *testing.T) {
|
||||
router := prepareRouter(t)
|
||||
|
||||
ns, bktName, prefix := "", "bucket", "prefix"
|
||||
router.middlewareSettings.denyByDefault = true
|
||||
|
||||
allowOperations(router, ns, []string{"s3:CreateBucket"}, nil)
|
||||
createBucket(router, ns, bktName)
|
||||
|
||||
// Add policies and check
|
||||
denyOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
|
||||
engineiam.CondStringNotEquals: engineiam.Condition{s3.PropertyKeyPrefix: []string{prefix}},
|
||||
})
|
||||
allowOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
|
||||
engineiam.CondStringEquals: engineiam.Condition{s3.PropertyKeyPrefix: []string{prefix}},
|
||||
})
|
||||
|
||||
listObjectsV1(router, ns, bktName, prefix, "", "")
|
||||
listObjectsV1Err(router, ns, bktName, "", "", "", apiErrors.ErrAccessDenied)
|
||||
listObjectsV1Err(router, ns, bktName, "invalid", "", "", apiErrors.ErrAccessDenied)
|
||||
})
|
||||
|
||||
t.Run("delimiter parameter, prohibit specific value", func(t *testing.T) {
|
||||
router := prepareRouter(t)
|
||||
|
||||
ns, bktName, delimiter := "", "bucket", "delimiter"
|
||||
router.middlewareSettings.denyByDefault = true
|
||||
|
||||
allowOperations(router, ns, []string{"s3:CreateBucket"}, nil)
|
||||
createBucket(router, ns, bktName)
|
||||
|
||||
// Add policies and check
|
||||
denyOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
|
||||
engineiam.CondStringEquals: engineiam.Condition{s3.PropertyKeyDelimiter: []string{delimiter}},
|
||||
})
|
||||
allowOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
|
||||
engineiam.CondStringNotEquals: engineiam.Condition{s3.PropertyKeyDelimiter: []string{delimiter}},
|
||||
})
|
||||
|
||||
listObjectsV1(router, ns, bktName, "", "", "")
|
||||
listObjectsV1(router, ns, bktName, "", "some-delimiter", "")
|
||||
listObjectsV1Err(router, ns, bktName, "", delimiter, "", apiErrors.ErrAccessDenied)
|
||||
})
|
||||
|
||||
t.Run("max-keys parameter, allow specific value", func(t *testing.T) {
|
||||
router := prepareRouter(t)
|
||||
|
||||
ns, bktName, maxKeys := "", "bucket", 10
|
||||
router.middlewareSettings.denyByDefault = true
|
||||
|
||||
allowOperations(router, ns, []string{"s3:CreateBucket"}, nil)
|
||||
createBucket(router, ns, bktName)
|
||||
|
||||
// Add policies and check
|
||||
denyOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
|
||||
engineiam.CondNumericNotEquals: engineiam.Condition{s3.PropertyKeyMaxKeys: []string{strconv.Itoa(maxKeys)}},
|
||||
})
|
||||
allowOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
|
||||
engineiam.CondNumericEquals: engineiam.Condition{s3.PropertyKeyMaxKeys: []string{strconv.Itoa(maxKeys)}},
|
||||
})
|
||||
|
||||
listObjectsV1(router, ns, bktName, "", "", strconv.Itoa(maxKeys))
|
||||
listObjectsV1Err(router, ns, bktName, "", "", "", apiErrors.ErrAccessDenied)
|
||||
listObjectsV1Err(router, ns, bktName, "", "", strconv.Itoa(maxKeys-1), apiErrors.ErrAccessDenied)
|
||||
listObjectsV1Err(router, ns, bktName, "", "", "invalid", apiErrors.ErrAccessDenied)
|
||||
})
|
||||
|
||||
t.Run("max-keys parameter, allow range of values", func(t *testing.T) {
|
||||
router := prepareRouter(t)
|
||||
|
||||
ns, bktName, maxKeys := "", "bucket", 10
|
||||
router.middlewareSettings.denyByDefault = true
|
||||
|
||||
allowOperations(router, ns, []string{"s3:CreateBucket"}, nil)
|
||||
createBucket(router, ns, bktName)
|
||||
|
||||
// Add policies and check
|
||||
denyOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
|
||||
engineiam.CondNumericGreaterThan: engineiam.Condition{s3.PropertyKeyMaxKeys: []string{strconv.Itoa(maxKeys)}},
|
||||
})
|
||||
allowOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
|
||||
engineiam.CondNumericLessThanEquals: engineiam.Condition{s3.PropertyKeyMaxKeys: []string{strconv.Itoa(maxKeys)}},
|
||||
})
|
||||
|
||||
listObjectsV1(router, ns, bktName, "", "", strconv.Itoa(maxKeys))
|
||||
listObjectsV1(router, ns, bktName, "", "", strconv.Itoa(maxKeys-1))
|
||||
listObjectsV1Err(router, ns, bktName, "", "", strconv.Itoa(maxKeys+1), apiErrors.ErrAccessDenied)
|
||||
})
|
||||
|
||||
t.Run("max-keys parameter, prohibit specific value", func(t *testing.T) {
|
||||
router := prepareRouter(t)
|
||||
|
||||
ns, bktName, maxKeys := "", "bucket", 10
|
||||
router.middlewareSettings.denyByDefault = true
|
||||
|
||||
allowOperations(router, ns, []string{"s3:CreateBucket"}, nil)
|
||||
createBucket(router, ns, bktName)
|
||||
|
||||
// Add policies and check
|
||||
denyOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
|
||||
engineiam.CondNumericEquals: engineiam.Condition{s3.PropertyKeyMaxKeys: []string{strconv.Itoa(maxKeys)}},
|
||||
})
|
||||
allowOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
|
||||
engineiam.CondNumericNotEquals: engineiam.Condition{s3.PropertyKeyMaxKeys: []string{strconv.Itoa(maxKeys)}},
|
||||
})
|
||||
|
||||
listObjectsV1(router, ns, bktName, "", "", "")
|
||||
listObjectsV1(router, ns, bktName, "", "", strconv.Itoa(maxKeys-1))
|
||||
listObjectsV1Err(router, ns, bktName, "", "", strconv.Itoa(maxKeys), apiErrors.ErrAccessDenied)
|
||||
})
|
||||
}
|
||||
|
||||
func denyOperations(router *routerMock, ns string, operations []string) {
|
||||
addPolicy(router, ns, "deny", engineiam.DenyEffect, operations)
|
||||
func allowOperations(router *routerMock, ns string, operations []string, conditions engineiam.Conditions) {
|
||||
addPolicy(router, ns, "allow", engineiam.AllowEffect, operations, conditions)
|
||||
}
|
||||
|
||||
func addPolicy(router *routerMock, ns string, id string, effect engineiam.Effect, operations []string) {
|
||||
func denyOperations(router *routerMock, ns string, operations []string, conditions engineiam.Conditions) {
|
||||
addPolicy(router, ns, "deny", engineiam.DenyEffect, operations, conditions)
|
||||
}
|
||||
|
||||
func addPolicy(router *routerMock, ns string, id string, effect engineiam.Effect, operations []string, conditions engineiam.Conditions) {
|
||||
policy := engineiam.Policy{
|
||||
Version: "2012-10-17",
|
||||
Statement: []engineiam.Statement{{
|
||||
Principal: map[engineiam.PrincipalType][]string{engineiam.Wildcard: {}},
|
||||
Effect: effect,
|
||||
Action: engineiam.Action(operations),
|
||||
Resource: engineiam.Resource{fmt.Sprintf(s3.ResourceFormatS3All)},
|
||||
Principal: map[engineiam.PrincipalType][]string{engineiam.Wildcard: {}},
|
||||
Effect: effect,
|
||||
Action: engineiam.Action(operations),
|
||||
Resource: engineiam.Resource{fmt.Sprintf(s3.ResourceFormatS3All)},
|
||||
Conditions: conditions,
|
||||
}},
|
||||
}
|
||||
|
||||
|
@ -439,6 +557,38 @@ func putObjectBase(router *routerMock, namespace, bktName, objName string) *http
|
|||
return w
|
||||
}
|
||||
|
||||
func listObjectsV1(router *routerMock, namespace, bktName, prefix, delimiter, maxKeys string) handlerResult {
|
||||
w := listObjectsV1Base(router, namespace, bktName, prefix, delimiter, maxKeys)
|
||||
resp := readResponse(router.t, w)
|
||||
require.Equal(router.t, s3middleware.ListObjectsV1Operation, resp.Method)
|
||||
return resp
|
||||
}
|
||||
|
||||
func listObjectsV1Err(router *routerMock, namespace, bktName, prefix, delimiter, maxKeys string, errCode apiErrors.ErrorCode) {
|
||||
w := listObjectsV1Base(router, namespace, bktName, prefix, delimiter, maxKeys)
|
||||
assertAPIError(router.t, w, errCode)
|
||||
}
|
||||
|
||||
func listObjectsV1Base(router *routerMock, namespace, bktName, prefix, delimiter, maxKeys string) *httptest.ResponseRecorder {
|
||||
queries := url.Values{}
|
||||
if len(prefix) > 0 {
|
||||
queries.Add(s3middleware.QueryPrefix, prefix)
|
||||
}
|
||||
if len(delimiter) > 0 {
|
||||
queries.Add(s3middleware.QueryDelimiter, delimiter)
|
||||
}
|
||||
if len(maxKeys) > 0 {
|
||||
queries.Add(s3middleware.QueryMaxKeys, maxKeys)
|
||||
}
|
||||
encoded := queries.Encode()
|
||||
|
||||
w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/"+bktName, nil)
|
||||
r.URL.RawQuery = encoded
|
||||
r.Header.Set(FrostfsNamespaceHeader, namespace)
|
||||
router.ServeHTTP(w, r)
|
||||
return w
|
||||
}
|
||||
|
||||
func TestOwnerIDRetrieving(t *testing.T) {
|
||||
chiRouter := prepareRouter(t)
|
||||
|
||||
|
|
139
cmd/s3-gw/app.go
139
cmd/s3-gw/app.go
|
@ -71,7 +71,9 @@ type (
|
|||
|
||||
policyStorage *policy.Storage
|
||||
|
||||
servers []Server
|
||||
servers []Server
|
||||
unbindServers []ServerInfo
|
||||
mu sync.RWMutex
|
||||
|
||||
controlAPI *grpc.Server
|
||||
|
||||
|
@ -88,6 +90,7 @@ type (
|
|||
logLevel zap.AtomicLevel
|
||||
maxClient maxClientsConfig
|
||||
defaultMaxAge int
|
||||
reconnectInterval time.Duration
|
||||
notificatorEnabled bool
|
||||
resolveZoneList []string
|
||||
isResolveListAllow bool // True if ResolveZoneList contains allowed zones
|
||||
|
@ -205,6 +208,7 @@ func newAppSettings(log *Logger, v *viper.Viper, key *keys.PrivateKey) *appSetti
|
|||
logLevel: log.lvl,
|
||||
maxClient: newMaxClients(v),
|
||||
defaultMaxAge: fetchDefaultMaxAge(v, log.logger),
|
||||
reconnectInterval: fetchReconnectInterval(v),
|
||||
notificatorEnabled: v.GetBool(cfgEnableNATS),
|
||||
frostfsidValidation: v.GetBool(cfgFrostfsIDValidationEnabled),
|
||||
}
|
||||
|
@ -699,17 +703,23 @@ func (a *App) Serve(ctx context.Context) {
|
|||
|
||||
a.startServices()
|
||||
|
||||
for i := range a.servers {
|
||||
go func(i int) {
|
||||
a.log.Info(logs.StartingServer, zap.String("address", a.servers[i].Address()))
|
||||
servs := a.getServers()
|
||||
|
||||
if err := srv.Serve(a.servers[i].Listener()); err != nil && err != http.ErrServerClosed {
|
||||
a.metrics.MarkUnhealthy(a.servers[i].Address())
|
||||
for i := range servs {
|
||||
go func(i int) {
|
||||
a.log.Info(logs.StartingServer, zap.String("address", servs[i].Address()))
|
||||
|
||||
if err := srv.Serve(servs[i].Listener()); err != nil && err != http.ErrServerClosed {
|
||||
a.metrics.MarkUnhealthy(servs[i].Address())
|
||||
a.log.Fatal(logs.ListenAndServe, zap.Error(err))
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
if len(a.unbindServers) != 0 {
|
||||
a.scheduleReconnect(ctx, srv)
|
||||
}
|
||||
|
||||
go func() {
|
||||
address := a.cfg.GetString(cfgControlGRPCEndpoint)
|
||||
a.log.Info(logs.StartingControlAPI, zap.String("address", address))
|
||||
|
@ -826,7 +836,7 @@ func (a *App) startServices() {
|
|||
}
|
||||
|
||||
func (a *App) initServers(ctx context.Context) {
|
||||
serversInfo := fetchServers(a.cfg)
|
||||
serversInfo := fetchServers(a.cfg, a.log)
|
||||
|
||||
a.servers = make([]Server, 0, len(serversInfo))
|
||||
for _, serverInfo := range serversInfo {
|
||||
|
@ -836,6 +846,7 @@ func (a *App) initServers(ctx context.Context) {
|
|||
}
|
||||
srv, err := newServer(ctx, serverInfo)
|
||||
if err != nil {
|
||||
a.unbindServers = append(a.unbindServers, serverInfo)
|
||||
a.metrics.MarkUnhealthy(serverInfo.Address)
|
||||
a.log.Warn(logs.FailedToAddServer, append(fields, zap.Error(err))...)
|
||||
continue
|
||||
|
@ -852,21 +863,24 @@ func (a *App) initServers(ctx context.Context) {
|
|||
}
|
||||
|
||||
func (a *App) updateServers() error {
|
||||
serversInfo := fetchServers(a.cfg)
|
||||
serversInfo := fetchServers(a.cfg, a.log)
|
||||
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
var found bool
|
||||
for _, serverInfo := range serversInfo {
|
||||
index := a.serverIndex(serverInfo.Address)
|
||||
if index == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
if serverInfo.TLS.Enabled {
|
||||
if err := a.servers[index].UpdateCert(serverInfo.TLS.CertFile, serverInfo.TLS.KeyFile); err != nil {
|
||||
return fmt.Errorf("failed to update tls certs: %w", err)
|
||||
ser := a.getServer(serverInfo.Address)
|
||||
if ser != nil {
|
||||
if serverInfo.TLS.Enabled {
|
||||
if err := ser.UpdateCert(serverInfo.TLS.CertFile, serverInfo.TLS.KeyFile); err != nil {
|
||||
return fmt.Errorf("failed to update tls certs: %w", err)
|
||||
}
|
||||
found = true
|
||||
}
|
||||
} else if unbind := a.updateUnbindServerInfo(serverInfo); unbind {
|
||||
found = true
|
||||
}
|
||||
found = true
|
||||
}
|
||||
|
||||
if !found {
|
||||
|
@ -876,15 +890,6 @@ func (a *App) updateServers() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (a *App) serverIndex(address string) int {
|
||||
for i := range a.servers {
|
||||
if a.servers[i].Address() == address {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (a *App) stopServices() {
|
||||
ctx, cancel := shutdownContext()
|
||||
defer cancel()
|
||||
|
@ -959,6 +964,31 @@ func (a *App) initHandler() {
|
|||
}
|
||||
}
|
||||
|
||||
func (a *App) getServer(address string) Server {
|
||||
for i := range a.servers {
|
||||
if a.servers[i].Address() == address {
|
||||
return a.servers[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) updateUnbindServerInfo(info ServerInfo) bool {
|
||||
for i := range a.unbindServers {
|
||||
if a.unbindServers[i].Address == info.Address {
|
||||
a.unbindServers[i] = info
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *App) getServers() []Server {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
return a.servers
|
||||
}
|
||||
|
||||
func (a *App) setRuntimeParameters() {
|
||||
if len(os.Getenv("GOMEMLIMIT")) != 0 {
|
||||
// default limit < yaml limit < app env limit < GOMEMLIMIT
|
||||
|
@ -974,3 +1004,60 @@ func (a *App) setRuntimeParameters() {
|
|||
zap.Int64("old_value", previous))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) scheduleReconnect(ctx context.Context, srv *http.Server) {
|
||||
go func() {
|
||||
t := time.NewTicker(a.settings.reconnectInterval)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
if a.tryReconnect(ctx, srv) {
|
||||
return
|
||||
}
|
||||
t.Reset(a.settings.reconnectInterval)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *App) tryReconnect(ctx context.Context, sr *http.Server) bool {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
a.log.Info(logs.ServerReconnecting)
|
||||
var failedServers []ServerInfo
|
||||
|
||||
for _, serverInfo := range a.unbindServers {
|
||||
fields := []zap.Field{
|
||||
zap.String("address", serverInfo.Address), zap.Bool("tls enabled", serverInfo.TLS.Enabled),
|
||||
zap.String("tls cert", serverInfo.TLS.CertFile), zap.String("tls key", serverInfo.TLS.KeyFile),
|
||||
}
|
||||
|
||||
srv, err := newServer(ctx, serverInfo)
|
||||
if err != nil {
|
||||
a.log.Warn(logs.ServerReconnectFailed, zap.Error(err))
|
||||
failedServers = append(failedServers, serverInfo)
|
||||
a.metrics.MarkUnhealthy(serverInfo.Address)
|
||||
continue
|
||||
}
|
||||
|
||||
go func() {
|
||||
a.log.Info(logs.StartingServer, zap.String("address", srv.Address()))
|
||||
a.metrics.MarkHealthy(serverInfo.Address)
|
||||
if err = sr.Serve(srv.Listener()); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
a.log.Warn(logs.ListenAndServe, zap.Error(err))
|
||||
a.metrics.MarkUnhealthy(serverInfo.Address)
|
||||
}
|
||||
}()
|
||||
|
||||
a.servers = append(a.servers, srv)
|
||||
a.log.Info(logs.ServerReconnectedSuccessfully, fields...)
|
||||
}
|
||||
|
||||
a.unbindServers = failedServers
|
||||
|
||||
return len(a.unbindServers) == 0
|
||||
}
|
||||
|
|
|
@ -59,6 +59,8 @@ const (
|
|||
defaultConstraintName = "default"
|
||||
|
||||
defaultNamespace = ""
|
||||
|
||||
defaultReconnectInterval = time.Minute
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -222,6 +224,9 @@ const ( // Settings.
|
|||
// Proxy.
|
||||
cfgProxyContract = "proxy.contract"
|
||||
|
||||
// Server.
|
||||
cfgReconnectInterval = "reconnect_interval"
|
||||
|
||||
// envPrefix is an environment variables prefix used for configuration.
|
||||
envPrefix = "S3_GW"
|
||||
)
|
||||
|
@ -244,6 +249,15 @@ func fetchConnectTimeout(cfg *viper.Viper) time.Duration {
|
|||
return connTimeout
|
||||
}
|
||||
|
||||
func fetchReconnectInterval(cfg *viper.Viper) time.Duration {
|
||||
reconnect := cfg.GetDuration(cfgReconnectInterval)
|
||||
if reconnect <= 0 {
|
||||
reconnect = defaultReconnectInterval
|
||||
}
|
||||
|
||||
return reconnect
|
||||
}
|
||||
|
||||
func fetchStreamTimeout(cfg *viper.Viper) time.Duration {
|
||||
streamTimeout := cfg.GetDuration(cfgStreamTimeout)
|
||||
if streamTimeout <= 0 {
|
||||
|
@ -611,8 +625,9 @@ func fetchPeers(l *zap.Logger, v *viper.Viper) []pool.NodeParam {
|
|||
return nodes
|
||||
}
|
||||
|
||||
func fetchServers(v *viper.Viper) []ServerInfo {
|
||||
func fetchServers(v *viper.Viper, log *zap.Logger) []ServerInfo {
|
||||
var servers []ServerInfo
|
||||
seen := make(map[string]struct{})
|
||||
|
||||
for i := 0; ; i++ {
|
||||
key := cfgServer + "." + strconv.Itoa(i) + "."
|
||||
|
@ -627,6 +642,11 @@ func fetchServers(v *viper.Viper) []ServerInfo {
|
|||
break
|
||||
}
|
||||
|
||||
if _, ok := seen[serverInfo.Address]; ok {
|
||||
log.Warn(logs.WarnDuplicateAddress, zap.String("address", serverInfo.Address))
|
||||
continue
|
||||
}
|
||||
seen[serverInfo.Address] = struct{}{}
|
||||
servers = append(servers, serverInfo)
|
||||
}
|
||||
|
||||
|
|
|
@ -74,6 +74,7 @@ func newServer(ctx context.Context, serverInfo ServerInfo) (*server, error) {
|
|||
|
||||
ln = tls.NewListener(ln, &tls.Config{
|
||||
GetCertificate: tlsProvider.GetCertificate,
|
||||
NextProtos: []string{"h2"}, // required to enable HTTP/2 requests in `http.Serve`
|
||||
})
|
||||
}
|
||||
|
||||
|
|
119
cmd/s3-gw/server_test.go
Normal file
119
cmd/s3-gw/server_test.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
const (
|
||||
expHeaderKey = "Foo"
|
||||
expHeaderValue = "Bar"
|
||||
)
|
||||
|
||||
func TestHTTP2TLS(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
certPath, keyPath := prepareTestCerts(t)
|
||||
|
||||
srv := &http.Server{
|
||||
Handler: http.HandlerFunc(testHandler),
|
||||
}
|
||||
|
||||
tlsListener, err := newServer(ctx, ServerInfo{
|
||||
Address: ":0",
|
||||
TLS: ServerTLSInfo{
|
||||
Enabled: true,
|
||||
CertFile: certPath,
|
||||
KeyFile: keyPath,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
port := tlsListener.Listener().Addr().(*net.TCPAddr).Port
|
||||
addr := fmt.Sprintf("https://localhost:%d", port)
|
||||
|
||||
go func() {
|
||||
_ = srv.Serve(tlsListener.Listener())
|
||||
}()
|
||||
|
||||
// Server is running, now send HTTP/2 request
|
||||
|
||||
tlsClientConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
|
||||
cliHTTP1 := http.Client{Transport: &http.Transport{TLSClientConfig: tlsClientConfig}}
|
||||
cliHTTP2 := http.Client{Transport: &http2.Transport{TLSClientConfig: tlsClientConfig}}
|
||||
|
||||
req, err := http.NewRequest("GET", addr, nil)
|
||||
require.NoError(t, err)
|
||||
req.Header[expHeaderKey] = []string{expHeaderValue}
|
||||
|
||||
resp, err := cliHTTP1.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
resp, err = cliHTTP2.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func testHandler(resp http.ResponseWriter, req *http.Request) {
|
||||
hdr, ok := req.Header[expHeaderKey]
|
||||
if !ok || len(hdr) != 1 || hdr[0] != expHeaderValue {
|
||||
resp.WriteHeader(http.StatusBadRequest)
|
||||
} else {
|
||||
resp.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func prepareTestCerts(t *testing.T) (certPath, keyPath string) {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{CommonName: "localhost"},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(time.Hour * 24 * 365),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
dir := t.TempDir()
|
||||
certPath = path.Join(dir, "cert.pem")
|
||||
keyPath = path.Join(dir, "key.pem")
|
||||
|
||||
certFile, err := os.Create(certPath)
|
||||
require.NoError(t, err)
|
||||
defer certFile.Close()
|
||||
|
||||
keyFile, err := os.Create(keyPath)
|
||||
require.NoError(t, err)
|
||||
defer keyFile.Close()
|
||||
|
||||
err = pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = pem.Encode(keyFile, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
|
||||
require.NoError(t, err)
|
||||
|
||||
return certPath, keyPath
|
||||
}
|
|
@ -33,6 +33,9 @@ S3_GW_SERVER_1_TLS_ENABLED=true
|
|||
S3_GW_SERVER_1_TLS_CERT_FILE=/path/to/tls/cert
|
||||
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
|
||||
|
|
|
@ -25,6 +25,8 @@ peers:
|
|||
priority: 2
|
||||
weight: 0.9
|
||||
|
||||
reconnect_interval: 1m
|
||||
|
||||
server:
|
||||
- address: 0.0.0.0:8080
|
||||
tls:
|
||||
|
|
313
docs/authentication.md
Normal file
313
docs/authentication.md
Normal file
|
@ -0,0 +1,313 @@
|
|||
# Authentication and authorization scheme
|
||||
|
||||
This document describes s3-gw authentication and authorization mechanism.
|
||||
|
||||
## General overview
|
||||
|
||||
Basic provisions:
|
||||
|
||||
* 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 to this
|
||||
process.
|
||||
|
||||
<a>
|
||||
<img src="images/authentication/auth-overview.svg" alt="Auth general overview"/>
|
||||
</a>
|
||||
|
||||
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 stores user info and access rules.
|
||||
|
||||
## Data auth process
|
||||
|
||||
Let's look at the process in more detail:
|
||||
|
||||
<a>
|
||||
<img src="images/authentication/auth-sequence.svg" alt="Auth sequence diagram"/>
|
||||
</a>
|
||||
|
||||
* 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 (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 (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 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
|
||||
such `OwnerID` exists in `frsotfsid` contract (that stores all registered valid users). If user doesn't exist in
|
||||
contract the `AccessDenied` is returned.
|
||||
|
||||
* **Authorization steps**:
|
||||
* To know if user has access right to do what he wants to do we must find appropriate access policies. Such policies
|
||||
are stored in `policy` contract and locally (can be manged using [control api](#control-auth-process)). So we need
|
||||
to get policies from contract and [check them](#policies) along with local to decide if user has access right. If
|
||||
he doesn't have such right the `AccessDenied` is returned.
|
||||
|
||||
* 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 response.
|
||||
|
||||
### AWS Signing
|
||||
|
||||
Every interaction with FrostFS S3 gateway is either authenticated or anonymous. This section explains request
|
||||
authentication with the AWS Signature Version 4 algorithm. More info in AWS documentation:
|
||||
|
||||
* [Authenticating Requests (AWS Signature Version 4)](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html)
|
||||
* [Signing AWS API requests](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-signing.html)
|
||||
|
||||
#### Authentication Methods
|
||||
|
||||
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 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,
|
||||
see [Authenticating Requests: Using the Authorization Header (AWS Signature Version 4)](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html).
|
||||
* **Query string parameters** - You can use a query string to express a request entirely in a URL. In this case, you use
|
||||
query parameters to provide request information, including the authentication information. Because the request
|
||||
signature is part of the URL, this type of URL is often referred to as a presigned URL. You can use presigned URLs to
|
||||
embed clickable links, which can be valid for up to seven days, in HTML. For more information,
|
||||
see [Authenticating Requests: Using Query Parameters (AWS Signature Version 4)](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html).
|
||||
|
||||
FrostFS S3 also supports browser-based uploads that use HTTP POST requests. With an HTTP POST request, you can upload
|
||||
content to FrostFS S3 directly from the browser. For information about authenticating POST requests,
|
||||
see [Browser-Based Uploads Using POST (AWS Signature Version 4)](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-UsingHTTPPOST.html).
|
||||
|
||||
#### Introduction to Signing Requests
|
||||
|
||||
Authentication information that you send in a request must include a signature. To calculate a signature, you first
|
||||
concatenate select request elements to form a string, referred to as the string to sign. You then use a signing key to
|
||||
calculate the hash-based message authentication code (HMAC) of the string to sign.
|
||||
|
||||
In AWS Signature Version 4, you don't use your secret access key to sign the request. Instead, you first use your secret
|
||||
access key to derive a signing key. The derived signing key is specific to the date, service, and Region. For more
|
||||
information about how to derive a signing key in different programming languages, see Examples of how to derive a
|
||||
signing key for Signature Version 4.
|
||||
|
||||
The following diagram illustrates the general process of computing a signature.
|
||||
|
||||
<a>
|
||||
<img src="images/authentication/aws-signing.png" alt="AWS Signing"/>
|
||||
</a>
|
||||
|
||||
The string to sign depends on the request type. For example, when you use the HTTP Authorization header or the query
|
||||
parameters for authentication, you use a varying combination of request elements to create the string to sign. For an
|
||||
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 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
|
||||
information that is contained in the request. If the signatures match, FrostFS S3 processes your request; otherwise, the
|
||||
request is rejected.
|
||||
|
||||
##### Signature Calculations for the Authorization Header
|
||||
|
||||
To calculate a signature, you first need a string to sign. You then calculate a HMAC-SHA256 hash of the string to sign
|
||||
by using a signing key. The following diagram illustrates the process, including the various components of the string
|
||||
that you create for signing.
|
||||
|
||||
When FrostFS S3 receives an authenticated request, it computes the signature and then compares it with the signature
|
||||
that you provided in the request. For that reason, you must compute the signature by using the same method that is used
|
||||
by FrostFS S3. The process of putting a request in an agreed-upon form for signing is called canonicalization.
|
||||
|
||||
<a>
|
||||
<img src="images/authentication/auth-header-signing.png" alt="Signature Calculations for the Authorization Header"/>
|
||||
</a>
|
||||
|
||||
See detains in [AWS documentation](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html).
|
||||
|
||||
#### s3-gw
|
||||
|
||||
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)
|
||||
* [Browser-Based Uploads Using POST](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-UsingHTTPPOST.html)
|
||||
|
||||
All these methods provide `AccessKeyId` and signature. Using `AccessKeyId` s3-gw can get `SecretAccessKey`
|
||||
(see [data auth](#data-auth-process)) to compute signature using exactly the same mechanics
|
||||
as [client does](#introduction-to-signing-requests). After signature calculation the s3-gw just compares signatures and
|
||||
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 to successful authentication in FrostFS
|
||||
storage node.
|
||||
|
||||
Based on this object s3 credentials are formed:
|
||||
|
||||
* `AccessKeyId` - is concatenated container id and object id (`<cid>0<oid>`) of `AccessBox` (
|
||||
e.g. `2XGRML5EW3LMHdf64W2DkBy1Nkuu4y4wGhUj44QjbXBi05ZNvs8WVwy1XTmSEkcVkydPKzCgtmR7U3zyLYTj3Snxf`)
|
||||
* `SecretAccessKey` - hex-encoded random generated 32 bytes (that is encrypted and stored in object payload)
|
||||
|
||||
> **Note**: sensitive info in `AccessBox` is [encrypted](#encryption), so only someone who posses specific private key
|
||||
> can decrypt such info.
|
||||
|
||||
`AccessBox` has the following structure:
|
||||
|
||||
<a>
|
||||
<img src="images/authentication/accessbox-object.svg" alt="AccessBox object structure"/>
|
||||
</a>
|
||||
|
||||
**Headers:**
|
||||
|
||||
`AccessBox` object has the following attributes (at least them, it also can contain custom one):
|
||||
|
||||
* `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))
|
||||
* `S3-Access-Box-CRDT-Name` - `AccessKeyId` of credentials to which current `AccessBox` is related (
|
||||
see [AccessBox versions](#accessbox-versions))
|
||||
* `FilePath` - just object name
|
||||
|
||||
**Payload:**
|
||||
|
||||
The `AccessBox` payload is an encoded [AccessBox protobuf type](../creds/accessbox/accessbox.proto) .
|
||||
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 one item from list it should process)
|
||||
* Encrypted tokens:
|
||||
* `SecretAccessKey` - hex-encoded random generated 32 bytes
|
||||
* Marshaled bearer token - more detail
|
||||
in [spec](https://git.frostfs.info/TrueCloudLab/frostfs-api/src/commit/4c68d92468503b10282c8a92af83a56f170c8a3a/acl/types.proto#L189)
|
||||
* Marshaled session token - more detail
|
||||
in [spec](https://git.frostfs.info/TrueCloudLab/frostfs-api/src/commit/4c68d92468503b10282c8a92af83a56f170c8a3a/session/types.proto#L89)
|
||||
* Container placement policies:
|
||||
* `LocationsConstraint` - name of location constraint that can be used to create bucket/container using s3
|
||||
credentials related to this `AccessBox`
|
||||
* Marshaled placement policy - more detail
|
||||
in [spec](https://git.frostfs.info/TrueCloudLab/frostfs-api/src/commit/4c68d92468503b10282c8a92af83a56f170c8a3a/netmap/types.proto#L111)
|
||||
|
||||
#### AccessBox versions
|
||||
|
||||
Imagine the following scenario:
|
||||
|
||||
* 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 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 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
|
||||
|
||||
To support scenario from previous section and find appropriate version of `AccessBox` (that contains more recent and
|
||||
relevant data) the following sequence is used:
|
||||
|
||||
<a>
|
||||
<img src="images/authentication/accessbox-search.svg" alt="AccessBox search process"/>
|
||||
</a>
|
||||
|
||||
* Search all object whose attribute `S3-Access-Box-CRDT-Name` is equal to `AccessKeyId` (extract container id
|
||||
from `AccessKeyId` that has format: `<cid>0<oid>`).
|
||||
* Get metadata for these object using `HEAD` requests (not `Get` to reduce network traffic)
|
||||
* Sort all these objects by creation epoch and object id
|
||||
* Pick last object id (If no object is found then extract object id from `AccessKeyId` that has format: `<cid>0<oid>`.
|
||||
We need to do this because versions of `AccessBox` can miss the `S3-Access-Box-CRDT-Name` attribute.)
|
||||
* Get appropriate object from FrostFS storage
|
||||
* Decrypt `AccessBox` (see [encryption](#encryption))
|
||||
|
||||
#### Encryption
|
||||
|
||||
Each `AccessBox` contains sensitive information (`AccessSecretKey`, bearer/session tokens etc.) that must be protected
|
||||
and available only to trusted parties (in our case it's a s3-gw).
|
||||
|
||||
To encrypt/decrypt data the authenticated encryption with associated
|
||||
data ([AEAD](https://en.wikipedia.org/wiki/Authenticated_encryption)) is used. The encryption algorithm
|
||||
is [ChaCha20-Poly1305](https://en.wikipedia.org/wiki/ChaCha20-Poly1305) ([RFC](https://datatracker.ietf.org/doc/html/rfc7905)).
|
||||
|
||||
Is the following algorithm the ECDSA keys (with curve implements NIST P-256 (FIPS 186-3, section D.2.3) also known as
|
||||
secp256r1 or prime256v1) is used (unless otherwise stated).
|
||||
|
||||
**Encryption:**
|
||||
|
||||
* Create ephemeral key (`SeedKey`), it's need to generate shared secret
|
||||
* Generate random 32-byte (that after hex-encoded be `SecretAccessKey`) or use existing secret access key
|
||||
(if `AccessBox` is being updated rather than creating brand new)
|
||||
* Generate shared secret as [ECDH](https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman)
|
||||
* Derive 32-byte key using shared secret from previous step with key derivation function based on
|
||||
HMAC with SHA256 [HKDF](https://en.wikipedia.org/wiki/HKDF)
|
||||
* Encrypt marshaled [Tokens](../creds/accessbox) using derived key
|
||||
with [ChaCha20-Poly1305](https://en.wikipedia.org/wiki/ChaCha20-Poly1305) algorithm without additional data.
|
||||
|
||||
**Decryption:**
|
||||
|
||||
* Get public part of `SeedKey` from `AccessBox`
|
||||
* Generate shared secret as follows:
|
||||
* Make scalar curve multiplication of public part of `SeedKey` and private part of s3-gw key
|
||||
* Use `X` part of multiplication (with zero padding at the beginning to fit 32-byte)
|
||||
* Derive 32-byte key using shared secret from previous step with key derivation function based on
|
||||
HMAC with SHA256 [HKDF](https://en.wikipedia.org/wiki/HKDF)
|
||||
* Decrypt encrypted marshaled [Tokens](../creds/accessbox) using derived key
|
||||
with [ChaCha20-Poly1305](https://en.wikipedia.org/wiki/ChaCha20-Poly1305) algorithm without additional data.
|
||||
|
||||
### Policies
|
||||
|
||||
The main repository that contains policy implementation is https://git.frostfs.info/TrueCloudLab/policy-engine.
|
||||
|
||||
Policies can be stored locally (using [control api](#control-auth-process)) or in `policy` contract. When policies check
|
||||
is performed the following algorithm is applied:
|
||||
|
||||
* Check local policies:
|
||||
* If any rule was matched return checking result.
|
||||
* Check contract policies:
|
||||
* If any rule was matched return checking result.
|
||||
* 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 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 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):
|
||||
|
||||
* `Owner` - address of owner that is performing request (this is taken from bearer token from `AccessBox`)
|
||||
* `frostfsid:groupID` - groups to which the owner belongs (this is taken from `frostfsid` contract)
|
||||
|
||||
## Control auth process
|
||||
|
||||
There are control path [grpc api](../pkg/service/control/service.proto) in s3-gw that also has their own authentication
|
||||
and authorization process.
|
||||
|
||||
But this process is quite straight forward:
|
||||
|
||||
* Get grpc request
|
||||
* Check if signing key belongs to [allowed key list](configuration.md#control-section) (that is located in config file)
|
||||
* Validate signature
|
||||
|
||||
For signing process the asymmetric encryption based on elliptic curves (`ECDSA_SHA512`) is used.
|
||||
For more details see the appropriate code
|
||||
in [frostfs-api](https://git.frostfs.info/TrueCloudLab/frostfs-api/src/commit/4c68d92468503b10282c8a92af83a56f170c8a3a/refs/types.proto#L94)
|
||||
and [frostfs-api-go](https://git.frostfs.info/TrueCloudLab/frostfs-api-go/src/commit/a85146250b312fcdd6da9a71285527fed544234f/refs/types.go#L38).
|
|
@ -1,73 +1,58 @@
|
|||
# S3 API support
|
||||
|
||||
Reference:
|
||||
|
||||
* [AWS S3 API Reference](https://docs.aws.amazon.com/AmazonS3/latest/API/s3-api.pdf)
|
||||
|
||||
| | Legend |
|
||||
|----|-------------------------------------------|
|
||||
| 🟢 | Supported |
|
||||
| 🟡 | Partially supported |
|
||||
| 🔵 | Not supported yet, but will be in future |
|
||||
| 🔴 | Not applicable or will never be supported |
|
||||
| | Legend |
|
||||
|-----|-------------------------------------------|
|
||||
| 🟢 | Supported |
|
||||
| 🟡 | Partially supported |
|
||||
| 🔵 | Not supported yet, but will be in future |
|
||||
| 🔴 | Not applicable or will never be supported |
|
||||
|
||||
## Object
|
||||
|
||||
| | Method | Comments |
|
||||
|----|------------------------|-----------------------------------------|
|
||||
| 🟢 | CopyObject | Done on gateway side |
|
||||
| 🟢 | DeleteObject | |
|
||||
| 🟢 | DeleteObjects | aka DeleteMultipleObjects |
|
||||
| 🟢 | GetObject | |
|
||||
| 🔴 | GetObjectTorrent | We don't plan implementing BT gateway |
|
||||
| 🟢 | HeadObject | |
|
||||
| 🟢 | ListParts | Parts loaded with MultipartUpload |
|
||||
| 🟢 | ListObjects | |
|
||||
| 🟢 | ListObjectsV2 | |
|
||||
| 🟢 | PutObject | Content-MD5 header deprecated |
|
||||
| 🔵 | SelectObjectContent | Need to have some Lambda to execute SQL |
|
||||
| 🔵 | WriteGetObjectResponse | Waiting for Lambda to be developed |
|
||||
| 🟢 | GetObjectAttributes | |
|
||||
| | Method | Comments |
|
||||
|-----|------------------------|-----------------------------------------|
|
||||
| 🟢 | CopyObject | Done on gateway side |
|
||||
| 🟢 | DeleteObject | |
|
||||
| 🟢 | DeleteObjects | aka DeleteMultipleObjects |
|
||||
| 🟢 | GetObject | |
|
||||
| 🔴 | GetObjectTorrent | We don't plan implementing BT gateway |
|
||||
| 🟢 | HeadObject | |
|
||||
| 🟢 | ListParts | Parts loaded with MultipartUpload |
|
||||
| 🟢 | ListObjects | |
|
||||
| 🟢 | ListObjectsV2 | |
|
||||
| 🟢 | PutObject | Content-MD5 header deprecated |
|
||||
| 🔵 | SelectObjectContent | Need to have some Lambda to execute SQL |
|
||||
| 🔵 | WriteGetObjectResponse | Waiting for Lambda to be developed |
|
||||
| 🟢 | GetObjectAttributes | |
|
||||
|
||||
## ACL
|
||||
|
||||
For now there are some limitations:
|
||||
* [Bucket policy](https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucket-policies.html) supports only one `Principal` per `Statement`.
|
||||
Principal must be `"AWS": "*"` (to refer all users) or `"CanonicalUser": "0313b1ac3a8076e155a7e797b24f0b650cccad5941ea59d7cfd51a024a8b2a06bf"` (hex encoded public key of desired user).
|
||||
* Resource in bucket policy is an array. Each item MUST contain bucket name, CAN contain object name (wildcards are not supported):
|
||||
```json
|
||||
{
|
||||
"Statement": [
|
||||
{
|
||||
"Resource": [
|
||||
"arn:aws:s3:::bucket",
|
||||
"arn:aws:s3:::bucket/some/object"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
* AWS conditions and wildcard are not supported in [resources](https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-arn-format.html)
|
||||
* Only `CanonicalUser` (with hex encoded public key) and `All Users Group` are supported in [ACL](https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html)
|
||||
|
||||
| | Method | Comments |
|
||||
|----|--------------|-----------------|
|
||||
| 🟡 | GetObjectAcl | See Limitations |
|
||||
| 🟡 | PutObjectAcl | See Limitations |
|
||||
| | Method | Comments |
|
||||
|-----|--------------|-----------------------------------|
|
||||
| 🟢 | GetObjectAcl | Objects can have only private acl |
|
||||
| 🔴 | PutObjectAcl | Use PutBucketPolicy instead |
|
||||
|
||||
## Locking
|
||||
|
||||
For now there are some limitations:
|
||||
|
||||
* Retention period can't be shortened, only extended.
|
||||
* You can't delete locks or object with unexpired lock.
|
||||
|
||||
| | Method | Comments |
|
||||
|-----|----------------------------|---------------------------|
|
||||
| 🟡 | GetObjectLegalHold | |
|
||||
| 🟢 | GetObjectLockConfiguration | GetBucketObjectLockConfig |
|
||||
| 🟡 | GetObjectRetention | |
|
||||
| 🟡 | PutObjectLegalHold | |
|
||||
| 🟢 | PutObjectLockConfiguration | PutBucketObjectLockConfig |
|
||||
| 🟡 | PutObjectRetention | |
|
||||
| | Method | Comments |
|
||||
|-----|----------------------------|-------------------------------|
|
||||
| 🟡 | GetObjectLegalHold | |
|
||||
| 🟢 | GetObjectLockConfiguration | aka GetBucketObjectLockConfig |
|
||||
| 🟡 | GetObjectRetention | |
|
||||
| 🟡 | PutObjectLegalHold | |
|
||||
| 🟢 | PutObjectLockConfiguration | aka PutBucketObjectLockConfig |
|
||||
| 🟡 | PutObjectRetention | |
|
||||
|
||||
## Multipart
|
||||
|
||||
|
@ -75,185 +60,219 @@ CompleteMultipartUpload operations may take long time to complete. Gateway
|
|||
sends whitespace characters to keep connection with the client alive. In this
|
||||
case, gateway is unable to set proper HTTP headers like `X-Amz-Version-Id`.
|
||||
|
||||
| | Method | Comments |
|
||||
|----|-------------------------|----------|
|
||||
| 🟢 | AbortMultipartUpload | |
|
||||
| 🟢 | CompleteMultipartUpload | |
|
||||
| 🟢 | CreateMultipartUpload | |
|
||||
| 🟢 | ListMultipartUploads | |
|
||||
| 🟢 | ListParts | |
|
||||
| 🟢 | UploadPart | |
|
||||
| 🟢 | UploadPartCopy | |
|
||||
| | Method | Comments |
|
||||
|-----|-------------------------|----------|
|
||||
| 🟢 | AbortMultipartUpload | |
|
||||
| 🟢 | CompleteMultipartUpload | |
|
||||
| 🟢 | CreateMultipartUpload | |
|
||||
| 🟢 | ListMultipartUploads | |
|
||||
| 🟢 | ListParts | |
|
||||
| 🟢 | UploadPart | |
|
||||
| 🟢 | UploadPartCopy | |
|
||||
|
||||
## Tagging
|
||||
|
||||
| | Method | Comments |
|
||||
|----|---------------------|----------|
|
||||
| 🟢 | DeleteObjectTagging | |
|
||||
| 🟢 | GetObjectTagging | |
|
||||
| 🟢 | PutObjectTagging | |
|
||||
| | Method | Comments |
|
||||
|-----|---------------------|----------|
|
||||
| 🟢 | DeleteObjectTagging | |
|
||||
| 🟢 | GetObjectTagging | |
|
||||
| 🟢 | PutObjectTagging | |
|
||||
|
||||
## Versioning
|
||||
|
||||
See also `GetObject` and other method parameters.
|
||||
|
||||
| | Method | Comments |
|
||||
|----|--------------------|--------------------------|
|
||||
| 🟢 | ListObjectVersions | ListBucketObjectVersions |
|
||||
| 🔵 | RestoreObject | |
|
||||
| | Method | Comments |
|
||||
|-----|--------------------|--------------------------|
|
||||
| 🟢 | ListObjectVersions | ListBucketObjectVersions |
|
||||
| 🔵 | RestoreObject | |
|
||||
|
||||
## Bucket
|
||||
|
||||
| | Method | Comments |
|
||||
|----|----------------------|-----------|
|
||||
| 🟢 | CreateBucket | PutBucket |
|
||||
| 🟢 | DeleteBucket | |
|
||||
| 🟢 | GetBucketLocation | |
|
||||
| 🟢 | HeadBucket | |
|
||||
| 🟢 | ListBuckets | |
|
||||
| 🔵 | PutPublicAccessBlock | |
|
||||
| | Method | Comments |
|
||||
|-----|----------------------|-----------|
|
||||
| 🟢 | CreateBucket | PutBucket |
|
||||
| 🟢 | DeleteBucket | |
|
||||
| 🟢 | GetBucketLocation | |
|
||||
| 🟢 | HeadBucket | |
|
||||
| 🟢 | ListBuckets | |
|
||||
| 🔵 | PutPublicAccessBlock | |
|
||||
|
||||
## Acceleration
|
||||
|
||||
| | Method | Comments |
|
||||
|----|----------------------------------|---------------------|
|
||||
| 🔴 | GetBucketAccelerateConfiguration | GetBucketAccelerate |
|
||||
| 🔴 | PutBucketAccelerateConfiguration | |
|
||||
| | Method | Comments |
|
||||
|-----|----------------------------------|---------------------|
|
||||
| 🔴 | GetBucketAccelerateConfiguration | GetBucketAccelerate |
|
||||
| 🔴 | PutBucketAccelerateConfiguration | |
|
||||
|
||||
## ACL
|
||||
|
||||
| | Method | Comments |
|
||||
|----|--------------|---------------------|
|
||||
| 🟡 | GetBucketAcl | See ACL limitations |
|
||||
| 🟡 | PutBucketAcl | See ACL Limitations |
|
||||
| | Method | Comments |
|
||||
|-----|--------------|------------------------------|
|
||||
| 🟡 | GetBucketAcl | Only canned acl is supported |
|
||||
| 🟡 | PutBucketAcl | Only canned acl is supported |
|
||||
|
||||
## Analytics
|
||||
|
||||
| | Method | Comments |
|
||||
|----|------------------------------------|----------|
|
||||
| 🔵 | DeleteBucketAnalyticsConfiguration | |
|
||||
| 🔵 | GetBucketAnalyticsConfiguration | |
|
||||
| 🔵 | ListBucketAnalyticsConfigurations | |
|
||||
| 🔵 | PutBucketAnalyticsConfiguration | |
|
||||
| | Method | Comments |
|
||||
|-----|------------------------------------|----------|
|
||||
| 🔵 | DeleteBucketAnalyticsConfiguration | |
|
||||
| 🔵 | GetBucketAnalyticsConfiguration | |
|
||||
| 🔵 | ListBucketAnalyticsConfigurations | |
|
||||
| 🔵 | PutBucketAnalyticsConfiguration | |
|
||||
|
||||
## CORS
|
||||
|
||||
| | Method | Comments |
|
||||
|----|------------------|----------|
|
||||
| 🟢 | DeleteBucketCors | |
|
||||
| 🟢 | GetBucketCors | |
|
||||
| 🟢 | PutBucketCors | |
|
||||
| | Method | Comments |
|
||||
|-----|------------------|----------|
|
||||
| 🟢 | DeleteBucketCors | |
|
||||
| 🟢 | GetBucketCors | |
|
||||
| 🟢 | PutBucketCors | |
|
||||
|
||||
## Encryption
|
||||
|
||||
| | Method | Comments |
|
||||
|----|------------------------|----------|
|
||||
| 🔵 | DeleteBucketEncryption | |
|
||||
| 🔵 | GetBucketEncryption | |
|
||||
| 🔵 | PutBucketEncryption | |
|
||||
| | Method | Comments |
|
||||
|-----|------------------------|----------|
|
||||
| 🔵 | DeleteBucketEncryption | |
|
||||
| 🔵 | GetBucketEncryption | |
|
||||
| 🔵 | PutBucketEncryption | |
|
||||
|
||||
## Inventory
|
||||
|
||||
| | Method | Comments |
|
||||
|----|------------------------------------|----------|
|
||||
| 🔵 | DeleteBucketInventoryConfiguration | |
|
||||
| 🔵 | GetBucketInventoryConfiguration | |
|
||||
| 🔵 | ListBucketInventoryConfigurations | |
|
||||
| 🔵 | PutBucketInventoryConfiguration | |
|
||||
| | Method | Comments |
|
||||
|-----|------------------------------------|----------|
|
||||
| 🔵 | DeleteBucketInventoryConfiguration | |
|
||||
| 🔵 | GetBucketInventoryConfiguration | |
|
||||
| 🔵 | ListBucketInventoryConfigurations | |
|
||||
| 🔵 | PutBucketInventoryConfiguration | |
|
||||
|
||||
## Lifecycle
|
||||
|
||||
| | Method | Comments |
|
||||
|----|---------------------------------|----------|
|
||||
| 🔵 | DeleteBucketLifecycle | |
|
||||
| 🔵 | GetBucketLifecycle | |
|
||||
| 🔵 | GetBucketLifecycleConfiguration | |
|
||||
| 🔵 | PutBucketLifecycle | |
|
||||
| 🔵 | PutBucketLifecycleConfiguration | |
|
||||
| | Method | Comments |
|
||||
|-----|---------------------------------|----------|
|
||||
| 🔵 | DeleteBucketLifecycle | |
|
||||
| 🔵 | GetBucketLifecycle | |
|
||||
| 🔵 | GetBucketLifecycleConfiguration | |
|
||||
| 🔵 | PutBucketLifecycle | |
|
||||
| 🔵 | PutBucketLifecycleConfiguration | |
|
||||
|
||||
## Logging
|
||||
|
||||
| | Method | Comments |
|
||||
|----|------------------|----------|
|
||||
| 🔵 | GetBucketLogging | |
|
||||
| 🔵 | PutBucketLogging | |
|
||||
| | Method | Comments |
|
||||
|-----|------------------|----------|
|
||||
| 🔵 | GetBucketLogging | |
|
||||
| 🔵 | PutBucketLogging | |
|
||||
|
||||
## Metrics
|
||||
|
||||
| | Method | Comments |
|
||||
|----|----------------------------------|----------|
|
||||
| 🔵 | DeleteBucketMetricsConfiguration | |
|
||||
| 🔵 | GetBucketMetricsConfiguration | |
|
||||
| 🔵 | ListBucketMetricsConfigurations | |
|
||||
| 🔵 | PutBucketMetricsConfiguration | |
|
||||
| | Method | Comments |
|
||||
|-----|----------------------------------|----------|
|
||||
| 🔵 | DeleteBucketMetricsConfiguration | |
|
||||
| 🔵 | GetBucketMetricsConfiguration | |
|
||||
| 🔵 | ListBucketMetricsConfigurations | |
|
||||
| 🔵 | PutBucketMetricsConfiguration | |
|
||||
|
||||
## Notifications
|
||||
|
||||
| | Method | Comments |
|
||||
|----|------------------------------------|---------------|
|
||||
| 🔵 | GetBucketNotification | |
|
||||
| 🔵 | GetBucketNotificationConfiguration | |
|
||||
| 🔵 | ListenBucketNotification | non-standard? |
|
||||
| 🔵 | PutBucketNotification | |
|
||||
| 🔵 | PutBucketNotificationConfiguration | |
|
||||
| | Method | Comments |
|
||||
|-----|------------------------------------|---------------|
|
||||
| 🔵 | GetBucketNotification | |
|
||||
| 🔵 | GetBucketNotificationConfiguration | |
|
||||
| 🔵 | ListenBucketNotification | non-standard? |
|
||||
| 🔵 | PutBucketNotification | |
|
||||
| 🔵 | PutBucketNotificationConfiguration | |
|
||||
|
||||
## Ownership controls
|
||||
|
||||
| | Method | Comments |
|
||||
|----|-------------------------------|----------|
|
||||
| 🔵 | DeleteBucketOwnershipControls | |
|
||||
| 🔵 | GetBucketOwnershipControls | |
|
||||
| 🔵 | PutBucketOwnershipControls | |
|
||||
| | Method | Comments |
|
||||
|-----|-------------------------------|----------|
|
||||
| 🔵 | DeleteBucketOwnershipControls | |
|
||||
| 🔵 | GetBucketOwnershipControls | |
|
||||
| 🔵 | PutBucketOwnershipControls | |
|
||||
|
||||
## Policy and replication
|
||||
|
||||
| | Method | Comments |
|
||||
|----|-------------------------|-----------------------------|
|
||||
| 🔵 | DeleteBucketPolicy | |
|
||||
| 🔵 | DeleteBucketReplication | |
|
||||
| 🔵 | DeletePublicAccessBlock | |
|
||||
| 🟡 | GetBucketPolicy | See ACL limitations |
|
||||
| 🔵 | GetBucketPolicyStatus | |
|
||||
| 🔵 | GetBucketReplication | |
|
||||
| 🟢 | PostPolicyBucket | Upload file using POST form |
|
||||
| 🟡 | PutBucketPolicy | See ACL limitations |
|
||||
| 🔵 | PutBucketReplication | |
|
||||
Bucket policy has the following limitations
|
||||
|
||||
* Supports only AWS principals in format `arn:aws:iam::<namespace>:user/<user>` or wildcard `*`.
|
||||
* No complex conditions (only conditions for groups now supported)
|
||||
|
||||
Simple valid policy example:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Principal": {
|
||||
"AWS": [
|
||||
"arn:aws:iam::111122223333:role/JohnDoe"
|
||||
]
|
||||
},
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:GetObject",
|
||||
"s3:GetObjectVersion"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Bucket policy status determines using the following scheme:
|
||||
|
||||
* If policy has statement with principal that is wildcard (`*`) then policy is considered as public
|
||||
|
||||
| | Method | Comments |
|
||||
|-----|-------------------------|---------------------------------------------------|
|
||||
| 🟢 | DeleteBucketPolicy | See Policy limitations |
|
||||
| 🔵 | DeleteBucketReplication | |
|
||||
| 🔵 | DeletePublicAccessBlock | |
|
||||
| 🟢 | GetBucketPolicy | See Policy limitations |
|
||||
| 🟢 | GetBucketPolicyStatus | See rule determining status in Policy limitations |
|
||||
| 🔵 | GetBucketReplication | |
|
||||
| 🟢 | PostPolicyBucket | Upload file using POST form |
|
||||
| 🟡 | PutBucketPolicy | See Policy limitations |
|
||||
| 🔵 | PutBucketReplication | |
|
||||
|
||||
## Request payment
|
||||
|
||||
| | Method | Comments |
|
||||
|----|-------------------------|----------|
|
||||
| 🔴 | GetBucketRequestPayment | |
|
||||
| 🔴 | PutBucketRequestPayment | |
|
||||
| | Method | Comments |
|
||||
|-----|-------------------------|----------|
|
||||
| 🔴 | GetBucketRequestPayment | |
|
||||
| 🔴 | PutBucketRequestPayment | |
|
||||
|
||||
## Tagging
|
||||
|
||||
| | Method | Comments |
|
||||
|----|---------------------|----------|
|
||||
| 🟢 | DeleteBucketTagging | |
|
||||
| 🟢 | GetBucketTagging | |
|
||||
| 🟢 | PutBucketTagging | |
|
||||
| | Method | Comments |
|
||||
|-----|---------------------|----------|
|
||||
| 🟢 | DeleteBucketTagging | |
|
||||
| 🟢 | GetBucketTagging | |
|
||||
| 🟢 | PutBucketTagging | |
|
||||
|
||||
## Tiering
|
||||
|
||||
| | Method | Comments |
|
||||
|----|---------------------------------------------|----------|
|
||||
| 🔵 | DeleteBucketIntelligentTieringConfiguration | |
|
||||
| 🔵 | GetBucketIntelligentTieringConfiguration | |
|
||||
| 🔵 | ListBucketIntelligentTieringConfigurations | |
|
||||
| 🔵 | PutBucketIntelligentTieringConfiguration | |
|
||||
| | Method | Comments |
|
||||
|-----|---------------------------------------------|----------|
|
||||
| 🔵 | DeleteBucketIntelligentTieringConfiguration | |
|
||||
| 🔵 | GetBucketIntelligentTieringConfiguration | |
|
||||
| 🔵 | ListBucketIntelligentTieringConfigurations | |
|
||||
| 🔵 | PutBucketIntelligentTieringConfiguration | |
|
||||
|
||||
## Versioning
|
||||
|
||||
| | Method | Comments |
|
||||
|----|---------------------|----------|
|
||||
| 🟢 | GetBucketVersioning | |
|
||||
| 🟢 | PutBucketVersioning | |
|
||||
| | Method | Comments |
|
||||
|-----|---------------------|----------|
|
||||
| 🟢 | GetBucketVersioning | |
|
||||
| 🟢 | PutBucketVersioning | |
|
||||
|
||||
## Website
|
||||
|
||||
| | Method | Comments |
|
||||
|----|---------------------|----------|
|
||||
| 🔵 | DeleteBucketWebsite | |
|
||||
| 🔵 | GetBucketWebsite | |
|
||||
| 🔵 | PutBucketWebsite | |
|
||||
| | Method | Comments |
|
||||
|-----|---------------------|----------|
|
||||
| 🔵 | DeleteBucketWebsite | |
|
||||
| 🔵 | GetBucketWebsite | |
|
||||
| 🔵 | PutBucketWebsite | |
|
||||
|
|
131
docs/bucket_policy.md
Normal file
131
docs/bucket_policy.md
Normal file
|
@ -0,0 +1,131 @@
|
|||
# Bucket policy
|
||||
|
||||
A bucket policy is a resource-based policy that you can use to grant access permissions to your S3 bucket and the
|
||||
objects in it https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucket-policies.html.
|
||||
|
||||
## Conditions
|
||||
|
||||
In AWS there are a lot of condition
|
||||
keys https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.htm
|
||||
but s3-gw currently supports only the following conditions in bucket policy:
|
||||
|
||||
> Note: all condition keys and values must be string formatted in json policy (even if they are numbers).
|
||||
|
||||
| Condition key | Description |
|
||||
|-------------------------------|---------------------------------------------------------------------------|
|
||||
| [s3:max-keys](#s3-max-keys) | Filters access by maximum number of keys returned in a ListBucket request |
|
||||
| [s3:delimiter](#s3-delimiter) | Filters access by delimiter parameter |
|
||||
| [s3:prefix](#s3-prefix) | Filters access by key name prefix |
|
||||
| [s3:VersionId](#s3-versionid) | Filters access by a specific object version |
|
||||
|
||||
Each key can be used only with specific set of
|
||||
operators https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html
|
||||
(it depends on type of key).
|
||||
|
||||
### s3 max-keys
|
||||
|
||||
**Key:** `s3:max-keys`
|
||||
|
||||
**Type:** `Numeric`
|
||||
|
||||
**Description:** Filters access by maximum number of keys returned in a ListBucket request
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": {
|
||||
"Effect": "Allow",
|
||||
"Principal": "*",
|
||||
"Action": "s3:ListBucket",
|
||||
"Resource": "arn:aws:s3:::example_bucket",
|
||||
"Condition": {
|
||||
"NumericLessThanEquals": {
|
||||
"s3:max-keys": "10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### s3 delimiter
|
||||
|
||||
**Key:** `s3:delimiter`
|
||||
|
||||
**Type:** `String`
|
||||
|
||||
**Description:** Filters access by delimiter parameter
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": {
|
||||
"Effect": "Allow",
|
||||
"Principal": "*",
|
||||
"Action": "s3:ListBucket",
|
||||
"Resource": "arn:aws:s3:::example_bucket",
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"s3:delimiter": "/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### s3 prefix
|
||||
|
||||
**Key:** `s3:prefix`
|
||||
|
||||
**Type:** `String`
|
||||
|
||||
**Description:** Filters access by key name prefix
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": {
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": [
|
||||
"arn:aws:iam::111122223333:user/JohnDoe"
|
||||
]
|
||||
},
|
||||
"Action": "s3:ListBucket",
|
||||
"Resource": "arn:aws:s3:::example_bucket",
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"s3:prefix": "home/JohnDoe"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### s3 VersionId
|
||||
|
||||
**Key:** `s3:VersionId`
|
||||
|
||||
**Type:** `String`
|
||||
|
||||
**Description:** Filters access by a specific object version
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": {
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": [
|
||||
"arn:aws:iam::111122223333:user/JohnDoe"
|
||||
]
|
||||
},
|
||||
"Action": "s3:GetObjectVersion",
|
||||
"Resource": "arn:aws:s3:::example_bucket/some-file.txt",
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"s3:VersionId": "AT2L3qER7CHGk4TDooocEzkz2RyqTm4Zh2b1QLzAhLbH"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
|
@ -218,6 +218,8 @@ max_clients_deadline: 30s
|
|||
allowed_access_key_id_prefixes:
|
||||
- Ck9BHsgKcnwfCTUSFm6pxhoNS4cBqgN2NQ8zVgPjqZDX
|
||||
- 3stjWenX15YwYzczMr88gy3CQr4NYFBQ8P7keGzH5QFn
|
||||
|
||||
reconnect_interval: 1m
|
||||
```
|
||||
|
||||
| Parameter | Type | SIGHUP reload | Default value | Description |
|
||||
|
@ -233,6 +235,7 @@ allowed_access_key_id_prefixes:
|
|||
| `max_clients_count` | `int` | no | `100` | Limits for processing of clients' requests. |
|
||||
| `max_clients_deadline` | `duration` | no | `30s` | Deadline after which the gate sends error `RequestTimeout` to a client. |
|
||||
| `allowed_access_key_id_prefixes` | `[]string` | no | | List of allowed `AccessKeyID` prefixes which S3 GW serve. If the parameter is omitted, all `AccessKeyID` will be accepted. |
|
||||
| `reconnect_interval` | `duration` | no | `1m` | Listeners reconnection interval. |
|
||||
|
||||
### `wallet` section
|
||||
|
||||
|
|
44
docs/images/authentication/accessbox-object.puml
Normal file
44
docs/images/authentication/accessbox-object.puml
Normal file
|
@ -0,0 +1,44 @@
|
|||
@startuml
|
||||
|
||||
package AccessBox {
|
||||
map Tokens {
|
||||
SecretKey => Private key
|
||||
BearerToken => Encoded bearer token
|
||||
SessionTokens => List of encoded session tokens
|
||||
}
|
||||
|
||||
map Gate {
|
||||
GateKey => Encoded public gate key
|
||||
Encrypted tokens *--> Tokens
|
||||
}
|
||||
|
||||
map ContainerPolicy {
|
||||
LocationConstraint => Policy name
|
||||
PlacementPolicy => Encoded placement policy
|
||||
}
|
||||
|
||||
map Box {
|
||||
SeedKey => Encoded public seed key
|
||||
List of Gates *--> Gate
|
||||
List of container policies *--> ContainerPolicy
|
||||
}
|
||||
|
||||
|
||||
map ObjectAttributes {
|
||||
Timestamp => 1710418478
|
||||
_~_SYSTEM_~_EXPIRATION_EPOCH => 10801
|
||||
S3-CRDT-Versions-Add => 5ZNvs8WVwy1XTmSEkcVkydPKzCgtmR7U3zyLYTj3Snxf,9bLtL1EsUpuSiqmHnqFf6RuT6x5QMLMNBqx7vCcCcNhy
|
||||
S3-Access-Box-CRDT-Name => 2XGRML5EW3LMHdf64W2DkBy1Nkuu4y4wGhUj44QjbXBi05ZNvs8WVwy1XTmSEkcVkydPKzCgtmR7U3zyLYTj3Snxf
|
||||
FilePath => 1710418478_access.box
|
||||
}
|
||||
|
||||
map FrostFSObject {
|
||||
Header *-> ObjectAttributes
|
||||
Payload *--> Box
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@enduml
|
60
docs/images/authentication/accessbox-object.svg
Normal file
60
docs/images/authentication/accessbox-object.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 13 KiB |
29
docs/images/authentication/accessbox-search.puml
Normal file
29
docs/images/authentication/accessbox-search.puml
Normal file
|
@ -0,0 +1,29 @@
|
|||
@startuml
|
||||
|
||||
User -> "S3-GW": AccessKey
|
||||
"S3-GW" -> "FrostFS Node": Search objects
|
||||
|
||||
note right
|
||||
Search by exact attribute matching:
|
||||
**S3-Access-Box-CRDT-Name:** //AccessKey//
|
||||
end note
|
||||
|
||||
"FrostFS Node" --> "S3-GW": AccessBox objects ids
|
||||
|
||||
"S3-GW" -> "FrostFS Node" : Head AccessBox objects
|
||||
"FrostFS Node" --> "S3-GW": AccessBox object headers
|
||||
|
||||
"S3-GW" -> "S3-GW": Choose latest AccessBox
|
||||
|
||||
note left
|
||||
Sort AccessBox headers by creation epoch
|
||||
and then by ObjectID
|
||||
Pick last
|
||||
end note
|
||||
|
||||
"S3-GW" -> "FrostFS Node" : Get AccessBox object
|
||||
"FrostFS Node" --> "S3-GW": AccessBox object
|
||||
|
||||
"S3-GW" -> "S3-GW": Decrypt and validate AccessBox
|
||||
|
||||
@enduml
|
39
docs/images/authentication/accessbox-search.svg
Normal file
39
docs/images/authentication/accessbox-search.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.3 KiB |
BIN
docs/images/authentication/auth-header-signing.png
Normal file
BIN
docs/images/authentication/auth-header-signing.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 133 KiB |
25
docs/images/authentication/auth-overview.puml
Normal file
25
docs/images/authentication/auth-overview.puml
Normal file
|
@ -0,0 +1,25 @@
|
|||
@startuml
|
||||
!include <c4/C4_Container.puml>
|
||||
AddElementTag("smart-contract", $bgColor=#0abab5)
|
||||
|
||||
Person(user, "User", "User with or without credentials")
|
||||
|
||||
System_Boundary(c1, "FrostFS") {
|
||||
Container(s3, "S3 Gateway", $descr="AWS S3 compatible gate")
|
||||
Container(stor, "FrostFS Storage", $descr="Storage service")
|
||||
}
|
||||
|
||||
System_Boundary(c3, "Blockchain") {
|
||||
Interface "NeoGo"
|
||||
Container(ffsid, "FrostFS ID", $tags="smart-contract", $descr="Stores namespaces and users")
|
||||
Container(policy, "Policy", $tags="smart-contract", $descr="Stores APE rules")
|
||||
}
|
||||
|
||||
Rel_R(user, s3, "Requests", "HTTP")
|
||||
Rel_R(s3, stor, "Get data to validate request, store objects")
|
||||
Rel_D(s3, NeoGo, "Get data to validate request")
|
||||
Rel("NeoGo", ffsid, "Fetch users")
|
||||
Rel("NeoGo", policy, "Fetch policies")
|
||||
|
||||
SHOW_LEGEND(true)
|
||||
@enduml
|
611
docs/images/authentication/auth-overview.svg
Normal file
611
docs/images/authentication/auth-overview.svg
Normal file
|
@ -0,0 +1,611 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" height="660px" preserveAspectRatio="none" style="width:851px;height:660px;background:#FFFFFF;" version="1.1" viewBox="0 0 851 660" width="851px" zoomAndPan="magnify"><defs/><g><!--MD5=[84dda40acb3410cad7262261daba2aaf]
|
||||
cluster c1--><g id="cluster_c1"><rect fill="none" height="152" rx="2.5" ry="2.5" style="stroke:#444444;stroke-width:1.0;stroke-dasharray:7.0,7.0;" width="587" x="258" y="7"/><text fill="#444444" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="71" x="516" y="23.8516">FrostFS</text><text fill="#444444" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="61" x="521" y="38.7637">[System]</text></g><!--MD5=[fb252dd5a834d4be8567d0df3f6bbec4]
|
||||
cluster c3--><g id="cluster_c3"><rect fill="none" height="301" rx="2.5" ry="2.5" style="stroke:#444444;stroke-width:1.0;stroke-dasharray:7.0,7.0;" width="405" x="259" y="243.5"/><text fill="#444444" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="95" x="414" y="260.3516">Blockchain</text><text fill="#444444" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="61" x="431" y="275.2637">[System]</text></g><!--MD5=[b165ca7cce796f881c879adda4a6bef9]
|
||||
entity s3--><g id="elem_s3"><rect fill="#438DD5" height="85.1875" rx="2.5" ry="2.5" style="stroke:#3C7FC0;stroke-width:0.5;" width="199" x="274.5" y="58"/><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="108" x="320" y="82.8516">S3 Gateway</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="10" x="369" y="97.7637">[]</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="372" y="113.5889"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="175" x="288.5" y="129.8857">AWS S3 compatible gate</text></g><!--MD5=[b631bc93683c8d3c6bcd86869bd62c2d]
|
||||
entity stor--><g id="elem_stor"><rect fill="#438DD5" height="85.1875" rx="2.5" ry="2.5" style="stroke:#3C7FC0;stroke-width:0.5;" width="169" x="660.5" y="58"/><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="149" x="670.5" y="82.8516">FrostFS Storage</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="10" x="740" y="97.7637">[]</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="743" y="113.5889"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="111" x="691.5" y="129.8857">Storage service</text></g><!--MD5=[d75780de534459f9083ff96c63e26824]
|
||||
entity NeoGo--><g id="elem_NeoGo"><ellipse cx="374" cy="323.5" fill="#F1F1F1" rx="8" ry="8" style="stroke:#181818;stroke-width:0.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="48" x="350" y="353.4951">NeoGo</text></g><!--MD5=[a1c7fbed12783ec305c3357d72c64f9e]
|
||||
entity ffsid--><g id="elem_ffsid"><rect fill="#0ABAB5" height="101.4844" rx="2.5" ry="2.5" style="stroke:#3C7FC0;stroke-width:0.5;" width="198" x="275" y="427.5"/><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="96" x="326" y="452.3516">FrostFS ID</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="10" x="369" y="467.2637">[]</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="372" y="483.0889"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="170" x="289" y="499.3857">Stores namespaces and</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="38" x="355" y="515.6826">users</text></g><!--MD5=[4361443624774238dacd0e01c3165ecf]
|
||||
entity policy--><g id="elem_policy"><rect fill="#0ABAB5" height="85.1875" rx="2.5" ry="2.5" style="stroke:#3C7FC0;stroke-width:0.5;" width="139" x="508.5" y="435.5"/><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="52" x="552" y="460.3516">Policy</text><text fill="#FFFFFF" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="10" x="573" y="475.2637">[]</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="576" y="491.0889"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="115" x="522.5" y="507.3857">Stores APE rules</text></g><!--MD5=[8fc3522a43f8c7199df5e09e5bb0188e]
|
||||
entity user--><g id="elem_user"><rect fill="#08427B" height="135.5156" rx="2.5" ry="2.5" style="stroke:#073B6F;stroke-width:0.5;" width="168" x="7" y="32.5"/><image height="48" width="48" x="67" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAACD0lEQVR4Xu2YoU4EMRCGT+4j8Ai8AhaH4QHgAUjQuFMECUgMIUgwJAgMhgQsAYUiJCiQIBBY+EITsjfTdme6V24v4c8vyGbb+ZjOtN0bNcvjQXmkH83WvYBWto6PLm6v7p7uH1/w2fXD+PBycX1Pv2l3IdDm/vn7x+dXQiAubRzoURa7gRZWd0iGRIiJbOnhnfYBQZNJjNbuyY2eJG8fkDE3bbG4ep6MHUAsgYxmE3nVs6VsBWJSGccsOlFPmLIViMzLOB7pCVO2AtHJMohH7Fh6zqitQK7m0rJvAVYgGcEpe//PLdDz65sM4pF9N7ICcXDKIB5Nv6j7tD0NoSdM2QrU9Gg0ewE1LqBhHR3BBdvj2vapnidjHxD/q6vd7Pvhr31AwcY8eXMTXAKECZZJFXuEq27aLgQK5uLMohCenGGuGewOxSjBvYBqeG6B+Nqiblggdjnc+ZXDy+FNFpFzw76O3UBAROuXh6FoiAcf5g9eTvUgzy0nWg6I8cXHRUpg5bOVBCo+KDpFajOf23GgPme7RSQ+lacIENUgJ6gg1k6HjgOlqnLqip4tEuhv0hNEMXUD0clyXE3p6pZA0S2nnvTlXwLJEZWlb7cTQH1+USgTN4VhAenm/wea1OCAOmqo6fE1WCb9WSKBah+rbUWPWAmE2Rvk0ApiB45eOyNAzU8xcTvj8KvkKEoOaIYeHNA3ZuygAvFMUO0AAAAASUVORK5CYII=" y="42.5"/><text fill="#FFFFFF" font-family="sans-serif" font-size="16" font-weight="bold" lengthAdjust="spacing" textLength="42" x="70" y="105.3516">User</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="89" y="122.1201"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="140" x="21" y="138.417">User with or without</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="79" x="51.5" y="154.7139">credentials</text></g><!--MD5=[52d0c115c7d06b979b7f69659773ccc0]
|
||||
link user to s3--><g id="link_user_s3"><path d="M175.14,100.5 C203.78,100.5 236.2,100.5 266.42,100.5 " fill="none" id="user-to-s3" style="stroke:#666666;stroke-width:1.0;"/><polygon fill="#666666" points="274.47,100.5,266.47,97.5,266.47,103.5,274.47,100.5" style="stroke:#666666;stroke-width:1.0;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="63" x="193.25" y="80.6387">Requests</text><text fill="#666666" font-family="sans-serif" font-size="12" font-style="italic" lengthAdjust="spacing" textLength="40" x="204.75" y="94.6074">[HTTP]</text></g><!--MD5=[22d466a8c2458259cbad703a0636b8fb]
|
||||
link s3 to stor--><g id="link_s3_stor"><path d="M473.91,100.5 C529.33,100.5 597.81,100.5 652.08,100.5 " fill="none" id="s3-to-stor" style="stroke:#666666;stroke-width:1.0;"/><polygon fill="#666666" points="660.32,100.5,652.32,97.5,652.32,103.5,660.32,100.5" style="stroke:#666666;stroke-width:1.0;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="136" x="497" y="80.6387">Get data to validate</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="150" x="492" y="94.6074">request, store objects</text></g><!--MD5=[8c98dd26c815ae7a8024bcc2d9dd4f66]
|
||||
link s3 to NeoGo--><g id="link_s3_NeoGo"><path d="M374,143.07 C374,192.21 374,271.65 374,305.9 " fill="none" id="s3-to-NeoGo" style="stroke:#666666;stroke-width:1.0;"/><polygon fill="#666666" points="374,314.3,377,306.3,371,306.3,374,314.3" style="stroke:#666666;stroke-width:1.0;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="136" x="375" y="210.6387">Get data to validate</text><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="53" x="418.5" y="224.6074">request</text></g><!--MD5=[67f0c0ebd6d2a23c30e202ac0cd81435]
|
||||
link NeoGo to ffsid--><g id="link_NeoGo_ffsid"><path d="M374,332.7 C374,348.95 374,386.52 374,419.25 " fill="none" id="NeoGo-to-ffsid" style="stroke:#666666;stroke-width:1.0;"/><polygon fill="#666666" points="374,427.45,377,419.45,371,419.45,374,427.45" style="stroke:#666666;stroke-width:1.0;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="79" x="375" y="394.6387">Fetch users</text></g><!--MD5=[d37da9b99a3aca6bfa9447725e2e6374]
|
||||
link NeoGo to policy--><g id="link_NeoGo_policy"><path d="M383.06,329.95 C399.4,339.87 434.74,361.78 463,382.5 C483.22,397.33 504.63,414.49 523.43,430.09 " fill="none" id="NeoGo-to-policy" style="stroke:#666666;stroke-width:1.0;"/><polygon fill="#666666" points="529.7,435.31,525.4816,427.88,521.6363,432.4858,529.7,435.31" style="stroke:#666666;stroke-width:1.0;"/><text fill="#666666" font-family="sans-serif" font-size="12" font-weight="bold" lengthAdjust="spacing" textLength="93" x="483" y="394.6387">Fetch policies</text></g><rect fill="none" height="16.2969" style="stroke:none;stroke-width:1.0;" width="236" x="584" y="568.5"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="59" x="584" y="581.4951">Legend</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="643" y="581.4951"> </text><rect fill="#08427B" height="16.2969" style="stroke:none;stroke-width:1.0;" width="236" x="584" y="584.7969"/><text fill="#073B6F" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="8" x="588" y="597.792">▯</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="596" y="597.792"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="49" x="604" y="597.792">person</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="657" y="597.792"> </text><rect fill="#438DD5" height="16.2969" style="stroke:none;stroke-width:1.0;" width="236" x="584" y="601.0938"/><text fill="#3C7FC0" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="8" x="588" y="614.0889">▯</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="596" y="614.0889"> </text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="68" x="604" y="614.0889">container</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="676" y="614.0889"> </text><rect fill="#0ABAB5" height="16.2969" style="stroke:none;stroke-width:1.0;" width="236" x="584" y="617.3906"/><text fill="#0ABAB5" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="8" x="588" y="630.3857">▯</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="596" y="630.3857"> </text><text fill="#66622E" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="208" x="604" y="630.3857">smart-contract (no text color)</text><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="4" x="816" y="630.3857"> </text><line style="stroke:none;stroke-width:1.0;" x1="584" x2="820" y1="568.5" y2="568.5"/><line style="stroke:none;stroke-width:1.0;" x1="584" x2="820" y1="584.7969" y2="584.7969"/><line style="stroke:none;stroke-width:1.0;" x1="584" x2="820" y1="601.0938" y2="601.0938"/><line style="stroke:none;stroke-width:1.0;" x1="584" x2="820" y1="617.3906" y2="617.3906"/><line style="stroke:none;stroke-width:1.0;" x1="584" x2="820" y1="633.6875" y2="633.6875"/><line style="stroke:none;stroke-width:1.0;" x1="584" x2="584" y1="568.5" y2="633.6875"/><line style="stroke:none;stroke-width:1.0;" x1="820" x2="820" y1="568.5" y2="633.6875"/><!--MD5=[c02d88aa5b998c40021c1d715125d393]
|
||||
@startuml
|
||||
!include <c4/C4_Container.puml>
|
||||
AddElementTag("smart-contract", $bgColor=#0abab5)
|
||||
|
||||
Person(user, "User", "User with or without credentials")
|
||||
|
||||
System_Boundary(c1, "FrostFS") {
|
||||
Container(s3, "S3 Gateway", $descr="AWS S3 compatible gate")
|
||||
Container(stor, "FrostFS Storage", $descr="Storage service")
|
||||
}
|
||||
|
||||
System_Boundary(c3, "Blockchain") {
|
||||
Interface "NeoGo"
|
||||
Container(ffsid, "FrostFS ID", $tags="smart-contract", $descr="Stores namespaces and users")
|
||||
Container(policy, "Policy", $tags="smart-contract", $descr="Stores APE rules")
|
||||
}
|
||||
|
||||
Rel_R(user, s3, "Requests", "HTTP")
|
||||
Rel_R(s3, stor, "Get data to validate request, store objects")
|
||||
Rel_D(s3, NeoGo, "Get data to validate request")
|
||||
Rel("NeoGo", ffsid, "Fetch users")
|
||||
Rel("NeoGo", policy, "Fetch policies")
|
||||
|
||||
SHOW_LEGEND(true)
|
||||
@enduml
|
||||
|
||||
@startuml
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
skinparam defaultTextAlignment center
|
||||
|
||||
skinparam wrapWidth 200
|
||||
skinparam maxMessageSize 150
|
||||
|
||||
skinparam LegendBorderColor transparent
|
||||
skinparam LegendBackgroundColor transparent
|
||||
skinparam LegendFontColor #FFFFFF
|
||||
|
||||
skinparam shadowing<<legendArea>> false
|
||||
skinparam rectangle<<legendArea>> {
|
||||
backgroundcolor #00000000
|
||||
bordercolor #00000000
|
||||
}
|
||||
|
||||
skinparam rectangle {
|
||||
StereotypeFontSize 12
|
||||
shadowing false
|
||||
}
|
||||
|
||||
skinparam database {
|
||||
StereotypeFontSize 12
|
||||
shadowing false
|
||||
}
|
||||
|
||||
skinparam queue {
|
||||
StereotypeFontSize 12
|
||||
shadowing false
|
||||
}
|
||||
|
||||
skinparam arrow {
|
||||
Color #666666
|
||||
FontColor #666666
|
||||
FontSize 12
|
||||
}
|
||||
|
||||
skinparam actor {
|
||||
StereotypeFontSize 12
|
||||
shadowing false
|
||||
style awesome
|
||||
}
|
||||
|
||||
skinparam person {
|
||||
StereotypeFontSize 12
|
||||
shadowing false
|
||||
}
|
||||
|
||||
skinparam package {
|
||||
StereotypeFontSize 6
|
||||
StereotypeFontColor transparent
|
||||
FontStyle plain
|
||||
BackgroundColor transparent
|
||||
}
|
||||
|
||||
skinparam rectangle<<boundary>> {
|
||||
Shadowing false
|
||||
StereotypeFontSize 6
|
||||
StereotypeFontColor transparent
|
||||
FontColor #444444
|
||||
BorderColor #444444
|
||||
BackgroundColor transparent
|
||||
BorderStyle dashed
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
skinparam rectangle<<person>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #08427B
|
||||
BorderColor #073B6F
|
||||
}
|
||||
skinparam database<<person>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #08427B
|
||||
BorderColor #073B6F
|
||||
}
|
||||
skinparam queue<<person>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #08427B
|
||||
BorderColor #073B6F
|
||||
}
|
||||
skinparam actor<<person>> {
|
||||
StereotypeFontColor #08427B
|
||||
FontColor #08427B
|
||||
BackgroundColor #08427B
|
||||
BorderColor #073B6F
|
||||
}
|
||||
skinparam person<<person>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #08427B
|
||||
BorderColor #073B6F
|
||||
}
|
||||
|
||||
|
||||
skinparam rectangle<<external_person>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #686868
|
||||
BorderColor #8A8A8A
|
||||
}
|
||||
skinparam database<<external_person>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #686868
|
||||
BorderColor #8A8A8A
|
||||
}
|
||||
skinparam queue<<external_person>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #686868
|
||||
BorderColor #8A8A8A
|
||||
}
|
||||
skinparam actor<<external_person>> {
|
||||
StereotypeFontColor #686868
|
||||
FontColor #686868
|
||||
BackgroundColor #686868
|
||||
BorderColor #8A8A8A
|
||||
}
|
||||
skinparam person<<external_person>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #686868
|
||||
BorderColor #8A8A8A
|
||||
}
|
||||
|
||||
|
||||
skinparam rectangle<<system>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #1168BD
|
||||
BorderColor #3C7FC0
|
||||
}
|
||||
skinparam database<<system>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #1168BD
|
||||
BorderColor #3C7FC0
|
||||
}
|
||||
skinparam queue<<system>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #1168BD
|
||||
BorderColor #3C7FC0
|
||||
}
|
||||
skinparam actor<<system>> {
|
||||
StereotypeFontColor #1168BD
|
||||
FontColor #1168BD
|
||||
BackgroundColor #1168BD
|
||||
BorderColor #3C7FC0
|
||||
}
|
||||
skinparam person<<system>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #1168BD
|
||||
BorderColor #3C7FC0
|
||||
}
|
||||
|
||||
|
||||
skinparam rectangle<<external_system>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #999999
|
||||
BorderColor #8A8A8A
|
||||
}
|
||||
skinparam database<<external_system>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #999999
|
||||
BorderColor #8A8A8A
|
||||
}
|
||||
skinparam queue<<external_system>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #999999
|
||||
BorderColor #8A8A8A
|
||||
}
|
||||
skinparam actor<<external_system>> {
|
||||
StereotypeFontColor #999999
|
||||
FontColor #999999
|
||||
BackgroundColor #999999
|
||||
BorderColor #8A8A8A
|
||||
}
|
||||
skinparam person<<external_system>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #999999
|
||||
BorderColor #8A8A8A
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
sprite $person [48x48/16] {
|
||||
000000000000000000000000000000000000000000000000
|
||||
000000000000000000000000000000000000000000000000
|
||||
0000000000000000000049BCCA7200000000000000000000
|
||||
0000000000000000006EFFFFFFFFB3000000000000000000
|
||||
00000000000000001CFFFFFFFFFFFF700000000000000000
|
||||
0000000000000001EFFFFFFFFFFFFFF80000000000000000
|
||||
000000000000000CFFFFFFFFFFFFFFFF6000000000000000
|
||||
000000000000007FFFFFFFFFFFFFFFFFF100000000000000
|
||||
00000000000001FFFFFFFFFFFFFFFFFFF900000000000000
|
||||
00000000000006FFFFFFFFFFFFFFFFFFFF00000000000000
|
||||
0000000000000BFFFFFFFFFFFFFFFFFFFF40000000000000
|
||||
0000000000000EFFFFFFFFFFFFFFFFFFFF70000000000000
|
||||
0000000000000FFFFFFFFFFFFFFFFFFFFF80000000000000
|
||||
0000000000000FFFFFFFFFFFFFFFFFFFFF80000000000000
|
||||
0000000000000DFFFFFFFFFFFFFFFFFFFF60000000000000
|
||||
0000000000000AFFFFFFFFFFFFFFFFFFFF40000000000000
|
||||
00000000000006FFFFFFFFFFFFFFFFFFFE00000000000000
|
||||
00000000000000EFFFFFFFFFFFFFFFFFF800000000000000
|
||||
000000000000007FFFFFFFFFFFFFFFFFF100000000000000
|
||||
000000000000000BFFFFFFFFFFFFFFFF5000000000000000
|
||||
0000000000000001DFFFFFFFFFFFFFF70000000000000000
|
||||
00000000000000000BFFFFFFFFFFFF500000000000000000
|
||||
0000000000000000005DFFFFFFFFA1000000000000000000
|
||||
0000000000000000000037ABB96100000000000000000000
|
||||
000000000000000000000000000000000000000000000000
|
||||
000000000000000000000000000000000000000000000000
|
||||
000000000000025788300000000005886410000000000000
|
||||
000000000007DFFFFFFD9643347BFFFFFFFB400000000000
|
||||
0000000004EFFFFFFFFFFFFFFFFFFFFFFFFFFB1000000000
|
||||
000000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFD200000000
|
||||
00000006FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE10000000
|
||||
0000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0000000
|
||||
000000BFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5000000
|
||||
000003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD000000
|
||||
000009FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF200000
|
||||
00000DFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF600000
|
||||
00000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000
|
||||
00001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA00000
|
||||
00001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB00000
|
||||
00001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB00000
|
||||
00001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB00000
|
||||
00001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA00000
|
||||
00000EFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF700000
|
||||
000006FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE100000
|
||||
0000008FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3000000
|
||||
000000014555555555555555555555555555555300000000
|
||||
000000000000000000000000000000000000000000000000
|
||||
000000000000000000000000000000000000000000000000
|
||||
}
|
||||
|
||||
sprite $person2 [48x48/16] {
|
||||
0000000000000000000049BCCA7200000000000000000000
|
||||
0000000000000000006EFFFFFFFFB3000000000000000000
|
||||
00000000000000001CFFFFFFFFFFFF700000000000000000
|
||||
0000000000000001EFFFFFFFFFFFFFF80000000000000000
|
||||
000000000000000CFFFFFFFFFFFFFFFF6000000000000000
|
||||
000000000000007FFFFFFFFFFFFFFFFFF100000000000000
|
||||
00000000000001FFFFFFFFFFFFFFFFFFF900000000000000
|
||||
00000000000006FFFFFFFFFFFFFFFFFFFF00000000000000
|
||||
0000000000000BFFFFFFFFFFFFFFFFFFFF40000000000000
|
||||
0000000000000EFFFFFFFFFFFFFFFFFFFF70000000000000
|
||||
0000000000000FFFFFFFFFFFFFFFFFFFFF80000000000000
|
||||
0000000000000FFFFFFFFFFFFFFFFFFFFF80000000000000
|
||||
0000000000000DFFFFFFFFFFFFFFFFFFFF60000000000000
|
||||
0000000000000AFFFFFFFFFFFFFFFFFFFF40000000000000
|
||||
00000000000006FFFFFFFFFFFFFFFFFFFE00000000000000
|
||||
00000000000000EFFFFFFFFFFFFFFFFFF800000000000000
|
||||
000000000000007FFFFFFFFFFFFFFFFFF100000000000000
|
||||
000000000000000BFFFFFFFFFFFFFFFF5000000000000000
|
||||
0000000000000001DFFFFFFFFFFFFFF70000000000000000
|
||||
00000000000000000BFFFFFFFFFFFF500000000000000000
|
||||
0000000000000000005DFFFFFFFFA1000000000000000000
|
||||
0000000000000000000037ABB96100000000000000000000
|
||||
000000000002578888300000000005888864100000000000
|
||||
0000000007DFFFFFFFFD9643347BFFFFFFFFFB4000000000
|
||||
00000004EFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB10000000
|
||||
0000007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2000000
|
||||
000006FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE100000
|
||||
00003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB00000
|
||||
0000BFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50000
|
||||
0003FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0000
|
||||
0009FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2000
|
||||
000DFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6000
|
||||
000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8000
|
||||
001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB000
|
||||
001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB000
|
||||
001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB000
|
||||
001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA000
|
||||
000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8000
|
||||
000DFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6000
|
||||
0009FFFFFFFF8FFFFFFFFFFFFFFFFFFFFFF8FFFFFFFF2000
|
||||
0003FFFFFFFF8FFFFFFFFFFFFFFFFFFFFFF8FFFFFFFD0000
|
||||
0000BFFFFFFF8FFFFFFFFFFFFFFFFFFFFFF8FFFFFFF50000
|
||||
00003FFFFFFF8FFFFFFFFFFFFFFFFFFFFFF8FFFFFFB00000
|
||||
000006FFFFFF8FFFFFFFFFFFFFFFFFFFFFF8FFFFFE100000
|
||||
0000007FFFFF8FFFFFFFFFFFFFFFFFFFFFF8FFFFD2000000
|
||||
00000004EFFF8FFFFFFFFFFFFFFFFFFFFFF8FFFB10000000
|
||||
0000000007DF8FFFFFFFFFFFFFFFFFFFFFF8FB4000000000
|
||||
000000000002578888888888888888888864100000000000
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
skinparam rectangle<<container>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #438DD5
|
||||
BorderColor #3C7FC0
|
||||
}
|
||||
skinparam database<<container>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #438DD5
|
||||
BorderColor #3C7FC0
|
||||
}
|
||||
skinparam queue<<container>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #438DD5
|
||||
BorderColor #3C7FC0
|
||||
}
|
||||
skinparam actor<<container>> {
|
||||
StereotypeFontColor #438DD5
|
||||
FontColor #438DD5
|
||||
BackgroundColor #438DD5
|
||||
BorderColor #3C7FC0
|
||||
}
|
||||
skinparam person<<container>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #438DD5
|
||||
BorderColor #3C7FC0
|
||||
}
|
||||
|
||||
|
||||
skinparam rectangle<<external_container>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #B3B3B3
|
||||
BorderColor #A6A6A6
|
||||
}
|
||||
skinparam database<<external_container>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #B3B3B3
|
||||
BorderColor #A6A6A6
|
||||
}
|
||||
skinparam queue<<external_container>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #B3B3B3
|
||||
BorderColor #A6A6A6
|
||||
}
|
||||
skinparam actor<<external_container>> {
|
||||
StereotypeFontColor #B3B3B3
|
||||
FontColor #B3B3B3
|
||||
BackgroundColor #B3B3B3
|
||||
BorderColor #A6A6A6
|
||||
}
|
||||
skinparam person<<external_container>> {
|
||||
StereotypeFontColor #FFFFFF
|
||||
FontColor #FFFFFF
|
||||
BackgroundColor #B3B3B3
|
||||
BorderColor #A6A6A6
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
skinparam rectangle<<smart-contract>> {
|
||||
BackgroundColor #0abab5
|
||||
}
|
||||
skinparam database<<smart-contract>> {
|
||||
BackgroundColor #0abab5
|
||||
}
|
||||
skinparam queue<<smart-contract>> {
|
||||
BackgroundColor #0abab5
|
||||
}
|
||||
skinparam actor<<smart-contract>> {
|
||||
StereotypeFontColor #0abab5
|
||||
FontColor #0abab5
|
||||
BackgroundColor #0abab5
|
||||
}
|
||||
skinparam person<<smart-contract>> {
|
||||
BackgroundColor #0abab5
|
||||
}
|
||||
|
||||
|
||||
|
||||
rectangle "<$person>\n==User\n\n User with or without credentials" <<person>> as user
|
||||
|
||||
rectangle "==FrostFS\n<size:12>[System]</size>" <<boundary>> as c1 {
|
||||
rectangle "==S3 Gateway\n//<size:12>[]</size>//\n\n AWS S3 compatible gate" <<container>> as s3
|
||||
rectangle "==FrostFS Storage\n//<size:12>[]</size>//\n\n Storage service" <<container>> as stor
|
||||
}
|
||||
|
||||
rectangle "==Blockchain\n<size:12>[System]</size>" <<boundary>> as c3 {
|
||||
Interface "NeoGo"
|
||||
rectangle "==FrostFS ID\n//<size:12>[]</size>//\n\n Stores namespaces and users" <<smart-contract>><<container>> as ffsid
|
||||
rectangle "==Policy\n//<size:12>[]</size>//\n\n Stores APE rules" <<smart-contract>><<container>> as policy
|
||||
}
|
||||
|
||||
user -RIGHT->> s3 : **Requests**\n//<size:12>[HTTP]</size>//
|
||||
s3 -RIGHT->> stor : **Get data to validate request, store objects**
|
||||
s3 -DOWN->> NeoGo : **Get data to validate request**
|
||||
NeoGo - ->> ffsid : **Fetch users**
|
||||
NeoGo - ->> policy : **Fetch policies**
|
||||
|
||||
hide stereotype
|
||||
legend right
|
||||
<#00000000,#00000000>|<color:#000000>**Legend**</color> |
|
||||
|<#08427B><color:#073B6F> <U+25AF></color> <color:#FFFFFF> person </color> |
|
||||
|<#438DD5><color:#3C7FC0> <U+25AF></color> <color:#FFFFFF> container </color> |
|
||||
|<#0abab5><color:#0abab5> <U+25AF></color> <color:#66622E> smart-contract (no text color) </color> |
|
||||
endlegend
|
||||
@enduml
|
||||
|
||||
PlantUML version 1.2022.13(Sat Nov 19 16:22:17 MSK 2022)
|
||||
(GPL source distribution)
|
||||
Java Runtime: OpenJDK Runtime Environment
|
||||
JVM: OpenJDK 64-Bit Server VM
|
||||
Default Encoding: UTF-8
|
||||
Language: en
|
||||
Country: US
|
||||
--></g></svg>
|
After Width: | Height: | Size: 25 KiB |
60
docs/images/authentication/auth-sequence.puml
Normal file
60
docs/images/authentication/auth-sequence.puml
Normal file
|
@ -0,0 +1,60 @@
|
|||
@startuml
|
||||
|
||||
participant User
|
||||
participant "S3-GW"
|
||||
collections "FrostFS Storage"
|
||||
|
||||
User -> "S3-GW": Request
|
||||
|
||||
group signed request
|
||||
|
||||
"S3-GW" -> "FrostFS Storage": Find Access Box
|
||||
"FrostFS Storage" -> "FrostFS Storage": Check request
|
||||
|
||||
alt #pink Check failure
|
||||
"FrostFS Storage" -->> "S3-GW": Access Denied
|
||||
"S3-GW" -->> User: Access Denied
|
||||
end
|
||||
|
||||
"FrostFS Storage" -->> "S3-GW": Access Box
|
||||
"S3-GW" -> "S3-GW": Check sign
|
||||
|
||||
alt #pink Check failure
|
||||
"S3-GW" -->> User: Access Denied
|
||||
end
|
||||
|
||||
"S3-GW" -> "frostfsid contract": Find user
|
||||
"frostfsid contract" -->> "S3-GW": User info
|
||||
"S3-GW" -> "S3-GW": Check user info
|
||||
|
||||
alt #pink Check failure
|
||||
"S3-GW" -->> User: Access Denied
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
"S3-GW" -> "policy contract": Get policies
|
||||
"policy contract" -->> "S3-GW": Policies
|
||||
"S3-GW" -> "S3-GW": Check policy
|
||||
|
||||
alt #pink Check failure
|
||||
"S3-GW" -->> User: Access Denied
|
||||
end
|
||||
|
||||
"S3-GW" -> "FrostFS Storage": User Request
|
||||
"FrostFS Storage" -> "FrostFS Storage": Check request
|
||||
|
||||
alt #pink Check failure
|
||||
"FrostFS Storage" -->> "S3-GW": Access Denied
|
||||
"S3-GW" -->> User: Access Denied
|
||||
end
|
||||
|
||||
"FrostFS Storage" -->> "S3-GW": Response
|
||||
"S3-GW" -->> User: Response
|
||||
|
||||
box "Neo Go"
|
||||
participant "frostfsid contract"
|
||||
participant "policy contract"
|
||||
end box
|
||||
|
||||
@enduml
|
70
docs/images/authentication/auth-sequence.svg
Normal file
70
docs/images/authentication/auth-sequence.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 19 KiB |
BIN
docs/images/authentication/aws-signing.png
Normal file
BIN
docs/images/authentication/aws-signing.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
25
go.mod
25
go.mod
|
@ -3,11 +3,11 @@ module git.frostfs.info/TrueCloudLab/frostfs-s3-gw
|
|||
go 1.20
|
||||
|
||||
require (
|
||||
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240215114728-2a124b95bc02
|
||||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231218084346-bce7ef18c83b
|
||||
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240327095603-491a47e7fe24
|
||||
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-20240301150205-6fe4e2541d0b
|
||||
git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240226094215-c960b1b08831
|
||||
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240402141549-3790142b10c7
|
||||
git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240412102212-530248de754c
|
||||
git.frostfs.info/TrueCloudLab/zapjournald v0.0.0-20240124114243-cb2e66427d02
|
||||
github.com/aws/aws-sdk-go v1.44.6
|
||||
github.com/bluele/gcache v0.0.2
|
||||
|
@ -15,7 +15,7 @@ require (
|
|||
github.com/google/uuid v1.3.1
|
||||
github.com/minio/sio v0.3.0
|
||||
github.com/nats-io/nats.go v1.13.1-0.20220121202836-972a071d373d
|
||||
github.com/nspcc-dev/neo-go v0.104.1-0.20231206061802-441eb8aa86be
|
||||
github.com/nspcc-dev/neo-go v0.105.0
|
||||
github.com/panjf2000/ants/v2 v2.5.0
|
||||
github.com/prometheus/client_golang v1.15.1
|
||||
github.com/prometheus/client_model v0.3.0
|
||||
|
@ -28,10 +28,11 @@ require (
|
|||
go.opentelemetry.io/otel v1.16.0
|
||||
go.opentelemetry.io/otel/trace v1.16.0
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/crypto v0.14.0
|
||||
golang.org/x/crypto v0.17.0
|
||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63
|
||||
golang.org/x/net v0.17.0
|
||||
google.golang.org/grpc v1.59.0
|
||||
google.golang.org/protobuf v1.31.0
|
||||
google.golang.org/protobuf v1.33.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -50,6 +51,7 @@ require (
|
|||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
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.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect
|
||||
github.com/hashicorp/golang-lru v0.6.0 // indirect
|
||||
|
@ -78,8 +80,10 @@ require (
|
|||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
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.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
|
||||
|
@ -88,11 +92,10 @@ 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/net v0.17.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/term v0.13.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/term v0.15.0 // indirect
|
||||
golang.org/x/text v0.14.0 // 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
|
||||
|
|
78
go.sum
78
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.20240215114728-2a124b95bc02 h1:SAoUNpK1KBcY9NwP3ZZwDMXB5bvGCQiHxpXCw6wdpAI=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240215114728-2a124b95bc02/go.mod h1:uY0AYmCznjZdghDnAk7THFIe1Vlg531IxUcus7ZfUJI=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231218084346-bce7ef18c83b h1:zdbOxyqkxRyOLc7/2oNFu5tBwwg0Q6+0tJM3RkAxHlE=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20231218084346-bce7ef18c83b/go.mod h1:YMFtNZy2MgeiSwt0t8lqk8dYBGzlbhmV1cbbstJJ6oY=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240327095603-491a47e7fe24 h1:uIkl0mKWwDICUZTbNWZ38HLYDBI9rMgdAhYQWZ0C9iQ=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240327095603-491a47e7fe24/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-20240301150205-6fe4e2541d0b h1:nLIWYXe4e1fWgpKeMfVke/CNBn388egh4fArFdvhfHw=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240301150205-6fe4e2541d0b/go.mod h1:XcgrbZ88XfvhAMxmZCQJ0dv6FyRSq6Mg2J7nN8uuO0k=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240402141549-3790142b10c7 h1:sjvYXV0WJAF4iNF3l0uhcN8zhXmpY1gYI0WyJpeFe6s=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240402141549-3790142b10c7/go.mod h1:i0RKqiF4z3UOxLSNwhHw+cUz/JyYWuTRpnn9ere4Y3w=
|
||||
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-20240226094215-c960b1b08831 h1:yK2iGQlg5kMmU47ZHor/g52mVS1xEgJSRQ4Olp76Cg8=
|
||||
git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240226094215-c960b1b08831/go.mod h1:YVL7yFaT0QNSpA0z+RHudLvrLwT+lsFYGyBSVc1ustI=
|
||||
git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240412102212-530248de754c h1:Ei15WKKLDXoFqEIe292szb3RfsyLqRZfeJX2FjFvz6k=
|
||||
git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240412102212-530248de754c/go.mod h1:H/AW85RtYxVTbcgwHW76DqXeKlsiCIOeNXHPqyDBrfQ=
|
||||
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=
|
||||
|
@ -66,6 +66,7 @@ github.com/aws/aws-sdk-go v1.44.6 h1:Y+uHxmZfhRTLX2X3khkdxCoTZAyGEX21aOUHe1U6geg
|
|||
github.com/aws/aws-sdk-go v1.44.6/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
|
||||
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/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=
|
||||
|
@ -87,6 +88,8 @@ github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWH
|
|||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
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/gnark-crypto v0.12.2-0.20231013160410-1f65e75b6dfb h1:f0BMgIjhZy4lSRHCXFbQst85f5agZAjtDMixQqBWNpc=
|
||||
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=
|
||||
|
@ -104,6 +107,8 @@ 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/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=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
|
@ -149,6 +154,7 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
|
|||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
|
@ -197,6 +203,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.2 h1:Dwmkdr5Nc/oBiXgJS3CDHNhJtIHkuZ3DZF5
|
|||
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.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=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
|
@ -228,6 +236,7 @@ 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/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/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296 h1:vU9tpM3apjYlLLeY23zRWJ9Zktr5jp+mloR942LEOpY=
|
||||
|
@ -241,12 +250,21 @@ 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.104.1-0.20231206061802-441eb8aa86be h1:nZ2Hi5JSXdq3JXDi/8lms1UXQDAA5LVGpOpcrf2bRVA=
|
||||
github.com/nspcc-dev/neo-go v0.104.1-0.20231206061802-441eb8aa86be/go.mod h1:dsu8+VDMgGF7QNtPFBU4seE3pxSq8fYCuk3A6he4+ZQ=
|
||||
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=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
|
||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/panjf2000/ants/v2 v2.5.0 h1:1rWGWSnxCsQBga+nQbA4/iY6VMeNoOIAM0ZWh9u3q2Q=
|
||||
github.com/panjf2000/ants/v2 v2.5.0/go.mod h1:cU93usDlihJZ5CfRGNDYsiBYvoilLvBF5Qp/BT2GNRE=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
||||
|
@ -301,6 +319,7 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
|
|||
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=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM=
|
||||
github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=
|
||||
github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
|
||||
github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
|
||||
|
@ -314,6 +333,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
|||
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.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=
|
||||
|
@ -353,8 +373,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
|||
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.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
|
@ -390,8 +410,10 @@ 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.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=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
|
@ -412,9 +434,11 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
|
|||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
|
@ -449,6 +473,7 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
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=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -457,7 +482,10 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -471,8 +499,10 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -487,12 +517,12 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -501,8 +531,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
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=
|
||||
|
@ -554,10 +584,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.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=
|
||||
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=
|
||||
|
@ -663,17 +695,22 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
|
|||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
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/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=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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.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=
|
||||
|
@ -689,3 +726,4 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
|
|||
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=
|
||||
|
|
|
@ -41,6 +41,11 @@ type Config struct {
|
|||
Key *keys.PrivateKey
|
||||
}
|
||||
|
||||
const (
|
||||
batchSize = 100
|
||||
iteratorChainsByPrefix = "iteratorChainsByPrefix"
|
||||
)
|
||||
|
||||
var _ policy.Contract = (*Client)(nil)
|
||||
|
||||
// New creates new Policy contract wrapper.
|
||||
|
@ -114,7 +119,8 @@ func (c *Client) RemoveChain(kind policycontract.Kind, entity string, name []byt
|
|||
}
|
||||
|
||||
func (c *Client) ListChains(kind policycontract.Kind, entity string, name []byte) ([][]byte, error) {
|
||||
items, err := c.policyContract.ListChainsByPrefix(big.NewInt(int64(kind)), entity, name)
|
||||
items, err := commonclient.ReadIteratorItems(c.actor, batchSize, c.contractHash, iteratorChainsByPrefix,
|
||||
big.NewInt(int64(kind)), entity, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ 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"
|
||||
"github.com/google/uuid"
|
||||
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
@ -25,7 +27,7 @@ type MorphRuleChainStorageConfig struct {
|
|||
Log *zap.Logger
|
||||
}
|
||||
|
||||
var _ engine.MorphRuleChainStorage = (*MorphRuleChainStorage)(nil)
|
||||
var _ engine.MorphRuleChainStorageReader = (*MorphRuleChainStorage)(nil)
|
||||
|
||||
const bucketPolicyPrefix = 'b'
|
||||
|
||||
|
@ -37,11 +39,11 @@ func NewMorphRuleChainStorage(config *MorphRuleChainStorageConfig) *MorphRuleCha
|
|||
}
|
||||
}
|
||||
|
||||
func (c *MorphRuleChainStorage) AddMorphRuleChain(chain.Name, engine.Target, *chain.Chain) (util.Uint256, uint32, error) {
|
||||
func (c *MorphRuleChainStorage) GetAdmin() (util.Uint160, error) {
|
||||
panic("should never be called")
|
||||
}
|
||||
|
||||
func (c *MorphRuleChainStorage) RemoveMorphRuleChain(chain.Name, engine.Target, chain.ID) (util.Uint256, uint32, error) {
|
||||
func (c *MorphRuleChainStorage) ListTargetsIterator(engine.TargetType) (uuid.UUID, result.Iterator, error) {
|
||||
panic("should never be called")
|
||||
}
|
||||
|
||||
|
@ -74,23 +76,25 @@ func (c *MorphRuleChainStorage) ListMorphRuleChains(name chain.Name, target engi
|
|||
}
|
||||
|
||||
func (c *MorphRuleChainStorage) PutBucketPolicy(ns string, cnrID cid.ID, policy []byte, chains []*chain.Chain) error {
|
||||
c.cache.Delete(cache.MorphPolicyCacheKey{Target: engine.NamespaceTarget(ns), Name: chain.S3})
|
||||
c.cache.Delete(cache.MorphPolicyCacheKey{Target: engine.ContainerTarget(cnrID.EncodeToString()), Name: chain.S3})
|
||||
|
||||
tx := c.contract.StartTx()
|
||||
tx.AddChain(policycontract.IAM, ns, getBucketPolicyName(cnrID), policy)
|
||||
|
||||
for i := range chains {
|
||||
tx.AddChain(policycontract.Namespace, ns, chains[i].ID, chains[i].Bytes())
|
||||
tx.AddChain(policycontract.Container, cnrID.EncodeToString(), chains[i].ID, chains[i].Bytes())
|
||||
}
|
||||
|
||||
return c.contract.SendTx(tx)
|
||||
}
|
||||
|
||||
func (c *MorphRuleChainStorage) DeleteBucketPolicy(ns string, cnrID cid.ID, chainID chain.ID) error {
|
||||
c.cache.Delete(cache.MorphPolicyCacheKey{Target: engine.NamespaceTarget(ns), Name: chain.S3})
|
||||
func (c *MorphRuleChainStorage) DeleteBucketPolicy(ns string, cnrID cid.ID, chainIDs []chain.ID) error {
|
||||
c.cache.Delete(cache.MorphPolicyCacheKey{Target: engine.ContainerTarget(cnrID.EncodeToString()), Name: chain.S3})
|
||||
|
||||
tx := c.contract.StartTx()
|
||||
tx.RemoveChain(policycontract.Namespace, ns, chainID)
|
||||
for _, chainID := range chainIDs {
|
||||
tx.RemoveChain(policycontract.Container, cnrID.EncodeToString(), chainID)
|
||||
}
|
||||
tx.RemoveChain(policycontract.IAM, ns, getBucketPolicyName(cnrID))
|
||||
|
||||
return c.contract.SendTx(tx)
|
||||
|
@ -100,13 +104,13 @@ func (c *MorphRuleChainStorage) GetBucketPolicy(ns string, cnrID cid.ID) ([]byte
|
|||
return c.contract.GetChain(policycontract.IAM, ns, getBucketPolicyName(cnrID))
|
||||
}
|
||||
|
||||
func (c *MorphRuleChainStorage) SaveACLChains(ns string, chains []*chain.Chain) error {
|
||||
c.cache.Delete(cache.MorphPolicyCacheKey{Target: engine.NamespaceTarget(ns), Name: chain.S3})
|
||||
c.cache.Delete(cache.MorphPolicyCacheKey{Target: engine.NamespaceTarget(ns), Name: chain.Ingress})
|
||||
func (c *MorphRuleChainStorage) SaveACLChains(cid string, chains []*chain.Chain) error {
|
||||
c.cache.Delete(cache.MorphPolicyCacheKey{Target: engine.ContainerTarget(cid), Name: chain.S3})
|
||||
c.cache.Delete(cache.MorphPolicyCacheKey{Target: engine.ContainerTarget(cid), Name: chain.Ingress})
|
||||
|
||||
tx := c.contract.StartTx()
|
||||
for i := range chains {
|
||||
tx.AddChain(policycontract.Namespace, ns, chains[i].ID, chains[i].Bytes())
|
||||
tx.AddChain(policycontract.Container, cid, chains[i].ID, chains[i].Bytes())
|
||||
}
|
||||
|
||||
return c.contract.SendTx(tx)
|
||||
|
|
|
@ -70,8 +70,8 @@ func (s *Storage) PutBucketPolicy(ns string, cnrID cid.ID, policy []byte, policy
|
|||
return s.morph.PutBucketPolicy(ns, cnrID, policy, policyChains)
|
||||
}
|
||||
|
||||
func (s *Storage) DeleteBucketPolicy(ns string, cnrID cid.ID, chainID chain.ID) error {
|
||||
return s.morph.DeleteBucketPolicy(ns, cnrID, chainID)
|
||||
func (s *Storage) DeleteBucketPolicy(ns string, cnrID cid.ID, chainIDs []chain.ID) error {
|
||||
return s.morph.DeleteBucketPolicy(ns, cnrID, chainIDs)
|
||||
}
|
||||
|
||||
func (s *Storage) GetBucketPolicy(ns string, cnrID cid.ID) ([]byte, error) {
|
||||
|
|
|
@ -169,6 +169,7 @@ func (w *PoolWrapper) GetSubTreeStream(ctx context.Context, bktInfo *data.Bucket
|
|||
RootID: rootID,
|
||||
Depth: depth,
|
||||
BearerToken: getBearer(ctx, bktInfo),
|
||||
Order: treepool.AscendingOrder,
|
||||
}
|
||||
|
||||
subTreeReader, err := w.p.GetSubTree(ctx, poolPrm)
|
||||
|
|
|
@ -66,7 +66,6 @@ const (
|
|||
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
|
||||
CouldntGetBucketVersioning = "couldn't get bucket versioning" // Warn in ../../api/handler/put.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
|
||||
|
@ -136,6 +135,9 @@ const (
|
|||
ControlAPIGetPolicy = "get policy request"
|
||||
ControlAPIListPolicies = "list policies request"
|
||||
PolicyValidationFailed = "policy validation failed"
|
||||
ServerReconnecting = "reconnecting server..."
|
||||
ServerReconnectedSuccessfully = "server reconnected successfully"
|
||||
ServerReconnectFailed = "failed to reconnect server"
|
||||
ParseTreeNode = "parse tree node"
|
||||
FailedToGetRealObjectSize = "failed to get real object size"
|
||||
CouldntDeleteObjectFromStorageContinueDeleting = "couldn't delete object from storage, continue deleting from tree"
|
||||
|
@ -149,4 +151,7 @@ const (
|
|||
FailedToGenerateRequestID = "failed to generate request id"
|
||||
InvalidBucketObjectLockEnabledHeader = "invalid X-Amz-Bucket-Object-Lock-Enabled header"
|
||||
InvalidTreeKV = "invalid tree service meta KV"
|
||||
FailedToWriteResponse = "failed to write response"
|
||||
WarnDuplicateAddress = "duplicate address"
|
||||
PolicyCouldntBeConvertedToNativeRules = "policy couldn't be converted to native rules, only s3 rules be applied"
|
||||
)
|
||||
|
|
|
@ -223,8 +223,6 @@ func (c *ServiceClientMemory) GetSubTree(_ context.Context, bktInfo *data.Bucket
|
|||
return nil, ErrNodeNotFound
|
||||
}
|
||||
|
||||
sortNode(tr.treeData)
|
||||
|
||||
node := tr.treeData.getNode(rootID)
|
||||
if node == nil {
|
||||
return nil, ErrNodeNotFound
|
||||
|
|
Loading…
Reference in a new issue